Backup-Mirror von github.com/Jeuners/Logpy-AgentOne
Find a file
Jeuner cc569d1ecb 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
2026-08-24 20:50:30 +02:00
docs fix: einrichten.sh bricht bei Leerzeichen im Projektpfad ab 2026-08-23 00:38:16 +02:00
pipe fix: bekannte Whisper-Halluzinationen filtern statt als Anliegen zu werten 2026-08-24 20:50:30 +02:00
prompts Praxis-Telefon-Agent: Anrufannahme, Transkription, Triage — vollständig on-prem 2026-08-22 22:37:17 +02:00
telefon feat: Mistral Voxtral als drittes Ansage-TTS-Backend, neuer Ansage-Text 2026-08-23 11:38:16 +02:00
test fix: FS_PORT konfigurierbar statt hardcoded, DECK_STAPEL-Quoting, Test-Audios 2026-08-23 00:04:12 +02:00
.env.example feat: Leitstand-Fixes, Sicher-Modus, Notfall-Alarm, Audit-Log + Loeschfristen 2026-08-24 19:13:09 +02:00
.gitignore Praxis-Telefon-Agent: Anrufannahme, Transkription, Triage — vollständig on-prem 2026-08-22 22:37:17 +02:00
README.md docs+feat: Loeschung selbstgesteuert statt Cron, README auf aktuellen Stand 2026-08-24 19:32:31 +02:00
requirements.txt docs: vollständige Einrichtungsanleitung, fehlende Abhängigkeiten ergänzt 2026-08-22 23:00:01 +02:00

Praxis-Telefon-Agent

Nimmt Anrufe (Sprachnachrichten) einer Hausarztpraxis entgegen, transkribiert sie lokal, kategorisiert das Anliegen und legt das Ergebnis strukturiert ab (lokal und optional in Nextcloud).

Vollständig on-prem — keine Patientendaten verlassen den Rechner.

Der Leitstand: Board mit Eingang, In Bearbeitung, Rückfragen und Erledigt, darüber der Verlauf, darunter die Dienste-Ampeln

Hinweis: Alle Namen, Rufnummern und Anliegen in diesem Repository sind erfunden — auch im Screenshot und in der Beispielkonfiguration. „Praxis Musterhausen" ist ein Platzhalter. Echte Zugangsdaten und Anrufdaten liegen außerhalb des Repositorys (.env, ablage/, telefon/).

Wer das Projekt auf einem weiteren Rechner einrichtet: docs/MITGABE.md sammelt die Erfahrungswerte, die in dieser Anleitung nicht stehen.

Stufen

  1. Die Pipe (fertig): Audio → Transkript → Kategorie → Ablage.
  2. Telefonie (steht): FreeSWITCH nimmt über den Plusnet-Trunk an, spielt die Ansage, nimmt auf und legt die Datei im Eingang der Pipe ab.
  3. Feinschliff (teilweise): Löschfristen , Notfall-Alarm — offen: Monitoring/Alerting bei Diensteausfall, Verschlüsselung at rest.

Bausteine

Datei Aufgabe
pipe/transcribe.py Spracherkennung (whisper.cpp / faster-whisper), lokal
pipe/categorize.py Kategorisierung via Ollama/Qwen, erzwungenes JSON-Schema
pipe/store.py Ablage lokal + Nextcloud (WebDAV, steckbar)
pipe/process_call.py Orchestrator / CLI
pipe/config.py Konfiguration aus .env
pipe/watch.py Beobachtet den Eingang und schiebt Aufnahmen durch die Pipe
pipe/server.py Leitstand: Live-Ansicht mit Status, Anrufen und Verlauf
pipe/protokoll.py Ereignisprotokoll (JSONL) für den Leitstand
pipe/audit.py Unveränderliches Audit-Log (append-only, siehe unten)
pipe/loeschen.py Löscht erledigte Anrufe nach Ablauf der Aufbewahrungsfrist
prompts/categorize_de.txt Kategorisierungs-Prompt (deutsch, mehrsprachig-tauglich)
telefon/ Ansage, FreeSWITCH-Vorlagen, Start- und Einrichtungsskripte

Kategorien

termin · rezept · ueberweisung · befund · verwaltung · beschwerden · notfall · rueckruf · sonstiges Dringlichkeit: niedrig · normal · hoch · notfall

Einrichtung

Getestet auf macOS (Apple Silicon). Rechnen Sie mit ~10 GB für Modelle.

1. Programme

brew install ffmpeg whisper-cpp freeswitch
brew install ollama && ollama serve &      # oder Ollama.app starten

2. Sprachmodell — ordnet die Anliegen ein:

ollama pull qwen3.5:latest                 # ~6,6 GB

Auf Rechnern mit 16 GB Arbeitsspeicher passt kein größeres Modell daneben, solange die Telefonie läuft.

3. Spracherkennung — whisper.cpp braucht ein ggml-Modell:

mkdir -p ~/whisper-models && cd ~/whisper-models
curl -LO https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin

4. Python-Pakete und Ansage-Stimme

