- 共享引擎(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 模板
271 lines
8.1 KiB
Python
271 lines
8.1 KiB
Python
import pytest
|
|
import torch
|
|
|
|
from tests.conftest import HAS_ACCEL
|
|
from tests.utils.audio import assert_audio_valid, sine_wave
|
|
|
|
DURATION_SEC = 10
|
|
STEPS = 8
|
|
STEPS_BASE = 50
|
|
|
|
|
|
def test_text_to_audio_base(sa3_base_model, maybe_save_audio):
|
|
model = sa3_base_model
|
|
sr = model.model_config["sample_rate"]
|
|
prompt = "trap drums, hip hop beat, 120bpm"
|
|
|
|
audio = model.generate(
|
|
prompt=prompt,
|
|
duration=DURATION_SEC,
|
|
steps=STEPS_BASE,
|
|
cfg_scale=7.0,
|
|
seed=1234,
|
|
)
|
|
maybe_save_audio(audio, sr, prompt)
|
|
assert_audio_valid(audio, DURATION_SEC, sr)
|
|
|
|
|
|
def test_text_to_audio(sa3_model, maybe_save_audio):
|
|
model = sa3_model
|
|
sr = model.model_config["sample_rate"]
|
|
prompt = "trap drums, hip hop beat, 120bpm"
|
|
|
|
audio = model.generate(
|
|
prompt=prompt,
|
|
negative_prompt="low-quality",
|
|
duration=DURATION_SEC,
|
|
steps=STEPS,
|
|
seed=1234,
|
|
)
|
|
maybe_save_audio(audio, sr, prompt)
|
|
assert_audio_valid(audio, DURATION_SEC, sr)
|
|
|
|
|
|
def test_inpainting(sa3_model, maybe_save_audio):
|
|
model = sa3_model
|
|
sr = model.model_config["sample_rate"]
|
|
channels = model.model_config.get("io_channels", 2)
|
|
inpaint_duration = 10.0
|
|
prompt = "big trumpet solo, jazz big band, 90bpm"
|
|
|
|
base_audio = sine_wave(
|
|
inpaint_duration,
|
|
sr,
|
|
channels=channels,
|
|
device=str(model.device),
|
|
half=model.model_half,
|
|
)
|
|
audio = model.generate(
|
|
prompt=prompt,
|
|
duration=inpaint_duration,
|
|
steps=STEPS,
|
|
inpaint_audio=(sr, base_audio),
|
|
inpaint_mask_start_seconds=2.0,
|
|
inpaint_mask_end_seconds=7.0,
|
|
)
|
|
maybe_save_audio(audio, sr, prompt)
|
|
assert_audio_valid(audio, inpaint_duration, sr)
|
|
|
|
|
|
def test_inpainting_multiple_regions(sa3_model, maybe_save_audio):
|
|
model = sa3_model
|
|
sr = model.model_config["sample_rate"]
|
|
channels = model.model_config.get("io_channels", 2)
|
|
inpaint_duration = 20.0
|
|
prompt = "jazz piano trio, upbeat swing, 120bpm"
|
|
|
|
base_audio = sine_wave(
|
|
inpaint_duration,
|
|
sr,
|
|
channels=channels,
|
|
device=str(model.device),
|
|
half=model.model_half,
|
|
)
|
|
audio = model.generate(
|
|
prompt=prompt,
|
|
duration=inpaint_duration,
|
|
steps=STEPS,
|
|
inpaint_audio=(sr, base_audio),
|
|
inpaint_mask_start_seconds=[2.0, 12.0],
|
|
inpaint_mask_end_seconds=[11.0, 19.0],
|
|
)
|
|
maybe_save_audio(audio, sr, prompt)
|
|
assert_audio_valid(audio, inpaint_duration, sr)
|
|
|
|
|
|
def test_continuation(sa3_model, maybe_save_audio):
|
|
model = sa3_model
|
|
sr = model.model_config["sample_rate"]
|
|
channels = model.model_config.get("io_channels", 2)
|
|
init_duration = 5.0
|
|
total_duration = 15.0
|
|
prompt = "thunderstorm with heavy rain"
|
|
|
|
base_audio = sine_wave(
|
|
init_duration,
|
|
sr,
|
|
channels=channels,
|
|
device=str(model.device),
|
|
half=model.model_half,
|
|
)
|
|
audio = model.generate(
|
|
prompt=prompt,
|
|
duration=total_duration,
|
|
steps=STEPS,
|
|
inpaint_audio=(sr, base_audio),
|
|
inpaint_mask_start_seconds=5.0,
|
|
inpaint_mask_end_seconds=15.0,
|
|
)
|
|
maybe_save_audio(audio, sr, prompt)
|
|
assert_audio_valid(audio, total_duration, sr)
|
|
|
|
|
|
def test_init_audio(sa3_model, maybe_save_audio):
|
|
model = sa3_model
|
|
sr = model.model_config["sample_rate"]
|
|
channels = model.model_config.get("io_channels", 2)
|
|
prompt = "funky bass groove"
|
|
|
|
init = sine_wave(
|
|
DURATION_SEC,
|
|
sr,
|
|
channels=channels,
|
|
device=str(model.device),
|
|
half=model.model_half,
|
|
)
|
|
|
|
audio = model.generate(
|
|
prompt=prompt,
|
|
duration=DURATION_SEC,
|
|
steps=STEPS,
|
|
init_audio=(sr, init),
|
|
init_noise_level=0.8,
|
|
)
|
|
maybe_save_audio(audio, sr, prompt)
|
|
assert_audio_valid(audio, DURATION_SEC, sr)
|
|
|
|
|
|
def test_init_audio_float32_into_half_model(sa3_model, maybe_save_audio):
|
|
model = sa3_model
|
|
if not model.model_half:
|
|
pytest.skip(
|
|
"model_half is False — dtype mismatch only occurs on half-precision models"
|
|
)
|
|
|
|
sr = model.model_config["sample_rate"]
|
|
channels = model.model_config.get("io_channels", 2)
|
|
prompt = "funky bass groove"
|
|
|
|
# Deliberately float32, regardless of model dtype — this was the bug trigger
|
|
init = sine_wave(
|
|
DURATION_SEC, sr, channels=channels, device=str(model.device), half=False
|
|
)
|
|
assert init.dtype == torch.float32
|
|
|
|
audio = model.generate(
|
|
prompt=prompt,
|
|
duration=DURATION_SEC,
|
|
steps=STEPS,
|
|
init_audio=(sr, init),
|
|
init_noise_level=0.8,
|
|
)
|
|
maybe_save_audio(audio, sr, prompt)
|
|
assert_audio_valid(audio, DURATION_SEC, sr)
|
|
|
|
|
|
def test_inpaint_audio_float32_into_half_model(sa3_model, maybe_save_audio):
|
|
model = sa3_model
|
|
if not model.model_half:
|
|
pytest.skip(
|
|
"model_half is False — dtype mismatch only occurs on half-precision models"
|
|
)
|
|
|
|
sr = model.model_config["sample_rate"]
|
|
channels = model.model_config.get("io_channels", 2)
|
|
inpaint_duration = 10.0
|
|
prompt = "big trumpet solo, jazz big band, 90bpm"
|
|
|
|
# Deliberately float32, regardless of model dtype — this was the bug trigger
|
|
base_audio = sine_wave(
|
|
inpaint_duration, sr, channels=channels, device=str(model.device), half=False
|
|
)
|
|
assert base_audio.dtype == torch.float32
|
|
|
|
audio = model.generate(
|
|
prompt=prompt,
|
|
duration=inpaint_duration,
|
|
steps=STEPS,
|
|
inpaint_audio=(sr, base_audio),
|
|
inpaint_mask_start_seconds=2.0,
|
|
inpaint_mask_end_seconds=7.0,
|
|
)
|
|
maybe_save_audio(audio, sr, prompt)
|
|
assert_audio_valid(audio, inpaint_duration, sr)
|
|
|
|
|
|
@pytest.mark.skipif(not HAS_ACCEL, reason="Batch inference requires a GPU/accelerator")
|
|
def test_batch_inference(sa3_model, maybe_save_audio):
|
|
model = sa3_model
|
|
sr = model.model_config["sample_rate"]
|
|
batch_size = 3
|
|
prompts = ["ocean waves", "summer breeze", "city traffic"]
|
|
|
|
neg_prompts = ["loud background noise", "bad quality", "loud background noise"]
|
|
durations = [5, 10, 20]
|
|
duration_padding_sec = 6 # Default, just defining here for clarity
|
|
|
|
audio_same_durations = model.generate(
|
|
prompt=prompts,
|
|
negative_prompt=neg_prompts,
|
|
duration=DURATION_SEC,
|
|
steps=STEPS,
|
|
batch_size=batch_size,
|
|
)
|
|
audio_different_durations = model.generate(
|
|
prompt=prompts,
|
|
negative_prompt=neg_prompts,
|
|
duration=durations,
|
|
steps=STEPS,
|
|
duration_padding_sec=duration_padding_sec,
|
|
batch_size=batch_size,
|
|
)
|
|
assert audio_same_durations.shape[0] == batch_size, (
|
|
f"Expected batch dim {batch_size}, got {audio_same_durations.shape[0]}"
|
|
)
|
|
# Validate each item in the batch individually and optionally save
|
|
for i, prompt in enumerate(prompts):
|
|
maybe_save_audio(audio_same_durations[i : i + 1], sr, prompt)
|
|
assert_audio_valid(audio_same_durations[i : i + 1], DURATION_SEC, sr)
|
|
|
|
# Check for diversity in outputs (not identical)
|
|
diffs = []
|
|
for i in range(batch_size):
|
|
for j in range(i + 1, batch_size):
|
|
diff = torch.mean(
|
|
torch.abs(audio_same_durations[i] - audio_same_durations[j])
|
|
).item()
|
|
diffs.append(diff)
|
|
avg_diff = sum(diffs) / len(diffs)
|
|
assert avg_diff > 0.01, f"Batch outputs are too similar (avg diff {avg_diff:.4f})"
|
|
|
|
# Validate different durations
|
|
for i, dur in enumerate(durations):
|
|
# Set expected_duration to be longer since we are not truncating
|
|
d = max(durations) + duration_padding_sec
|
|
maybe_save_audio(
|
|
audio_different_durations[i : i + 1], sr, f"{prompts[i]}_{dur}s"
|
|
)
|
|
assert_audio_valid(audio_different_durations[i : i + 1], d, sr)
|
|
|
|
audio_shared_neg = model.generate(
|
|
prompt=prompts,
|
|
negative_prompt="low quality, noise",
|
|
duration=durations,
|
|
steps=STEPS,
|
|
duration_padding_sec=duration_padding_sec,
|
|
batch_size=batch_size,
|
|
)
|
|
d = max(durations) + duration_padding_sec
|
|
for i in range(batch_size):
|
|
assert_audio_valid(audio_shared_neg[i : i + 1], d, sr)
|