- 设计:docs/design/23-hunt.md + voice-ids(84 id,166条)+ 13号断言登记,§15记录10+1条设计裁决 - 服务器:零依赖WebSocket(ws.js)+ 房间管理/服务端权威战斗/bot AI(hunt-room.js),断线30s宽限resume回槽、超时转bot;修复package.json模块类型误判的既有破坏 - 客户端:web/hunt/ 全套(教学T0-T4/听觉视野裁剪R1-R8/结算排行榜/guide接入),playtest 211断言 - 引擎:audio.js 后向兼容 opts.pan(StereoPannerNode),web-engine-api.md 同步 - 资产:166条TTS语音(QC无离群)+ 13件合成sfx(真峰值≤-3dBFS)+ 2首Mureka BGM,终检全绿 - 平衡:bot调参至挂机局71%失守/C、3bot局71% A/B(hunt-balance-scan.mjs 24种子扫描) - 接线:大厅卡片、profiles.js hunt桶、README/roadmap MMO路线条目 验证:hunt-sim 16/16 + api-smoke 12/12 + hunt playtest 211/211 + 既有10游戏与引擎回归零回退 + 真服务器WS端到端冒烟通过
109 lines
5.1 KiB
Python
109 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""全项目音频资产终检(R5)。
|
||
|
||
检查 11 个游戏 web/<game>/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/<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",
|
||
"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())
|