pip3 install -r requirements.txt
# Piper lädt seine Stimmen über HTTPS - ohne CA-Bundle schlägt das fehl:
export SSL_CERT_FILE=$(python3 -m certifi)
python3 -m piper.download_voices --download-dir ~/piper-voices \
        de_DE-thorsten_emotional-medium

5. Konfiguration

cp .env.example .env      # SIP-Zugang, Nextcloud, Leitstand-Passwort eintragen
./telefon/ansage_bauen.sh
./telefon/freeswitch/einrichten.sh

6. Starten

./telefon/starten.sh      # prüft die Voraussetzungen und startet alles

Stolpersteine

  • FreeSWITCH liest aus dem Cellar. starten.sh übergibt deshalb -conf auf /opt/homebrew/etc/freeswitch; sonst landen Trunk und Dialplan in einem Verzeichnis, das jedes Homebrew-Upgrade überschreibt.
  • Event-Socket-Port. fs_cli erwartet 8021. Ist der belegt, in autoload_configs/event_socket.conf.xml einen freien Port eintragen und FS_PORT in starten.sh anpassen.
  • Rotierende Provider-Adressen. Löst der SIP-Hostname auf mehrere IPs auf, bricht die Registrierung ständig ab: die Anmeldung landet auf einem anderen Server als die Anfrage. SIP_PROXY auf einen Host setzen, der auf genau eine IP zeigt.
  • Der Leitstand im Netz verlangt LEITSTAND_PASS, sonst startet er nicht.

Nutzung

cp .env.example .env        # bei Bedarf anpassen
python3 -m pipe.process_call test/rezept.wav --nummer 021031234567

Ergebnis unter ablage/JJJJ-MM-TT/HH-MM-SS_<nummer>/:

  • meta.json — strukturierte Felder (maschinenlesbar)
  • zusammenfassung.md — für Menschen
  • aufnahme.<ext> — Audiokopie

Telefonie (Stufe 2)

FreeSWITCH registriert sich mit dem SIP-Konto der Praxis beim Provider, nimmt eingehende Anrufe an, spielt die Ansage und nimmt auf. Die Aufnahme landet unter telefon/eingang/JJJJMMTT-HHMMSS_<nummer>.wav; pipe.watch erkennt sie, schiebt sie durch die Pipe und räumt sie nach telefon/verarbeitet/ weg.

./telefon/ansage_bauen.sh              # Ansage aus telefon/ansage.txt erzeugen
./telefon/freeswitch/einrichten.sh     # Trunk + Dialplan in FreeSWITCH eintragen
./telefon/starten.sh                   # startet FreeSWITCH, Verarbeitung, Leitstand

Danach läuft der Leitstand unter http://127.0.0.1:8088 — das Arbeitsbrett der Praxis, unabhängig von Nextcloud:

  • Ampeln für Telefon, Verarbeitung, Spracherkennung und Nextcloud
  • die Anrufe als Board mit den Spalten Eingang · In Bearbeitung · Rückfragen · Erledigt, verschiebbar per Ziehen oder über die Knöpfe auf der Karte
  • je Karte Kategorie, Anrufer, Dringlichkeit als Farbe, Anliegen, Stichworte, Abspielknopf für die Aufnahme und das Transkript zum Aufklappen
  • die Rufnummer ist anklickbar (tel:) und startet den Rückruf in der Telefon-App des Rechners; liegt die Karte noch im Eingang, wandert sie dabei nach In Bearbeitung
  • darunter der laufende Verlauf — vom Klingeln über die Erkennung bis zur Ablage
  • Sicher-Modus (🙈-Knopf oben links): blurrt die Rufnummern auf allen Karten, für Screenshots/Bildschirmaufnahmen. Merkt sich den Zustand pro Browser (localStorage), übersteht also einen Reload.
  • Notfall-Alarm: liegt eine Karte mit Dringlichkeit notfall nicht im letzten Stapel (Erledigt), pulsiert ihr Rahmen, ein rotes Banner erscheint und alle 60s ertönt ein Doppel-Piepton (Web Audio API, keine Datei nötig) — bis die Karte nach Erledigt verschoben wird. Browser blocken Autoplay-Audio ohne vorherige Interaktion: ein Klick irgendwo auf die Seite schaltet den Ton frei.

Standardmäßig hört der Leitstand nur auf 127.0.0.1. Soll er von anderen Rechnern der Praxis erreichbar sein, LEITSTAND_HOST=0.0.0.0 setzen — dann ist LEITSTAND_PASS Pflicht, sonst startet er nicht: die Seiten zeigen Transkripte und Aufnahmen von Patienten, das darf nicht offen im Netz stehen. Der Schutz gilt auch für die Schnittstellen und die Audiodateien.

Wichtig zu 0.0.0.0: das bindet auf alle Netzwerk-Interfaces des Rechners — nicht nur auf ein VPN/Tailscale. Läuft der Rechner zusätzlich in einem normalen WLAN/LAN, ist der Leitstand (passwortgeschützt) auch dort erreichbar, nicht nur im VPN. Wer das nicht will, bindet gezielt auf die VPN-IP statt auf 0.0.0.0 (z. B. LEITSTAND_HOST=100.x.x.x bei Tailscale) oder blockt den Port per Firewall-Regel für alle anderen Interfaces.

