# -*- coding: utf-8 -*- """voice 引用一致性校验(26 号推广工程配套)。 对每款深度重构游戏,扫描 game.js(含 index.html / sessions)里引用的全部 'voice/' 字面量,对照 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//) 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()