Diarisierung: Marker-Zwangsbedingungen absichern, Voiceprints als cannot-link; LLM: Sektions-Pfad ab 9k Zeichen

#79 verlor einen von vier Sprechern und lieferte eine 1082-Zeichen-Zusammenfassung.

Diarisierung: ein zweiter, falsch verankerter Marker verschmolz Saschas Cluster
in Merles (Kosinus -0,727). must-link prueft jetzt die akustische Aehnlichkeit,
Anker werden nach Trefferqualitaet gewichtet, und enrolled Stimmen wirken als
cannot-link -- damit ist der Box-Besitzer immer ein eigener Sprecher.

LLM: der Whole-Pass komprimierte das erste Thema weg. Sektions-Pfad jetzt ab
9000 Zeichen, kleinere Chunks, woertlicher Kontext aus dem Vorgaenger-Chunk und
ein gezielter Pruef-Aufruf gegen ueberdehnte Einzelaussagen.

#79: 4 Sprecher, Zusammenfassung ~5300 Zeichen, 7 Themenbloecke.
Regression: #70 -> 4, #68 -> 2, #74 -> 20 Sprecher unveraendert.
This commit is contained in:
2026-07-29 16:09:30 +02:00
parent c76f26abaf
commit 810cee1dba
9 changed files with 438 additions and 38 deletions
+50 -4
View File
@@ -22,9 +22,12 @@ flowchart TB
FOLD -- nein --> STRONG["starker Cluster"]
WEAK --> MK
STRONG --> MK{"Live-Namensmarker vorhanden?"}
MK -- ja --> CON["must-link: gleicher Name = 1 Cluster<br/>cannot-link: verschiedene Namen<br/>dürfen NIE verschmelzen"]
MK -- nein --> B
CON --> B["Stufe B: weiter mergen bis Schwelle<br/>(GMM-Wert) ODER bis gepinnte Anzahl"]
MK -- ja --> CON["must-link: gleicher Name = 1 Cluster,<br/>ABER nur wenn die Cluster akustisch passen<br/>(cos ≥ 0,6 × gelernte Schwelle) —<br/>sonst Marker verwerfen (falsch getippter Zeitpunkt)<br/>cannot-link: verschiedene Namen verschmelzen NIE"]
MK -- nein --> VP
CON --> VP{"Bekannte Stimmabdrücke?"}
VP -- ja --> VPC["Cluster ohne Marker gegen Voiceprints prüfen<br/>(RAW-Kosinus ≥ 0,55 + Abstand 0,05 zum Zweitbesten,<br/>ein Name höchstens 1 Cluster)<br/>→ zusätzliches cannot-link<br/>sichert vor allem den Box-Besitzer"]
VP -- nein --> B
VPC --> B["Stufe B: weiter mergen bis Schwelle<br/>(GMM-Wert) ODER bis gepinnte Anzahl"]
GMM --> B
B --> LAB["Turn-Label = dauergewichtete<br/>Mehrheit seiner Fenster"]
LAB --> OUT["finale Sprecher<br/>(#74: 44 → 20)"]
@@ -93,7 +96,50 @@ sequenceDiagram
`applyMarkers()` führt sie zusammen (nutzergestützter Fix für Rest-Übersegmentierung).
- **Nachtragen:** im Detail einer Aufnahme kann an der aktuellen Abspielposition ein Marker gesetzt werden
(`POST /api/recordings/:id/markers`), „↻ Zuordnen" rechnet die Zuordnung neu (`apply-markers`, ohne sherpa).
- Marker-Namen sind manuell = autoritativ; die akustische Auto-Identifikation füllt nur Lücken.
- Marker-Namen sind manuell = autoritativ; die akustische Auto-Identifikation füllt nur Lücken —
**mit einer Ausnahme, siehe unten** (unsicher verankerter Marker verliert gegen einen Stimmabdruck).
## Ein falsch getippter Marker kostete einen Sprecher (2026-07-29)
**Symptom (Aufnahme #79, 23-min-Jour-fixe, 4 Personen):** nur 3 Sprecher, Sascha fehlte komplett.
**Ursache, gemessen im Labor:** Stufe A + Faltung lieferten korrekt **4 starke Cluster**. Es gab 4 Marker
mit 3 Namen — „Merle" war zweimal getippt worden, der zweite davon landete auf **Saschas** Cluster. Das
alte must-link verschmolz beide „Merle"-Cluster bedingungslos, also Sascha in Merle hinein. Ihre
Kosinus-Ähnlichkeit war **−0,727**, d. h. das Signal war eindeutig da — es wurde nur nicht ausgewertet.
**Drei Absicherungen:**
| Ebene | vorher | jetzt |
|---|---|---|
| Anker-Gewicht (`markerAnchors`) | jeder Marker zählt 1 | Marker IM Turn zählt 1,0, bei 25 s Tippverzögerung 0,2 |
| must-link (`clusterWindows`) | verschmilzt immer | nur bei `cos ≥ 0,6 × gelernter Schwelle`, sonst Marker verwerfen + loggen |
| Cluster-Fold (`applyMarkers`, server.js) | ab `conf ≠ low` | **nur `conf = high`**; `mid` darf noch benennen, aber nicht mehr verschmelzen |
Zusätzlich: trägt EIN Cluster Marker mehrerer Namen, war er schon vor Stufe B verschmolzen — das wird
jetzt als Warnung geloggt statt still den lauteren Vote zu nehmen.
## Stimmabdrücke als Trennkriterium — der Besitzer zählt immer (2026-07-29)
Die Stimm-Identifikation lief bisher **nach** dem Clustering (`identifySpeakers`): sie konnte benennen,
was herauskam, aber nie verhindern, dass zwei bekannte Personen in einem Cluster landen. Gerade der
Besitzer der Box ist enrolled und spricht in JEDER Aufnahme — genau er verschwand in #79.
Jetzt gehen die Voiceprints **in** die Diarisierung (`diarize(wav, n, {markers, voiceprints})`, gefüllt in
`rediarize()` und `transcribeAndAnalyze()`). Jedes noch namenlose Cluster wird gegen die Abdrücke geprüft;
sitzt einer sicher darauf, wirkt der Name wie ein Marker-Tag = cannot-link.
- Vergleich im **RAW**-Raum (Voiceprints sind unzentriert gespeichert), deshalb streng: `DIAR_VP_MIN` 0,55
**und** 0,05 Abstand zum Zweitbesten, ein Name beansprucht höchstens ein Cluster (bester Score gewinnt).
- Namensschlüssel = **Vorname, kleingeschrieben**: derselbe Mensch ist als „Sascha" und „Sascha Wolf"
enrolled (ebenso Kim/Kim Nebendahl, Fabienne/Fabienne Nachtsheim) — zwei Einträge dürfen nicht zu einem
cannot-link zwischen seinen eigenen Clustern führen.
- Gegenprobe ohne Regression: #79 → 4 (vorher 3), #70 → 4, #68 → 2, #74 → 20 (unverändert mit und ohne
Voiceprints). Auf #79 wird Sascha mit cos 0,861 erkannt, auf #74 fünfzehn Personen (Kai 0,656 … Niklas 0,966).
**Nachwirkung im server:** ein Name, der aus einem Marker mit `conf ≠ high` stammt, verliert gegen einen
widersprechenden Stimmabdruck (`identify #id: Stimmabdruck "X" schlägt unsicheren Marker "Y"`). In #79
hatte der Marker bei 6,6 s Saschas Cluster „Markus" getauft.
## Harte Grenzen / Sackgassen