初始提交: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:
@@ -0,0 +1,187 @@
|
||||
"""Cross-platform (Windows/macOS/Linux) compatibility tests for the TFLite CLI.
|
||||
|
||||
Run directly — stdlib unittest only, no pytest, NO model downloads, no network:
|
||||
|
||||
python scripts/test_windows_compat.py
|
||||
|
||||
Covers the three Windows-specific failure modes this release guards against:
|
||||
1. weights.ensure_local materializes model files without symlink privileges
|
||||
(symlink -> hardlink -> copy fallback chain);
|
||||
2. the CLI's banners/emoji print cleanly under a legacy console code page
|
||||
(the guarded sys.stdout.reconfigure(encoding="utf-8") at script top);
|
||||
3. --play dispatches to the right playback backend per platform
|
||||
(afplay / winsound / aplay) without executing anything.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
# Keep this test's own output safe under legacy Windows console code pages
|
||||
# (the subprocess tests below spawn FRESH interpreters, so this does not mask them).
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent # <project>/scripts
|
||||
PROJECT_DIR = SCRIPTS_DIR.parent # <project>
|
||||
sys.path.insert(0, str(SCRIPTS_DIR)) # import weights / sa3_tflite / examples
|
||||
|
||||
import weights # noqa: E402
|
||||
|
||||
|
||||
class TestEnsureLocalLinkOrCopy(unittest.TestCase):
|
||||
"""ensure_local must materialize the target on every OS, with or without
|
||||
symlink privileges (Windows without Developer Mode raises WinError 1314)."""
|
||||
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="sa3_citest_")
|
||||
self.addCleanup(self._tmp.cleanup)
|
||||
self.tmp = Path(self._tmp.name)
|
||||
self.fake_cache = self.tmp / "hf_cache"
|
||||
self.fake_cache.mkdir()
|
||||
# Redirect the project root so nothing is written into the repo checkout.
|
||||
self._orig_script_dir = weights.SCRIPT_DIR
|
||||
weights.SCRIPT_DIR = self.tmp / "project"
|
||||
self.addCleanup(lambda: setattr(weights, "SCRIPT_DIR", self._orig_script_dir))
|
||||
# Register a fake manifest entry + suppress the (non-ASCII) HF login tip
|
||||
# so no in-process print depends on console encoding.
|
||||
self.rel = "models/tflite/_citest/fake_model.bin"
|
||||
weights.FLAT_MANIFEST[self.rel] = "tflite/_citest/fake_model.bin"
|
||||
self.addCleanup(weights.FLAT_MANIFEST.pop, self.rel, None)
|
||||
self._orig_tip = weights._LOGIN_TIP_SHOWN
|
||||
weights._LOGIN_TIP_SHOWN = True
|
||||
self.addCleanup(lambda: setattr(weights, "_LOGIN_TIP_SHOWN", self._orig_tip))
|
||||
self.payload = b"not a real tflite model \x00\x01\x02" * 100
|
||||
|
||||
def _fake_hf_hub_download(self, repo_id, filename):
|
||||
self.assertEqual(repo_id, weights.REPO_ID)
|
||||
cached = self.fake_cache / filename.replace("/", "__")
|
||||
cached.write_bytes(self.payload)
|
||||
return str(cached)
|
||||
|
||||
def test_ensure_local_materializes_target(self):
|
||||
"""End-to-end: monkeypatched download -> target exists and is readable.
|
||||
Uses whatever link mode the OS grants (symlink where privileged,
|
||||
hardlink/copy otherwise) — all satisfy exists()/is_present()."""
|
||||
import huggingface_hub
|
||||
with mock.patch.object(huggingface_hub, "hf_hub_download",
|
||||
self._fake_hf_hub_download):
|
||||
target = weights.ensure_local(self.rel, verbose=False)
|
||||
self.assertTrue(target.exists(), f"target not materialized: {target}")
|
||||
self.assertEqual(target.read_bytes(), self.payload)
|
||||
self.assertTrue(weights.is_present(self.rel))
|
||||
|
||||
def test_fallback_chain_needs_no_symlink_privilege(self):
|
||||
"""Force symlink_to to fail (as on Windows without Developer Mode,
|
||||
WinError 1314) and verify the hardlink/copy fallback still materializes
|
||||
a readable, non-symlink target. Runs identically on every OS."""
|
||||
cached = self.fake_cache / "src.bin"
|
||||
cached.write_bytes(self.payload)
|
||||
target = self.tmp / "project" / "models" / "linked.bin"
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
boom = OSError(1314, "A required privilege is not held by the client")
|
||||
with mock.patch.object(type(target), "symlink_to", side_effect=boom):
|
||||
weights._link_or_copy(cached, target)
|
||||
self.assertTrue(target.exists())
|
||||
self.assertFalse(target.is_symlink())
|
||||
self.assertEqual(target.read_bytes(), self.payload)
|
||||
|
||||
def test_copy_fallback_when_hardlink_also_fails(self):
|
||||
"""Symlink AND hardlink refused (e.g. cross-volume) -> plain copy."""
|
||||
cached = self.fake_cache / "src2.bin"
|
||||
cached.write_bytes(self.payload)
|
||||
target = self.tmp / "project" / "models" / "copied.bin"
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with mock.patch.object(type(target), "symlink_to",
|
||||
side_effect=OSError(1314, "no symlink privilege")), \
|
||||
mock.patch.object(weights, "_LINK_FALLBACK_SHOWN", True), \
|
||||
mock.patch("os.link", side_effect=OSError(17, "cross-device link")):
|
||||
weights._link_or_copy(cached, target)
|
||||
self.assertTrue(target.exists())
|
||||
self.assertFalse(target.is_symlink())
|
||||
self.assertEqual(target.read_bytes(), self.payload)
|
||||
|
||||
|
||||
class TestConsoleEncoding(unittest.TestCase):
|
||||
"""The banners use box-drawing + emoji. Run the scripts in FRESH interpreters
|
||||
with PYTHONUTF8/PYTHONIOENCODING stripped, so stdout is a pipe under the
|
||||
runner's native default encoding (cp1252 on Windows without the in-script
|
||||
reconfigure fix) — a UnicodeEncodeError would make these exit nonzero."""
|
||||
|
||||
@staticmethod
|
||||
def _clean_env():
|
||||
env = os.environ.copy()
|
||||
env.pop("PYTHONUTF8", None)
|
||||
env.pop("PYTHONIOENCODING", None)
|
||||
return env
|
||||
|
||||
def test_sa3_tflite_help(self):
|
||||
r = subprocess.run(
|
||||
[sys.executable, str(SCRIPTS_DIR / "sa3_tflite.py"), "--help"],
|
||||
capture_output=True, env=self._clean_env(), timeout=120)
|
||||
self.assertEqual(
|
||||
r.returncode, 0,
|
||||
f"--help failed\nstdout:\n{r.stdout.decode('utf-8', 'replace')}\n"
|
||||
f"stderr:\n{r.stderr.decode('utf-8', 'replace')}")
|
||||
out = r.stdout.decode("utf-8", "replace")
|
||||
self.assertIn("--prompt", out)
|
||||
self.assertIn("--play", out)
|
||||
|
||||
def test_examples_block(self):
|
||||
# examples.py is a module (no __main__): render the full emoji block.
|
||||
r = subprocess.run(
|
||||
[sys.executable, "-c",
|
||||
"import examples; examples.print_example_commands()"],
|
||||
capture_output=True, env=self._clean_env(), cwd=str(SCRIPTS_DIR),
|
||||
timeout=120)
|
||||
self.assertEqual(
|
||||
r.returncode, 0,
|
||||
f"examples block failed\nstdout:\n{r.stdout.decode('utf-8', 'replace')}\n"
|
||||
f"stderr:\n{r.stderr.decode('utf-8', 'replace')}")
|
||||
self.assertIn("Generate audio", r.stdout.decode("utf-8", "replace"))
|
||||
|
||||
|
||||
class TestPlayBackendDispatch(unittest.TestCase):
|
||||
"""--play platform dispatch, without playing anything."""
|
||||
|
||||
def _backend(self):
|
||||
import sa3_tflite
|
||||
return sa3_tflite._play_backend
|
||||
|
||||
def test_darwin_uses_afplay(self):
|
||||
self.assertEqual(self._backend()("darwin"), ("subprocess", ["afplay"]))
|
||||
|
||||
def test_windows_uses_winsound(self):
|
||||
kind, argv = self._backend()("win32")
|
||||
self.assertEqual(kind, "winsound")
|
||||
self.assertIsNone(argv)
|
||||
if sys.platform == "win32":
|
||||
import winsound # stdlib on Windows — the module the dispatcher relies on
|
||||
self.assertTrue(hasattr(winsound, "PlaySound"))
|
||||
|
||||
def test_linux_uses_aplay_or_prints_path(self):
|
||||
kind, argv = self._backend()("linux")
|
||||
if kind == "subprocess":
|
||||
self.assertEqual(argv[0], "aplay")
|
||||
else:
|
||||
self.assertEqual((kind, argv), ("none", None))
|
||||
|
||||
def test_current_platform_resolves(self):
|
||||
kind, argv = self._backend()()
|
||||
self.assertIn(kind, ("subprocess", "winsound", "none"))
|
||||
if kind == "subprocess":
|
||||
self.assertIsInstance(argv, list)
|
||||
else:
|
||||
self.assertIsNone(argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user