diff --git a/pipe/process_call.py b/pipe/process_call.py index 735cbef..ca95d2b 100644 --- a/pipe/process_call.py +++ b/pipe/process_call.py @@ -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 …") - auswertung = categorize.kategorisiere(t.text) + 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", diff --git a/pipe/transcribe.py b/pipe/transcribe.py index a168406..8c74ae4 100644 --- a/pipe/transcribe.py +++ b/pipe/transcribe.py @@ -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, )