- 共享引擎(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 模板
158 lines
5.9 KiB
Python
158 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Internal install plumbing — do NOT invoke directly. Use ../install.sh from the project root.
|
|
|
|
Called by install.sh after it has set up uv + .venv. Responsibilities:
|
|
1. (Optionally) pip-install requirements.txt — skipped when INSTALL_SKIP_PIP=1
|
|
is set (install.sh sets this since uv already handled deps).
|
|
2. Ask which DiT bundles to download, then fetch them from HuggingFace.
|
|
3. Print example commands the user can copy-paste.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import platform
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# This file lives in <project>/scripts/. SCRIPT_DIR points at the project
|
|
# root (where models/, requirements.txt, .venv/, sa3 wrapper live).
|
|
SCRIPT_DIR = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent)) # `from weights import …`
|
|
|
|
MIN_PY = (3, 10)
|
|
SKIP_PIP = os.environ.get("INSTALL_SKIP_PIP") == "1"
|
|
|
|
|
|
def step(msg: str) -> None:
|
|
print(f"\n\033[1;36m→ {msg}\033[0m")
|
|
|
|
|
|
def check_environment() -> None:
|
|
"""Bail out early with a clear message if the interpreter is wrong."""
|
|
py = sys.version_info
|
|
if (py.major, py.minor) < MIN_PY:
|
|
print(
|
|
f"\n\033[1;31merror\033[0m: this script is running under Python "
|
|
f"{py.major}.{py.minor}, but MLX requires Python "
|
|
f"{MIN_PY[0]}.{MIN_PY[1]}+.\n\n"
|
|
f"Re-run install.py with a Python {MIN_PY[0]}.{MIN_PY[1]}+ interpreter, e.g.:\n"
|
|
f" /path/to/python3.11 install.py\n\n"
|
|
f"On macOS you can get one via:\n"
|
|
f" brew install python@3.11\n"
|
|
f" /opt/homebrew/bin/python3.11 install.py\n"
|
|
f"or with pyenv:\n"
|
|
f" pyenv install 3.11.10 && pyenv local 3.11.10 && python install.py\n",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
if platform.system() != "Darwin" or platform.machine() != "arm64":
|
|
print(
|
|
f"\n\033[1;33mwarning\033[0m: this stack is Apple-Silicon-only "
|
|
f"(MLX is Metal-backed). Detected {platform.system()}/{platform.machine()} "
|
|
f"— pip install will probably fail when it tries to fetch MLX.",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
|
|
def pip_install_requirements() -> None:
|
|
if SKIP_PIP:
|
|
step("Dependencies already installed by install.sh (uv) — skipping pip step")
|
|
return
|
|
step("Installing Python dependencies")
|
|
req = SCRIPT_DIR / "requirements.txt"
|
|
|
|
# Prefer pip; fall back to uv if pip isn't available in this interpreter
|
|
# (e.g. user is running install.py directly inside a uv-created venv
|
|
# that wasn't seeded with pip).
|
|
pip_available = subprocess.run(
|
|
[sys.executable, "-m", "pip", "--version"],
|
|
capture_output=True,
|
|
).returncode == 0
|
|
|
|
if pip_available:
|
|
cmd = [sys.executable, "-m", "pip", "install", "-r", str(req)]
|
|
elif _have("uv"):
|
|
cmd = ["uv", "pip", "install", "--python", sys.executable, "-r", str(req)]
|
|
else:
|
|
print(
|
|
f"\n\033[1;31merror\033[0m: neither pip nor uv is available in this "
|
|
f"interpreter ({sys.executable}).\n"
|
|
f"Install pip with `python -m ensurepip --upgrade`, or use ./install.sh "
|
|
f"which sets everything up via uv.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
print(f" $ {' '.join(cmd)}")
|
|
subprocess.check_call(cmd)
|
|
|
|
|
|
def _have(binary: str) -> bool:
|
|
"""True if the given binary is on PATH."""
|
|
from shutil import which
|
|
return which(binary) is not None
|
|
|
|
|
|
def main() -> None:
|
|
import argparse
|
|
ap = argparse.ArgumentParser(
|
|
description="SA3 MLX install — non-interactive bundle downloader + post-install help.",
|
|
epilog="Normally invoked via ../install.sh, not directly.",
|
|
)
|
|
ap.add_argument("--download", default="",
|
|
metavar="BUNDLES",
|
|
help="Comma-separated list of bundles to pre-download "
|
|
"(medium, sm-music, sm-sfx). Without this flag, "
|
|
"nothing is downloaded — sa3_mlx.py will fetch any "
|
|
"missing weights from HuggingFace on first use.")
|
|
cli = ap.parse_args()
|
|
|
|
check_environment()
|
|
pip_install_requirements()
|
|
|
|
# Import after pip install so a fresh checkout works.
|
|
from weights import DIT_BUNDLES, SHARED, BUNDLE_SIZES, bundle_status, ensure_local
|
|
|
|
step("Current weights status")
|
|
for name in DIT_BUNDLES:
|
|
present, total = bundle_status(name)
|
|
mark = "✓" if present == total else " "
|
|
print(f" [{mark}] {name:9s} {present}/{total} files present ({BUNDLE_SIZES[name]})")
|
|
|
|
chosen: list[str] = []
|
|
if cli.download.strip():
|
|
chosen = [b.strip() for b in cli.download.split(",") if b.strip()]
|
|
unknown = [b for b in chosen if b not in DIT_BUNDLES]
|
|
if unknown:
|
|
print(f"\n\033[1;31merror\033[0m: unknown bundle(s): {', '.join(unknown)}. "
|
|
f"Choices: {', '.join(DIT_BUNDLES)}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if chosen:
|
|
step(f"Downloading {len(chosen)} bundle(s): {', '.join(chosen)}")
|
|
seen: set[str] = set()
|
|
for name in chosen:
|
|
print(f"\n[{name}]")
|
|
items = DIT_BUNDLES[name] + SHARED
|
|
for rel, _hf in items:
|
|
if rel in seen:
|
|
continue
|
|
seen.add(rel)
|
|
ensure_local(rel)
|
|
else:
|
|
step("No --download set — weights will auto-download on first ./sa3 use")
|
|
print(f" To pre-download instead, pass: {sys.executable.split('/')[-1]} install.py --download medium")
|
|
print(f" or: ./install.sh --download medium,sm-music")
|
|
|
|
from examples import print_example_commands, BOLD, GREEN, RESET
|
|
print_example_commands(header=f"{BOLD}{GREEN}✓ Install complete.{RESET} Try these commands:")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt:
|
|
print("\nAborted.")
|
|
sys.exit(130)
|