Files
SoundGame/web/harness/live
xsl 566241e05d feat(platform/P1): 配对码跨设备 + 认领打磨 + 键盘语音双套资产(两 agent 并行 + 集成收口)
配对码与认领(B):
- POST /api/pair/start|join:6 位码 10 分钟单次有效、仅存内存零泄露面、5 连错 per-IP 封锁 10 分钟;join=新设备档案按桶 LWW 并入老档案+token 改绑(复用 claim-merge);日志脱敏
- 大厅:生成配对码(speechSynthesis 逐位朗读+aria-live+重听)/从其他设备转入(one-time-code 输入框)/从本机移除/401 过期自愈引导/aria 补全/玩家名可听化
- api-smoke 12/12(配对全流程/频控/封锁/过期自愈/移除/digitsToCn)+ CDP 真浏览器 E2E 22/22

键盘语音双套资产(A,路线 a):
- 164 变体句(8 游戏 grep 实测,超出调研预估 153:含轻触/点两下/按住变体)×2 性别=328 生成 656 文件 36.9 分钟;触屏主资产零字节改动(PCM 逐样本比对后还原)
- 引擎 _candidates 三跳路由 key→gender→平铺(inputMode 跟随);缓存键 mode|gender|id;engine-input 新增 K9 路由断言(72 条)
- check_all_assets 增 key 列(两性别一致且 ⊆ 主集)8 游戏全绿;voice_text.json 增 __key__ 表;live control.mjs key 会话对照键盘措辞验收(集成)
- voice-ids 文档:8 游戏键盘变体小节由 KEY_LINES 程序化生成(与台词表逐字断言一致);补齐 storyline/escape/simon 三个历史缺失文档;design README 增 new8 索引
- mock 适配:playtest persona 加 maxTouchPoints=1 保持触屏链断言语义零改动

回归:12 套件全绿(8 playtest + guide 122 + engine-voice 19 + engine-input 72 + api-smoke 12/12)
同批入库:批次 2/3 已完稿设计文档(17-abyss/18-detective/19-cipher 全套,20-22 在途)
2026-09-10 00:43:23 +08:00
..

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)。