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

184 lines
7.1 KiB
Python

"""Shared weights manifest + downloader.
Maps every weight file the runtime needs to its position in the
`stabilityai/stable-audio-3-optimized` HuggingFace repo.
`install.py` calls `ensure_local` upfront for the bundles the user picks.
`sa3_mlx.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.
"""
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/mlx/foo.npz") 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
# weight files (local relative path on the left, HF repo path on the right).
# T5Gemma is in SHARED because every bundle needs it.
DIT_BUNDLES: dict[str, list[tuple[str, str]]] = {
"medium": [
("models/mlx/dit_medium_f16.npz", "MLX/dit_medium_f16.npz"),
("models/mlx/same_l_decoder_f32.npz", "MLX/same_l_decoder_f32.npz"),
("models/mlx/same_l_encoder_f32.npz", "MLX/same_l_encoder_f32.npz"),
],
"sm-music": [
("models/mlx/dit_sm-music_f16.npz", "MLX/dit_sm-music_f16.npz"),
("models/mlx/same_s_decoder_f32.npz", "MLX/same_s_decoder_f32.npz"),
("models/mlx/same_s_encoder_f32.npz", "MLX/same_s_encoder_f32.npz"),
],
"sm-sfx": [
("models/mlx/dit_sm-sfx_f16.npz", "MLX/dit_sm-sfx_f16.npz"),
("models/mlx/same_s_decoder_f32.npz", "MLX/same_s_decoder_f32.npz"),
("models/mlx/same_s_encoder_f32.npz", "MLX/same_s_encoder_f32.npz"),
],
}
SHARED: list[tuple[str, str]] = [
("models/mlx/t5gemma_f16.npz", "MLX/t5gemma_f16.npz"),
]
# Base-model DiT weights — needed only for LoRA *training* (lora_train_mlx.py
# trains on the BASE checkpoint, not the ARC weights inference uses). Not part
# of any install bundle; auto-downloaded lazily by the trainer via ensure_local.
TRAINING_BASE: list[tuple[str, str]] = [
("models/mlx/dit_sm-music-base_f16.npz", "MLX/dit_sm-music-base_f16.npz"),
("models/mlx/dit_sm-sfx-base_f16.npz", "MLX/dit_sm-sfx-base_f16.npz"),
("models/mlx/dit_medium-base_f16.npz", "MLX/dit_medium-base_f16.npz"),
]
# Human-friendly bundle sizes (for the install prompt).
BUNDLE_SIZES = {
"medium": "5.9 GB (medium DiT + SAME-L codec)",
"sm-music": "1.3 GB (small music DiT + SAME-S codec)",
"sm-sfx": "1.3 GB (small sfx DiT + SAME-S codec)",
}
# Flat (local_rel_path → hf_path) lookup — used by sa3_mlx.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 _rel, _hf in TRAINING_BASE:
FLAT_MANIFEST[_rel] = _hf
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
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 weight 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)
# Symlink keeps the HF cache canonical (one copy on disk) while exposing
# the file at the project-relative path the runtime expects.
if target.is_symlink():
target.unlink()
target.symlink_to(cached)
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)