diff --git a/xtts/whisper/bridge.py b/xtts/whisper/bridge.py index 38794a6..7ec3e73 100644 --- a/xtts/whisper/bridge.py +++ b/xtts/whisper/bridge.py @@ -109,7 +109,27 @@ class WhisperRunner: segments, info = self.model.transcribe( audio, language=language, beam_size=beam_size, vad_filter=vad_filter, ) - text = " ".join(seg.text.strip() for seg in segments) + # Per-segment no_speech_prob auswerten: faster-whisper liefert das + # mit. Bei Stille/Rauschen halluziniert Whisper bekannte YouTube- + # Untertitel-Patterns ("Untertitelung des ZDF", "Vielen Dank fuer's + # Zuschauen", ...). Segmente mit hohem no_speech_prob filtern wir + # raus. Plus: bekannte Hallucination-Patterns explizit blacklisten. + kept = [] + for seg in segments: + # no_speech_prob: 1.0 = sicher Stille; 0.0 = sicher Sprache. + # Threshold 0.6 ist nicht zu strikt (echte leise Sprache geht + # noch durch) und nicht zu locker (Halluzinationen werden + # zuverlaessig erwischt). + nsp = getattr(seg, "no_speech_prob", 0.0) + if nsp is not None and nsp >= 0.6: + continue + stext = (seg.text or "").strip() + if not stext: + continue + if _is_known_hallucination(stext): + continue + kept.append(stext) + text = " ".join(kept) return text, info.duration loop = asyncio.get_event_loop() @@ -117,6 +137,61 @@ class WhisperRunner: return await loop.run_in_executor(None, _run) +# Bekannte Whisper-Halluzinations-Patterns. Tritt typisch bei Stille oder +# Rauschen auf — Whispers Trainings-Corpus enthaelt Stunden von YouTube- +# Videos mit diesen Untertitel-Outros. Substring-Match (case-insensitive) +# ueber gestrippten Text. Wenn ein Segment EXAKT (nach Normalisierung) so +# aussieht, ist's mit ~99% Sicherheit eine Halluzination. +_HALLUCINATION_PHRASES = ( + "untertitelung des zdf", + "untertitel im auftrag des zdf", + "untertitelung im auftrag des zdf", + "untertitel der amara.org community", + "untertitel von stephanie geiges", + "amara.org", + "untertitel: kerstin grass", + "vielen dank fuers zuschauen", + "vielen dank fürs zuschauen", + "vielen dank für's zuschauen", + "vielen dank fuer's zuschauen", + "vielen dank für das zuschauen", + "vielen dank fuer das zuschauen", + "danke für's zuschauen", + "danke fürs zuschauen", + "danke fuers zuschauen", + "subs by", + "subtitle by", + "subtitles by", + "thanks for watching", +) + + +def _normalize_for_hallu(text: str) -> str: + """Lowercase + trailing-Satzzeichen/Whitespace strippen. Jahreszahlen + (4 Ziffern am Ende) auch entfernen — 'Untertitelung des ZDF, 2020' + matcht damit auf 'untertitelung des zdf'.""" + t = text.lower().strip() + # Entferne trailing punctuation incl. comma+digits + while t and t[-1] in ".,!? \t\n": + t = t[:-1] + # 4-stellige Jahreszahl am Ende + import re + t = re.sub(r"[,\s]+\d{4}$", "", t).strip() + while t and t[-1] in ".,!? \t\n": + t = t[:-1] + return t + + +def _is_known_hallucination(text: str) -> bool: + norm = _normalize_for_hallu(text) + if not norm: + return True + for pat in _HALLUCINATION_PHRASES: + if pat in norm: + return True + return False + + def ffmpeg_to_float32(audio_b64: str, mime_type: str) -> np.ndarray: """Dekodiert beliebiges Audio-Format → 16kHz mono float32 PCM.""" if "mp4" in mime_type or "m4a" in mime_type or "aac" in mime_type: