用户系统(B,游戏零改动): - POST /api/device 首访 0 交互静默建档(自动玩家一/二…,per-IP 频控);断网降级纯本地照玩 - 大厅档案切换器(切换=flush 上云→快照回灌→倒灌→服务器回拉,profileId 分桶修同设备串档;aria menu 编号可听) - claim 可选认领(命名+scrypt PIN);旧 users/sessions 单向迁移(PIN 明文即刻哈希,已真实数据验证);login 兼容+claim-merge 桶级 LWW;logout;/server/ 静态封禁 - PUT baseUpdatedAt 守卫(legacy 放行+警告);GET 无参全桶回拉(修 7/8 游戏跨设备只 PUT 不 GET) - web/profiles.js 分桶纯函数 + api-smoke.mjs 7/7(建档/LWW/claim-merge/迁移/频控/封禁/分桶 H1-H4) 键盘双端(A,游戏代码零改动): - gesture.js 键盘通道 +137 行:←↑↓→/WASD=滑动、空格=单击(走 300ms 双击窗与触摸共享虚拟指针)、回车=即时 doubletap、按住空格 ≥450ms=长按;repeat 抑制/表单守卫/方向键 preventDefault - settings.input(auto/touch/key)+ gesture.inputMode();遮罩键盘解锁与文案自适应(按空格开始/轻触开始) - 8 游戏遮罩改用自适应默认文案;大厅设置面板「操作方式」+页脚键位提示 - live control.mjs 键盘注入通道(CDP dispatchKeyEvent);engine-input.test 61 断言 回归:12 套件全绿(8 playtest + guide + engine-voice + engine-input + api-smoke 7/7);8001 服务器已重启加载新 API,/api/device 端到端验证通过 P1 待排:153 句×双性 TTS 键盘语音变体 / 6 位配对码跨设备 / 认领 UI 打磨(research/platform/00-synthesis.md §3)
live/ — 全拟真玩家 agent 基础设施
用真实无头 Chromium 黑盒驱动任意一款 SoundGame(纯音频 HTML5 游戏), 录制 Web Audio 真实混音输出,用本地 SenseVoice ASR「听懂」旁白是否 完整播完/被截断,外部 agent 通过 HTTP 逐步玩游戏(每步根据听到的内容决策)。
零依赖(Node ≥22 全局 WebSocket/fetch + 一个 Python 文件用 funasr),
不改 web/engine/ 与游戏代码,全部探针经 CDP Page.addScriptToEvaluateOnNewDocument
预注入(patch 原型)。
组成
| 文件 | 职责 |
|---|---|
cdp.mjs |
极简 CDP 客户端:spawn chromium(--headless=new --remote-debugging-port=0),WebSocket 多路复用,Target.createTarget+attachToTarget(flatten) 后用 sessionId 发 Page.*/Runtime.evaluate/Input.dispatchTouchEvent |
inject.js |
预注入探针(纯文本,控制端读文件注入):fetch/decodeAudioData/getChannelData/ABSN start/stop/connect/vibrate/console 七类 patch;混音经 tap GainNode → MediaRecorder(opus) → base64 chunks |
control.mjs |
HTTP 控制服务器(端口 8123):会话/手势/事件/转写/落盘;每 500ms drain 页面 __live(chunks 追加 mix.webm,事件换算到录音时基) |
listen_server.py |
SenseVoice 常驻进程(stdin/stdout JSON Lines):ffmpeg 解码 webm → 16k wav,按 voice 事件窗口切片转写 + astats 能量判定 + difflib 相似度 |
voice 检测原理:引擎 voice 层走 SoundTouch 管线(ScriptProcessorNode 从
AudioBuffer 抽读),不经 AudioBufferSourceNode.start。探针 patch
AudioBuffer.prototype.getChannelData——引擎里只有 SoundTouch 的
WebAudioBufferSource.extract 对 voice buffer 调它——以此检出 voice 段的
开始/时长,用 ScriptProcessorNode.disconnect(引擎 _killVoice 必经)
检出结束。bgm/sfx 走 AudioBufferSourceNode.start/stop,直接挂钩。
时间轴对齐:事件时间戳是 performance.now()(ms);__live.recStartT 是
recorder 启动当刻的同源时间戳;ta = (t - recStartT)/1000 即录音时间轴秒
(误差 ≈ AudioContext resume 延迟,几十 ms,ASR 窗口 ±0.3s 余量吸收)。
判定逻辑(截断双保险)
- 事件层:
vend.ta < t0 + dur/tempo - 0.15s→ 引擎级截断(tempo=设置语速, 长按快进窗口按 1.7 倍速修正预期结束时刻)。 - ASR 层:窗口
[t0-0.15, t1+0.35]切片转写,与voice_text.json期望文本 做difflib.SequenceMatcher(去<|...|>标签/标点/小写归一化),<0.6→ 内容不完整/错配。 - 窗口 RMS < -45dB →
SILENCE;无 voice 事件的区间(间隙探针)有能量 →SFX_BGM_ONLY,无能量 →SILENCE。 - verdict 优先级:
TRUNCATED(事件层)>SILENCE>MISMATCH>OK。
用法
前置:游戏服务器在 8001(PORT=8001 node web/server/server.js)。
node web/harness/live/control.mjs # 8123;首次启动加载 SenseVoice 约 10-20s
curl 示例会话
# 1. 开会话(settings 接受友好名 normal/low/off 或引擎枚举值 1.0/0.3/0)
curl -X POST localhost:8123/session -H 'content-type: application/json' -d '{
"game": "rpg",
"settings": {"voice":"male","speed":"normal","bgm":"normal","guide":"full"}
}'
# 2. 第一个动作必须是 tap(解锁启动遮罩,否则全部无声)
curl -X POST localhost:8123/gesture -H 'content-type: application/json' -d '{"type":"tap"}'
# 3. 循环:听 → 决策 → 动手势
curl "localhost:8123/listen" # 自上次以来新判定的段落
curl "localhost:8123/events?since=0" # 增量原始事件(下次用返回的 next 当 since)
curl -X POST localhost:8123/gesture -H 'content-type: application/json' \
-d '{"type":"swiperight"}' # tap/doubletap/swipeup/swipedown/swipeleft/swiperight/longpress
# x,y 缺省屏幕中心;swipe 默认 200px/300ms
# 4. 复盘与收尾
curl "localhost:8123/transcript" # 合并时间轴全量(JSON)
curl -X POST localhost:8123/stop # 落盘 4 产物,关页签;服务器保持运行
/listen 返回行:
{"t0": 53.68, "t1": 55.56, "voiceId": "desc_path",
"expected": "林间小路。风穿过树叶,脚下的落叶沙沙作响。",
"heard": "林间小路。", "similarity": 0.3636, "verdict": "TRUNCATED"}
产物落到 web/harness/live/out/<game>-<ts>/:mix.webm(真实混音)、
events.jsonl(全事件,含录音时基 ta)、transcript.md(人读时间轴)、
verdicts.json(全部 voice 段 + 判定)。
已知限制
- 短句(≤6 字)ASR 同音误识会把相似度拉到 0.6 以下 → 偶发
MISMATCH假阳性(例:「前进,去岔路口」被听成「前进却差了」)。源资产直转是对的, 问题出在混音背景(bgm/ambience)下的短切片;verdict 是「ASR 层」判定, 事件层TRUNCATED/SILENCE不受影响。需要高置信时以similarity数值 而非仅 verdict 做决策。 - 单会话:同时只开一个游戏页签,
POST /session会顶掉前一个(不落盘)。 - 录音 t=0 与事件时基的对齐误差 ≈ AudioContext resume 延迟(实测几十 ms), 不影响 ±0.3s 余量的窗口切分。
- MediaRecorder 不可用时探针降级为只记事件不录音(
__live.recState='unavailable', 事件流里有recfail);此时/listen的 ASR 层不可用。 - 间隙探针只覆盖相邻 voice 段之间与首段之前的 ≥1.5s 区间;末尾开放区间不探测。
- chromium profile 写在
out/.profiles/,浏览器随控制服务器退出时清理。
环境
chromium(snap)在 PATH;node≥22;ffmpeg在 PATH。python3需 funasr + torch(SenseVoiceSmall 模型走 modelscope 缓存)。- 环境变量:
LIVE_PORT(默认 8123)、GAME_BASE(默认http://localhost:8001)。