test_vosk_stt.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für das Vosk STT 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("vosk_plugin", plugin_path)
  14. vosk_module = importlib.util.module_from_spec(spec)
  15. spec.loader.exec_module(vosk_module)
  16. VoskSTTPlugin = vosk_module.VoskSTTPlugin
  17. VoskSTTProvider = vosk_module.VoskSTTProvider
  18. VOSK_MODELS = vosk_module.VOSK_MODELS
  19. from trixy_core.audio.stt import STTConfig, STTState
  20. class TestVoskModelsCatalog:
  21. """Tests für den Vosk-Modell-Katalog."""
  22. def test_models_catalog_not_empty(self):
  23. """Prüft, dass der Modellkatalog nicht leer ist."""
  24. assert len(VOSK_MODELS) > 0
  25. def test_models_have_required_fields(self):
  26. """Prüft, dass alle Modelle die erforderlichen Felder haben."""
  27. required_fields = ["name", "url", "size", "description"]
  28. for model_id, model_info in VOSK_MODELS.items():
  29. for field in required_fields:
  30. assert field in model_info, f"Modell {model_id} fehlt Feld '{field}'"
  31. def test_german_models_available(self):
  32. """Prüft, dass deutsche Modelle verfügbar sind."""
  33. assert "de" in VOSK_MODELS
  34. assert "de-small" in VOSK_MODELS
  35. def test_model_urls_are_valid(self):
  36. """Prüft, dass alle Modell-URLs gültige URLs sind."""
  37. for model_id, model_info in VOSK_MODELS.items():
  38. url = model_info["url"]
  39. assert url.startswith("https://"), f"Modell {model_id} hat ungültige URL"
  40. assert ".zip" in url, f"Modell {model_id} URL sollte ein ZIP-Archiv sein"
  41. class TestVoskSTTProvider:
  42. """Tests für den VoskSTTProvider."""
  43. @pytest.fixture
  44. def provider(self, tmp_path):
  45. """Erstellt einen Provider für Tests."""
  46. config = STTConfig(language="de-DE")
  47. return VoskSTTProvider(
  48. config=config,
  49. models_dir=tmp_path / "models",
  50. model_name="de-small",
  51. auto_download=False,
  52. )
  53. def test_provider_name(self, provider):
  54. """Prüft den Provider-Namen."""
  55. assert provider.name == "vosk"
  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. def test_supports_streaming(self, provider):
  62. """Prüft Streaming-Unterstützung."""
  63. assert provider.supports_streaming is True
  64. def test_supports_word_timings(self, provider):
  65. """Prüft Wort-Timings-Unterstützung."""
  66. assert provider.supports_word_timings is True
  67. def test_initial_state(self, provider):
  68. """Prüft den initialen Zustand."""
  69. assert provider._model_loaded is False
  70. assert provider._model is None
  71. assert provider._recognizer is None
  72. @pytest.mark.asyncio
  73. async def test_initialize_without_model_raises(self, provider):
  74. """Prüft, dass initialize ohne Modell fehlschlägt."""
  75. with pytest.raises(RuntimeError):
  76. await provider.initialize()
  77. @pytest.mark.asyncio
  78. async def test_transcribe_without_init_raises(self, provider, sample_audio_16khz):
  79. """Prüft, dass transcribe ohne Initialisierung fehlschlägt."""
  80. with pytest.raises(RuntimeError, match="Vosk-Modell nicht geladen"):
  81. await provider.transcribe(sample_audio_16khz)
  82. def test_calculate_confidence_empty(self, provider):
  83. """Prüft die Confidence-Berechnung bei leerem Ergebnis."""
  84. result = {"text": ""}
  85. assert provider._calculate_confidence(result) == 0.0
  86. def test_calculate_confidence_with_words(self, provider):
  87. """Prüft die Confidence-Berechnung mit Wörtern."""
  88. result = {
  89. "result": [
  90. {"word": "hallo", "conf": 0.9},
  91. {"word": "welt", "conf": 0.8},
  92. ]
  93. }
  94. confidence = provider._calculate_confidence(result)
  95. assert confidence == pytest.approx(0.85)
  96. class TestVoskSTTPlugin:
  97. """Tests für das VoskSTTPlugin."""
  98. @pytest.fixture
  99. def plugin(self, mock_application, tmp_path):
  100. """Erstellt ein Plugin für Tests."""
  101. plugin_path = tmp_path / "stt_vosk"
  102. plugin_path.mkdir()
  103. (plugin_path / "models").mkdir()
  104. config = {
  105. "name": "Vosk STT",
  106. "enabled": True,
  107. "model": "de-small",
  108. "language": "de-DE",
  109. "auto_download": False,
  110. }
  111. return VoskSTTPlugin(mock_application, plugin_path, config)
  112. def test_plugin_initialization(self, plugin):
  113. """Prüft die Plugin-Initialisierung."""
  114. assert plugin._provider is None
  115. assert plugin.config.get("model") == "de-small"
  116. def test_plugin_path(self, plugin):
  117. """Prüft den Plugin-Pfad."""
  118. assert plugin.plugin_path.exists()
  119. @pytest.mark.asyncio
  120. async def test_on_unload_without_provider(self, plugin):
  121. """Prüft on_unload ohne initialisierten Provider."""
  122. await plugin.on_unload()
  123. assert plugin._provider is None
  124. class TestVoskDownload:
  125. """Tests für die Download-Funktionalität."""
  126. @pytest.mark.asyncio
  127. async def test_download_model_unknown_model_raises(self, tmp_path):
  128. """Prüft, dass unbekannte Modelle einen Fehler werfen."""
  129. config = STTConfig(language="de-DE")
  130. provider = VoskSTTProvider(
  131. config=config,
  132. models_dir=tmp_path / "models",
  133. model_name="unknown_model",
  134. auto_download=True,
  135. )
  136. with pytest.raises(RuntimeError, match="Unbekanntes Modell"):
  137. await provider.initialize()
  138. class TestVoskEventHandling:
  139. """Tests für die Event-Verarbeitung."""
  140. @pytest.fixture
  141. def plugin_with_provider(self, mock_application, tmp_path):
  142. """Erstellt ein Plugin mit Mock-Provider."""
  143. plugin_path = tmp_path / "stt_vosk"
  144. plugin_path.mkdir()
  145. (plugin_path / "models").mkdir()
  146. config = {
  147. "name": "Vosk STT",
  148. "enabled": True,
  149. "model": "de-small",
  150. "language": "de-DE",
  151. "auto_download": False,
  152. }
  153. plugin = VoskSTTPlugin(mock_application, plugin_path, config)
  154. # Mock-Provider setzen
  155. mock_provider = MagicMock()
  156. mock_provider.is_ready = True
  157. mock_provider.transcribe = AsyncMock(return_value=MagicMock(
  158. text="test text",
  159. confidence=0.95,
  160. language="de-DE",
  161. duration_seconds=1.0,
  162. processing_time_ms=100,
  163. word_timings=[],
  164. ))
  165. plugin._provider = mock_provider
  166. return plugin
  167. # Integration Tests (benötigen echte Installation)
  168. @pytest.mark.integration
  169. @pytest.mark.requires_vosk
  170. class TestVoskIntegration:
  171. """Integrationstests für Vosk STT (benötigen Installation)."""
  172. @pytest.fixture
  173. def provider_with_model(self, tmp_path):
  174. """Erstellt einen Provider mit echtem Modell."""
  175. config = STTConfig(language="de-DE")
  176. return VoskSTTProvider(
  177. config=config,
  178. models_dir=tmp_path / "models",
  179. model_name="de-small",
  180. auto_download=True,
  181. )
  182. @pytest.mark.asyncio
  183. @pytest.mark.slow
  184. async def test_full_transcription_flow(self, provider_with_model, sample_audio_16khz):
  185. """Testet den vollständigen Transkriptions-Flow."""
  186. await provider_with_model.initialize()
  187. result = await provider_with_model.transcribe(sample_audio_16khz)
  188. assert result.text is not None
  189. assert result.provider == "vosk"
  190. assert result.processing_time_ms > 0
  191. await provider_with_model.shutdown()