test_coqui_tts.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für das Coqui TTS Plugin.
  4. """
  5. import pytest
  6. from pathlib import Path
  7. from unittest.mock import MagicMock, AsyncMock, patch
  8. import importlib.util
  9. # Plugin-main.py direkt laden
  10. plugin_path = Path(__file__).parent.parent / "main.py"
  11. spec = importlib.util.spec_from_file_location("coqui_tts_plugin", plugin_path)
  12. coqui_module = importlib.util.module_from_spec(spec)
  13. spec.loader.exec_module(coqui_module)
  14. CoquiTTSPlugin = coqui_module.CoquiTTSPlugin
  15. CoquiTTSProvider = coqui_module.CoquiTTSProvider
  16. COQUI_MODELS = coqui_module.COQUI_MODELS
  17. from trixy_core.audio.tts import TTSConfig, TTSState
  18. class TestCoquiModelsCatalog:
  19. """Tests für den Coqui-Modell-Katalog."""
  20. def test_models_catalog_not_empty(self):
  21. """Prüft, dass der Modellkatalog nicht leer ist."""
  22. assert len(COQUI_MODELS) > 0
  23. def test_models_have_required_fields(self):
  24. """Prüft, dass alle Modelle die erforderlichen Felder haben."""
  25. required_fields = ["name", "language", "gender", "description", "sample_rate"]
  26. for model_id, model_info in COQUI_MODELS.items():
  27. for field in required_fields:
  28. assert field in model_info, f"Modell {model_id} fehlt Feld '{field}'"
  29. def test_german_models_available(self):
  30. """Prüft, dass deutsche Modelle verfügbar sind."""
  31. german_models = [m for m, info in COQUI_MODELS.items() if info["language"] == "de-DE"]
  32. assert len(german_models) >= 1
  33. def test_thorsten_models_available(self):
  34. """Prüft, dass Thorsten-Modelle (deutsche Stimme) verfügbar sind."""
  35. thorsten_models = [m for m in COQUI_MODELS.keys() if "thorsten" in m]
  36. assert len(thorsten_models) >= 1
  37. def test_model_ids_are_valid_paths(self):
  38. """Prüft, dass Modell-IDs gültige Coqui-Pfade sind."""
  39. for model_id in COQUI_MODELS.keys():
  40. assert model_id.startswith("tts_models/"), f"Modell {model_id} hat ungültiges Präfix"
  41. class TestCoquiTTSProvider:
  42. """Tests für den CoquiTTSProvider."""
  43. @pytest.fixture
  44. def provider(self, tmp_path):
  45. """Erstellt einen Provider für Tests."""
  46. config = TTSConfig(language="de-DE", voice_id="tts_models/de/thorsten/tacotron2-DDC")
  47. return CoquiTTSProvider(
  48. config=config,
  49. models_dir=tmp_path / "models",
  50. model_name="tts_models/de/thorsten/tacotron2-DDC",
  51. use_cuda=False,
  52. )
  53. def test_provider_name(self, provider):
  54. """Prüft den Provider-Namen."""
  55. assert provider.name == "coqui"
  56. def test_supported_languages(self, provider):
  57. """Prüft die unterstützten Sprachen."""
  58. languages = provider.supported_languages
  59. assert "de-DE" in languages
  60. assert "en-US" in languages
  61. assert "fr-FR" in languages
  62. def test_supports_streaming(self, provider):
  63. """Prüft, dass Streaming nicht unterstützt wird."""
  64. assert provider.supports_streaming is False
  65. def test_initial_state(self, provider):
  66. """Prüft den initialen Zustand."""
  67. assert provider._model_loaded is False
  68. assert provider._tts is None
  69. def test_cuda_config(self):
  70. """Prüft die CUDA-Konfiguration."""
  71. config = TTSConfig(language="de-DE")
  72. provider = CoquiTTSProvider(
  73. config=config,
  74. models_dir=Path("/tmp/models"),
  75. model_name="tts_models/de/thorsten/tacotron2-DDC",
  76. use_cuda=True,
  77. )
  78. assert provider._use_cuda is True
  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="Coqui TTS nicht geladen"):
  83. await provider.synthesize("Test")
  84. @pytest.mark.asyncio
  85. async def test_get_voices(self, provider):
  86. """Prüft die get_voices Methode (vor Initialisierung)."""
  87. # Vor Initialisierung ist der Voice-Katalog basierend auf COQUI_MODELS
  88. await provider._load_voices()
  89. voices = await provider.get_voices()
  90. assert len(voices) > 0
  91. class TestCoquiTTSPlugin:
  92. """Tests für das CoquiTTSPlugin."""
  93. @pytest.fixture
  94. def plugin(self, mock_application, tmp_path):
  95. """Erstellt ein Plugin für Tests."""
  96. plugin_path = tmp_path / "tts_coqui"
  97. plugin_path.mkdir()
  98. (plugin_path / "models").mkdir()
  99. config = {
  100. "name": "Coqui TTS",
  101. "enabled": True,
  102. "model": "tts_models/de/thorsten/tacotron2-DDC",
  103. "language": "de-DE",
  104. "use_cuda": False,
  105. }
  106. return CoquiTTSPlugin(mock_application, plugin_path, config)
  107. def test_plugin_initialization(self, plugin):
  108. """Prüft die Plugin-Initialisierung."""
  109. assert plugin._provider is None
  110. assert plugin.config.get("model") == "tts_models/de/thorsten/tacotron2-DDC"
  111. def test_plugin_path(self, plugin):
  112. """Prüft den Plugin-Pfad."""
  113. assert plugin.plugin_path.exists()
  114. @pytest.mark.asyncio
  115. async def test_on_unload_without_provider(self, plugin):
  116. """Prüft on_unload ohne initialisierten Provider."""
  117. await plugin.on_unload()
  118. assert plugin._provider is None
  119. class TestCoquiMultiSpeaker:
  120. """Tests für Multi-Speaker-Funktionalität."""
  121. def test_voice_id_with_speaker(self):
  122. """Prüft Voice-ID mit Speaker-Extraktion."""
  123. voice_id = "tts_models/multilingual/multi-dataset/your_tts:speaker_name"
  124. if ":" in voice_id:
  125. model_id, speaker = voice_id.split(":", 1)
  126. assert model_id == "tts_models/multilingual/multi-dataset/your_tts"
  127. assert speaker == "speaker_name"
  128. def test_multilingual_model_exists(self):
  129. """Prüft, dass ein multilinguales Modell verfügbar ist."""
  130. multilingual = [m for m, info in COQUI_MODELS.items() if info["language"] == "multi"]
  131. assert len(multilingual) >= 1
  132. # Integration Tests (benötigen echte Installation)
  133. @pytest.mark.integration
  134. class TestCoquiTTSIntegration:
  135. """Integrationstests für Coqui TTS (benötigen Installation)."""
  136. @pytest.fixture(autouse=True)
  137. def skip_if_not_installed(self):
  138. """Überspringt Tests wenn TTS nicht installiert ist."""
  139. pytest.importorskip("TTS", reason="Coqui TTS nicht installiert")
  140. @pytest.fixture
  141. def provider_with_model(self, tmp_path):
  142. """Erstellt einen Provider mit echtem Modell."""
  143. config = TTSConfig(language="de-DE", voice_id="tts_models/de/thorsten/tacotron2-DDC")
  144. return CoquiTTSProvider(
  145. config=config,
  146. models_dir=tmp_path / "models",
  147. model_name="tts_models/de/thorsten/tacotron2-DDC",
  148. use_cuda=False,
  149. )
  150. @pytest.mark.asyncio
  151. @pytest.mark.slow
  152. async def test_full_synthesis_flow(self, provider_with_model):
  153. """Testet den vollständigen Synthese-Flow."""
  154. await provider_with_model.initialize()
  155. result = await provider_with_model.synthesize("Hallo Welt")
  156. assert result.audio_data is not None
  157. assert len(result.audio_data) > 0
  158. assert result.sample_rate > 0
  159. assert result.duration_seconds > 0
  160. assert result.provider == "coqui"
  161. await provider_with_model.shutdown()