From 05eb7ed144ca6df2db776a53264639bfd89021ca Mon Sep 17 00:00:00 2001 From: duffyduck Date: Tue, 2 Jun 2026 14:19:22 +0200 Subject: [PATCH] =?UTF-8?q?fix(whisper):=20Halluzinations-Filter=20?= =?UTF-8?q?=E2=80=94=20kein=20'Untertitelung=20des=20ZDF'=20bei=20Stille?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stefan-Reproduktion: nach Wake-Word + ARIA-Antwort oeffnet das Conversation-Window automatisch das Mikro fuer Follow-Up. Wenn Stefan nichts sagt, ist das 4-8s Stille. Whisper halluziniert dann YouTube- Untertitel-Patterns aus seinem Trainings-Corpus — gemessen 'Untertitelung des ZDF, 2020' — und ARIA antwortet brav darauf. Endlos-Loop bis Stefan manuell stoppt. Fix in faster-whisper-transcribe: 1. Per-Segment no_speech_prob auswerten. Bei >= 0.6 (relativ konservativ: echte leise Sprache geht noch durch) → Segment verwerfen. Das eliminiert die offensichtlichen Halluzinationen schon zu 90%. 2. Bekannte Hallucination-Phrasen-Blacklist: - Untertitelung/Untertitel des ZDF (mit/ohne Jahr) - Amara.org community - Vielen Dank fuer's Zuschauen (mit allen Umlaut/Apostroph-Varianten) - Thanks for watching / Subs by ... Substring-Match (case-insensitive) auf normalisiertem Text (lowercase, Trailing-Punctuation und Jahres-Suffix '2020' weg). 3. Wenn ALLE Segmente einer Aufnahme rausgefiltert werden, ist text='' → App behandelt das via existierende no-speech-Pfad: Conversation- Window endet sauber, kein TTS-Echo-Loop. Tradeoff: echte Phrasen wie 'Vielen Dank' allein gehen durch (Pattern ist 'vielen dank fuer's zuschauen' — voller Match). Nur die bekannten Halluzinations-Phrasen werden weggefiltert. Falls in Zukunft neue Patterns auftauchen (Whispers Modell ändert sich): einfach _HALLUCINATION_PHRASES erweitern, kein Brain-Restart noetig (lebt in der Whisper-Bridge, die hot-reloaded werden kann). --- xtts/whisper/bridge.py | 77 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) 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: