auswertung.rueckrufnummer wurde schon immer extrahiert, aber im Leitstand nie angezeigt - nur die CLIP-Nummer war sichtbar. Ruft jemand von fremdem Anschluss an und nennt eine andere Rueckrufnummer, ging die im UI unter. Neue Zeile "☎️ Rückruf an: ..." nur wenn sie sich von der CLIP-Nummer unterscheidet (sonst Redundanz), klickbar (tel:), mit demselben "Text"- Quellenlabel wie beim Namen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs |
||
|---|---|---|
| docs | ||
| pipe | ||
| prompts | ||
| telefon | ||
| test | ||
| .env.example | ||
| .gitignore | ||
| README.md | ||
| requirements.txt | ||
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.
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
- Die Pipe (fertig): Audio → Transkript → Kategorie → Ablage.
- Telefonie (steht): FreeSWITCH nimmt über den Plusnet-Trunk an, spielt die Ansage, nimmt auf und legt die Datei im Eingang der Pipe ab.
- 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:8b # ~5,2 GB, reines Textmodell (kein Vision-Ballast)
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-confauf/opt/homebrew/etc/freeswitch; sonst landen Trunk und Dialplan in einem Verzeichnis, das jedes Homebrew-Upgrade überschreibt. - Event-Socket-Port.
fs_clierwartet 8021. Ist der belegt, inautoload_configs/event_socket.conf.xmleinen freien Port eintragen undFS_PORTinstarten.shanpassen. - 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_PROXYauf 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 Menschenaufnahme.<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 nachIn 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
notfallnicht 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 nachErledigtverschoben 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 auf0.0.0.0(z. B.LEITSTAND_HOST=100.x.x.xbei 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.txterfüllt die ersten beiden Punkte noch nicht (kein Notfall-Hinweis, keine Aufnahme-Einwilligung) — Text anpassen undansage_bauen.shneu laufen lassen, bevor die Anlage im echten Praxisbetrieb ans Netz geht.
