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.
This commit is contained in:
Claude
2026-07-27 17:00:45 +02:00
parent 432c4b14dd
commit 48fa6b4f9e
17 changed files with 1357 additions and 67 deletions
+23
View File
@@ -0,0 +1,23 @@
// Replay the REAL production clustering (diar.js clusterWindows) on cached
// window embeddings, so thresholds can be tuned without re-running sherpa or the
// embedding pass. Usage:
// [DIAR_COS_FINE=..] [DIAR_COS_FINAL=..] node lab-eval.mjs <emb.json> [expect] [numSpeakers] [markers.json]
import fs from 'node:fs';
import { clusterWindows } from './diar-v2.js';
const [file, expect, numSp, markerFile] = process.argv.slice(2);
const d = JSON.parse(fs.readFileSync(file, 'utf8'));
const ok = d.wins.filter(w => w.v).map(w => ({ ti: w.ti, start: w.s, end: w.e, vec: w.v }));
const markers = markerFile ? JSON.parse(fs.readFileSync(markerFile, 'utf8')) : [];
const segs = clusterWindows(d.turns, ok, +(numSp || 0), markers);
const talk = new Map(), first = new Map(), last = new Map();
for (const s of segs) {
talk.set(s.speaker, (talk.get(s.speaker) || 0) + (s.end - s.start));
if (!first.has(s.speaker)) first.set(s.speaker, s.start);
last.set(s.speaker, s.end);
}
const order = [...talk.entries()].sort((a, b) => b[1] - a[1]);
console.log(`RESULT ${d.model.split('/').pop()} fine=${process.env.DIAR_COS_FINE || 0.35} final=${process.env.DIAR_COS_FINAL || 0.30}` +
` → speakers=${order.length}${expect ? ` (expect ${expect})` : ''} segs=${segs.length}`);
console.log(' ' + order.map(([sp, t]) => `${sp.replace('speaker_', '')}:${t.toFixed(0)}s[${(first.get(sp) / 60).toFixed(1)}-${(last.get(sp) / 60).toFixed(1)}]`).join(' '));