#!/usr/bin/env python3 # -*- coding: utf-8 -*- """全项目音频资产终检(R5)。 检查 11 个游戏 web//audio/(批次 3 起含 hunt;hunt 的 bot 喊话落 audio/sfx/npc_*,不在 voice 平铺豁免范围,23-hunt.md §15 裁决 7): 1. 每个 .ogg 都有同名 .m4a(反之亦然) 2. voice/male 与 voice/female 的 id 集合一致(storyline/rpg 的平铺 NPC/角色句除外) 3. 键盘变体目录 voice//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", "abyss", "detective", "hunt"] # 平铺 voice 文件(角色/NPC,单音色,不做性别集合一致性要求) FLAT_OK = {"storyline", "rpg", "radio", "abyss", "detective"} 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,批次 2 起含 abyss/detective,批次 3 起含 hunt,2026-09-13)\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/abyss/detective 平铺角色句按约定豁免)、键盘变体目录两性别一致且 ⊆ 主集合。") return 0 if __name__ == "__main__": sys.exit(main())