diff --git a/docs/projektplan.md b/docs/projektplan.md index 47a9111..007f915 100644 --- a/docs/projektplan.md +++ b/docs/projektplan.md @@ -704,28 +704,37 @@ axum-extra = { version = "0.12", features = ["cookie"] } # passend zu axum 0.8 ## Entwicklungsreihenfolge ### Phase 1 — Server-Grundgerüst -- [ ] Axum Setup, Routing, Authentifizierung (API-Key → Arzt-Mapping) -- [ ] Filesystem-Struktur anlegen -- [ ] .env Konfiguration (dotenvy) -- [ ] Logging (tracing + tracing-appender) -- [ ] Upload-Endpunkt mit ACK-Response -- [ ] Docker Container für Axum auf Ubuntu Server (kein GPU nötig) -- [ ] faster-whisper Container mit HTTP-API (NVIDIA Container Toolkit) -- [ ] Ollama einrichten, `gemma3:4b` pullen +- [x] Axum Setup, Routing, Authentifizierung (API-Key → User-Mapping via `users.toml`) +- [x] Filesystem-Struktur anlegen +- [x] .env Konfiguration (dotenvy) +- [x] Logging (tracing + tracing-appender) +- [x] Upload-Endpunkt mit ACK-Response +- [x] Docker Container für Axum auf Ubuntu Server (kein GPU nötig) +- [x] faster-whisper Container mit HTTP-API (NVIDIA Container Toolkit) — jetzt eigener `whisper/`-Service, siehe Phase 2b.5 +- [x] Ollama einrichten, Modell gepullt (`gemma4:latest` statt `gemma3:4b`, siehe Abweichungen) - [ ] nginx auf Unraid + Let's Encrypt + Security Headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy) ### Phase 2 — Transkriptions-Pipeline -- [ ] Transkriptions-Queue (tokio mpsc channel) -- [ ] GPU-Phasen-Worker (Greedy-Zyklus: Whisper-Phase → Ollama-Phase → Aufräumen) -- [ ] faster-whisper HTTP-Client (reqwest, POST /transcribe, multipart Audio) -- [ ] Ollama HTTP-Client (reqwest, POST /api/chat, keep_alive: 0) -- [ ] Oneliner-Generierung (einmalig beim ersten Transkript, danach unveränderlich) -- [ ] Queue-Recovery bei Serverstart (Filesystem-Scan) +- [x] Transkriptions-Queue (tokio mpsc channel) +- [ ] GPU-Phasen-Worker (Greedy-Zyklus: Whisper-Phase → Ollama-Phase → Aufräumen) — aktuell kein striktes Phasenmodell, siehe Abweichungen +- [x] faster-whisper HTTP-Client (reqwest, POST `/asr`, multipart Audio) +- [x] Ollama HTTP-Client (reqwest, POST /api/chat) — `keep_alive=300` statt `0`, siehe Abweichungen +- [x] Oneliner-Generierung (einmalig, erst geschrieben wenn ein sinnvoller Oneliner vorliegt; sonst Retry beim nächsten Transkript) +- [x] Queue-Recovery bei Serverstart (Filesystem-Scan) - [ ] faster-whisper Health-Check + Retry - [ ] Ollama Health-Check + Retry - [ ] Lazy Cleanup bei Falllisten-Zugriff (zum Entfernen markierte Fälle vom Vortag löschen) - [ ] Retention-Prüfung bei Zugriff (Audio/Transkripte nach RETENTION_*_DAYS) +### Phase 2b.5 — Eigener faster-whisper-Service (`whisper/`) +- [x] FastAPI-Wrapper um `faster-whisper` mit fest verdrahteten Anti-Halluzinations-Params (`condition_on_previous_text=False`, `temperature=0.0`, `vad_filter=True`) +- [x] Endpoints: `POST /asr` (txt/json), `GET /health`, `GET /info` +- [x] Modell `large-v3-turbo` via `WHISPER_MODEL` umschaltbar, Modelle persistent in Docker-Volume +- [x] Hotwords-Support (faster-whisper 1.2.1) als Form-Feld durchgereicht +- [x] Dockerfile (CUDA 12.1 + cuDNN), `docker-compose.yml`, `deploy.sh` (rsync + `/health`-Polling) +- [x] README mit Begründung (Halluzinations-Befund), Build- und Deploy-Anleitung +- [x] Deployed auf minerva:9001, `WHISPER_URL` in `server/.env` umgestellt + ### Phase 3 — Fallverwaltung - [ ] Drei States: Empfangen → Transkribiert → Ausgewertet - [ ] State-Übergang: erst "Transkribiert" wenn alle Aufnahmen eines Falls fertig @@ -747,11 +756,11 @@ axum-extra = { version = "0.12", features = ["cookie"] } # passend zu axum 0.8 - [ ] `AuthenticatedArzt`-Extractor: Session-Token → Arzt auflösen, bei ungültiger/abgelaufener Session → Redirect `/web/login` - [ ] `ValidCaseId`-Extractor: `case_id` als UUIDv4 validieren (`uuid::Uuid::parse_str`), bei Fehler → 400 - [ ] Login-Seite (`GET /web/login`, `POST /web/login`) -- [ ] askama Templates +- [x] askama Templates (Listenansicht `cases.html`) - [ ] SSE-Endpunkt (`GET /web/events`), Session-Check bei Aufbau + periodisch bei Heartbeat - [ ] Vanilla-JS EventSource-Client -- [ ] Übersicht mit drei States (Empfangen/Transkribiert/Ausgewertet) -- [ ] Fall-Detail: Transkripte pro Aufnahme einsehen +- [ ] Übersicht mit drei States (Empfangen/Transkribiert/Ausgewertet) — aktuell nur flache Liste mit Transkript + Oneliner +- [x] Fall-Detail: Transkripte pro Aufnahme einsehen (inline in Listenansicht, Audio-Stream `/web/audio/...`) - [ ] Fall abschließen / Alle abschließen - [ ] Dokumentansicht - [ ] Nachtrag-Hinweis bei Aufnahmen nach Abschluss (⚠ "N neue Aufnahmen seit Abschluss") @@ -765,6 +774,8 @@ axum-extra = { version = "0.12", features = ["cookie"] } # passend zu axum 0.8 **Hinweis:** Ein früher Proof-of-Concept (minimale Aufnahme + Upload auf echter Pixel Watch) sollte parallel zu Phase 2–3 stattfinden, um Wear-OS-spezifische Einschränkungen (Doze-Mode, Foreground-Service-Limits, Battery-Optimization) frühzeitig aufzudecken. +**Stand-in, solange keine Hardware:** `scripts/dictate.sh` simuliert den Watch-Flow via ffmpeg-PulseAudio-Aufnahme + Upload gegen den laufenden Server (Modi: neuer Fall / aktuellen Fall fortsetzen / refresh). State in `/tmp/doctate-current-case`. So lassen sich Transkription und Oneliner ohne Watch testen. + - [ ] Wear OS Projekt in Android Studio - [ ] Jetpack Compose UI (ein Screen pro Eintrag, fullscreen, Swipe-Navigation) - [ ] Vertikale Fallliste (Neu ganz oben, heutige Fälle darunter) @@ -823,15 +834,15 @@ axum-extra = { version = "0.12", features = ["cookie"] } # passend zu axum 0.8 | Thema | Status | |---|---| -| Whisper Modell | large-v3 via faster-whisper (CTranslate2, int8) — ggf. large-v3-turbo oder deutsches Fine-Tuning | -| Ollama Modell | Gemma 3 4B — ggf. Llama 3.1 8B falls Qualität nicht reicht | +| Whisper Modell | **Entschieden:** `large-v3-turbo` via eigener `whisper/`-Service, float16, optional Hotwords per Request. Qualitativer Vergleich gegen `large-v3` zeigte: gleichwertig bei Fachvokabular, nur Hotwords bringen echte Fehlerreduktion. Deutsches Fine-Tuning (primeline) getestet, lieferte schlechtere Ergebnisse. Umschaltbar via `WHISPER_MODEL`. | +| Ollama Modell | **Entschieden (vorläufig):** `gemma4:latest` (läuft). Llama 3.1 8B als Option, wenn Oneliner-Qualität nicht reicht. | | Ionos Modell | Noch zu evaluieren | | Watch Hardware | Pixel Watch 2 oder 3 (LTE empfohlen) | | Authentifizierung Browser | Entschieden: Serverseitiges Session-Token (256-Bit, Cookie mit `Secure`/`HttpOnly`/`SameSite=Strict`), Ablauf 8 h. User-Identität ausschließlich aus Session, nie aus URL (IDOR-Prävention). Login gegen User-Passwort in `users.toml`. | | Prompt-Qualität | Erfordert Testing mit echten Diktaten | | Transkript editierbar? | Read-only oder editierbar vor Abschluss — offen | | Sicheres Löschen | Reicht rm oder Overwrite nötig? — offen | -| faster-whisper HTTP-API | Bestehender Container nutzen oder eigener FastAPI-Wrapper — offen | +| faster-whisper HTTP-API | **Entschieden:** eigener FastAPI-Wrapper (`whisper/`). Grund siehe Phase 2b.5 / Abweichungen. | | Recovery-UI (Webinterface) | Dedizierte Ansicht für verspätet eingetroffene Aufnahmen (Anhören / Ins Dokument / Verwerfen) — optional, Bedarf im Echtbetrieb evaluieren | --- @@ -858,3 +869,9 @@ axum-extra = { version = "0.12", features = ["cookie"] } # passend zu axum 0.8 | m4a-Preprocessing | nicht erwähnt | Server remuxt m4a mit `-movflags faststart` vor Whisper-Aufruf | Bekannter Bug in whisper-asr-webservice ([Issue #97](https://github.com/ahmetoner/whisper-asr-webservice/issues/97)): m4a-Dateien >20s liefern leere Transkripte, weil Android `MediaRecorder` das `moov atom` ans Dateiende schreibt. `faststart`-Remuxing (verlustfrei, <1s) verschiebt die Metadaten an den Anfang und behebt das Problem. ffmpeg wird dafür im Axum-Container mitgeliefert. | | Terminologie | "Arzt/Ärzte" | "User" | Rollen-System: nicht nur Ärzte, auch MTAs und ggf. Admins | | Erster Upload neue case_id | "Fall unbekannt → ACK gone" | Neuer Fall anlegen, ACK "received" | Sonst würde der allererste Upload einer neuen case_id immer abgelehnt. "gone" nur für gelöschte Fälle. | +| STT-Container | `onerahmet/openai-whisper-asr-webservice` | Eigener `whisper/`-FastAPI-Wrapper auf Port 9001 | Kein verfügbarer Wrapper (ahmetoner, speaches, linuxserver, hwdsl2) reicht `condition_on_previous_text=False` und feste `temperature=0.0` durch. Tests zeigten 2/13 Runs mit katastrophalen Halluzinationen (russisch/chinesisch) bei `large-v3` mit Wrapper-Defaults. Eigener Service hardcodet die Anti-Halluzinations-Params. | +| Whisper-Modell | `large-v3` int8 | `large-v3-turbo` float16 + optional Hotwords | Turbo: gleicher 32-Layer-Encoder wie large-v3, Decoder destilliert auf 4 Layer — gleiche Qualität auf europäischen Sprachen, ~1.6 GB VRAM statt ~3 GB. Ermöglicht Koexistenz mit Ollama im 12-GB-VRAM. Hotwords (faster-whisper 1.2.1) bringen den größten Qualitätssprung bei Fachvokabular (in Experiment 8/8 korrekt vs. 3/8 ohne). | +| Ollama-Modell | `gemma3:4b` | `gemma4:latest` | Neuere Version beim Aufsetzen des Ollama-Containers verfügbar. Llama 3.1 8B als Fallback, falls Oneliner-Qualität nicht reicht. | +| GPU-Phasen-Worker | Strikter Whisper↔Ollama-Wechsel mit Ollama `keep_alive=0` | Turbo (~1.6 GB) + Ollama gemma4 (~9 GB) bleiben parallel geladen, `OLLAMA_KEEP_ALIVE=300` | Dank Turbo-VRAM-Footprint passen beide gleichzeitig in die GPU. Spart das Modell-Reload (~2 s pro Oneliner). Strikter Phasenwechsel bleibt als Fallback, falls später größere Modelle (`large-v3`, Llama 3.1 8B) zurückkehren. | +| faster-whisper Port | 10300 (ahmetoner) | 9001 (eigener Service) | Neuer Service auf freiem Port; alter Container optional parallel belassen. | +| Test-Client für Watch-Flow | Erst ab Phase 5 mit echter Hardware | `scripts/dictate.sh` ab Phase 2/3 als Stand-in (ffmpeg + curl + interaktiver c/n/r/q-Loop) | Erlaubt End-to-End-Tests der Server-Pipeline ohne Watch-Hardware, solange die Pixel Watch nicht verfügbar ist. |