Files
SoundGame/tools/check_all_assets.py
T
xsl 566241e05d feat(platform/P1): 配对码跨设备 + 认领打磨 + 键盘语音双套资产(两 agent 并行 + 集成收口)
配对码与认领(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 在途)
2026-09-10 00:43:23 +08:00

107 lines
4.8 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""全项目音频资产终检(R5)。
检查 8 个游戏 web/<game>/audio/:
1. 每个 .ogg 都有同名 .m4a(反之亦然)
2. voice/male 与 voice/female 的 id 集合一致(storyline/rpg 的平铺 NPC/角色句除外)
3. 键盘变体目录 voice/<gender>/key/(P1 路线 a):
male/female 两性别 key 集合一致;key 集合 ⊆ 主(性别目录)集合——变体只
覆盖含操作词的句子,其余 id 由引擎回退主资产
4. 无 0 字节文件;ogg/m4a 均可被 ffprobe 解码且时长 > 0
输出 Markdown 报告到 stdout(重定向进 docs/audio/final-asset-check.md)。
"""
import os
import subprocess
import sys
ROOT = "/home/xsl/SoundGame/web"
GAMES = ["storyline", "escape", "simon", "rhythm", "space", "rpg", "radio", "dungeon"]
# 平铺 voice 文件(角色/NPC,单音色,不做性别集合一致性要求)
FLAT_OK = {"storyline", "rpg", "radio"}
def ids(d, ext):
if not os.path.isdir(d):
return set()
return {f[: -len(ext)] for f in os.listdir(d) if f.endswith(ext)}
def main():
problems = []
print("# 全项目音频资产终检(R5 建立,批次 1 起含 radio/dungeon,2026-09-01)\n")
print("| 游戏 | ogg | m4a | 0字节 | 解码失败 | voice male/female id 一致 | key male/female 一致且 ⊆ 主集 |")
print("|---|---|---|---|---|---|---|")
for g in GAMES:
base = os.path.join(ROOT, g, "audio")
oggs, m4as, zero, bad = [], [], [], []
for dirpath, _dirs, files in os.walk(base):
for f in files:
p = os.path.join(dirpath, f)
if os.path.getsize(p) == 0:
zero.append(p)
if f.endswith(".ogg"):
oggs.append(p)
elif f.endswith(".m4a"):
m4as.append(p)
# 双格式配对
o_set = {p[:-4] for p in oggs}
m_set = {p[:-4] for p in m4as}
for p in sorted(o_set - m_set):
problems.append(f"{g}: 缺 m4a —— {p}.ogg")
for p in sorted(m_set - o_set):
problems.append(f"{g}: 缺 ogg —— {p}.m4a")
# 解码校验(抽查全量 ffprobe)
for p in oggs + m4as:
r = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "csv=p=0", p], capture_output=True, text=True)
try:
if float(r.stdout.strip()) <= 0:
bad.append(p)
except ValueError:
bad.append(p)
for p in zero:
problems.append(f"{g}: 0 字节 —— {p}")
for p in bad:
problems.append(f"{g}: 解码失败/时长为 0 —— {p}")
# voice 性别集合一致性
vm = ids(os.path.join(base, "voice", "male"), ".ogg")
vf = ids(os.path.join(base, "voice", "female"), ".ogg")
if vm == vf and vm:
pair = f"✅ 各 {len(vm)}"
else:
pair = f"❌ male={len(vm)} female={len(vf)}"
for i in sorted(vm - vf):
problems.append(f"{g}: voice 缺 female/{i}")
for i in sorted(vf - vm):
problems.append(f"{g}: voice 缺 male/{i}")
# 键盘变体目录(P1 路线 a):两性别 key 集合一致 + key ⊆ 主集合
km = ids(os.path.join(base, "voice", "male", "key"), ".ogg")
kf = ids(os.path.join(base, "voice", "female", "key"), ".ogg")
if km == kf and km <= vm:
kpair = f"✅ 各 {len(km)} ⊆ 主集" if km else "—(无变体目录)"
else:
kpair = f"❌ male={len(km)} female={len(kf)}"
for i in sorted(km - kf):
problems.append(f"{g}: key 变体缺 female/key/{i}")
for i in sorted(kf - km):
problems.append(f"{g}: key 变体缺 male/key/{i}")
for i in sorted((km | kf) - (vm | vf)):
problems.append(f"{g}: key 变体 {i} 不在主集合(key 必须 ⊆ 主集)")
flat = ids(os.path.join(base, "voice"), ".ogg")
flat_note = f"(平铺 {len(flat)} 个角色/NPC 句,豁免)" if g in FLAT_OK and flat else \
(f"(⚠️ 非豁免游戏存在平铺 {len(flat)} 个)" if flat and g not in FLAT_OK else "")
print(f"| {g} | {len(oggs)} | {len(m4as)} | {len(zero)} | {len(bad)} | {pair}{flat_note} | {kpair} |")
print()
if problems:
print(f"## 问题清单({len(problems)} 条)\n")
for p in problems:
print(f"- {p}")
return 1
print("## 结论\n\n全部通过:双格式配对完整、无 0 字节、全部可解码、voice 性别目录 id 集合一致(storyline/rpg/radio 平铺角色句按约定豁免)、键盘变体目录两性别一致且 ⊆ 主集合。")
return 0
if __name__ == "__main__":
sys.exit(main())