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:
+27
-4
@@ -71,11 +71,34 @@ export function cosine(a, b) {
|
||||
return d / (Math.sqrt(na) * Math.sqrt(nb) + 1e-9);
|
||||
}
|
||||
|
||||
// Best enrolled voiceprint for an embedding, or null if below threshold.
|
||||
// Beste PERSON für ein Embedding, oder null unter der Schwelle.
|
||||
//
|
||||
// Bewertet wird pro Person mit ihrem BESTEN Abdruck (Maximum, kein Mittel): die
|
||||
// Proben einer Person stammen aus verschiedenen Sessions und Headsets, und ein
|
||||
// Mittel über sie verwischt genau das, was die Stimme ausmacht. `second` ist der
|
||||
// beste Wert einer ANDEREN Person — nur damit ist ein Abstands-Kriterium
|
||||
// überhaupt sinnvoll. Vorher lief der Abstand über einzelne ABDRÜCKE, und zwei
|
||||
// Abdrücke derselben Person (Kosinus 0,928) haben ihn zuverlässig gerissen: die
|
||||
// Zwangsbedingung in diar.js war dadurch faktisch abgeschaltet.
|
||||
export function bestMatch(emb, voiceprints, threshold = THRESHOLD) {
|
||||
let best = null, bs = -1;
|
||||
for (const v of voiceprints) { const s = cosine(emb, v.vec); if (s > bs) { bs = s; best = v; } }
|
||||
return (best && bs >= threshold) ? { id: best.id, name: best.name, score: bs } : null;
|
||||
const perPerson = new Map(); // Identität → bester Kosinus
|
||||
const meta = new Map();
|
||||
for (const v of voiceprints) {
|
||||
const key = v.person_id != null ? `p${v.person_id}` : `n:${String(v.name || '').toLowerCase()}`;
|
||||
const s = cosine(emb, v.vec);
|
||||
if (!perPerson.has(key) || s > perPerson.get(key)) {
|
||||
perPerson.set(key, s);
|
||||
meta.set(key, { personId: v.person_id ?? null, name: v.name, printId: v.id });
|
||||
}
|
||||
}
|
||||
let bestKey = null, s1 = -Infinity, s2 = -Infinity;
|
||||
for (const [key, s] of perPerson) {
|
||||
if (s > s1) { s2 = s1; s1 = s; bestKey = key; }
|
||||
else if (s > s2) s2 = s;
|
||||
}
|
||||
if (bestKey === null || s1 < threshold) return null;
|
||||
const m = meta.get(bestKey);
|
||||
return { id: m.printId, personId: m.personId, key: bestKey, name: m.name, score: s1, second: s2 };
|
||||
}
|
||||
|
||||
export function status() { return { model: EMB, modelId: modelId(), present: fs.existsSync(EMB), running: !!proc, threshold: THRESHOLD }; }
|
||||
|
||||
Reference in New Issue
Block a user