| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- # -*- coding: utf-8 -*-
- """
- STT (Speech-to-Text) Interface.
- Definiert die Schnittstelle für STT-Provider (Plugins).
- """
- from abc import ABC, abstractmethod
- from dataclasses import dataclass, field
- from datetime import datetime
- from enum import Enum
- from typing import Any, AsyncIterator
- class STTState(Enum):
- """Zustand des STT-Providers."""
- UNINITIALIZED = "uninitialized"
- READY = "ready"
- PROCESSING = "processing"
- ERROR = "error"
- @dataclass
- class STTConfig:
- """Konfiguration für STT-Provider."""
- # Audio-Format
- sample_rate: int = 16000
- channels: int = 1
- sample_width: int = 2 # 16-bit = 2 bytes
- # Sprache
- language: str = "de-DE"
- alternative_languages: list[str] = field(default_factory=list)
- # Verhalten
- enable_punctuation: bool = True
- enable_profanity_filter: bool = False
- max_alternatives: int = 1
- # Performance
- use_gpu: bool = False
- num_threads: int = 4
- # Modell
- model_path: str | None = None
- model_name: str | None = None
- @dataclass
- class WordTiming:
- """Timing-Information für ein Wort."""
- word: str
- start_time: float # Sekunden
- end_time: float
- confidence: float = 1.0
- @dataclass
- class STTResult:
- """Ergebnis einer STT-Transkription."""
- # Hauptergebnis
- text: str
- confidence: float = 1.0
- # Alternativen
- alternatives: list[str] = field(default_factory=list)
- # Metadaten
- language: str = "de-DE"
- duration_seconds: float = 0.0
- processing_time_ms: float = 0.0
- # Detaillierte Informationen
- word_timings: list[WordTiming] = field(default_factory=list)
- is_final: bool = True
- # Provider-Info
- provider: str = ""
- model: str = ""
- # Zusätzliche Daten
- raw_result: Any = None
- def to_dict(self) -> dict:
- """Konvertiert zu Dictionary."""
- return {
- "text": self.text,
- "confidence": self.confidence,
- "alternatives": self.alternatives,
- "language": self.language,
- "duration_seconds": self.duration_seconds,
- "processing_time_ms": self.processing_time_ms,
- "is_final": self.is_final,
- "provider": self.provider,
- "model": self.model,
- "word_timings": [
- {
- "word": w.word,
- "start": w.start_time,
- "end": w.end_time,
- "confidence": w.confidence,
- }
- for w in self.word_timings
- ],
- }
- class STTProvider(ABC):
- """
- Abstrakte Basisklasse für STT-Provider.
- Plugins implementieren diese Klasse um Speech-to-Text
- Funktionalität bereitzustellen.
- Events:
- - stt_started: STT-Verarbeitung gestartet
- - stt_completed: STT erfolgreich abgeschlossen
- - stt_error: Fehler bei STT
- - stt_partial: Teilergebnis (bei Streaming)
- """
- def __init__(self, config: STTConfig | None = None):
- """
- Initialisiert den Provider.
- Args:
- config: STT-Konfiguration
- """
- self._config = config or STTConfig()
- self._state = STTState.UNINITIALIZED
- self._model_loaded = False
- @property
- def config(self) -> STTConfig:
- """Aktuelle Konfiguration."""
- return self._config
- @property
- def state(self) -> STTState:
- """Aktueller Zustand."""
- return self._state
- @property
- def is_ready(self) -> bool:
- """Prüft ob Provider bereit ist."""
- return self._state == STTState.READY
- @property
- @abstractmethod
- def name(self) -> str:
- """Name des Providers (z.B. 'whisper', 'vosk')."""
- pass
- @property
- @abstractmethod
- def supported_languages(self) -> list[str]:
- """Liste unterstützter Sprachen."""
- pass
- @property
- def supports_streaming(self) -> bool:
- """Prüft ob Streaming unterstützt wird."""
- return False
- @property
- def supports_word_timings(self) -> bool:
- """Prüft ob Wort-Timings unterstützt werden."""
- return False
- # === Lifecycle ===
- @abstractmethod
- async def initialize(self) -> None:
- """
- Initialisiert den Provider (lädt Modell, etc.).
- Raises:
- RuntimeError: Bei Initialisierungsfehler
- """
- pass
- @abstractmethod
- async def shutdown(self) -> None:
- """Fährt den Provider herunter und gibt Ressourcen frei."""
- pass
- # === Transkription ===
- @abstractmethod
- async def transcribe(
- self,
- audio_data: bytes,
- language: str | None = None,
- ) -> STTResult:
- """
- Transkribiert Audio-Daten zu Text.
- Args:
- audio_data: Raw PCM Audio-Daten (16kHz, 16-bit, mono)
- language: Sprache (optional, default aus Config)
- Returns:
- STTResult mit Transkription
- Raises:
- RuntimeError: Bei Transkriptionsfehler
- """
- pass
- async def transcribe_file(
- self,
- file_path: str,
- language: str | None = None,
- ) -> STTResult:
- """
- Transkribiert eine Audio-Datei.
- Args:
- file_path: Pfad zur Audio-Datei
- language: Sprache (optional)
- Returns:
- STTResult mit Transkription
- """
- import wave
- with wave.open(file_path, "rb") as wf:
- audio_data = wf.readframes(wf.getnframes())
- return await self.transcribe(audio_data, language)
- # === Streaming (optional) ===
- async def transcribe_stream(
- self,
- audio_stream: AsyncIterator[bytes],
- language: str | None = None,
- ) -> AsyncIterator[STTResult]:
- """
- Transkribiert einen Audio-Stream (Streaming-Modus).
- Args:
- audio_stream: Async Iterator von Audio-Chunks
- language: Sprache (optional)
- Yields:
- STTResult mit Teilergebnissen und finalem Ergebnis
- """
- # Default-Implementierung: Sammle alle Daten und transkribiere
- chunks = []
- async for chunk in audio_stream:
- chunks.append(chunk)
- audio_data = b"".join(chunks)
- result = await self.transcribe(audio_data, language)
- yield result
- # === Utilities ===
- def validate_audio(self, audio_data: bytes) -> bool:
- """
- Validiert Audio-Daten.
- Args:
- audio_data: Raw Audio-Daten
- Returns:
- True wenn valide
- """
- if not audio_data:
- return False
- # Mindestlänge prüfen (0.1 Sekunden)
- min_samples = int(self._config.sample_rate * 0.1)
- min_bytes = min_samples * self._config.sample_width * self._config.channels
- return len(audio_data) >= min_bytes
- def get_audio_duration(self, audio_data: bytes) -> float:
- """
- Berechnet Audio-Dauer in Sekunden.
- Args:
- audio_data: Raw Audio-Daten
- Returns:
- Dauer in Sekunden
- """
- bytes_per_sample = self._config.sample_width * self._config.channels
- num_samples = len(audio_data) // bytes_per_sample
- return num_samples / self._config.sample_rate
|