feat(persons): eine Person, mehrere Namen und mehrere Stimmproben
Die Identitaet war der NAME. Derselbe Mensch, einmal "Sascha" und einmal "Sascha Wolf" benannt, existierte zweimal und konnte zwei Cluster benennen -- Aufnahme #101 (34 s, zwei Menschen) zeigte Michael/Sascha/Sprecher 3/ Sascha Wolf. Der Vornamens-Trick in diar.js hat das ueberdeckt, setzte aber auch "Markus Ufer" und "Markus Witsch" gleich (Kosinus 0,241). Neu: persons + person_names (Name = PK, gehoert genau einer Person), voiceprints.person_id. Mehrere Proben pro Person (max. 8) statt eines laufenden Mittels; bewertet wird die beste Probe. Score UND Abstand rechnen zwischen Personen -- vorher rissen zwei Abdruecke desselben Menschen (0,928) die Abstandspruefung, womit die Zwangsbedingung faktisch aus war. diar.identityKey bildet Marker-Namen und Abdruecke auf p<person_id> ab. GUI "Stimmen" zeigt Personen mit Namens-Chips: Namen hinzufuegen/entfernen, Personen zusammenfuehren, Proben verwerfen. Altbestand geleert (Dump in backup/), weil lab/vpcos.mjs neben echten Dubletten (Jens ~ Jens Kuppler = 1,000) auch kontaminierte Abdruecke zeigte: Johannes lag mit 0,912 an Sascha Wolf, aber 0,115 an Johannes Jeckel. Verifiziert an #101: Sascha Wolf als zweiter Name derselben Person -> Re-Diarisierung benennt nur noch Michael + Sascha statt drei Namen fuer zwei Menschen. GUI mit Playwright geprueft, keine JS-Fehler.
This commit is contained in:
+77
-9
@@ -25,7 +25,7 @@ flowchart TB
|
||||
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 -- ja --> VPC["Cluster ohne Marker gegen Voiceprints prüfen<br/>(RAW-Kosinus ≥ 0,55 + Abstand 0,05 zur<br/>nächstbesten PERSON, eine Person 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
|
||||
@@ -86,7 +86,7 @@ sequenceDiagram
|
||||
D-->>S: Segmente
|
||||
S->>S: applyMarkers(): resolveMarkers() → Cluster benennen
|
||||
S->>DB: speaker_names + markers.speaker/conf
|
||||
S->>DB: upsertVoiceprint(name) je benanntem Sprecher
|
||||
S->>DB: learnVoice(name) je benanntem Sprecher<br/>(neue Probe der PERSON hinter dem Namen)
|
||||
```
|
||||
|
||||
- **Auflösung** (`diar.resolveMarkers`): asymmetrisches Fenster um den Marker (25 s zurück / 8 s vorwärts,
|
||||
@@ -130,10 +130,14 @@ Jetzt gehen die Voiceprints **in** die Diarisierung (`diarize(wav, n, {markers,
|
||||
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.
|
||||
**und** 0,05 Abstand zum Zweitbesten, eine **Person** beansprucht höchstens ein Cluster (bester Score gewinnt).
|
||||
- Schlüssel ist seit 2026-08-07 die **Person** (`diar.identityKey` über `person_names`), nicht mehr der
|
||||
kleingeschriebene Vorname. Der Vornamens-Trick sollte „Sascha" und „Sascha Wolf" zusammenhalten, setzte dabei
|
||||
aber auch „Markus Ufer" und „Markus Witsch" gleich (Kosinus 0,241 = zwei Menschen). Siehe „Eine Person,
|
||||
mehrere Namen" unten.
|
||||
- Ebenfalls seit 2026-08-07: Score und Abstand rechnen **pro Person über deren besten Abdruck**. Vorher lief
|
||||
beides über einzelne Abdrücke — zwei Abdrücke desselben Menschen (0,928) rissen die Abstandsprüfung
|
||||
zuverlässig, die Zwangsbedingung war also faktisch abgeschaltet.
|
||||
- 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).
|
||||
|
||||
@@ -199,6 +203,68 @@ flowchart LR
|
||||
726 Turns → 1932 Fenster → **8 Sprecher**, 6 Live-Marker verankert, Diarisierung 13,5 min, keine
|
||||
OOM-Meldung.
|
||||
|
||||
## Eine Person, mehrere Namen (seit 2026-08-07)
|
||||
|
||||
Bis dahin war der **Name** die Identität. Das trägt nicht: derselbe Mensch wird in einer Aufnahme als
|
||||
„Sascha" und in der nächsten als „Sascha Wolf" benannt, und schon existierten zwei Einträge derselben
|
||||
Stimme. Auf Aufnahme #101 (34 s, **zwei** Menschen) hieß das Ergebnis: `Michael`, `Sascha`,
|
||||
`Sprecher 3`, `Sascha Wolf` — vier Sprecher, einer davon doppelt.
|
||||
|
||||
**Messung des Altbestands** (42 Abdrücke, `lab/vpcos.mjs`, Kosinus innerhalb gleicher Vornamen):
|
||||
|
||||
| Paar | cos | Befund |
|
||||
|---|---|---|
|
||||
| `Jens` ~ `Jens Kuppler` | **1,000** | exakte Dublette |
|
||||
| `Sascha` ~ `Sascha Wolf` | 0,928 | dieselbe Stimme, zwei Einträge |
|
||||
| `Kim` ~ `Kim Nebendahl` | 0,899 | dieselbe Stimme |
|
||||
| `Fabienne` ~ `Fabienne Nachtsheim` | 0,785 | dieselbe Stimme |
|
||||
| `Markus` ~ `Markus Witsch` | 0,429 | verschiedene Menschen |
|
||||
| `Fabio` ~ `Fabio Haase` | 0,261 | passt nicht zusammen |
|
||||
| `Johannes` ~ `Johannes Jeckel` | 0,115 | passt nicht zusammen |
|
||||
| `Michael Pastler` ~ `Michael Wagner` | −0,011 | klar verschiedene Menschen |
|
||||
|
||||
Daraus folgen zwei Dinge. Erstens: **automatisch nach Vornamen gruppieren wäre falsch** — genau das tat
|
||||
der alte `normName`-Trick in `diar.js`, und er setzte damit „Markus Ufer" und „Markus Witsch" gleich.
|
||||
Zweitens: ein Teil des Bestands war **kontaminiert** (`Johannes` lag mit 0,912 an *Sascha Wolf*, aber mit
|
||||
0,115 an *Johannes Jeckel*), also aus einem falsch verschmolzenen Cluster gelernt — dasselbe Muster wie
|
||||
bei Merle am 2026-07-29. Der Bestand wurde deshalb geleert (Dump: `backup/voiceprints-2026-08-07.sql`)
|
||||
und wird neu gelernt.
|
||||
|
||||
**Modell jetzt:**
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
persons ||--|{ person_names : "heißt auch"
|
||||
persons ||--o{ voiceprints : "klingt wie"
|
||||
persons { int id string display_name }
|
||||
person_names { string name int person_id }
|
||||
voiceprints { int id int person_id string vec string model }
|
||||
```
|
||||
|
||||
- `person_names.name` ist Primärschlüssel → ein Name gehört zu **genau einer** Person. Die Kollation
|
||||
`utf8mb4_general_ci` macht das absichtlich case-insensitiv („sascha" = „Sascha").
|
||||
- **Mehrere Proben pro Person** statt eines laufenden Mittels (`AIME_MAX_PRINTS`, Standard 8, älteste
|
||||
fallen raus). Proben aus verschiedenen Sessions und Headsets zu mitteln verwischt gerade das, was die
|
||||
Stimme ausmacht; bewertet wird eine Person mit ihrem **besten** Abdruck.
|
||||
- **Abstand zwischen Personen, nicht zwischen Abdrücken** (`spk.bestMatch`, `diar.clusterWindows`). Vorher
|
||||
rissen zwei Abdrücke desselben Menschen (0,928) die 0,05-Abstandsprüfung — die Zwangsbedingung war
|
||||
damit faktisch abgeschaltet.
|
||||
- Identität überall gleich: `diar.identityKey(name, persons)` bildet Marker-Namen UND Abdrücke auf
|
||||
`p<person_id>` ab, `db.personNameMap()` liefert die Karte. Ohne Personen-Eintrag zählt der **volle**
|
||||
Name (nicht mehr nur der Vorname).
|
||||
- GUI „Stimmen" zeigt Personen mit ihren Namen als Chips: Namen hinzufügen/entfernen, zwei Personen
|
||||
zusammenführen, Proben verwerfen. Vergibt man einen Namen, der schon einer anderen Person gehört,
|
||||
werden beide zusammengeführt — dann ist genau das gemeint.
|
||||
|
||||
**Verifiziert an #101:** „Michael" und „Sascha" neu gelernt, „Sascha Wolf" als zweiter Name an dieselbe
|
||||
Person gehängt → Re-Diarisierung benennt nur noch `{"speaker_00":"Michael","speaker_01":"Sascha"}` statt
|
||||
vorher drei Namen für zwei Menschen, Log: `Cluster 1 = bekannte Stimme "Sascha" (cos 0,917)`.
|
||||
|
||||
⚠️ **Was das NICHT löst:** die Cluster-Anzahl. #101 bleibt bei 4 Clustern für 2 Menschen (17 Roh-Cluster
|
||||
→ 13 schwache gefaltet → 4), die beiden unbenannten heißen weiter „Sprecher 1/2". Das ist die
|
||||
Über-Segmentierung kurzer Clips, diagnostiziert am 2026-07-31 (Mittelwert-Zentrierung erzeugt bei wenigen
|
||||
Fenstern einen negativen Sockel von −1/(n−1)) — ein eigener, noch offener Punkt.
|
||||
|
||||
## Harte Grenzen / Sackgassen
|
||||
|
||||
- **Überlappendes gleichzeitiges Sprechen** ist mit Mono-Mix prinzipiell nicht trennbar.
|
||||
@@ -218,14 +284,16 @@ 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 --> PERS[("persons + person_names<br/>ein Mensch, mehrere Namen")]
|
||||
PERS --> VP[("voiceprints-Tabelle<br/>mehrere Proben PRO PERSON<br/>(max. 8, älteste fallen raus)")]
|
||||
API --> NAMES[("recordings.speaker_names<br/>JSON, manuell gewinnt")]
|
||||
VP --> AUTOID
|
||||
```
|
||||
|
||||
- Beim Lernen wird der längste zusammenhängende Sprech-Abschnitt (≥ 1,5 s, **max. 20–25 s**) des Sprechers als
|
||||
TitaNet-Embedding in den Zentroid gefaltet (`db.upsertVoiceprint`) → zukünftige Aufnahmen identifizieren die
|
||||
Person automatisch.
|
||||
TitaNet-Embedding **als neue Probe der Person** gespeichert (`db.learnVoice`) → zukünftige Aufnahmen
|
||||
identifizieren die Person automatisch. Bis 2026-08-07 wurde stattdessen EIN Zentroid pro Name laufend
|
||||
gemittelt; das verwischt gerade die Merkmale, wenn die Proben aus verschiedenen Sessions und Headsets kommen.
|
||||
- ⚠️ **Warum die Schnitte kurz sein MÜSSEN (2026-07-27 root-caused):** das TitaNet-ONNX bricht bei langen
|
||||
Eingaben ab — reproduziert bei 180 s:
|
||||
`Non-zero status code returned while running Where node … Attempting to broadcast an axis by a dimension
|
||||
|
||||
@@ -3,6 +3,43 @@
|
||||
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
|
||||
Was, Warum, betroffene Dateien/Backups).
|
||||
|
||||
## 2026-08-07 (3) — Eine Person darf mehrere Namen haben (und mehrere Stimmproben)
|
||||
|
||||
**Warum:** die Identität war der NAME. Derselbe Mensch, in einer Aufnahme „Sascha" und in der nächsten
|
||||
„Sascha Wolf" genannt, existierte damit zweimal — und beide Einträge konnten je ein Cluster benennen.
|
||||
Aufnahme #101 (34 s, **zwei** Menschen) zeigte deshalb `Michael`, `Sascha`, `Sprecher 3`, `Sascha Wolf`.
|
||||
`diar.js` hat das mit einem Vornamens-Trick überdeckt, der aber „Markus Ufer" und „Markus Witsch"
|
||||
gleichsetzt (Kosinus 0,241 = zwei Menschen).
|
||||
|
||||
**Neu:** Tabellen `persons` + `person_names` (Name = Primärschlüssel, gehört zu genau einer Person),
|
||||
`voiceprints.person_id`. Eine Person darf beliebig viele Namen **und** beliebig viele Stimmproben haben
|
||||
(`AIME_MAX_PRINTS`, Standard 8) — bewertet wird sie mit ihrer **besten** Probe statt mit einem laufenden
|
||||
Mittel über verschiedene Sessions und Headsets. Score UND Abstand rechnen jetzt zwischen **Personen**;
|
||||
vorher rissen zwei Abdrücke desselben Menschen die 0,05-Abstandsprüfung, wodurch die Zwangsbedingung in
|
||||
der Diarisierung faktisch abgeschaltet war. Identität überall: `diar.identityKey()` bildet Marker-Namen
|
||||
und Abdrücke auf `p<person_id>` ab.
|
||||
|
||||
**GUI „Stimmen":** Personen statt Abdrücke — Namen als Chips (hinzufügen/entfernen), zwei Personen
|
||||
zusammenführen, Proben verwerfen. Ein Name, der schon einer anderen Person gehört, führt beide zusammen.
|
||||
|
||||
**Altbestand geleert** (Dump `backup/voiceprints-2026-08-07.sql`, 42 Abdrücke): die Messung mit
|
||||
`lab/vpcos.mjs` zeigte neben echten Dubletten (`Jens` ~ `Jens Kuppler` = **1,000**) auch kontaminierte
|
||||
Einträge — `Johannes` lag mit 0,912 an *Sascha Wolf*, aber mit 0,115 an *Johannes Jeckel*, war also aus
|
||||
einem falsch verschmolzenen Cluster gelernt (wie Merle am 2026-07-29). Neu lernen statt reparieren.
|
||||
|
||||
**Verifiziert an #101:** „Michael" + „Sascha" neu gelernt, „Sascha Wolf" als zweiter Name derselben Person
|
||||
→ Re-Diarisierung benennt `{"speaker_00":"Michael","speaker_01":"Sascha"}` (vorher drei Namen für zwei
|
||||
Menschen), Log `Cluster 1 = bekannte Stimme "Sascha" (cos 0,917)`. GUI mit Playwright geprüft: keine
|
||||
JS-Fehler, zwei Personen, Chips „Sascha" + „Sascha Wolf".
|
||||
|
||||
⚠️ **Nicht behoben, eigener Punkt:** die Cluster-ANZAHL. #101 bleibt 4 Cluster für 2 Menschen; die beiden
|
||||
unbenannten heißen „Sprecher 1/2". Ursache ist die Über-Segmentierung kurzer Clips (Diagnose 2026-07-31,
|
||||
Kap. 04) — dafür liegen drei Optionen vor, keine davon ist gewählt.
|
||||
|
||||
Dateien: `app/db.js`, `app/spk.js`, `app/diar.js`, `app/server.js`, `app/recorder.js`,
|
||||
`app/web/src/main.js`, `app/web/src/style.css` (Board-Backups `.bak-persons`), `app/lab/vpcos.mjs`,
|
||||
Kapitel [04 – Sprechertrennung](04-diarization.md).
|
||||
|
||||
## 2026-08-07 (2) — „KI-Analyse fehlgeschlagen" bei einer 34-Sekunden-Aufnahme: zwei Modelle gleichzeitig
|
||||
|
||||
**Symptom:** Aufnahme #101 (34 s, 2 Personen) — Transkript und Sprecherspuren da, aber
|
||||
|
||||
Reference in New Issue
Block a user