Files
SoundGame/tools/validate_voice_refs.py
T
xsl ee5d4d2544 feat(playbook): 26 号交互手册全量落地——剩余 12 款深度重构 + 语音全量重制
storyline/escape/simon 首批之后的推广收官:15 款全部按 26 号模式运行
(发言权/回执/三档提示/迷茫接管/手势阶梯/断点复述),旧「双击重听/格挡/
菜单确认」等反模式口径全线废除,开场零规则念白(首句即动手)。

游戏层(12 款):rpg(格挡=前摇窗口内再单击)/ rhythm / space / radio /
dungeon / abyss / detective / cipher / teahouse / keeper / bridge /
hunt(联机切片):
- 每款 game.js 接引擎六模块 + makeInputPipeline(doubleTapWindow:0);
  四向玩法类 swipeCore + 长按帮助;双击是玩法的(space 护盾 / hunt
  旋风斩)按手势阶梯阶段 3 分档恢复双击窗口(doubletapCore)
- 每款 playtest 重写:玩法断言 + §6.2 乱点机器人(无声操作 / 被动
  原句重复 / 打断失败 = 0);15 款 3900+ 断言全绿
- 12 份 docs/design/26-apply-*.md 施工单 + tools/voice_lines_26/
  台词清单 + voice-ids 文档同步 + 转写会话剧本按新口径重写

引擎层(web/engine/,6 处修复):pipeline 增 doubletapCore;novice
teachLine 带 voice/ 前缀(此前教学句被静默丢弃);confusion.record
前置到新手门(锁定手势的乱点也计信号);InvalidHints 播完恢复解释前
状态(根治孤儿 BUSY);WaitPoint takeover(2) 台账去重;台账只在
speak 真正播出时登记。遗留引擎债登记 roadmap §3.1。

语音资产:台词表合并(新增 ~250 系统句 / 改 ~50 旧口径 / 删 ~110
废止句;合并器 tools/merge_voice_lines_26.py + 引用校验器
tools/validate_voice_refs.py),GPU 重录 1536 wav → 4204 ogg/m4a
落地,check_all_assets + validate_voice_refs 双绿;键盘变体同步扩表。

大厅/文档:15 款统一新交互口径(「新交互」徽标退役);README /
HANDOVER / rpg README 旧口径修正;roadmap P0-P3 置完成。
2026-10-02 12:33:20 +08:00

104 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""voice 引用一致性校验(26 号推广工程配套)。
对每款深度重构游戏,扫描 game.js(含 index.html / sessions)里引用的全部
'voice/<id>' 字面量,对照 tools/gen_voice_dual.py 的台词表(GAMES + NPCS):
1. 引用了但台词表没有 → 多半是接线笔误(未生成就先报出来)
2. NPC 表 / 平铺目录里存在但双性别表没有 → 合法(角色音平铺),单独计数
3. 台词表里有但代码从不引用 → 死句(列出来人工裁定:可能被 session 或
链式构造间接引用,只警告不判错)
用法:.venv-tts/bin/python tools/validate_voice_refs.py [game ...]
不带参数 = 校验全部 15 款。
"""
import importlib.util
import json
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(ROOT, "tools"))
spec = importlib.util.spec_from_file_location(
"gen_voice_dual", os.path.join(ROOT, "tools", "gen_voice_dual.py"))
gvd = importlib.util.module_from_spec(spec)
spec.loader.exec_module(gvd)
VOICE_RE = re.compile(r"['\"]voice/([A-Za-z0-9_]+)['\"]")
# 每个 game 要扫描的源文件(相对 web/<game>/)
SRC_FILES = ["game.js", "index.html"]
# 会话剧本也引用 id(transcribe 用),只用于「死句」判定的豁免
SESSION_GLOB = os.path.join(ROOT, "web", "harness", "sessions", "{game}.mjs")
def scan_game(game):
refs = set()
gdir = os.path.join(ROOT, "web", game)
for name in SRC_FILES:
p = os.path.join(gdir, name)
if not os.path.exists(p):
continue
with open(p, encoding="utf-8") as f:
refs |= set(VOICE_RE.findall(f.read()))
# 会话剧本(豁免死句用)
ses = set()
sp = SESSION_GLOB.format(game=game)
if os.path.exists(sp):
with open(sp, encoding="utf-8") as f:
ses |= set(VOICE_RE.findall(f.read()))
return refs, ses
def flat_ids(game):
"""已有资产 id:顶层平铺(角色/NPC 单音色)+ male/female 目录文件名
(含早期轮次落盘但不在台词表的「在库沿用」资产,如 rhythm 的 gd_*)。"""
vdir = os.path.join(ROOT, "web", game, "audio", "voice")
if not os.path.isdir(vdir):
return set()
ids = {f.rsplit(".", 1)[0] for f in os.listdir(vdir) if "." in f}
for sub in ("male", "female"):
d = os.path.join(vdir, sub)
if os.path.isdir(d):
ids |= {f.rsplit(".", 1)[0] for f in os.listdir(d) if "." in f}
return ids
def main():
games = sys.argv[1:] or list(gvd.GAMES.keys())
bad = 0
for game in games:
# 双性别台词表 + NPC 平铺表 + 平铺资产文件 + 键盘变体(同 id)
table = {fid for fid, _i, _t in gvd.GAMES.get(game, [])}
npc = {fid for fid, _s, _in, _t in gvd.NPCS.get(game, [])}
flat = flat_ids(game)
key = {fid for fid, _t in gvd.KEY_LINES.get(game, [])}
refs, ses = scan_game(game)
# 引用解析:id 直配;动态前缀引用('voice/' + x 拼接)不在本工具能力内,
# 游戏若声明 DYNAMIC_VOICE_PREFIX 则跳过该项
# 尾下划线 = 动态前缀拼接('voice/btl_loot_' + n),不在直配能力内
unresolved = sorted(
r for r in refs
if not r.endswith("_") and r not in table and r not in npc and r not in flat)
# 死句:表里有、代码与会话都不引用
dead = sorted(
fid for fid in table
if fid not in refs and fid not in ses and fid not in key)
status = "OK " if not unresolved else "BAD"
if unresolved:
bad += 1
print(f"[{status}] {game}: 引用 {len(refs)},表内 {len(table)},NPC {len(npc)},未解析 {len(unresolved)},疑似死句 {len(dead)}")
for u in unresolved:
print(f" 未解析: voice/{u}")
for d in dead[:10]:
print(f" 死句?: {d}")
if len(dead) > 10:
print(f" …另有 {len(dead) - 10} 条")
sys.exit(1 if bad else 0)
if __name__ == "__main__":
main()