fix(whisper): Halluzinations-Filter — kein 'Untertitelung des ZDF' bei Stille
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).
This commit is contained in:
+76
-1
@@ -109,7 +109,27 @@ class WhisperRunner:
|
|||||||
segments, info = self.model.transcribe(
|
segments, info = self.model.transcribe(
|
||||||
audio, language=language, beam_size=beam_size, vad_filter=vad_filter,
|
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
|
return text, info.duration
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
@@ -117,6 +137,61 @@ class WhisperRunner:
|
|||||||
return await loop.run_in_executor(None, _run)
|
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:
|
def ffmpeg_to_float32(audio_b64: str, mime_type: str) -> np.ndarray:
|
||||||
"""Dekodiert beliebiges Audio-Format → 16kHz mono float32 PCM."""
|
"""Dekodiert beliebiges Audio-Format → 16kHz mono float32 PCM."""
|
||||||
if "mp4" in mime_type or "m4a" in mime_type or "aac" in mime_type:
|
if "mp4" in mime_type or "m4a" in mime_type or "aac" in mime_type:
|
||||||
|
|||||||
Reference in New Issue
Block a user