# 03 — Aufnahme-Pipeline
Vom Klick auf „Stop“ bis zur fertigen Analyse. Crash-sicher: Audio + DB-Zeile werden **vor** der langen
Verarbeitung persistiert; Transkription/Analyse laufen asynchron im Hintergrund (GUI pollt).
## Ablauf
```mermaid
sequenceDiagram
participant GUI as Web-GUI
participant SRV as server.js
participant REC as recorder.js
participant WH as Whisper (NPU-Encoder)
participant DI as diar.js (sherpa, CPU)
participant LLM as llm.js (Gemma, NPU)
participant DB as MariaDB
GUI->>SRV: (während der Aufnahme) POST /api/record/marker {name}
SRV->>REC: addMarker() → Namensmarker mit Offset im Puffer
GUI->>SRV: Stop
SRV->>REC: stopAndSave()
REC->>REC: parecord beenden, WAV nach recordings/ verschieben
REC-)REC: encodeMp3() — Hintergrund, kein await
REC->>DB: WAV-Zeile (status=transcribing)
SRV->>DB: addMarkers() → Tabelle markers
Note over REC: kehrt in ~0,04 s zurück — GUI frei
GUI->>GUI: Kontextbox klappt auf (Freitext, seit 2026-08-17)
GUI->>SRV: PATCH /api/recordings/:id/context {context_note}
SRV->>DB: setContextNote() → recordings.context_note
Note over GUI,DB: Die Transkription läuft noch Minuten — der getippte Kontext ist rechtzeitig da (Kap. 05)
REC->>REC: A76-Takt kappen (1,6 GHz)
REC->>WH: transcribeWav() [BatchedInferencePipeline, batch=8]
WH-->>REC: Wörter + Zeitstempel
REC->>DI: diarize(…, {markers}) [Timeout = Dauer × 2,5, min. 20 min]
Note over DI: v2: Fenster-Embeddings + gelernte Schwelle, Marker als must-/cannot-link (Kap. 04)
DI-->>REC: Sprecher-Segmente
REC->>DB: setTranscript (status=done)
SRV->>SRV: applyMarkers(): Cluster benennen + Stimmen lernen
REC->>REC: finally: Takt zurück
SRV->>LLM: analyzeRecording() (fire-and-forget, analysis_status=pending)
LLM->>DB: getContextNote() — Notiz erst JETZT gelesen (Kap. 05)
LLM->>DB: Titel/Summary/Teilnehmer/Todos (analysis_status=done, analysis_ms)
GUI->>SRV: Polling analysis_status
```
## Pfadwahl Transkription
```mermaid
flowchart TB
A["WAV fertig"] --> B{"Dauer ≤ 30 s? (SHORT_SEC)"}
B -- ja --> C["Warm-Daemon transcribe_daemon_cpu.py CPU-only, koexistiert mit residentem rkllm kein Modell-Swap → 10-s-Clip in ~53 s statt 120 s"]
B -- nein --> D["transcribe_cli: faster-whisper large-v3-turbo Encoder auf NPU, Decoder CPU, batch=8 RTF ≈ 0,39"]
C --> E["Diarization"]
D --> E
```
- `TRANSCRIBER=npu` (Default) / `=cpu` als Fallback-Schalter in recorder.js.
- Whisper-Kalibrierung: medium beam1 hatte 11,3 % WER vs. large-v3 — beam5 schlechter, `--initial-prompt`
mit Begriffsliste **zerstört** den Output (Echo-Loop). Eigennamen-Korrektur passiert deshalb im Text-Layer (Glossar, Kap. 05).
## Zustandsmodell einer Aufnahme
```mermaid
stateDiagram-v2
[*] --> recording
recording --> transcribing: stopAndSave() — WAV/MP3/Zeile SOFORT persistiert
transcribing --> done_transcript: Transkript + Diarization gespeichert
transcribing --> failed: Absturz/Fehler → GUI zeigt Retry
failed --> transcribing: POST /retranscribe
done_transcript --> analyzing: analysis_status=pending
analyzing --> analyzed: analysis_status=done (analysis_ms gesetzt)
analyzing --> analysis_error: GUI-Button "Erneut versuchen"
analysis_error --> analyzing: POST /reanalyze bzw. /reanalyze-llm
```
- Hängende `transcribing`-Zeile = Absturz mitten in der Diarization (Transkript wird erst NACH Diar gespeichert)
→ kein Auto-Resume, GUI-Button „Neu transkribieren“.
- **NPU-Lock:** alle schweren Jobs (transcribe / analyze / re-diarize) laufen seriell durch `withNpuLock` —
zwei gleichzeitig fertig werdende Aufnahmen haben sich sonst gegenseitig zerlegt.
- **Takt-Cap gilt für ALLE drei schweren Pfade:** `transcribeWav`, `rediarize` UND seit 2026-07-16 auch
`analyzeRecording` (LLM lief vorher ungedeckelt auf 95–97 °C, Kap. 05).
- **GUI-Anzeige „⏱ Analysezeit“:** `analysis_started_at`/`analysis_ms` in der DB; beim Polling auf
`analysis_status` achten, NICHT auf `status` (sonst wirkt die Summary „leer“).
- **Kopf der Detailansicht:** `🗓 ` (Helfer `recStamp(r)` aus `created_at`)
steht seit 2026-09-29 vor der Analysezeit — aus Suche, Projekt-Pin oder Direktlink heraus ist links keine
Listenzeile markiert, der Aufnahmezeitpunkt wäre sonst nirgends sichtbar.
- **GUI-Wartetext bei `transcribing`:** „📝 Transkription läuft… lange Aufnahmen dauern.“
(`web/src/main.js:1378`). Seit 2026-09-09 ohne den Klammerzusatz zum Takt-Cap — auf Wunsch des Nutzers;
das Cap selbst ist unverändert aktiv.
## Der Stop-Knopf: sofortige Antwort, MP3 im Hintergrund
Bis 2026-08-20 hing `POST /api/record/stop` so lange, wie der MP3-Encode dauerte — **gemessen ~1,4 s pro
aufgenommener Minute** (aus `wav_file`-Zeitstempel + `duration_ms` gegen `created_at`):
| Aufnahme | Dauer | Stop-Antwort |
|---|---|---|
| #114 | 35,5 min | 50,9 s |
| #118 | 30,8 min | 43,8 s |
| #111 | 20,8 min | 30,4 s |
| #117 | 1,1 min | 2,0 s |
Sichtbar war in dieser Zeit **nichts**: die GUI grante nur den Knopf aus (`opacity .55`) und stoppte die
Uhr. Der Nutzer hielt die Anwendung für hängengeblieben, wechselte den Reiter — was `render()` auslöst und
den Knopf frisch (also klickbar) aufbaut, obwohl `state.recording` noch `true` war — und drückte ein
zweites Mal Stop. Der zweite Stop lief zwangsläufig in `not recording`, weil `stopAndSave()` das
`current`-Objekt schon in seiner ersten Zeile auf `null` setzt.
```mermaid
sequenceDiagram
autonumber
participant U as Nutzer
participant GUI as Web-GUI
participant SRV as server.js
participant FF as ffmpeg (Hintergrund)
rect rgba(229,89,94,.12)
Note over U,SRV: VORHER — Encode blockierte die Antwort
U->>GUI: Klick Stop
GUI->>SRV: POST /api/record/stop
Note over GUI: Knopf grau, Uhr steht, kein Toast (bis 51 s)
U->>GUI: Reiterwechsel → render() → Knopf wieder aktiv
U->>GUI: Klick Stop (zum zweiten Mal)
GUI->>SRV: POST /api/record/stop
SRV--)GUI: 500 „not recording"
SRV--)GUI: (erste Antwort trifft jetzt ein)
end
rect rgba(126,182,124,.12)
Note over U,FF: NACHHER — Antwort in 0,04 s
U->>GUI: Klick Stop
GUI->>GUI: state.stopping = true → ⏳, gesperrt, Toast mit geschätzter Wartezeit
GUI->>SRV: POST /api/record/stop
SRV->>SRV: parecord beenden, WAV verschieben, DB-Zeile
SRV-)FF: encodeMp3() ohne await
SRV--)GUI: {id, …} nach ~0,04 s
FF-->>FF: rec_*.mp3.part → rename → rec_*.mp3
GUI->>GUI: Player lädt alle 5 s nach, solange /media 404 liefert
end
```
Drei Teile hat der Fix:
1. **`recorder.js stopAndSave()`** verschiebt erst das WAV nach `recordings/` (das ist die Arbeitskopie für
Whisper) und ruft `encodeMp3()` dann **ohne `await`** auf. Geschrieben wird nach `.mp3.part`,
danach `fs.renameSync` — so bekommt der Browser nie eine halbe Datei zu hören, sondern bis zum
Umbenennen ein 404. **`-f mp3` ist dabei Pflicht:** ffmpeg leitet den Muxer aus der Dateiendung ab und
scheitert an `.part` mit `Error initializing the muxer … Invalid argument` (erst so aufgefallen —
beide Encode-Zweige fielen still aus, das MP3 fehlte ganz).
2. **`state.stopping` in der GUI** überlebt jeden `render()`. Der Knopf bleibt darum auch nach einem
Reiterwechsel gesperrt (zeigt ⏳), die Uhr friert ein, die Live-Karte in der Liste sagt „Aufnahme wird
gespeichert…", und ein Toast nennt die geschätzte Wartezeit. Ein zweiter Klick wird verworfen; kommt
trotzdem ein `not recording` zurück (zweites Browserfenster, F5 mitten im Speichern), meldet die GUI
sachlich „Aufnahme war bereits gestoppt" statt eines roten Fehlers.
3. **Der Player lädt nach:** `#mp3wait` zeigt „Wiedergabe-Datei wird noch erzeugt", das `error`-Ereignis
des `