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 置完成。
104 lines
4.1 KiB
Python
104 lines
4.1 KiB
Python
# -*- 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()
|