Add per-user whisper settings for hotwords and initial prompt
This commit introduces the capability to configure user-specific Whisper settings, including language, hotwords, and initial prompts. These settings are stored in `users.toml` and are passed to the Whisper service for more tailored transcription results. New unit tests have been added to verify that the `transcribe` function correctly forwards these optional settings to the Whisper client and omits them when they are not provided. Additionally, a new directory `tests/fixtures/dictations` has been created to store M4A audio files, their expected transcripts, and associated hotword files. This serves as a regression corpus for the Whisper pipeline. A README file explains the structure and conventions for adding new fixtures. Several new fixtures have been added, covering various medical domains and transcription scenarios.
This commit is contained in:
@@ -725,6 +725,7 @@ axum-extra = { version = "0.12", features = ["cookie"] } # passend zu axum 0.8
|
||||
- [ ] 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)
|
||||
- [x] Per-User Whisper-Settings in `users.toml` (`[user.whisper]` mit `language`, `hotwords`, `initial_prompt`) — Worker reicht sie pro Upload an den `whisper/`-Service durch
|
||||
|
||||
### 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`)
|
||||
@@ -880,3 +881,4 @@ axum-extra = { version = "0.12", features = ["cookie"] } # passend zu axum 0.8
|
||||
| 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. |
|
||||
| Admin-Log vs. Arzt-UI | Nur Arzt-UI geplant | Zusätzlich frühes Admin-Log unter `/web/` (flache Liste aller Fälle, Transkripte, Oneliner) | Gebaut, bevor das Arzt-UI (Session, States, Fall-Detail) existiert, um die Pipeline während Entwicklung inspizieren zu können. Soll bleiben, aber später hinter `role = "admin"` geschützt; das Arzt-UI wird separat entwickelt. |
|
||||
| Hotwords | Nicht vorgesehen | Per-User-Feld `[user.whisper].hotwords` in `users.toml` | Experiment zeigte: Hotwords sind der einzige Hebel, der Whisper-Fachvokabular zuverlässig verbessert (8/8 vs. 3/8 ohne). Pro-User statt global, weil Kardiologie/Orthopädie/Psychiatrie völlig andere Begriffslisten brauchen. Gleiches Schema lässt sich später auf `[user.oneliner]` erweitern. |
|
||||
|
||||
@@ -135,6 +135,76 @@ async fn whisper_client_times_out() {
|
||||
assert!(matches!(err, WhisperError::Http(_)), "expected Http error, got {err:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn whisper_client_forwards_hotwords_and_prompt_when_set() {
|
||||
let server = MockServer::start().await;
|
||||
|
||||
// Accept any POST; inspect captured body below. language query param is
|
||||
// still matched strictly to verify override from settings.
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/asr"))
|
||||
.and(query_param("language", "en"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string("ok"))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let settings = WhisperUserSettings {
|
||||
language: Some("en".into()),
|
||||
hotwords: Some("HOCM Valsalva".into()),
|
||||
initial_prompt: Some("Kardiologie".into()),
|
||||
};
|
||||
transcribe(
|
||||
&client,
|
||||
&server.uri(),
|
||||
&fixture("sample.m4a"),
|
||||
Duration::from_secs(10),
|
||||
&settings,
|
||||
)
|
||||
.await
|
||||
.expect("transcribe failed");
|
||||
|
||||
let received = server.received_requests().await.unwrap();
|
||||
let body = String::from_utf8_lossy(&received[0].body);
|
||||
assert!(body.contains("name=\"hotwords\""), "hotwords part missing");
|
||||
assert!(body.contains("HOCM Valsalva"), "hotwords value missing: {body}");
|
||||
assert!(body.contains("name=\"initial_prompt\""), "initial_prompt part missing");
|
||||
assert!(body.contains("Kardiologie"), "initial_prompt value missing");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn whisper_client_omits_empty_optional_fields() {
|
||||
let server = MockServer::start().await;
|
||||
|
||||
// Default settings = all None → only `audio_file` part, language defaults
|
||||
// to `de`. Assert that neither optional field appears in the body.
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/asr"))
|
||||
.and(query_param("language", "de"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string("ok"))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
transcribe(
|
||||
&client,
|
||||
&server.uri(),
|
||||
&fixture("sample.m4a"),
|
||||
Duration::from_secs(10),
|
||||
&WhisperUserSettings::default(),
|
||||
)
|
||||
.await
|
||||
.expect("transcribe failed");
|
||||
|
||||
// Inspect the captured request to confirm absence of the optional parts.
|
||||
let received = server.received_requests().await.unwrap();
|
||||
let body = String::from_utf8_lossy(&received[0].body);
|
||||
assert!(!body.contains("name=\"hotwords\""), "hotwords part leaked: {body}");
|
||||
assert!(!body.contains("name=\"initial_prompt\""), "initial_prompt part leaked: {body}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn recovery_enqueues_only_pending_recordings() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
# Dictation fixtures
|
||||
|
||||
Persistent, committed test recordings. These are the regression corpus for the
|
||||
Whisper pipeline — `scripts/regress_whisper.sh` iterates over them and compares
|
||||
live output against the golden transcripts.
|
||||
|
||||
## File layout
|
||||
|
||||
Per fixture, three files share a common stem:
|
||||
|
||||
| File | Required | Purpose |
|
||||
|------------------------|----------|-------------------------------------------------------------------|
|
||||
| `<name>.m4a` | yes | Raw AAC recording as produced by `scripts/dictate.sh` / the watch |
|
||||
| `<name>.expected.txt` | yes | Golden transcript, hand-reviewed — the regression target |
|
||||
| `<name>.hotwords.txt` | no | One-line hotwords list if the fixture is meant to test Fachvokabular |
|
||||
| `<name>.notes.md` | no | Short description: what this case exercises, any gotchas |
|
||||
|
||||
## Naming convention
|
||||
|
||||
`<domain>_<topic>[_<variant>]` — all lowercase, snake_case, no spaces:
|
||||
|
||||
- `cardio_hocm_valsalva`
|
||||
- `cardio_hocm_valsalva_quiet` (same content, low SNR variant)
|
||||
- `ortho_knie_meniskus`
|
||||
- `psych_ptbs_flashbacks`
|
||||
|
||||
The prefix encodes the medical domain so we can group by specialty when we
|
||||
start per-user hotwords tuning.
|
||||
|
||||
## Adding a new fixture
|
||||
|
||||
1. Record via `./scripts/dictate.sh -n`. Speak a realistic case.
|
||||
2. Note the case UUID the script prints.
|
||||
3. Copy `$DATA_PATH/<slug>/open/<case_id>/<timestamp>.m4a`
|
||||
→ `tests/fixtures/dictations/<name>.m4a`.
|
||||
4. Copy `$DATA_PATH/<slug>/open/<case_id>/<timestamp>.transcript.txt`
|
||||
→ `tests/fixtures/dictations/<name>.expected.txt`.
|
||||
5. **Review the transcript** — fix any Whisper errors by hand. This is the
|
||||
golden reference; accept nothing that is actually wrong.
|
||||
6. If the fixture tests Fachvokabular, add `<name>.hotwords.txt` with the
|
||||
domain-specific words, one space-separated line.
|
||||
7. Commit all three/four files together.
|
||||
|
||||
## What not to commit here
|
||||
|
||||
- Personal / real patient data — everything here must be synthetic.
|
||||
- Long recordings (>2 min) — keep fixtures tight, we want fast regression runs.
|
||||
- Multiple takes of the same content unless they test a specific variant (noise,
|
||||
speaker, tempo).
|
||||
@@ -0,0 +1 @@
|
||||
Beginn mit Bisoprolol 2,5 mg täglich bei erhöhtem Blutdruck und Herzfrequenz. Anpassung nach Diagnostik. Weiteres Vorgehen wie besprochen.
|
||||
@@ -0,0 +1 @@
|
||||
Palpitationen Dyspnoe Ruhe-EKG Repolarisationsstörungen Belastungs-EKG Echokardiographie Bisoprolol HOCM Septumhypertrophie LVOT Valsalva Troponin Ejektionsfraktion Mitralklappeninsuffizienz
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
Patient zur kardialen Abklärung vorstellig bei bekannten Palpitationen und diskreter Dyspnoe. Ruhe-EKG zeigt unspezifische Repolarisationsstörungen. Weiterführendes Belastungs-EKG und Echokardiographie geplant. Medikamentöse Einstellung nach Ergebnissen. Wiedervorstellung nach Diagnostik. Bitte beachten Sie, dass dies keine medizinische Beratung darstellt; konsultieren Sie einen Arzt.
|
||||
@@ -0,0 +1 @@
|
||||
Palpitationen Dyspnoe Ruhe-EKG Repolarisationsstörungen Belastungs-EKG Echokardiographie Bisoprolol HOCM Septumhypertrophie LVOT Valsalva Troponin Ejektionsfraktion Mitralklappeninsuffizienz
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
Patient präsentiert sich mit systemischer Hypertonie, konsekutivem Linksherzversagen und kritischer kardialer Dekompensation. Echokardiographisch verifizierte reduzierte Ejektionsfraktion bei signifikanter Mitralklappeninsuffizienz. Zusätzlich chronische Niereninsuffizienz Stadium IV mit Azidose und Elektrolytentgleisung. Verdacht auf paraneoplastisches Syndrom bei progredienter Malignomlast. Stationäres Therapiemanagement mittels intravenöser Diuretika, Inotropika und Dialyse. Prognostische Relevanz hochgradig infaust. Weiteres Vorgehen wie besprochen.
|
||||
@@ -0,0 +1 @@
|
||||
Palpitationen Dyspnoe Ruhe-EKG Repolarisationsstörungen Belastungs-EKG Echokardiographie Bisoprolol HOCM Septumhypertrophie LVOT Valsalva Troponin Ejektionsfraktion Mitralklappeninsuffizienz
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
Verordnung von Mebeverin 135 mg dreimal täglich präprandial. Weiteres Vorgehen wie besprochen.
|
||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
Patient stellt sich vor mit krampfartigen Bauchschmerzen und Blähungen. Symptome bestehen seit einigen Wochen, verstärkt postprandial. Verdacht auf Reizdarmsyndrom. Stuhluntersuchung und großes Blutbild veranlasst. Ernährungsumstellung und probiotische Behandlung empfohlen. Wiedervorstellung nach Befundeingang.
|
||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
Zusätzlich symptomatische Behandlung mit Spasmolytikum bei akuten Beschwerden. Weiteres Vorgehen wie besprochen.
|
||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
Zusätzlich Anmeldung beim nationalen Krebsregister veranlasst. Weiteres Vorgehen wie besprochen.
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
Patient zur Erstvorstellung bei histologisch gesichertem Mammakarzinom. Stadium und biologisches Profil in Abklärung. Interdisziplinäres Therapiekonzept wird erstellt, potenziell Chemotherapie und Operation. Weitere Diagnostik veranlasst. Wiedervorstellung nach Ergebnissen. Bitte beachten Sie, dass dies keine medizinische Beratung darstellt;—
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
Patient zeigt sich uneinsichtig bezüglich der notwendigen Diagnostik und Therapie, äußert Complianceprobleme. Weiteres Vorgehen wie besprochen.
|
||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
Patient mit akuten Knieschmerzen nach Bagatelltrauma vorstellig. Verdacht auf Meniskusläsion. Klinische Diagnostik erfolgt, MRT zur Bestätigung angefordert. Konservative Therapie mittels Physiotherapie und Schmerzmitteln begonnen. Wiedervorstellung nach MRT.
|
||||
Binary file not shown.
Reference in New Issue
Block a user