# -*- coding: utf-8 -*- """ Tests für das Piper TTS Plugin. """ import pytest from pathlib import Path from unittest.mock import MagicMock, AsyncMock, patch # Plugin-Module importieren import sys import importlib.util # Plugin-main.py direkt laden plugin_path = Path(__file__).parent.parent / "main.py" spec = importlib.util.spec_from_file_location("piper_plugin", plugin_path) piper_module = importlib.util.module_from_spec(spec) spec.loader.exec_module(piper_module) PiperTTSPlugin = piper_module.PiperTTSPlugin PiperTTSProvider = piper_module.PiperTTSProvider PIPER_VOICES = piper_module.PIPER_VOICES HUGGINGFACE_REPO = piper_module.HUGGINGFACE_REPO from trixy_core.audio.tts import TTSConfig, TTSState class TestPiperVoicesCatalog: """Tests für den Piper-Stimmen-Katalog.""" def test_voices_catalog_not_empty(self): """Prüft, dass der Stimmenkatalog nicht leer ist.""" assert len(PIPER_VOICES) > 0 def test_voices_have_required_fields(self): """Prüft, dass alle Stimmen die erforderlichen Felder haben.""" required_fields = ["name", "language", "gender", "description", "sample_rate", "hf_path"] for voice_id, voice_info in PIPER_VOICES.items(): for field in required_fields: assert field in voice_info, f"Stimme {voice_id} fehlt Feld '{field}'" def test_german_voices_available(self): """Prüft, dass deutsche Stimmen verfügbar sind.""" german_voices = [v for v in PIPER_VOICES.values() if v["language"] == "de-DE"] assert len(german_voices) >= 1 def test_default_voice_exists(self): """Prüft, dass die Standard-Stimme existiert.""" assert "de_DE-thorsten-medium" in PIPER_VOICES class TestPiperTTSProvider: """Tests für den PiperTTSProvider.""" @pytest.fixture def provider(self, tmp_path): """Erstellt einen Provider für Tests.""" config = TTSConfig(language="de-DE", voice_id="de_DE-thorsten-medium") return PiperTTSProvider( config=config, models_dir=tmp_path / "models", voice_id="de_DE-thorsten-medium", auto_download=False, ) def test_provider_name(self, provider): """Prüft den Provider-Namen.""" assert provider.name == "piper" def test_supported_languages(self, provider): """Prüft die unterstützten Sprachen.""" languages = provider.supported_languages assert "de-DE" in languages assert "en-US" in languages def test_supports_streaming(self, provider): """Prüft Streaming-Unterstützung.""" assert provider.supports_streaming is True def test_initial_state(self, provider): """Prüft den initialen Zustand.""" assert provider._model_loaded is False assert provider._piper is None @pytest.mark.asyncio async def test_get_voices(self, provider): """Prüft die get_voices Methode.""" all_voices = await provider.get_voices() assert len(all_voices) > 0 german_voices = await provider.get_voices(language="de-DE") assert all(v.language == "de-DE" for v in german_voices) @pytest.mark.asyncio async def test_initialize_without_model_raises(self, provider): """Prüft, dass initialize ohne Modell fehlschlägt.""" # Ohne echte piper-Installation oder Modell sollte es fehlschlagen with pytest.raises(RuntimeError): await provider.initialize() @pytest.mark.asyncio async def test_synthesize_without_init_raises(self, provider): """Prüft, dass synthesize ohne Initialisierung fehlschlägt.""" with pytest.raises(RuntimeError, match="Piper nicht geladen"): await provider.synthesize("Test") class TestPiperTTSPlugin: """Tests für das PiperTTSPlugin.""" @pytest.fixture def plugin(self, mock_application, tmp_path): """Erstellt ein Plugin für Tests.""" plugin_path = tmp_path / "tts_piper" plugin_path.mkdir() (plugin_path / "models").mkdir() config = { "name": "Piper TTS", "enabled": True, "voice": "de_DE-thorsten-medium", "language": "de-DE", "auto_download": False, } return PiperTTSPlugin(mock_application, plugin_path, config) def test_plugin_initialization(self, plugin): """Prüft die Plugin-Initialisierung.""" assert plugin._provider is None assert plugin.config.get("voice") == "de_DE-thorsten-medium" def test_plugin_path(self, plugin, tmp_path): """Prüft den Plugin-Pfad.""" assert plugin.plugin_path.exists() @pytest.mark.asyncio async def test_on_unload_without_provider(self, plugin): """Prüft on_unload ohne initialisierten Provider.""" # Sollte nicht fehlschlagen await plugin.on_unload() assert plugin._provider is None class TestPiperDownload: """Tests für die Download-Funktionalität.""" @pytest.mark.asyncio async def test_download_model_unknown_voice_raises(self, tmp_path): """Prüft, dass unbekannte Stimmen einen Fehler werfen.""" config = TTSConfig(language="de-DE") provider = PiperTTSProvider( config=config, models_dir=tmp_path / "models", voice_id="unknown_voice", auto_download=True, ) with pytest.raises(RuntimeError, match="Unbekannte Stimme"): await provider._download_model() def test_huggingface_repo_configured(self): """Prüft, dass das Hugging Face Repository konfiguriert ist.""" assert HUGGINGFACE_REPO == "rhasspy/piper-voices" # Integration Tests (benötigen echte Installation) @pytest.mark.integration @pytest.mark.requires_piper class TestPiperIntegration: """Integrationstests für Piper TTS (benötigen Installation).""" @pytest.fixture def provider_with_model(self, tmp_path): """Erstellt einen Provider mit echtem Modell.""" config = TTSConfig(language="de-DE", voice_id="de_DE-thorsten-medium") return PiperTTSProvider( config=config, models_dir=tmp_path / "models", voice_id="de_DE-thorsten-medium", auto_download=True, ) @pytest.mark.asyncio @pytest.mark.slow async def test_full_synthesis_flow(self, provider_with_model): """Testet den vollständigen Synthese-Flow.""" await provider_with_model.initialize() result = await provider_with_model.synthesize("Hallo Welt") assert result.audio_data is not None assert len(result.audio_data) > 0 assert result.sample_rate > 0 assert result.duration_seconds > 0 assert result.provider == "piper" await provider_with_model.shutdown()