Files
Aime/docs/03-aufnahme-pipeline.md
T
schakal00783andClaude Opus 5 d9554c590c fix(audio): Wiedergabe-MP3 als CBR ohne Xing-Tabelle -- Wortmarkierung war 28 s voraus
Die Markierung im Transkript stand auf einem Wort, gesprochen wurde eins vier
Zeilen weiter oben (gemessen an #85: 24 s Vorlauf).

Nicht die Ursache -- jeweils per Gegenprobe ausgeschlossen: die Wortzeiten
(Ausschnitte bei 600/1240/3400 s einzeln transkribiert, enthalten exakt die
Wörter der DB), MP3- vs WAV-Zeitachse (identisch), ffmpeg-Seek (trifft),
ontimeupdate-Logik (korrekt).

Ursache war der Sprung im Browser: in einer VBR-Datei gibt es keine feste
Byte-pro-Sekunde-Regel, also nutzt er die Xing-Tabelle -- 100 Stützstellen,
bei 63 Minuten ein 38-s-Raster. Er landet auf der Stützstelle davor, meldet
über currentTime aber die angefragte Zeit. Nachgerechnet: Ziel 20:40 spielt
ab 20:11,5 (-28,5 s).

Fix: -b:a 64k -ar 48000 -ac 1 -write_xing 0 statt -qscale:a 3. Byte-Position
proportional zur Zeit. Die Tabelle muss auch bei CBR weg, sie rastert auf
1/256 der Dateigröße (~15 s pro Stunde, Restfehler bis -6,4 s gemessen). Ohne
sie rechnet der Decoder linear: Restfehler 0,02 s an neun Stichpunkten.

lab/recode-cbr.sh schreibt den Altbestand einmalig um (Quelle WAV, sonst die
MP3 selbst), lab/tocerr.py ist das Messwerkzeug dahinter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 23:40:29 +02:00

132 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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->>DB: WAV + MP3 + Zeile (status=transcribing)
SRV->>DB: addMarkers() → Tabelle markers
Note over REC: kehrt nach ~1,5 s zurück — GUI frei
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,<br/>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: 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?<br/>(SHORT_SEC)"}
B -- ja --> C["Warm-Daemon transcribe_daemon_cpu.py<br/>CPU-only, koexistiert mit residentem rkllm<br/>kein Modell-Swap → 10-s-Clip in ~53 s statt 120 s"]
B -- nein --> D["transcribe_cli: faster-whisper large-v3-turbo<br/>Encoder auf NPU, Decoder CPU, batch=8<br/>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“).
## Wiedergabe-MP3: konstante Bitrate, keine Xing-Tabelle
Die WAV ist die Arbeitskopie (Whisper, Diarization), die MP3 ist nur fürs Abspielen im Browser —
`highpass=f=80` gegen Rumpeln, `loudnorm` für gleichmäßige Lautstärke. Beim Codec sind aber **zwei
Einstellungen nicht verhandelbar**, sonst läuft die Wortmarkierung im Transkript aus dem Takt:
```
-codec:a libmp3lame -b:a 64k -ar 48000 -ac 1 -write_xing 0
```
**Kein VBR** (früher `-qscale:a 3`) und **keine Xing-Tabelle**. Der Grund liegt im Sprung, nicht in der
Wiedergabe:
```mermaid
flowchart LR
A["Klick auf 20:40<br/>audio.currentTime = 1240"] --> B{Wie findet der<br/>Browser das Byte?}
B -->|"VBR: Xing-Tabelle<br/>100 Stützstellen ≈ 38 s Raster"| C["landet bei 20:11,5<br/>meldet aber 20:40"]
B -->|"CBR ohne Tabelle:<br/>Byte = Zeit × Bitrate"| D["landet bei 20:40,0<br/>Fehler 0,02 s"]
C --> E["Ton hinkt 28 s nach,<br/>Markierung läuft voraus"]
D --> F["synchron"]
```
Nachgemessen an #85 (63 min): ein Sprung auf 20:40 spielte in Wahrheit ab **20:11,5 (−28,5 s)** — genau
das Bild, das der Nutzer meldete („markiert ist ein Wort vier Zeilen unter dem, was ich höre"). Die
Wortzeitstempel selbst waren dabei korrekt; per Gegenprobe transkribierte Ausschnitte an t = 600/1240/3400 s
enthielten exakt die Wörter, die die Datenbank dort führt.
Die Tabelle muss auch bei CBR weg: sie speichert Byte-Positionen in 1/256 der Dateigröße, was bei einer
Stunde Audio rund 15 s Rundung sind (gemessen: bis −6,4 s Restfehler). Ohne sie rechnet der Decoder
linear — bei konstanter Bitrate exakt.
64 kbit CBR entspricht praktisch der bisherigen Durchschnittsrate (68 kbit); #85 schrumpfte von 32,3 auf
30,3 MB. Der Altbestand wurde einmalig mit `lab/recode-cbr.sh` umgeschrieben (Quelle: die WAV, wo noch
vorhanden — sonst die MP3 selbst, dann ohne erneute Filter).
## API-Endpunkte (Auszug)
| Endpunkt | Zweck |
|---|---|
| `POST /api/recordings/:id/retranscribe` | Hängende/fehlgeschlagene Transkription neu starten |
| `POST /api/recordings/:id/reanalyze` `{numSpeakers?}` | Diarization **immer** neu (leer/0 = automatisch, Zahl = Anzahl gepinnt) + LLM neu |
| `POST /api/recordings/:id/reanalyze-llm` | Nur LLM neu, Diarization/Wörter bleiben |
| `POST /api/recordings/:id/speaker` `{speaker,name,enroll}` | Sprecher benennen + optional Stimme lernen |
| `/api/glossary` (CRUD) | Eigennamen-Glossar |
## Performance-Kennzahlen (71-min-Referenz #69)
| Schritt | Wert |
|---|---|
| Transkription (NPU, batch=8) | RTF ≈ 0,39 |
| Diarization (CPU) | RTF ≈ 0,19 |
| Komplett E2E | RTF ≈ 0,9 |
| LLM-Analyse (Gemma, 10 Chunks + Reduce) | ~20 min |
| Kurzclip (10 s) komplett | ~53 s |