Die Seite aktualisiert sich alle drei Sekunden; während man eine Karte zieht, pausiert das. Der Bearbeitungsstand liegt als stapel.json im jeweiligen Anrufordner, also lokal — die Übertragung nach Nextcloud-Deck kommt später.

Der SIP-Zugang steht in der .env (SIP_USER, SIP_DOMAIN, SIP_PASS). Das Einrichtungsskript trägt das Passwort in die FreeSWITCH-Konfiguration ein und setzt deren Rechte auf 600 — im Git liegen nur Vorlagen mit Platzhaltern.

Text ändern: telefon/ansage.txt bearbeiten, ansage_bauen.sh erneut ausführen. Für den Praxisbetrieb muss die Ansage den Notruf 112 nennen und auf die Aufzeichnung hinweisen, bevor der Signalton kommt — beides ist bei einer Arztpraxis Pflicht, siehe „Offener Punkt" unter Datenschutz weiter unten.

Gesprochen wird lokal, wahlweise mit Piper (natürlicher, Modelle unter ~/piper-voices) oder dem macOS-say als Rückfall. Die Stimme de_DE-thorsten_emotional-medium kennt acht Sprechweisen, wählbar über ANSAGE_EMOTION: 0 amused · 1 angry · 2 disgusted · 3 drunk · 4 neutral · 5 sleepy · 6 surprised · 7 whisper.

Nextcloud aktivieren

NEXTCLOUD_URL, NEXTCLOUD_USER, NEXTCLOUD_PASS in .env setzen (App-Passwort, nicht das Login-Passwort). Dann lädt jeder Anruf zusätzlich nach NEXTCLOUD_ORDNER/JJJJ-MM-TT/…. Fehlt die Konfiguration, bleibt alles lokal.

Deck-Board

Einschalten mit NEXTCLOUD_DECK=1 in der .env — ohne diesen Schalter legt die Pipe kein Board an, auch wenn DECK_STAPEL und DECK_TEILEN gesetzt sind.

Jeder Anruf bekommt eine Karte auf dem Board Anrufe. Die Stapel bilden den Arbeitsablauf ab — Eingang · In Bearbeitung · Rückfragen · Erledigt, anpassbar über DECK_STAPEL. Neue Anrufe landen immer im ersten Stapel; die Praxis zieht sie weiter. Kategorie und Anrufer stehen im Kartentitel (17:52 · Rezept · Klaus Allofs), die Dringlichkeit als farbiges Label. Das Board gehört dem Konto, mit dem die Pipe arbeitet — ohne Freigabe legt sie zwar Karten an, die Praxis sieht aber ein leeres Deck. Deshalb DECK_TEILEN in der .env auf die Nextcloud-Nutzer setzen (kommagetrennt), die es sehen sollen; die Freigabe wird bei jedem Anruf sichergestellt.

Aufbewahrungsfrist & Audit-Log

LOESCHFRIST_TAGE in der .env (0 = aus) legt fest, nach wie vielen Tagen ein erledigter Anruf (Aufnahme + Transkript, kompletter Ordner) gelöscht wird — lokal und, falls hochgeladen, auch bei Nextcloud. Nur Anrufe im letzten Stapel (Erledigt) werden angefasst; alles andere bleibt unangetastet, egal wie alt — niemand soll einen liegen gebliebenen Anruf verlieren, bevor ihn jemand gesehen hat.

Kein Cron/launchd nötig: pipe.watch (läuft ohnehin dauerhaft, siehe telefon/starten.sh) prüft das alle 24h selbst mit. Manueller Aufruf für Tests:

python3 -m pipe.loeschen --pruefen    # nur anzeigen, was fällig wäre
python3 -m pipe.loeschen              # tatsächlich löschen

Jede Löschung (und jedes andere Pipe-Ereignis) landet zusätzlich unveränderlich in telefon/audit.log — anders als verlauf.log (fürs Leitstand-UI, wird auf 2000 Zeilen gekürzt) wird diese Datei nie überschrieben, nur angehängt, und zusätzlich per macOS chflags uappnd gegen nachträgliches Ändern/Kürzen abgesichert (auch der eigene Prozess kommt danach nur noch per Anhängen rein).

Datenschutz (DSGVO)

Gesundheitsdaten — Pflichten für den Produktivbetrieb (Stufe 2):

  • Aufnahme-Ansage am Gesprächsanfang.
  • Notfall-Ansage zuerst (112 / 116117); der Agent gibt keine medizinischen Ratschläge.
  • Verschlüsselung at rest, Zugriffskontrolle.
  • Löschfristen — siehe oben (LOESCHFRIST_TAGE).
  • Verarbeitung lokal (Whisper + Qwen) — kein Cloud-Dienst im Datenpfad.

Offener Punkt: Die aktuelle telefon/ansage.txt erfüllt die ersten beiden Punkte noch nicht (kein Notfall-Hinweis, keine Aufnahme-Einwilligung) — Text anpassen und ansage_bauen.sh neu laufen lassen, bevor die Anlage im echten Praxisbetrieb ans Netz geht.