配对码与认领(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 在途)
71 lines
2.9 KiB
Python
71 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
||
"""从 tools/gen_voice_dual.py 台词表提取 id -> 台词全文 映射,输出 JSON。
|
||
|
||
供试玩转写 harness(web/harness/)把语音播放事件转写成人类可读文本。
|
||
台词表是唯一事实源:新台词先改进 gen_voice_dual.py,再跑本脚本刷新 JSON。
|
||
|
||
用法:.venv-tts/bin/python tools/extract_voice_text.py
|
||
输出:web/harness/voice_text.json {"game": {"id": "全文", ...}}
|
||
"""
|
||
import ast
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
sys.path.insert(0, str(ROOT / "tools"))
|
||
|
||
import gen_voice_dual as gvd # noqa: E402
|
||
|
||
out = {}
|
||
for game, lines in gvd.GAMES.items():
|
||
table = {}
|
||
for fid, _ins, text in lines:
|
||
if fid in table:
|
||
raise SystemExit(f"重复 id:{game}/{fid}")
|
||
table[fid] = text
|
||
# NPC 单音色平铺(gen_voice_dual.NPCS:id, speaker, instruct, 台词)
|
||
for fid, _sp, _ins, text in gvd.NPCS.get(game, []):
|
||
if fid in table:
|
||
raise SystemExit(f"重复 id:{game}/{fid}")
|
||
table[fid] = text
|
||
out[game] = table
|
||
|
||
# 键盘变体(P1 路线 a):独立顶层表 "__key__",不与主表混放——现有消费者
|
||
# (mock.mjs / transcribe.mjs / live/control.mjs 的 VOICE_TEXT[game][id] 查找)
|
||
# 行为零改动;键盘措辞会话(input=key)按 VOICE_TEXT.__key__[game][id] 取期望文本。
|
||
out["__key__"] = {
|
||
game: {fid: text for fid, text in gvd.KEY_LINES[game]}
|
||
for game in gvd.GAMES
|
||
}
|
||
|
||
|
||
def parse_legacy(py_path: Path) -> dict:
|
||
"""AST 解析旧版 gen_voice.py:提取 ('id', ("Speaker", "instruct"), "text") 元组。
|
||
不能 import——这些脚本顶层直接执行生成。"""
|
||
tree = ast.parse(py_path.read_text(encoding="utf-8"))
|
||
found = {}
|
||
for node in ast.walk(tree):
|
||
if not isinstance(node, ast.Tuple) or len(node.elts) != 3:
|
||
continue
|
||
a, _b, c = node.elts
|
||
if (isinstance(a, ast.Constant) and isinstance(a.value, str)
|
||
and isinstance(c, ast.Constant) and isinstance(c.value, str)
|
||
and len(c.value) >= 2):
|
||
found[a.value] = c.value
|
||
return found
|
||
|
||
|
||
# 旧 flavor 脚本补齐角色台词(storyline 林晚等、escape 角色音);主表优先,不覆盖
|
||
for game, legacy in [("storyline", ROOT / "app/src/storyline/gen/gen_voice.py"),
|
||
("escape", ROOT / "app/src/escape/gen/gen_voice.py")]:
|
||
for fid, text in parse_legacy(legacy).items():
|
||
out[game].setdefault(fid, text)
|
||
|
||
dest = ROOT / "web" / "harness" / "voice_text.json"
|
||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||
dest.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
|
||
total = sum(len(v) for k, v in out.items() if k != "__key__")
|
||
key_total = sum(len(v) for v in out["__key__"].values())
|
||
print(f"OK {dest.relative_to(ROOT)} games={len(out) - 1} lines={total} key_variants={key_total}")
|