初始提交: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 模板
This commit is contained in:
+219
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Interactive build menu for the SA3 TRT engines.
|
||||
|
||||
Detects the current GPU's architecture, lists which engines are already built
|
||||
under ../models/<arch>/ and which are still missing, then loops asking you what
|
||||
to build next. There's also a "Build all missing" option at the bottom.
|
||||
|
||||
Usage:
|
||||
python build.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Make _arch.py importable when build.py is invoked from any cwd.
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
|
||||
from _arch import detect_arch, arch_dir as _arch_dir, repo_root # noqa: E402
|
||||
|
||||
|
||||
# ─── Colors (auto-detect TTY) ───────────────────────────────────────────────
|
||||
if sys.stdout.isatty():
|
||||
BOLD, DIM = "\033[1m", "\033[2m"
|
||||
RED, GREEN = "\033[31m", "\033[32m"
|
||||
YELLOW, CYAN = "\033[33m", "\033[36m"
|
||||
MAGENTA = "\033[35m"
|
||||
RESET = "\033[0m"
|
||||
else:
|
||||
BOLD = DIM = RED = GREEN = YELLOW = CYAN = MAGENTA = RESET = ""
|
||||
|
||||
|
||||
def fmt_size(n_bytes: int) -> str:
|
||||
units = ["B", "KB", "MB", "GB"]
|
||||
s = float(n_bytes)
|
||||
for u in units:
|
||||
if s < 1024 or u == "GB":
|
||||
return f"{s:.0f} {u}" if u in ("B", "KB") else f"{s:.1f} {u}"
|
||||
s /= 1024
|
||||
return f"{s:.1f} GB"
|
||||
|
||||
|
||||
# Each menu item dispatches `build_from_onnx.py <name>` which pulls the canonical
|
||||
# ONNX from HuggingFace and compiles it for the local GPU arch. `outputs` are
|
||||
# paths relative to ../models/<arch>/ — used to mark a target as built.
|
||||
def _from_onnx(name):
|
||||
return [sys.executable, "build_from_onnx.py", name]
|
||||
|
||||
|
||||
TARGETS = [
|
||||
{"label": "t5gemma (text encoder + tokenizer)",
|
||||
"command": _from_onnx("t5gemma"),
|
||||
"outputs": ["t5gemma/t5gemma_fp16.trt", "t5gemma/tokenizer.json"]},
|
||||
# The autoencoders build through build_autoencoders.py: they need two optimization profiles
|
||||
# and (decoders) a grafted limiter, which build_from_onnx.py's single-profile path cannot
|
||||
# express. One command does all eight and verifies them.
|
||||
{"label": "SAME-L + SAME-S autoencoders (8 engines: enc/dec x canonical/fp8)",
|
||||
"command": [sys.executable, str(SCRIPTS_DIR / "build_autoencoders.py")],
|
||||
"outputs": ["same-l/enc_fp16_chunkable.trt", "same-l/dec_fp16_chunkable_limiter.trt",
|
||||
"same-l/enc_fp8_chunkable.trt", "same-l/dec_fp8_chunkable_limiter.trt",
|
||||
"same-s/enc_bf16_chunkable.trt", "same-s/dec_bf16_chunkable_limiter.trt",
|
||||
"same-s/enc_fp8_chunkable.trt", "same-s/dec_fp8_chunkable_limiter.trt"]},
|
||||
# DiT engines now build from pre-processed FP16-mixed ONNX on HF;
|
||||
# build_from_onnx.py does the simple STRONGLY_TYPED compile.
|
||||
# Medium ships TWO engines: fp16-mixed (the DEFAULT since 2026-07 — its
|
||||
# attention core now fuses, see below) and bf16 (built from the raw fp32
|
||||
# dit.onnx with BuilderFlag.BF16). sm-music/sm-sfx are fp16-mixed only.
|
||||
#
|
||||
# fp16-mixed became the medium default when the RoPE island was bounded so
|
||||
# QK^T + Softmax run FP16: the engine went 0 -> 96 fused MHA nodes and 4.3x
|
||||
# faster at L=4096, which removed the speed reason to prefer bf16. It is also
|
||||
# the accurate one — teacher-forced velocity cos 1.0000 vs the FP32 engine at
|
||||
# every length, where bf16 evaluates RoPE's rotation angle too coarsely and
|
||||
# drifts at long sequence (the angle reaches ~4155 rad at L=4092, where bf16's
|
||||
# spacing is 32 rad — more than a full 2*pi rotation).
|
||||
{"label": "DiT medium (SA3-M, FP16-mixed — medium DEFAULT, attention-fused)",
|
||||
"command": _from_onnx("sa3-m"),
|
||||
"outputs": ["sa3-m/dit_fp16.trt"]},
|
||||
{"label": "DiT medium (SA3-M, bf16 — selectable; drifts at long sequence)",
|
||||
"command": _from_onnx("sa3-m-bf16"),
|
||||
"outputs": ["sa3-m/dit_bf16.trt"]},
|
||||
{"label": "DiT medium (SA3-M, fp8 — selectable; MAX-SPEED clean tier, ~1.3x over fp16)",
|
||||
"command": _from_onnx("sa3-m-fp8"),
|
||||
"outputs": ["sa3-m/dit_fp8.trt"]},
|
||||
{"label": "DiT sm-music (FP16-mixed)",
|
||||
"command": _from_onnx("sa3-sm-music"),
|
||||
"outputs": ["sa3-sm-music/dit_fp16.trt"]},
|
||||
{"label": "DiT sm-sfx (FP16-mixed)",
|
||||
"command": _from_onnx("sa3-sm-sfx"),
|
||||
"outputs": ["sa3-sm-sfx/dit_fp16.trt"]},
|
||||
# FP32 variants — opt-in. ~2× engine size, ~2× slower, but bit-equivalent
|
||||
# to PyTorch eager. Useful for precision-debug or reference comparisons.
|
||||
{"label": "[opt-in] same-l decoder FP32",
|
||||
"command": _from_onnx("same-l-decoder-fp32"),
|
||||
"outputs": ["same-l/dec_dynamic_fp32.trt"]},
|
||||
{"label": "[opt-in] same-s decoder FP32",
|
||||
"command": _from_onnx("same-s-decoder-fp32"),
|
||||
"outputs": ["same-s/dec_dynamic_fp32.trt"]},
|
||||
{"label": "[opt-in] DiT medium FP32",
|
||||
"command": _from_onnx("sa3-m-fp32"),
|
||||
"outputs": ["sa3-m/dit_fp32.trt"]},
|
||||
{"label": "[opt-in] DiT sm-music FP32",
|
||||
"command": _from_onnx("sa3-sm-music-fp32"),
|
||||
"outputs": ["sa3-sm-music/dit_fp32.trt"]},
|
||||
{"label": "[opt-in] DiT sm-sfx FP32",
|
||||
"command": _from_onnx("sa3-sm-sfx-fp32"),
|
||||
"outputs": ["sa3-sm-sfx/dit_fp32.trt"]},
|
||||
]
|
||||
|
||||
|
||||
def target_status(target: dict, arch_dir: Path) -> tuple[bool, list[tuple[str, int]]]:
|
||||
"""Return (all_built, [(rel_path, size_bytes_or_-1), ...])."""
|
||||
rows = []
|
||||
all_built = True
|
||||
for rel in target["outputs"]:
|
||||
p = arch_dir / rel
|
||||
if p.exists() and p.stat().st_size > 0:
|
||||
rows.append((rel, p.stat().st_size))
|
||||
else:
|
||||
rows.append((rel, -1))
|
||||
all_built = False
|
||||
return all_built, rows
|
||||
|
||||
|
||||
def render_menu(arch: str, arch_dir: Path) -> list[bool]:
|
||||
"""Print the menu. Returns a list parallel to TARGETS of bool 'is_built'."""
|
||||
print()
|
||||
print(f" {BOLD}GPU arch:{RESET} {CYAN}{arch}{RESET}")
|
||||
try:
|
||||
rel_dir = arch_dir.relative_to(repo_root())
|
||||
rel_dir_str = str(rel_dir)
|
||||
except ValueError:
|
||||
rel_dir_str = str(arch_dir)
|
||||
print(f" {BOLD}Output dir:{RESET} {DIM}{rel_dir_str}/{RESET}")
|
||||
print()
|
||||
|
||||
label_w = max(len(t["label"]) for t in TARGETS)
|
||||
built_flags = []
|
||||
for i, t in enumerate(TARGETS, start=1):
|
||||
all_built, rows = target_status(t, arch_dir)
|
||||
built_flags.append(all_built)
|
||||
head_mark = f"{GREEN}✓{RESET}" if all_built else f"{RED}✗{RESET}"
|
||||
print(f" {BOLD}[{i}]{RESET} {head_mark} {t['label']:<{label_w}}")
|
||||
for rel, sz in rows:
|
||||
tick = f"{GREEN}✓{RESET}" if sz >= 0 else f"{RED}✗{RESET}"
|
||||
size_s = fmt_size(sz) if sz >= 0 else f"{DIM}(missing){RESET}"
|
||||
print(f" {tick} {DIM}{rel}{RESET} {size_s}")
|
||||
|
||||
n_missing = built_flags.count(False)
|
||||
print()
|
||||
if n_missing == 0:
|
||||
print(f" {BOLD}{GREEN}[A]{RESET} Build all missing {DIM}(nothing missing — all engines built){RESET}")
|
||||
else:
|
||||
print(f" {BOLD}{YELLOW}[A]{RESET} Build all missing {DIM}({n_missing} target(s)){RESET}")
|
||||
print(f" {BOLD}{DIM}[Q]{RESET} Quit")
|
||||
return built_flags
|
||||
|
||||
|
||||
def run_build(target: dict) -> bool:
|
||||
"""Invoke a build script in build/. Streams output. Returns True on success."""
|
||||
print()
|
||||
print(f" {CYAN}→{RESET} {BOLD}{target['label']}{RESET}")
|
||||
print(f" {DIM}cmd: {' '.join(target['command'])}{RESET}")
|
||||
print()
|
||||
# Always run from build/ so relative imports + ../models/ resolve correctly.
|
||||
rc = subprocess.call(target["command"], cwd=str(SCRIPTS_DIR))
|
||||
if rc == 0:
|
||||
print()
|
||||
print(f" {GREEN}✓ built {target['label']}{RESET}")
|
||||
return True
|
||||
print()
|
||||
print(f" {RED}✗ build failed (exit {rc}): {target['label']}{RESET}")
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
arch = detect_arch()
|
||||
arch_dir = Path(_arch_dir(arch))
|
||||
print()
|
||||
print(f"{BOLD}{CYAN}━━━ SA3 TRT engine build menu ━━━{RESET}")
|
||||
|
||||
while True:
|
||||
built_flags = render_menu(arch, arch_dir)
|
||||
print()
|
||||
try:
|
||||
choice = input(f" Choose [1-{len(TARGETS)} / A / Q]: ").strip().lower()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print()
|
||||
return 0
|
||||
|
||||
if choice in ("q", "quit", "exit", ""):
|
||||
return 0
|
||||
|
||||
if choice in ("a", "all"):
|
||||
missing = [t for t, ok in zip(TARGETS, built_flags) if not ok]
|
||||
if not missing:
|
||||
print(f" {DIM}Nothing to build.{RESET}")
|
||||
continue
|
||||
print()
|
||||
print(f" {BOLD}Building {len(missing)} missing target(s) in order...{RESET}")
|
||||
for t in missing:
|
||||
if not run_build(t):
|
||||
print(f" {YELLOW}Stopping due to error. Re-run to retry.{RESET}")
|
||||
break
|
||||
continue
|
||||
|
||||
if choice.isdigit():
|
||||
idx = int(choice)
|
||||
if 1 <= idx <= len(TARGETS):
|
||||
run_build(TARGETS[idx - 1])
|
||||
continue
|
||||
|
||||
print(f" {RED}invalid choice: {choice}{RESET}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user