docs: komplette CallRec-Dokumentation mit Mermaid-Diagrammen
Architektur, Hardware/Thermik, PipeWire-Audio-Topologie, Aufnahme-Pipeline, Diarization (Anchor-Merge), LLM-Analyse (Gemma-3-4B/NPU), iPhone-BT-HFP, Betrieb/Troubleshooting und Changelog-Chronik. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# 04 — Sprechertrennung (Diarization) & Stimm-Profile
|
||||
|
||||
**Anforderung:** akustische Sprecher-ID ist Pflicht (Raumspinne: 3–4 Personen auf einem BT-Kanal).
|
||||
Sprecheranzahl soll **dynamisch** erkannt werden (Personen kommen/gehen) — kein festes Pinnen nötig.
|
||||
|
||||
## Pipeline
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
WAV["Mono-WAV"] --> SHERPA["sherpa-onnx Clustering<br/>liefert VIELE Roh-Cluster<br/>(z. B. konstant 27 bei einem 3-Personen-Call — deterministisch)"]
|
||||
SHERPA --> EMB["Jedes Cluster neu einbetten<br/>TitaNet-Embedding (spk.embed)"]
|
||||
EMB --> SORT["Cluster nach Länge sortieren<br/>(längste zuerst = stabile Reihenfolge)"]
|
||||
SORT --> ANCHOR{"Cluster ≥ 3 s?<br/>(DIAR_MIN_SPEAKER_SEC)"}
|
||||
ANCHOR -- "ja (Anchor)" --> JOIN{"Kosinus ≥ 0,5 zu<br/>bestehendem Sprecher?<br/>(DIAR_MERGE_THRESHOLD)"}
|
||||
JOIN -- ja --> MERGE["dem Sprecher zuordnen"]
|
||||
JOIN -- nein --> NEW["neuen Sprecher eröffnen"]
|
||||
ANCHOR -- "nein (kurz/Streu)" --> NEAREST["IMMER zum nächstliegenden Anchor<br/>(darf NIE neuen Sprecher eröffnen)"]
|
||||
MERGE --> OUT["finale Sprecher (z. B. 27 → 3)"]
|
||||
NEW --> OUT
|
||||
NEAREST --> OUT
|
||||
```
|
||||
|
||||
## Warum Greedy-Anchor-Merge (Geschichte der Fixes)
|
||||
|
||||
1. **Schwellwert-Tuning nutzlos:** Sweep auf 2-Personen-Clip: Threshold 0,7 → 12 Sprecher … 0,95 → 8. Nie 2.
|
||||
2. **Union-Find (1. Konsolidierung) instabil:** paarweises Mergen bildet transitive Ketten (A~B~C zieht alles
|
||||
zusammen). Ein Grenzfall-Link um Kosinus ≈ 0,5 kippte je nach FP-Rauschen (Multi-Thread-TitaNet unter
|
||||
CPU-Last) → derselbe Call ergab 2 / 3 / 7 Sprecher in drei Läufen.
|
||||
3. **Greedy-Anchor-Merge (aktuell, `diar.js consolidateSpeakers`):** nur lange Cluster (verlässliche
|
||||
Embeddings) dürfen Sprecher eröffnen, Abarbeitung längste-zuerst (ordnungsstabil), kurze Cluster können nur
|
||||
zugeordnet werden → Rauschen kann nichts mehr fragmentieren. Verifiziert: 5/5 Läufe konstant 27 → 3.
|
||||
|
||||
## Harte Grenzen / Sackgassen
|
||||
|
||||
- **Überlappendes gleichzeitiges Sprechen** ist mit Mono-Mix prinzipiell nicht trennbar.
|
||||
- Geclipptes Audio (0 dBFS) zerstört Embeddings → Fantasie-Sprecher (deshalb Gain-Staging, Kap. 02).
|
||||
- sherpa-Quantisierung q5_0: langsamer UND schlechter auf diesem aarch64-Build — tot.
|
||||
- Diarization-Timeout skaliert mit Dauer (`diarTimeoutMs()` = Dauer × 2,5, min. 20 min) — der feste
|
||||
10-min-Timeout hat 46-min-Aufnahmen abgebrochen. (`parse-options.cc:373`-Ausgabe ist harmloses argv-Echo.)
|
||||
|
||||
## Sprecher-Namen & Stimm-Profile (Enrollment)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
DIAR["Diarization: speaker_00, speaker_01, …"] --> AUTOID["identifySpeakers():<br/>Abgleich gegen gespeicherte Voiceprints"]
|
||||
AUTOID --> MERGE2["mergeSpeakerNames():<br/>füllt nur Lücken, manuelle Namen gewinnen"]
|
||||
GUI["GUI Sprecher-Editor:<br/>Name eintragen + 'lernen'"] --> API["POST /speaker {speaker, name, enroll}"]
|
||||
API --> VP[("voiceprints-Tabelle<br/>1 Zentroid pro Name<br/>(laufender Mittelwert, Spalte n)")]
|
||||
API --> NAMES[("recordings.speaker_names<br/>JSON, manuell gewinnt")]
|
||||
VP --> AUTOID
|
||||
```
|
||||
|
||||
- Beim Lernen wird der längste zusammenhängende Sprech-Abschnitt (≥ 1,5 s) des Sprechers als TitaNet-Embedding
|
||||
in den Zentroid gefaltet (`db.upsertVoiceprint`) → zukünftige Aufnahmen identifizieren die Person automatisch.
|
||||
- Teilnehmerliste der Analyse kommt seit 2026-07-15 **aus der Diarization** (Namen wo bekannt, sonst
|
||||
„Sprecher N“) — nicht mehr vom LLM (das listete alle bloß *erwähnten* Namen, 11 statt 3). Siehe Kap. 05.
|
||||
|
||||
## Re-Diarization
|
||||
|
||||
- GUI „↻ Analyse neu“ → Prompt: leer = automatisch (konsolidiert), Zahl = Anzahl gepinnt
|
||||
(`--clustering.num-clusters`). Läuft immer unter A76-Takt-Cap (lüfterlos!).
|
||||
- `db.replaceWords()` tauscht nur die Sprecher-Zuordnung; Text/Zeitstempel bleiben.
|
||||
Reference in New Issue
Block a user