初始提交: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,270 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user