test_piper_tts.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für das Piper TTS Plugin.
  4. """
  5. import pytest
  6. from pathlib import Path
  7. from unittest.mock import MagicMock, AsyncMock, patch
  8. # Plugin-Module importieren
  9. import sys
  10. import importlib.util
  11. # Plugin-main.py direkt laden
  12. plugin_path = Path(__file__).parent.parent / "main.py"
  13. spec = importlib.util.spec_from_file_location("piper_plugin", plugin_path)
  14. piper_module = importlib.util.module_from_spec(spec)
  15. spec.loader.exec_module(piper_module)
  16. PiperTTSPlugin = piper_module.PiperTTSPlugin
  17. PiperTTSProvider = piper_module.PiperTTSProvider
  18. PIPER_VOICES = piper_module.PIPER_VOICES
  19. HUGGINGFACE_REPO = piper_module.HUGGINGFACE_REPO
  20. from trixy_core.audio.tts import TTSConfig, TTSState
  21. class TestPiperVoicesCatalog:
  22. """Tests für den Piper-Stimmen-Katalog."""
  23. def test_voices_catalog_not_empty(self):
  24. """Prüft, dass der Stimmenkatalog nicht leer ist."""
  25. assert len(PIPER_VOICES) > 0
  26. def test_voices_have_required_fields(self):
  27. """Prüft, dass alle Stimmen die erforderlichen Felder haben."""
  28. required_fields = ["name", "language", "gender", "description", "sample_rate", "hf_path"]
  29. for voice_id, voice_info in PIPER_VOICES.items():
  30. for field in required_fields:
  31. assert field in voice_info, f"Stimme {voice_id} fehlt Feld '{field}'"
  32. def test_german_voices_available(self):
  33. """Prüft, dass deutsche Stimmen verfügbar sind."""
  34. german_voices = [v for v in PIPER_VOICES.values() if v["language"] == "de-DE"]
  35. assert len(german_voices) >= 1
  36. def test_default_voice_exists(self):
  37. """Prüft, dass die Standard-Stimme existiert."""
  38. assert "de_DE-thorsten-medium" in PIPER_VOICES
  39. class TestPiperTTSProvider:
  40. """Tests für den PiperTTSProvider."""
  41. @pytest.fixture
  42. def provider(self, tmp_path):
  43. """Erstellt einen Provider für Tests."""
  44. config = TTSConfig(language="de-DE", voice_id="de_DE-thorsten-medium")
  45. return PiperTTSProvider(
  46. config=config,
  47. models_dir=tmp_path / "models",
  48. voice_id="de_DE-thorsten-medium",
  49. auto_download=False,
  50. )
  51. def test_provider_name(self, provider):
  52. """Prüft den Provider-Namen."""
  53. assert provider.name == "piper"
  54. def test_supported_languages(self, provider):
  55. """Prüft die unterstützten Sprachen."""
  56. languages = provider.supported_languages
  57. assert "de-DE" in languages
  58. assert "en-US" in languages
  59. def test_supports_streaming(self, provider):
  60. """Prüft Streaming-Unterstützung."""
  61. assert provider.supports_streaming is True
  62. def test_initial_state(self, provider):
  63. """Prüft den initialen Zustand."""
  64. assert provider._model_loaded is False
  65. assert provider._piper is None
  66. @pytest.mark.asyncio
  67. async def test_get_voices(self, provider):
  68. """Prüft die get_voices Methode."""
  69. all_voices = await provider.get_voices()
  70. assert len(all_voices) > 0
  71. german_voices = await provider.get_voices(language="de-DE")
  72. assert all(v.language == "de-DE" for v in german_voices)
  73. @pytest.mark.asyncio
  74. async def test_initialize_without_model_raises(self, provider):
  75. """Prüft, dass initialize ohne Modell fehlschlägt."""
  76. # Ohne echte piper-Installation oder Modell sollte es fehlschlagen
  77. with pytest.raises(RuntimeError):
  78. await provider.initialize()
  79. @pytest.mark.asyncio
  80. async def test_synthesize_without_init_raises(self, provider):
  81. """Prüft, dass synthesize ohne Initialisierung fehlschlägt."""
  82. with pytest.raises(RuntimeError, match="Piper nicht geladen"):
  83. await provider.synthesize("Test")
  84. class TestPiperTTSPlugin:
  85. """Tests für das PiperTTSPlugin."""
  86. @pytest.fixture
  87. def plugin(self, mock_application, tmp_path):
  88. """Erstellt ein Plugin für Tests."""
  89. plugin_path = tmp_path / "tts_piper"
  90. plugin_path.mkdir()
  91. (plugin_path / "models").mkdir()
  92. config = {
  93. "name": "Piper TTS",
  94. "enabled": True,
  95. "voice": "de_DE-thorsten-medium",
  96. "language": "de-DE",
  97. "auto_download": False,
  98. }
  99. return PiperTTSPlugin(mock_application, plugin_path, config)
  100. def test_plugin_initialization(self, plugin):
  101. """Prüft die Plugin-Initialisierung."""
  102. assert plugin._provider is None
  103. assert plugin.config.get("voice") == "de_DE-thorsten-medium"
  104. def test_plugin_path(self, plugin, tmp_path):
  105. """Prüft den Plugin-Pfad."""
  106. assert plugin.plugin_path.exists()
  107. @pytest.mark.asyncio
  108. async def test_on_unload_without_provider(self, plugin):
  109. """Prüft on_unload ohne initialisierten Provider."""
  110. # Sollte nicht fehlschlagen
  111. await plugin.on_unload()
  112. assert plugin._provider is None
  113. class TestPiperDownload:
  114. """Tests für die Download-Funktionalität."""
  115. @pytest.mark.asyncio
  116. async def test_download_model_unknown_voice_raises(self, tmp_path):
  117. """Prüft, dass unbekannte Stimmen einen Fehler werfen."""
  118. config = TTSConfig(language="de-DE")
  119. provider = PiperTTSProvider(
  120. config=config,
  121. models_dir=tmp_path / "models",
  122. voice_id="unknown_voice",
  123. auto_download=True,
  124. )
  125. with pytest.raises(RuntimeError, match="Unbekannte Stimme"):
  126. await provider._download_model()
  127. def test_huggingface_repo_configured(self):
  128. """Prüft, dass das Hugging Face Repository konfiguriert ist."""
  129. assert HUGGINGFACE_REPO == "rhasspy/piper-voices"
  130. # Integration Tests (benötigen echte Installation)
  131. @pytest.mark.integration
  132. @pytest.mark.requires_piper
  133. class TestPiperIntegration:
  134. """Integrationstests für Piper TTS (benötigen Installation)."""
  135. @pytest.fixture
  136. def provider_with_model(self, tmp_path):
  137. """Erstellt einen Provider mit echtem Modell."""
  138. config = TTSConfig(language="de-DE", voice_id="de_DE-thorsten-medium")
  139. return PiperTTSProvider(
  140. config=config,
  141. models_dir=tmp_path / "models",
  142. voice_id="de_DE-thorsten-medium",
  143. auto_download=True,
  144. )
  145. @pytest.mark.asyncio
  146. @pytest.mark.slow
  147. async def test_full_synthesis_flow(self, provider_with_model):
  148. """Testet den vollständigen Synthese-Flow."""
  149. await provider_with_model.initialize()
  150. result = await provider_with_model.synthesize("Hallo Welt")
  151. assert result.audio_data is not None
  152. assert len(result.audio_data) > 0
  153. assert result.sample_rate > 0
  154. assert result.duration_seconds > 0
  155. assert result.provider == "piper"
  156. await provider_with_model.shutdown()