Files
SoundGame Dev f0f0a9b50e 初始提交:3 款纯音频游戏 + 共享引擎 + 完整文档与资产管线
- 共享引擎(app/src/main):GestureEngine 6 手势识别、AudioEngine 三层音频
  (ExoPlayer BGM 交叉淡入 / 对白 ducking / SoundPool 低延迟)、Game/GameEngine
  状态机接口、MainActivity 全屏封面 + TalkBack
- 《声命线》storyline:末世互动广播剧,3 幕 8 抉择点 4 结局
- 《声东击西》reflex:听声辨位反应,12 波战斗 + BGM 三档 BPM 变速
- 《暗室》escape:3x3 网格密室逃脱,钥匙→铁柜→保险丝→电箱→大门谜题链
- 文档:setup(从零搭建)/ architecture(架构)/ engine-api(引擎 API)/
  asset-pipeline(资产流水线)/ games/*(三款游戏设计文档)
- 资产管线:Qwen3-TTS 对白 + 程序合成音效/BGM + Mureka BGM 的 gen 脚本与台账,
  全部 ogg 资产入库可复现
- env.d 环境脚本:android/tts 入库;sfx/mureka 含密钥不入库,仅提交 .example.sh 模板
2026-08-29 13:00:34 +08:00

222 lines
9.7 KiB
Python
Raw Permalink 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.
"""Sanity-check every shipped npz and every CLI config of sa3_mlx.py.
Run from inside the sa3_mlx/ folder:
python test_all_configs.py
"""
from __future__ import annotations
import os, subprocess, sys, time, wave, tempfile
from pathlib import Path
import numpy as np
REPO = Path(__file__).resolve().parent.parent # project root (scripts/ is one level down)
PY = sys.executable
# ─── expected weight files ────────────────────────────────────────────────
EXPECTED_NPZ = {
"models/mlx/dit_sm-music_f16.npz": {"min_keys": 400, "must_have_prefix": "cond."},
"models/mlx/dit_sm-sfx_f16.npz": {"min_keys": 400, "must_have_prefix": "cond."},
"models/mlx/dit_medium_f16.npz": {"min_keys": 500, "must_have_prefix": "cond."},
"models/mlx/same_s_encoder_f32.npz": {"min_keys": 100},
"models/mlx/same_s_decoder_f32.npz": {"min_keys": 100},
"models/mlx/same_l_encoder_f32.npz": {"min_keys": 200},
"models/mlx/same_l_decoder_f32.npz": {"min_keys": 200},
"models/mlx/t5gemma_f16.npz": {"min_keys": 130, "must_have_key": "TOKENIZER_MODEL"},
}
# ─── helpers ──────────────────────────────────────────────────────────────
def check_npz(path: Path, spec: dict) -> tuple[bool, str]:
"""Open the npz and verify it has the expected shape/keys."""
try:
z = np.load(path, allow_pickle=True)
except Exception as e:
return False, f"load failed: {e}"
keys = list(z.files)
if len(keys) < spec["min_keys"]:
return False, f"only {len(keys)} keys (expected ≥ {spec['min_keys']})"
if "must_have_prefix" in spec:
prefix = spec["must_have_prefix"]
if not any(k.startswith(prefix) for k in keys):
return False, f"no keys with prefix {prefix!r}"
if "must_have_key" in spec:
if spec["must_have_key"] not in keys:
return False, f"missing key {spec['must_have_key']!r}"
# Sanity: actually read one tensor without error
try:
for k in keys[:3]:
_ = np.asarray(z[k])
except Exception as e:
return False, f"tensor read failed: {e}"
return True, f"{len(keys)} keys"
def read_wav(path: str) -> tuple[np.ndarray, int]:
with wave.open(path, "rb") as w:
nch = w.getnchannels()
sr = w.getframerate()
n = w.getnframes()
pcm = np.frombuffer(w.readframes(n), dtype=np.int16).reshape(-1, nch).T.astype(np.float32) / 32767.0
return pcm, sr
def run_cli(name: str, extra_args: list[str], expected_seconds: float) -> tuple[bool, str]:
"""Run sa3_mlx.py with the given args; sanity-check the output WAV."""
out = Path(tempfile.gettempdir()) / f"sa3_test_{name}.wav"
cmd = [PY, str(REPO / "scripts" / "sa3_mlx.py"),
"--seed", "42",
"--out", str(out)] + extra_args
if "--steps" not in extra_args: # default to 4 steps for speed
cmd += ["--steps", "4"]
t0 = time.time()
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=180,
cwd=str(REPO))
except subprocess.TimeoutExpired:
return False, f"TIMEOUT after 180s"
dt = time.time() - t0
if proc.returncode != 0:
last_err = proc.stderr.strip().split("\n")[-1] if proc.stderr.strip() else "(no stderr)"
return False, f"exit {proc.returncode}: {last_err[:120]}"
if not out.exists():
return False, "no output WAV produced"
pcm, sr = read_wav(str(out))
# Quality gates: correct duration, non-NaN, not all-silent.
actual_secs = pcm.shape[-1] / sr
if abs(actual_secs - expected_seconds) > 0.1:
return False, f"duration {actual_secs:.2f}s ≠ requested {expected_seconds:.2f}s"
if not np.isfinite(pcm).all():
return False, "audio contains NaN/Inf"
peak = float(np.abs(pcm).max())
rms = float(np.sqrt((pcm ** 2).mean()))
if peak < 0.005 or rms < 0.0005:
return False, f"effectively silent (peak={peak:.4f} rms={rms:.4f})"
out.unlink(missing_ok=True)
return True, f"{dt:.1f}s peak={peak:.3f} rms={rms:.3f}"
# ─── tests ────────────────────────────────────────────────────────────────
def test_weights():
print("\n[ phase 1 ] verify every shipped npz loads cleanly\n")
fails = []
for relpath, spec in EXPECTED_NPZ.items():
full = REPO / relpath
if not full.exists():
print(f" ✗ {relpath:42s} FILE MISSING")
fails.append(relpath)
continue
ok, info = check_npz(full, spec)
mark = "✓" if ok else "✗"
print(f" {mark} {relpath:42s} {info}")
if not ok:
fails.append(relpath)
return fails
def test_configs():
print("\n[ phase 2 ] end-to-end CLI configurations (steps=4, 3s clips)\n")
SECONDS = "3"
EXPECTED_SEC = 3.0
# Will use this as init-audio for audio-to-audio + inpaint tests.
# Generate one short clip with sm-music first.
init_wav = Path(tempfile.gettempdir()) / "sa3_test_init.wav"
print(" [setup] generating a small init audio for a2a/inpaint tests …")
subprocess.run([PY, str(REPO / "scripts" / "sa3_mlx.py"),
"--prompt", "drums",
"--dit", "sm-music", "--decoder", "same-s",
"--seconds", SECONDS, "--seed", "1", "--steps", "4",
"--out", str(init_wav)],
capture_output=True, check=True, cwd=str(REPO))
print(f" [setup] init audio at {init_wav}\n")
matrix = [
# text-to-audio: each DiT × natural decoder
("t2a-music", ["--prompt", "lofi house", "--dit", "sm-music", "--decoder", "same-s", "--seconds", SECONDS]),
("t2a-sfx", ["--prompt", "rain on roof", "--dit", "sm-sfx", "--decoder", "same-s", "--seconds", SECONDS]),
("t2a-medium", ["--prompt", "piano solo", "--dit", "medium", "--decoder", "same-l", "--seconds", SECONDS]),
# cross-pairings (DiT/decoder mismatch — should still produce audio, may sound odd)
("cross-music-L", ["--prompt", "lofi", "--dit", "sm-music", "--decoder", "same-l", "--seconds", SECONDS]),
("cross-medium-S", ["--prompt", "piano", "--dit", "medium", "--decoder", "same-s", "--seconds", SECONDS]),
# mode: audio-to-audio
("a2a-music", ["--prompt", "jazz fusion", "--dit", "sm-music", "--decoder", "same-s", "--seconds", SECONDS,
"--init-audio", str(init_wav), "--init-noise-level", "0.6"]),
("a2a-medium", ["--prompt", "ambient", "--dit", "medium", "--decoder", "same-l", "--seconds", SECONDS,
"--init-audio", str(init_wav), "--init-noise-level", "0.6"]),
# mode: inpainting
("inpaint-music", ["--prompt", "guitar solo", "--dit", "sm-music", "--decoder", "same-s", "--seconds", SECONDS,
"--init-audio", str(init_wav), "--inpaint-range", "1,2"]),
# CFG variants
("cfg3-music", ["--prompt", "techno beat", "--dit", "sm-music", "--decoder", "same-s", "--seconds", SECONDS,
"--cfg", "3.0"]),
("cfg3-neg", ["--prompt", "techno beat", "--dit", "sm-music", "--decoder", "same-s", "--seconds", SECONDS,
"--cfg", "3.0", "--negative-prompt", "vocals, drums"]),
("cfg0.5", ["--prompt", "techno", "--dit", "sm-music", "--decoder", "same-s", "--seconds", SECONDS,
"--cfg", "0.5"]),
("cfg-apg0-vanilla",["--prompt", "techno", "--dit", "sm-music", "--decoder", "same-s", "--seconds", SECONDS,
"--cfg", "3.0", "--apg", "0.0"]),
# dtype: fp32 DiT
("fp32-music", ["--prompt", "lofi", "--dit", "sm-music", "--decoder", "same-s", "--seconds", SECONDS,
"--dit-dtype", "fp32"]),
# different step counts
("steps1", ["--prompt", "lofi", "--dit", "sm-music", "--decoder", "same-s", "--seconds", SECONDS,
"--steps", "1"]),
("steps16", ["--prompt", "lofi", "--dit", "sm-music", "--decoder", "same-s", "--seconds", SECONDS,
"--steps", "16"]),
# --no-free-models
("no-free", ["--prompt", "lofi", "--dit", "sm-music", "--decoder", "same-s", "--seconds", SECONDS,
"--no-free-models"]),
# empty prompt (unconditional)
("empty-prompt", ["--prompt", "", "--dit", "sm-music", "--decoder", "same-s", "--seconds", SECONDS]),
]
fails = []
for name, args in matrix:
ok, info = run_cli(name, args, EXPECTED_SEC)
mark = "✓" if ok else "✗"
print(f" {mark} {name:24s} {info}")
if not ok:
fails.append((name, info))
init_wav.unlink(missing_ok=True)
return fails
def main():
print("=" * 68)
print("sa3_mlx full-stack self-test")
print("=" * 68)
npz_fails = test_weights()
config_fails = test_configs()
print("\n" + "=" * 68)
print("SUMMARY")
print("=" * 68)
if not npz_fails and not config_fails:
print("✓ ALL PASS")
return 0
print(f"npz failures : {len(npz_fails)}")
for f in npz_fails: print(f" - {f}")
print(f"config failures: {len(config_fails)}")
for name, info in config_fails: print(f" - {name}: {info}")
return 1
if __name__ == "__main__":
sys.exit(main())