Files
Aime/docs/03-aufnahme-pipeline.md
T
Claude 48fa6b4f9e Diarization v2: Fenster-Clustering mit gelernter Schwelle + Live-Namensmarker
#74 (22 Teilnehmer) ergab automatisch 44 Sprecher, dieselbe Stimme mehrfach.
Ursache gemessen: ein Zentroid pro sherpa-Cluster aus max. 3 Schnitten, fester
Kosinus auf rohen Embeddings (Kreuz-Turn-Kosinus ~0,4 = keine Trennschaerfe).

v2: sherpa liefert nur Turn-Grenzen, Identitaet aus 3-s-Fenster-Embeddings,
Mittelwert-Zentrierung, Average-Linkage-AHC, Weak-Fold, Schwelle pro Aufnahme
per 2-Gauss-EM gelernt. Validiert: #74 44->20, #70 ->4, #68 ->2.

Live-Namensmarker: Namen waehrend der Aufnahme tippen; Zeitstempel benennt
spaeter den Cluster und dient als must-/cannot-link-Zwangsbedingung.
2026-07-27 17:00:45 +02:00

4.4 KiB
Raw Blame History

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

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

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

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“).

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