#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.
53 lines
2.4 KiB
JavaScript
53 lines
2.4 KiB
JavaScript
// Which merge threshold does a recording itself suggest? Fits a 2-component
|
|
// Gaussian mixture (EM) to the pairwise window-similarity histogram — the
|
|
// "different speaker" and "same speaker" modes — and takes the point where the
|
|
// two weighted densities cross. Compares that with the fixed thresholds.
|
|
import fs from 'node:fs';
|
|
const f = x => x.toFixed(3);
|
|
const norm = v => { let s = 0; for (const x of v) s += x * x; s = Math.sqrt(s) + 1e-9; return v.map(x => x / s); };
|
|
const dot = (a, b) => { let s = 0; for (let i = 0; i < a.length; i++) s += a[i] * b[i]; return s; };
|
|
|
|
for (const file of process.argv.slice(2)) {
|
|
const d = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
const wins = d.wins.filter(w => w.v);
|
|
let X = wins.map(w => norm(w.v));
|
|
const dim = X[0].length, mu = new Array(dim).fill(0);
|
|
for (const v of X) for (let i = 0; i < dim; i++) mu[i] += v[i] / X.length;
|
|
X = X.map(v => norm(v.map((x, i) => x - mu[i])));
|
|
const n = X.length;
|
|
// sample pairs (deterministic stride, no RNG)
|
|
const vals = [];
|
|
for (let k = 1; k < 400000 && vals.length < 60000; k++) {
|
|
const i = (k * 7919) % n, j = (k * 104729 + 13) % n;
|
|
if (i !== j) vals.push(dot(X[i], X[j]));
|
|
}
|
|
// EM for 2 Gaussians
|
|
let m1 = -0.1, m2 = 0.5, s1 = 0.15, s2 = 0.15, w1 = 0.8;
|
|
const N = (x, m, s) => Math.exp(-((x - m) ** 2) / (2 * s * s)) / (s * Math.sqrt(2 * Math.PI));
|
|
for (let it = 0; it < 100; it++) {
|
|
let sw1 = 0, sm1 = 0, sm2 = 0, sv1 = 0, sv2 = 0, sw2 = 0;
|
|
for (const x of vals) {
|
|
const a = w1 * N(x, m1, s1), b = (1 - w1) * N(x, m2, s2);
|
|
const r = a / (a + b + 1e-300);
|
|
sw1 += r; sw2 += 1 - r; sm1 += r * x; sm2 += (1 - r) * x;
|
|
}
|
|
const nm1 = sm1 / sw1, nm2 = sm2 / sw2;
|
|
for (const x of vals) {
|
|
const a = w1 * N(x, m1, s1), b = (1 - w1) * N(x, m2, s2);
|
|
const r = a / (a + b + 1e-300);
|
|
sv1 += r * (x - nm1) ** 2; sv2 += (1 - r) * (x - nm2) ** 2;
|
|
}
|
|
m1 = nm1; m2 = nm2;
|
|
s1 = Math.max(0.02, Math.sqrt(sv1 / sw1)); s2 = Math.max(0.02, Math.sqrt(sv2 / sw2));
|
|
w1 = sw1 / vals.length;
|
|
}
|
|
// crossing point between the means: w1*N1(x) = w2*N2(x)
|
|
let lo = m1, hi = m2, t = (lo + hi) / 2;
|
|
for (let i = 0; i < 60; i++) {
|
|
t = (lo + hi) / 2;
|
|
const g = w1 * N(t, m1, s1) - (1 - w1) * N(t, m2, s2);
|
|
if (g > 0) lo = t; else hi = t;
|
|
}
|
|
console.log(`${file.split('.').slice(0, 2).join('.')}: diff N(${f(m1)},${f(s1)}) w=${f(w1)} | same N(${f(m2)},${f(s2)}) w=${f(1 - w1)} → crossing=${f(t)}`);
|
|
}
|