"""Tests for app.py wiring: on-upload analysis, demo loading, and the full finish_song pipeline with the SA3 engine stubbed (no GPU, no weights). Verifies the real DSP (analyze, enhance, stitch) runs end-to-end on the bundled demo and produces a valid 44.1 kHz stereo file. Skips if gradio isn't installed.""" import os import numpy as np import pytest import soundfile as sf pytest.importorskip("gradio") import app # noqa: E402 import engine # noqa: E402 DEMO = app.PUSHBACK_DEMO _HAVE_DEMO = os.path.exists(DEMO) need_demo = pytest.mark.skipif(not _HAVE_DEMO, reason="bundled demo clip absent") class _Prog: """stand-in for gr.Progress — records the (frac, desc) stages it's told.""" def __init__(self): self.calls = [] def __call__(self, frac, desc=None): self.calls.append((frac, desc)) @need_demo def test_load_demo_returns_clip(): assert app.load_demo() == DEMO assert os.path.exists(DEMO) @need_demo def test_analyze_on_upload_reports_and_enables(): info_update, btn_update = app.analyze_on_upload(DEMO) md = info_update["value"] assert "KEY" in md and "TEMPO" in md and "BPM" in md assert info_update["visible"] is True assert btn_update["interactive"] is True def test_analyze_on_upload_empty_disables_button(): _, btn_update = app.analyze_on_upload(None) assert btn_update["interactive"] is False @need_demo def test_finish_song_pipeline_with_stubbed_engine(monkeypatch): """stub engine.continue_audio with a fast synthetic tail; the rest of the pipeline (enhance, analyze, stitch, write) runs for real.""" def fake_continue_audio(clip_path, total_seconds, prompt="", progress=None, **kw): if progress: progress("reading") progress("composing") progress("finalizing") new_secs = total_seconds - 29.53 # demo is ~29.53s n = int(max(new_secs, 5) * engine.SR) t = np.arange(n) / engine.SR wave = 0.2 * np.sin(2 * np.pi * 220 * t).astype(np.float32) tail = np.stack([wave, wave]) return tail, 29.53, engine.SR monkeypatch.setattr(engine, "continue_audio", fake_continue_audio) prog = _Prog() out_path, summary = app.finish_song(DEMO, 60, "", False, progress=prog) assert os.path.exists(out_path) y, sr = sf.read(out_path) assert sr == 44100 assert y.ndim == 2 and y.shape[1] == 2 # 44.1k stereo assert len(y) / sr > 50 # ~60s finished assert float(np.abs(y).max()) <= 1.0 assert float(np.sqrt(np.mean(y ** 2))) > 1e-3 # not silent assert "BPM" in summary and "Stable Audio 3" in summary # progress streamed several stages and ended at 100% assert len(prog.calls) >= 4 assert prog.calls[-1][0] == 1.0 @need_demo def test_finish_song_remaster_path(monkeypatch): def fake_continue_audio(clip_path, total_seconds, prompt="", progress=None, **kw): n = int(15 * engine.SR) wave = 0.2 * np.sin(2 * np.pi * 220 * np.arange(n) / engine.SR) return np.stack([wave, wave]).astype(np.float32), 29.53, engine.SR monkeypatch.setattr(engine, "continue_audio", fake_continue_audio) out_path, _ = app.finish_song(DEMO, 45, "", True, progress=_Prog()) assert os.path.exists(out_path) def test_finish_song_rejects_empty_input(): with pytest.raises(Exception): app.finish_song(None, 60, "", False, progress=_Prog())