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

246 lines
10 KiB
Python

"""Shared weights manifest + downloader.
Maps every TFLite / LiteRT model file the runtime needs to its position in the
`stabilityai/stable-audio-3-optimized` HuggingFace repo (under `tflite/…`).
`install.py` calls `ensure_local` upfront for the bundles the user picks.
`sa3_tflite.py` calls `ensure_local` lazily, just before each model loads — so
a fresh checkout with no weights still works if the user is willing to wait for
the first run to download them.
The SentencePiece tokenizer is BUNDLED at models/tokenizer.model (the .tflite
T5Gemma is encoder-only), so it's not in this manifest.
"""
from __future__ import annotations
from pathlib import Path
REPO_ID = "stabilityai/stable-audio-3-optimized"
# weights.py lives in <project>/scripts/; SCRIPT_DIR points at the project
# root so the local rel paths in the manifest ("models/tflite/foo.tflite")
# resolve against the actual project layout.
SCRIPT_DIR = Path(__file__).resolve().parent.parent
# Bundles the install script offers to the user. Each maps to a list of
# model files (local relative path on the left, HF repo path on the right).
# T5Gemma is in SHARED because every bundle needs it. The two small DiTs
# share the SAME-S codec; medium uses the SAME-L codec.
DIT_BUNDLES: dict[str, list[tuple[str, str]]] = {
"sm-music": [
("models/tflite/sa3-sm-music/dit_fp32.tflite", "tflite/sa3-sm-music/dit_fp32.tflite"),
("models/tflite/same-s/enc_fp32.tflite", "tflite/same-s/enc_fp32.tflite"),
("models/tflite/same-s/dec_fp32.tflite", "tflite/same-s/dec_fp32.tflite"),
],
"sm-sfx": [
("models/tflite/sa3-sm-sfx/dit_fp32.tflite", "tflite/sa3-sm-sfx/dit_fp32.tflite"),
("models/tflite/same-s/enc_fp32.tflite", "tflite/same-s/enc_fp32.tflite"),
("models/tflite/same-s/dec_fp32.tflite", "tflite/same-s/dec_fp32.tflite"),
],
"medium": [
("models/tflite/sa3-m/dit_fp32.tflite", "tflite/sa3-m/dit_fp32.tflite"),
("models/tflite/same-l/enc_fp32.tflite", "tflite/same-l/enc_fp32.tflite"),
("models/tflite/same-l/dec_fp32.tflite", "tflite/same-l/dec_fp32.tflite"),
],
}
SHARED: list[tuple[str, str]] = [
("models/tflite/t5gemma/encoder_fp16.tflite", "tflite/t5gemma/encoder_fp16.tflite"),
]
# Human-friendly bundle sizes (for the install prompt). Exact, from HF metadata.
BUNDLE_SIZES = {
"sm-music": "2.3 GB (small music DiT + SAME-S codec, all fp32)",
"sm-sfx": "2.3 GB (small sfx DiT + SAME-S codec, all fp32)",
"medium": "9.5 GB (medium DiT + SAME-L codec, all fp32)",
}
# T5Gemma (shared, fp16) adds ~0.6 GB the first time any bundle is fetched.
# Quantized DiT + decoder variants (selected via sa3_tflite.py --precision; wXaY =
# weight/activation bit-widths, "16" = fp16). Not part of the install bundles — they
# lazy-download on first use. w16a32 = fp16 weights (half size, ≈lossless, slower on
# CPU); w8a32 / w8a8-dyn = GPTQ-calibrated int8 weights.
QUANT_PRECISIONS = ("w16a32", "w8a32", "w8a8-dyn")
PRECISIONS = ("fp32",) + QUANT_PRECISIONS # everything --precision accepts
DIT_SUBDIR = {"sm-music": "sa3-sm-music", "sm-sfx": "sa3-sm-sfx", "medium": "sa3-m"}
def dit_rel(dit: str, precision: str = "fp32") -> str:
"""Local rel path of a DiT model file for (family, precision)."""
return f"models/tflite/{DIT_SUBDIR[dit]}/dit_{precision}.tflite"
def dec_rel(dec: str, precision: str = "fp32") -> str:
"""Local rel path of a SAME codec decoder file for (codec, precision)."""
return f"models/tflite/{dec}/dec_{precision}.tflite"
def enc_rel(dec: str, precision: str = "fp32") -> str:
"""Local rel path of a SAME codec encoder file for (codec, precision).
Encoder int8 is GPTQ-calibrated on real audio (torch-free, in-place on the
tflite graph) — w8a32 measures 36/46 dB latent PSNR (same-s/same-l)."""
return f"models/tflite/{dec}/enc_{precision}.tflite"
# Flat (local_rel_path → hf_path) lookup — used by sa3_tflite.py for lazy
# auto-download at load time.
FLAT_MANIFEST: dict[str, str] = {}
for _items in DIT_BUNDLES.values():
for _rel, _hf in _items:
FLAT_MANIFEST[_rel] = _hf
for _rel, _hf in SHARED:
FLAT_MANIFEST[_rel] = _hf
for _prec in QUANT_PRECISIONS:
for _fam in DIT_SUBDIR:
_rel = dit_rel(_fam, _prec)
FLAT_MANIFEST[_rel] = _rel.replace("models/tflite/", "tflite/", 1)
for _dec in ("same-s", "same-l"):
for _rel in (dec_rel(_dec, _prec), enc_rel(_dec, _prec)):
FLAT_MANIFEST[_rel] = _rel.replace("models/tflite/", "tflite/", 1)
def _hf_token_configured() -> bool:
"""True if any HF token is set — env var or cached login on disk."""
import os
if os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN"):
return True
try:
from huggingface_hub import get_token # huggingface_hub ≥ 0.19
return bool(get_token())
except ImportError:
try:
from huggingface_hub import HfFolder
return bool(HfFolder.get_token())
except Exception:
return False
except Exception:
return False
_LOGIN_TIP_SHOWN = False
_LINK_FALLBACK_SHOWN = False
def _link_or_copy(cached: Path, target: Path) -> None:
"""Expose the HF-cached file at `target`: symlink → hardlink → copy.
A symlink keeps the HF cache canonical (one copy on disk) and is used
wherever possible. On Windows, creating symlinks requires Developer Mode
or admin rights (WinError 1314), so fall back to a hardlink (privilege-
free and zero-copy, but same-volume only), then to a plain copy (works
everywhere, at the cost of a second copy on disk). Unlike a symlink, a
hardlink/copy won't track future HF-cache updates — acceptable, because
downloads are content-addressed: a changed file resolves to a new cache
path, and `ensure_local` only ever links each target once.
"""
global _LINK_FALLBACK_SHOWN
try:
target.symlink_to(cached)
return
except OSError:
pass
import os
import shutil
try:
os.link(cached, target) # hardlink: no privilege needed, same volume only
mode = "hardlink"
except OSError:
shutil.copy2(cached, target)
mode = "copy"
if not _LINK_FALLBACK_SHOWN:
_LINK_FALLBACK_SHOWN = True
print(f" (symlinks unavailable on this system — materialized as a {mode} instead)")
def _show_hf_login_tip_once() -> None:
"""Print a one-time login suggestion if no HF token is configured.
Anonymous downloads work but have a ~50 GB/day soft cap on HF's LFS CDN
and lower aggregate bandwidth — a free token effectively removes both.
Stays silent if a token is already in place.
"""
global _LOGIN_TIP_SHOWN
if _LOGIN_TIP_SHOWN:
return
_LOGIN_TIP_SHOWN = True
if _hf_token_configured():
return
import sys
YEL = "\033[1;33m" if sys.stdout.isatty() else ""
BOLD = "\033[1m" if sys.stdout.isatty() else ""
DIM = "\033[2m" if sys.stdout.isatty() else ""
RST = "\033[0m" if sys.stdout.isatty() else ""
print()
print(f" {YEL}⚠ not logged in to HuggingFace{RST} — anonymous downloads work but are")
print(f" rate-limited (~50 GB/day cap on the LFS CDN). For faster, higher-limit")
print(f" downloads, log in once with a free read-only token:")
print()
print(f" 1. create an account at {BOLD}https://huggingface.co/join{RST}")
print(f" 2. generate a token at {BOLD}https://huggingface.co/settings/tokens{RST}")
print(f" {DIM}('Read' scope is enough){RST}")
print(f" 3. save it on this machine — pick one:")
print(f" {BOLD}hf auth login{RST} {DIM}# modern (huggingface_hub ≥ 1.0){RST}")
print(f" {BOLD}huggingface-cli login{RST} {DIM}# classic; still works{RST}")
print(f" {BOLD}export HF_TOKEN=hf_xxx{RST} {DIM}# one-off / scripts{RST}")
print()
def ensure_local(local_rel_path: str, verbose: bool = True) -> Path:
"""Resolve a model file to an absolute local path, downloading if missing.
Files are streamed into the HuggingFace cache (~/.cache/huggingface/hub/)
and symlinked into the project at `local_rel_path` so the on-disk layout
looks the same whether the file was bundled or downloaded.
"""
target = SCRIPT_DIR / local_rel_path
if target.exists() or target.is_symlink():
return target
if local_rel_path not in FLAT_MANIFEST:
raise FileNotFoundError(
f"{local_rel_path} is not in the weights manifest — can't auto-download."
)
# First-download tip: nudge users toward logging in to HF for better limits.
# No-op if a token is already configured.
_show_hf_login_tip_once()
hf_filename = FLAT_MANIFEST[local_rel_path]
if verbose:
print(f" ↓ downloading {hf_filename} (from {REPO_ID})")
try:
from huggingface_hub import hf_hub_download
except ImportError as e:
raise RuntimeError(
"huggingface_hub is required to auto-download weights.\n"
"Run: pip install huggingface_hub\n"
"Or run the install.py script in this directory."
) from e
cached = hf_hub_download(repo_id=REPO_ID, filename=hf_filename)
target.parent.mkdir(parents=True, exist_ok=True)
# Expose the file at the project-relative path the runtime expects.
# Symlink when possible (keeps the HF cache canonical — one copy on disk);
# falls back to hardlink → copy on systems without symlink privileges
# (Windows without Developer Mode). The `exists() or is_symlink()`
# early-exits above and in is_present() are satisfied by every mode.
if target.is_symlink():
target.unlink()
_link_or_copy(Path(cached), target)
return target
def is_present(local_rel_path: str) -> bool:
"""True if the file exists locally (does not trigger a download)."""
p = SCRIPT_DIR / local_rel_path
return p.exists() or p.is_symlink()
def bundle_status(bundle: str) -> tuple[int, int]:
"""Returns (present_count, total_count) for the bundle (including SHARED)."""
items = DIT_BUNDLES[bundle] + SHARED
present = sum(1 for rel, _ in items if is_present(rel))
return present, len(items)