105 lines
5.9 KiB
Markdown
105 lines
5.9 KiB
Markdown
# 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`)。
|
||
|
||
```bash
|
||
node web/harness/live/control.mjs # 8123;首次启动加载 SenseVoice 约 10-20s
|
||
```
|
||
|
||
### curl 示例会话
|
||
|
||
```bash
|
||
# 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` 返回行:
|
||
|
||
```json
|
||
{"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`)。
|