fix: bekannte Whisper-Halluzinationen filtern statt als Anliegen zu werten

"Untertitelung des ZDF, 2020" (Whisper-Trainingsdaten-Artefakt bei Stille/
sofortigem Auflegen) tauchte 5x identisch auf sehr kurzen Aufnahmen (1,7-4s)
auf und wurde von der Kategorisierung als echtes Anliegen fehlinterpretiert.
transcribe.py erkennt jetzt eine kleine Liste bekannter Whisper-
Halluzinationsphrasen (ZDF/WDR/NDR/SWR-Untertitel-Boilerplate u.ae.) und
leert den Text stattdessen.

Dabei aufgefallen: categorize.kategorisiere() wirft bei leerem Transkript
absichtlich einen Fehler - das griff bisher nie, weil Whisper nie leer
zurueckkam. Ohne Fix waere jedes "Anrufer legt nach dem Signalton sofort
auf" (normales, haeufiges Verhalten) jetzt als Kategorisierungs-Fehler nach
telefon/fehler/ verschwunden statt auf dem Board zu erscheinen.
process_call.verarbeite() faengt das jetzt ab und legt einen regulaeren
"kein Anliegen hinterlassen"-Datensatz an (sonstiges/niedrig).

Die 5 bereits gespeicherten ZDF-Fehleintraege in ablage/ wurden manuell
nachkorrigiert (transkript/auswertung), Stapel-/Archiv-Status blieb
unangetastet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
This commit is contained in:
Jeuner 2026-08-24 20:50:30 +02:00
parent 2e767f3737
commit cc569d1ecb
2 changed files with 49 additions and 2 deletions

View file

@ -28,7 +28,24 @@ def verarbeite(audio: str, anrufer_nummer: str | None = None,
print(f" Text: {t.text[:160]}{'' if len(t.text) > 160 else ''}")
print("▸ Kategorisiere …")
if t.text.strip():
auswertung = categorize.kategorisiere(t.text)
else:
# Kein Text erkannt: Anrufer hat nach dem Signalton nicht gesprochen
# oder sofort aufgelegt (oder eine bekannte Whisper-Halluzination
# wurde rausgefiltert, siehe transcribe._ist_bekannte_halluzination).
# Das ist ein normaler, haeufiger Fall - kein Kategorisierungs-Fehler,
# der die Aufnahme nach telefon/fehler/ verschwinden lassen sollte.
print(" Kein Text erkannt - kein Anliegen hinterlassen.")
auswertung = {
"kategorie": "sonstiges", "dringlichkeit": "niedrig",
"anrufer_name": None, "rueckrufnummer": None,
"rueckruf_gewuenscht": False, "sprache": t.sprache or "de",
"anliegen_kurz": "Kein Anliegen hinterlassen - Anrufer hat nach "
"dem Signalton nicht gesprochen oder sofort "
"aufgelegt.",
"stichworte": [],
}
print(f" Kategorie: {auswertung['kategorie']} · Dringlichkeit: {auswertung['dringlichkeit']}")
protokoll.schreibe(
"notfall" if auswertung["dringlichkeit"] == "notfall" else "anruf",

View file

@ -12,6 +12,7 @@ from __future__ import annotations
import json
import os
import re
import subprocess
import tempfile
from dataclasses import dataclass, field
@ -30,6 +31,30 @@ class Transkript:
segmente: list[dict] = field(default_factory=list)
# Bekannte Whisper-Halluzinationen: bei Stille/Rauschen (z.B. Anrufer legt
# gleich nach dem Signalton auf) erfindet Whisper haeufig auswendig gelernte
# Untertitel-Textbausteine aus den Trainingsdaten, statt "kein Text" zu
# liefern - "Untertitelung des ZDF, 2020" trat bei uns mehrfach auf identisch
# kurzen (1,7-4s) Aufnahmen auf und wurde anschliessend von der Kategorisierung
# als echtes Anliegen fehlinterpretiert. whisper.cpp's eingebauter
# no-speech-Schwellwert (--no-speech-thold, Default 0.6) faengt das nicht
# zuverlaessig ab. Statt einer echten VAD (zusaetzliches Modell noetig) reicht
# hier ein Blocklist-Abgleich gegen die dokumentiert haeufigsten Phrasen.
_BEKANNTE_HALLUZINATIONEN = [
re.compile(p, re.IGNORECASE) for p in (
r"untertitel(ung)?\s+(des|im auftrag des|von)\s+(zdf|wdr|ndr|swr|br|mdr|hr|rbb)",
r"copyright\s+(wdr|zdf|ndr|swr|br)\s*\d{4}",
r"untertitel\s+von\s+stephanie\s+geiges",
r"vielen\s+dank\s+f(ü|u)rs?\s+zuschauen",
r"amara\.org",
)
]
def _ist_bekannte_halluzination(text: str) -> bool:
return any(m.search(text) for m in _BEKANNTE_HALLUZINATIONEN)
def _letzte_zeile(ausgabe: str | None) -> str:
"""Letzte nicht-leere Zeile einer Fehlerausgabe (für knappe Meldungen)."""
zeilen = [z for z in (ausgabe or "").strip().splitlines() if z.strip()]
@ -117,6 +142,8 @@ def _mit_whispercpp(audio: Path) -> Transkript:
text_ganz = " ".join(s["text"] for s in segmente).strip()
sprache = roh.get("result", {}).get("language", config.WHISPER_SPRACHE)
dauer = segmente[-1]["ende"] if segmente else 0.0
if _ist_bekannte_halluzination(text_ganz):
text_ganz, segmente = "", []
return Transkript(text=text_ganz, sprache=sprache,
sprache_wahrscheinlichkeit=1.0, dauer=dauer, segmente=segmente)
@ -139,8 +166,11 @@ def _mit_faster_whisper(audio: Path) -> Transkript:
if t:
teile.append(t)
seg_liste.append({"start": round(s.start, 2), "ende": round(s.end, 2), "text": t})
text_ganz = " ".join(teile).strip()
if _ist_bekannte_halluzination(text_ganz):
text_ganz, seg_liste = "", []
return Transkript(
text=" ".join(teile).strip(), sprache=info.language,
text=text_ganz, sprache=info.language,
sprache_wahrscheinlichkeit=round(info.language_probability, 3),
dauer=round(info.duration, 2), segmente=seg_liste,
)