"""Tests fuer den lokalen Piper-TTS-Provider (offline, mit Fake-Binary).""" import asyncio import json import shutil import stat import pytest import app.providers.tts.piper as piper_mod from app.providers.tts.piper import PiperTTSProvider @pytest.fixture(autouse=True) def _force_binary_path(monkeypatch): """Diese Tests pruefen den Binary-Fallback (Fake-piper). Ist die piper-Python-Lib installiert, wuerde sonst der In-Process-Pfad das Fake-Modell zu laden versuchen.""" monkeypatch.setattr(piper_mod, "_PIPER_LIB", False) # PCM-Nutzlast des Fake-Binaries. 4000 Bytes = 2000 s16le-Samples. FAKE_PCM = b"\x01\x02" * 2000 def _run(coro): return asyncio.run(coro) def _make_fake_voice(tmp_path, sample_rate): """Legt ein Fake-piper-Binary + Stimmmodell (.onnx/.onnx.json) an.""" voices = tmp_path / "voices" voices.mkdir() (voices / "de_DE-test.onnx").write_bytes(b"fake-model") (voices / "de_DE-test.onnx.json").write_text( json.dumps({"audio": {"sample_rate": sample_rate}}) ) # Fake-Binary: ignoriert Argumente, schreibt feste Roh-PCM-Bytes nach stdout. binp = tmp_path / "piper" binp.write_text( "#!/usr/bin/env python3\n" "import sys\n" f"sys.stdout.buffer.write({FAKE_PCM!r})\n" ) binp.chmod(binp.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) return str(binp), str(voices) def test_piper_returns_pcm_without_resampling(tmp_path): # Native Rate == Ziel-Rate -> kein ffmpeg noetig, Bytes unveraendert. binp, voices = _make_fake_voice(tmp_path, sample_rate=24000) provider = PiperTTSProvider(binp, voices, "de_DE-test", target_rate=24000) pcm = _run(provider.synthesize("Hallo Welt")) assert pcm == FAKE_PCM def test_piper_wraps_wav_on_request(tmp_path): binp, voices = _make_fake_voice(tmp_path, sample_rate=24000) provider = PiperTTSProvider(binp, voices, "de_DE-test", target_rate=24000) wav = _run(provider.synthesize("Hallo", audio_format="wav")) assert wav.startswith(b"RIFF") and b"WAVE" in wav[:16] def test_piper_rejects_empty_text(tmp_path): binp, voices = _make_fake_voice(tmp_path, sample_rate=24000) provider = PiperTTSProvider(binp, voices, "de_DE-test") with pytest.raises(ValueError): _run(provider.synthesize(" ")) def test_piper_unknown_voice_raises(tmp_path): binp, voices = _make_fake_voice(tmp_path, sample_rate=24000) provider = PiperTTSProvider(binp, voices, "gibt-es-nicht") with pytest.raises(RuntimeError): _run(provider.synthesize("Hallo")) @pytest.mark.skipif(not shutil.which("ffmpeg"), reason="ffmpeg nicht installiert") def test_piper_resamples_when_rates_differ(tmp_path): # Native 16000 -> Ziel 24000: ffmpeg laeuft, Ergebnis ist gueltiges (nicht leeres) PCM. binp, voices = _make_fake_voice(tmp_path, sample_rate=16000) provider = PiperTTSProvider(binp, voices, "de_DE-test", target_rate=24000) pcm = _run(provider.synthesize("Hallo")) assert isinstance(pcm, bytes) and len(pcm) > 0