Files
SoundGame/tools/stable-audio-3/tests/conftest.py
T
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

148 lines
5.1 KiB
Python

from __future__ import annotations
import re
from pathlib import Path
import pytest
try: # torch is absent in the torch-free MLX venv; MLX-only tests don't need it
import torch
import torchaudio
from stable_audio_3 import AutoencoderModel, StableAudioModel
from stable_audio_3.model_configs import ae_models, base_models
except ModuleNotFoundError:
torch = None
torchaudio = None
AutoencoderModel = StableAudioModel = None
ae_models = ()
base_models = ()
# ---------------------------------------------------------------------------
# Hardware detection — used by fixtures and tests to gate GPU-only paths
# ---------------------------------------------------------------------------
HAS_CUDA = torch is not None and torch.cuda.is_available()
HAS_MPS = torch is not None and torch.backends.mps.is_available()
HAS_ACCEL = HAS_CUDA or HAS_MPS
ACCEL_DEVICE = "cuda" if HAS_CUDA else ("mps" if HAS_MPS else "cpu")
# ---------------------------------------------------------------------------
# CLI options
# ---------------------------------------------------------------------------
def pytest_addoption(parser):
parser.addoption(
"--save-audio",
action="store_true",
default=False,
help="Save generated audio to disk. Files are written to test_audio_outputs/.",
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture(scope="session")
def device():
"""Best available compute device for this session."""
return ACCEL_DEVICE
@pytest.fixture(scope="session", params=["small-music", "small-sfx", "medium"])
def sa3_model(request):
"""Session-scoped model fixture parametrized over model sizes.
small-music — loads via from_pretrained("small-music"); runs on CPU or accelerator.
small-sfx — loads via from_pretrained("small-sfx"); runs on CPU or accelerator.
medium — requires a CUDA GPU; skipped otherwise.
"""
name = request.param
if name in ("small-music", "small-sfx"):
return StableAudioModel.from_pretrained(name, device=ACCEL_DEVICE)
if name == "medium":
if not HAS_CUDA:
pytest.skip("Medium model requires a CUDA GPU — none detected")
return StableAudioModel.from_pretrained("medium", device=ACCEL_DEVICE)
@pytest.fixture(scope="session", params=list(base_models))
def sa3_base_model(request):
"""Session-scoped fixture for base (un-fine-tuned) models.
small-*-base — runs on CUDA if available, otherwise CPU (MPS is skipped because
APG projection requires float64 which MPS does not support).
medium-base — requires a CUDA GPU; skipped otherwise.
"""
name = request.param
if name == "medium-base" and not HAS_CUDA:
pytest.skip("medium-base requires a CUDA GPU — none detected")
device = "cuda" if HAS_CUDA else "cpu"
return StableAudioModel.from_pretrained(name, device=device)
@pytest.fixture(scope="session", params=list(ae_models))
def autoencoder(request):
"""Session-scoped autoencoder model fixture parametrized over AE model sizes.
same-l requires a CUDA GPU; skipped otherwise.
"""
name = request.param
if name == "same-l" and not HAS_CUDA:
pytest.skip(f"{name} requires a CUDA GPU — none detected")
return AutoencoderModel.from_pretrained(name, device=ACCEL_DEVICE)
@pytest.fixture
def maybe_save_audio(request):
"""Return a callable that saves audio to disk when --save-audio is passed.
Usage in tests:
def test_foo(sa3_model, maybe_save_audio):
audio = sa3_model.generate(prompt="drums", ...)
maybe_save_audio(audio, sr, "drums")
Files are written to test_audio_outputs/{test_name[param]}_{prompt_slug}.wav.
Does nothing when --save-audio is not set.
"""
enabled = request.config.getoption("--save-audio")
def _save(audio: torch.Tensor, sample_rate: int, prompt: str) -> None:
if not enabled:
return
out_dir = Path("test_audio_outputs")
out_dir.mkdir(exist_ok=True)
slug = re.sub(r"[^\w]+", "_", prompt).strip("_")[:40] or "audio"
test_name = request.node.name # e.g. test_text_to_audio[small]
filename = out_dir / f"{test_name}_{slug}.wav"
torchaudio.save(str(filename), audio.squeeze(0).cpu(), sample_rate)
return _save
# ---------------------------------------------------------------------------
# Environment checks
# ---------------------------------------------------------------------------
@pytest.mark.skipif(
not HAS_ACCEL, reason="Flash attention check requires a GPU/accelerator"
)
def test_flash_attention_available(sa3_model, request):
"""Verify flash_attn is importable on GPU environments (medium model only)."""
if request.node.callspec.params.get("sa3_model") != "medium":
pytest.skip("Flash attention check is medium-model only")
try:
import flash_attn # noqa: F401
except ImportError:
pytest.fail(
"flash_attn is not installed. Install via: uv sync --extra flash-attn"
)