diff --git a/app/api/ws.py b/app/api/ws.py index 9b3df72..ebdb075 100644 --- a/app/api/ws.py +++ b/app/api/ws.py @@ -212,15 +212,21 @@ async def _voice_turn(websocket, store, user, session_id, audio, fmt, options): return try: - # Gesprochene Sprache automatisch erkennen (egal welche). Weicht sie von der - # Zielsprache ab, wird die Anfrage in die Zielsprache übersetzt — so erscheint - # und verarbeitet das System sie immer in der eingestellten Sprache. - transcript, detected = await orchestrator.stt.transcribe_detect( - audio, fmt=fmt, language=None - ) - if (transcript and transcript.strip() and detected - and detected.lower() != (route.language or "").lower()): - transcript = await orchestrator.translate(transcript, route.language) + if settings.stt_force_language: + # Feste Zielsprache statt Auto-Detect -> robuster bei kurzen/verrauschten + # (Handy-)Clips; verhindert "falsche Sprache erkannt = Blödsinn". + transcript, _ = await orchestrator.stt.transcribe_detect( + audio, fmt=fmt, language=route.language + ) + else: + # Gesprochene Sprache automatisch erkennen; weicht sie von der Zielsprache + # ab, wird in die Zielsprache übersetzt (Fremdsprachen-Diktat). + transcript, detected = await orchestrator.stt.transcribe_detect( + audio, fmt=fmt, language=None + ) + if (transcript and transcript.strip() and detected + and detected.lower() != (route.language or "").lower()): + transcript = await orchestrator.translate(transcript, route.language) effective_voice = voice_for_route(route.tts_provider, route.language, route.voice_gender) except Exception as exc: await websocket.send_json({"type": "error", "status": 502, "detail": str(exc)}) diff --git a/app/config.py b/app/config.py index e3ff763..4c2ffc1 100644 --- a/app/config.py +++ b/app/config.py @@ -131,9 +131,10 @@ class Settings(BaseSettings): local_llm_max_tokens: int = 0 # 0 = serverseitiges Limit (-n) local_llm_temperature: float = 0.3 local_llm_top_p: float = 0.9 # Nucleus-Sampling (0.0–1.0) - faster_whisper_model: str = "base" # tiny|base|small|medium|large-v3 + faster_whisper_model: str = "small" # tiny|base|small|medium|large-v3 faster_whisper_device: str = "auto" # auto|cpu|cuda - faster_whisper_compute_type: str = "default" # default|int8|float16|int8_float16 + faster_whisper_compute_type: str = "int8" # default|int8|float16|int8_float16 + stt_force_language: bool = True # STT mit fester Zielsprache (robust); False = Auto-Detect + Übersetzung # --- Lokales TTS (piper) ------------------------------------------------- piper_bin: str = "piper" # Pfad/Name des piper-Binaries piper_voices_dir: str = str(Path.home() / ".local" / "share" / "piper" / "voices") diff --git a/app/providers/stt/faster_whisper.py b/app/providers/stt/faster_whisper.py index 6cd4399..d3b3821 100644 --- a/app/providers/stt/faster_whisper.py +++ b/app/providers/stt/faster_whisper.py @@ -42,7 +42,13 @@ class FasterWhisperProvider(STTProvider): self, audio_bytes: bytes, language: str | None ) -> tuple[str, str | None]: model = _load_model(self.model_size, self.device, self.compute_type) - segments, info = model.transcribe(io.BytesIO(audio_bytes), language=language) + segments, info = model.transcribe( + io.BytesIO(audio_bytes), + language=language, + vad_filter=True, # Stille/Rauschen entfernen -> weniger Halluzinationen + vad_parameters={"min_silence_duration_ms": 500}, + condition_on_previous_text=False, # kurze Befehle: kein Kontext-Drift + ) text = "".join(segment.text for segment in segments).strip() detected = getattr(info, "language", None) return text, detected diff --git a/tests/test_stt_quality.py b/tests/test_stt_quality.py new file mode 100644 index 0000000..c3ff295 --- /dev/null +++ b/tests/test_stt_quality.py @@ -0,0 +1,34 @@ +import app.providers.stt.faster_whisper as fw +from app.config import settings + + +def test_stt_config_defaults(): + assert settings.faster_whisper_model == "small" + assert settings.faster_whisper_compute_type == "int8" + assert settings.stt_force_language is True + + +class _Seg: + def __init__(self, text): + self.text = text + + +class _Info: + language = "de" + + +def test_transcribe_passes_vad_and_language(monkeypatch): + captured = {} + + class _FakeModel: + def transcribe(self, audio, **kw): + captured.update(kw) + return [_Seg("hallo welt")], _Info() + + monkeypatch.setattr(fw, "_load_model", lambda *a, **k: _FakeModel()) + prov = fw.FasterWhisperProvider(model_size="small", device="cpu", compute_type="int8") + text, detected = prov._transcribe_sync(b"AAAA", "de") + assert text == "hallo welt" and detected == "de" + assert captured["language"] == "de" + assert captured["vad_filter"] is True + assert captured["condition_on_previous_text"] is False diff --git a/tests/test_ws.py b/tests/test_ws.py index 51fcbdf..30c9fd5 100644 --- a/tests/test_ws.py +++ b/tests/test_ws.py @@ -179,6 +179,8 @@ def test_ws_voice_translates_foreign_language_to_target(monkeypatch): monkeypatch.setitem(deps.STT_REGISTRY, "ss", lambda s: StubSTT()) monkeypatch.setitem(deps.LLM_REGISTRY, "ll", lambda s: StubLLM()) monkeypatch.setitem(deps.TTS_REGISTRY, "tt", lambda s: StubTTS()) + # Übersetzungs-Pfad ist opt-in: feste Sprache abschalten -> Auto-Detect + Übersetzung. + monkeypatch.setattr("app.config.settings.stt_force_language", False) opts = {"stt_provider": "ss", "llm_provider": "ll", "tts_provider": "tt", "output_endpoint": "loopback", "language": "de"} with client.websocket_connect("/ws/voice") as ws: