diff --git a/pipe/categorize.py b/pipe/categorize.py index 5055c65..1b3f1e3 100644 --- a/pipe/categorize.py +++ b/pipe/categorize.py @@ -9,6 +9,7 @@ Zwei Backends (Auswahl siehe pipe.modellwahl): from __future__ import annotations import json +import re import time import urllib.error import urllib.request @@ -22,6 +23,35 @@ KATEGORIEN = [ ] DRINGLICHKEITEN = ["niedrig", "normal", "hoch", "notfall"] +# Deterministisches Sicherheitsnetz, unabhaengig vom LLM: dieselben Symptome, +# die der Prompt selbst als Notfall-Beispiele nennt (Atemnot, Brustschmerz, +# Bewusstlosigkeit, starke Blutung). Grund: getestet mit qwen3:8b, 3/3 +# identische Laeufe stuften "starke Brustschmerzen ... kriege kaum Luft" nur +# als "hoch" statt "notfall" ein - obwohl das Modell selbst "Brustschmerzen" +# und "Atemnot" als stichworte erkannte. Bei Notfall-Erkennung darf man sich +# nicht allein auf ein LLM verlassen. Eskaliert nur nach oben, nie nach unten +# - ein Fehlalarm kostet einen Blick, ein uebersehener Notfall kann einen +# Patienten kosten (siehe auch die "im Zweifel hoeher"-Regel im Prompt). +_NOTFALL_MUSTER = [re.compile(p, re.IGNORECASE) for p in ( + r"atemnot", + r"(kriege|bekomme|krieg)\w*\s+(kaum|keine|schwer)\s+luft", + r"kann\s+(kaum|nicht)\s+(mehr\s+)?atmen", + r"brust\w*schmerz", + r"schmerzen?\s+in\s+der\s+brust", + r"bewusstlos", + r"ohnm[aä]chtig", + r"nicht\s+ansprechbar", + r"blutet\s+(stark|heftig|sehr)", + r"starke?\s+blutung", +)] + + +def _notfall_sicherheitsnetz(transkript: str, d: dict) -> dict: + if d.get("dringlichkeit") != "notfall" and any(m.search(transkript) for m in _NOTFALL_MUSTER): + d["kategorie"] = "notfall" + d["dringlichkeit"] = "notfall" + return d + # JSON-Schema, das Ollama dem Modell als Ausgabeformat aufzwingt. _SCHEMA = { "type": "object", @@ -77,7 +107,8 @@ def kategorisiere(transkript: str) -> tuple[dict, dict]: meta["backend"] = backend meta["modell"] = modell meta["dauer_s"] = round(time.monotonic() - start, 2) - return _bereinige(ergebnis), meta + ergebnis = _notfall_sicherheitsnetz(transkript, _bereinige(ergebnis)) + return ergebnis, meta def _mit_ollama(transkript: str, modell: str) -> tuple[dict, dict]: diff --git a/pipe/server.py b/pipe/server.py index 3c696cc..00a7613 100644 --- a/pipe/server.py +++ b/pipe/server.py @@ -404,6 +404,9 @@ h1{font-size:19px;margin:0} .stand{color:var(--muted);font-size:13px;font-variant-numeric:tabular-nums;margin-left:auto} .modell-select{background:var(--karte);border:1px solid var(--rand);color:var(--fg);border-radius:8px; padding:6px 10px;font-size:13px;font-family:inherit;cursor:pointer;max-width:280px} +.verbindung-banner{display:none;align-items:center;gap:9px;background:var(--unklar);color:#1a1500; +font-weight:700;font-size:14px;padding:9px 14px;border-radius:9px;margin:9px 0} +.verbindung-banner.zeigen{display:flex} .dienste{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:9px} .dienst{background:var(--karte);border:1px solid var(--rand);border-radius:9px;padding:9px 11px; display:flex;align-items:center;gap:9px} @@ -493,6 +496,7 @@ color:var(--gut);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:
+ @@ -663,13 +667,37 @@ function zeichneVerlauf(v){ if(unten)el.scrollTop=el.scrollHeight; } +// Verbindungsverlust sichtbar machen statt still zu verharren: hole() +// schluckt Netzwerkfehler und gibt nur null zurueck - ohne dieses Tracking +// sah man dem Board nicht an, dass es seit X Minuten nicht mehr aktualisiert +// wurde. Erst ab 2 aufeinanderfolgenden Fehlversuchen zeigen (~6s), damit ein +// einzelner kurzer Netz-Hakler nicht sofort Alarm ausloest. +let verbindungsFehler=0, letzteAktualisierung=null; +function zeichneVerbindung(){ + const banner=document.getElementById('verbindungBanner'); + banner.classList.toggle('zeigen',verbindungsFehler>=2); + if(verbindungsFehler>=2){ + const seit=letzteAktualisierung?letzteAktualisierung.toLocaleTimeString('de-DE'):'nie'; + document.getElementById('verbindungText').textContent= + `Keine Verbindung zum Server — zuletzt aktualisiert: ${seit}`; + } +} + async function takt(){ if(pausiert)return; // nicht neu zeichnen, waehrend jemand zieht // Auch nicht neu zeichnen, waehrend eine Aufnahme laeuft - zeichneBoard() // ersetzt die Karten per innerHTML und reisst damit jedes spielende //