stt.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. # -*- coding: utf-8 -*-
  2. """
  3. STT (Speech-to-Text) Interface.
  4. Definiert die Schnittstelle für STT-Provider (Plugins).
  5. """
  6. from abc import ABC, abstractmethod
  7. from dataclasses import dataclass, field
  8. from datetime import datetime
  9. from enum import Enum
  10. from typing import Any, AsyncIterator
  11. class STTState(Enum):
  12. """Zustand des STT-Providers."""
  13. UNINITIALIZED = "uninitialized"
  14. READY = "ready"
  15. PROCESSING = "processing"
  16. ERROR = "error"
  17. @dataclass
  18. class STTConfig:
  19. """Konfiguration für STT-Provider."""
  20. # Audio-Format
  21. sample_rate: int = 16000
  22. channels: int = 1
  23. sample_width: int = 2 # 16-bit = 2 bytes
  24. # Sprache
  25. language: str = "de-DE"
  26. alternative_languages: list[str] = field(default_factory=list)
  27. # Verhalten
  28. enable_punctuation: bool = True
  29. enable_profanity_filter: bool = False
  30. max_alternatives: int = 1
  31. # Performance
  32. use_gpu: bool = False
  33. num_threads: int = 4
  34. # Modell
  35. model_path: str | None = None
  36. model_name: str | None = None
  37. @dataclass
  38. class WordTiming:
  39. """Timing-Information für ein Wort."""
  40. word: str
  41. start_time: float # Sekunden
  42. end_time: float
  43. confidence: float = 1.0
  44. @dataclass
  45. class STTResult:
  46. """Ergebnis einer STT-Transkription."""
  47. # Hauptergebnis
  48. text: str
  49. confidence: float = 1.0
  50. # Alternativen
  51. alternatives: list[str] = field(default_factory=list)
  52. # Metadaten
  53. language: str = "de-DE"
  54. duration_seconds: float = 0.0
  55. processing_time_ms: float = 0.0
  56. # Detaillierte Informationen
  57. word_timings: list[WordTiming] = field(default_factory=list)
  58. is_final: bool = True
  59. # Provider-Info
  60. provider: str = ""
  61. model: str = ""
  62. # Zusätzliche Daten
  63. raw_result: Any = None
  64. def to_dict(self) -> dict:
  65. """Konvertiert zu Dictionary."""
  66. return {
  67. "text": self.text,
  68. "confidence": self.confidence,
  69. "alternatives": self.alternatives,
  70. "language": self.language,
  71. "duration_seconds": self.duration_seconds,
  72. "processing_time_ms": self.processing_time_ms,
  73. "is_final": self.is_final,
  74. "provider": self.provider,
  75. "model": self.model,
  76. "word_timings": [
  77. {
  78. "word": w.word,
  79. "start": w.start_time,
  80. "end": w.end_time,
  81. "confidence": w.confidence,
  82. }
  83. for w in self.word_timings
  84. ],
  85. }
  86. class STTProvider(ABC):
  87. """
  88. Abstrakte Basisklasse für STT-Provider.
  89. Plugins implementieren diese Klasse um Speech-to-Text
  90. Funktionalität bereitzustellen.
  91. Events:
  92. - stt_started: STT-Verarbeitung gestartet
  93. - stt_completed: STT erfolgreich abgeschlossen
  94. - stt_error: Fehler bei STT
  95. - stt_partial: Teilergebnis (bei Streaming)
  96. """
  97. def __init__(self, config: STTConfig | None = None):
  98. """
  99. Initialisiert den Provider.
  100. Args:
  101. config: STT-Konfiguration
  102. """
  103. self._config = config or STTConfig()
  104. self._state = STTState.UNINITIALIZED
  105. self._model_loaded = False
  106. @property
  107. def config(self) -> STTConfig:
  108. """Aktuelle Konfiguration."""
  109. return self._config
  110. @property
  111. def state(self) -> STTState:
  112. """Aktueller Zustand."""
  113. return self._state
  114. @property
  115. def is_ready(self) -> bool:
  116. """Prüft ob Provider bereit ist."""
  117. return self._state == STTState.READY
  118. @property
  119. @abstractmethod
  120. def name(self) -> str:
  121. """Name des Providers (z.B. 'whisper', 'vosk')."""
  122. pass
  123. @property
  124. @abstractmethod
  125. def supported_languages(self) -> list[str]:
  126. """Liste unterstützter Sprachen."""
  127. pass
  128. @property
  129. def supports_streaming(self) -> bool:
  130. """Prüft ob Streaming unterstützt wird."""
  131. return False
  132. @property
  133. def supports_word_timings(self) -> bool:
  134. """Prüft ob Wort-Timings unterstützt werden."""
  135. return False
  136. # === Lifecycle ===
  137. @abstractmethod
  138. async def initialize(self) -> None:
  139. """
  140. Initialisiert den Provider (lädt Modell, etc.).
  141. Raises:
  142. RuntimeError: Bei Initialisierungsfehler
  143. """
  144. pass
  145. @abstractmethod
  146. async def shutdown(self) -> None:
  147. """Fährt den Provider herunter und gibt Ressourcen frei."""
  148. pass
  149. # === Transkription ===
  150. @abstractmethod
  151. async def transcribe(
  152. self,
  153. audio_data: bytes,
  154. language: str | None = None,
  155. ) -> STTResult:
  156. """
  157. Transkribiert Audio-Daten zu Text.
  158. Args:
  159. audio_data: Raw PCM Audio-Daten (16kHz, 16-bit, mono)
  160. language: Sprache (optional, default aus Config)
  161. Returns:
  162. STTResult mit Transkription
  163. Raises:
  164. RuntimeError: Bei Transkriptionsfehler
  165. """
  166. pass
  167. async def transcribe_file(
  168. self,
  169. file_path: str,
  170. language: str | None = None,
  171. ) -> STTResult:
  172. """
  173. Transkribiert eine Audio-Datei.
  174. Args:
  175. file_path: Pfad zur Audio-Datei
  176. language: Sprache (optional)
  177. Returns:
  178. STTResult mit Transkription
  179. """
  180. import wave
  181. with wave.open(file_path, "rb") as wf:
  182. audio_data = wf.readframes(wf.getnframes())
  183. return await self.transcribe(audio_data, language)
  184. # === Streaming (optional) ===
  185. async def transcribe_stream(
  186. self,
  187. audio_stream: AsyncIterator[bytes],
  188. language: str | None = None,
  189. ) -> AsyncIterator[STTResult]:
  190. """
  191. Transkribiert einen Audio-Stream (Streaming-Modus).
  192. Args:
  193. audio_stream: Async Iterator von Audio-Chunks
  194. language: Sprache (optional)
  195. Yields:
  196. STTResult mit Teilergebnissen und finalem Ergebnis
  197. """
  198. # Default-Implementierung: Sammle alle Daten und transkribiere
  199. chunks = []
  200. async for chunk in audio_stream:
  201. chunks.append(chunk)
  202. audio_data = b"".join(chunks)
  203. result = await self.transcribe(audio_data, language)
  204. yield result
  205. # === Utilities ===
  206. def validate_audio(self, audio_data: bytes) -> bool:
  207. """
  208. Validiert Audio-Daten.
  209. Args:
  210. audio_data: Raw Audio-Daten
  211. Returns:
  212. True wenn valide
  213. """
  214. if not audio_data:
  215. return False
  216. # Mindestlänge prüfen (0.1 Sekunden)
  217. min_samples = int(self._config.sample_rate * 0.1)
  218. min_bytes = min_samples * self._config.sample_width * self._config.channels
  219. return len(audio_data) >= min_bytes
  220. def get_audio_duration(self, audio_data: bytes) -> float:
  221. """
  222. Berechnet Audio-Dauer in Sekunden.
  223. Args:
  224. audio_data: Raw Audio-Daten
  225. Returns:
  226. Dauer in Sekunden
  227. """
  228. bytes_per_sample = self._config.sample_width * self._config.channels
  229. num_samples = len(audio_data) // bytes_per_sample
  230. return num_samples / self._config.sample_rate