// Features behind the review ordering, shared by the fitter (fit-order.mjs) and // the page builder (build-um.mjs) so the two cannot drift apart -- a score built // from differently-computed features would silently rank on nonsense. export const FEATS = ["dur", "f0", "spread", "rms", "margin", "isoB", "isoA", "isoMin", "overlap", "isUm"]; export function featuresOf(c, words) { let before = -Infinity, after = Infinity, overlap = 0; for (const w of words) { if (w.end > c.start + 0.02 && w.start < c.end - 0.02) overlap += 1; if (w.end <= c.start) before = Math.max(before, w.end); if (w.start >= c.end && after === Infinity) after = w.start; } const isoB = Number.isFinite(before) ? Math.min(3, c.start - before) : 3; const isoA = Number.isFinite(after) ? Math.min(3, after - c.end) : 3; return { dur: c.dur ?? 0, f0: c.f0 ?? 0, spread: Math.min(400, c.spreadCents ?? 0), rms: Math.max(-60, c.rms ?? -60), margin: Math.min(0.5, Math.min(c.marginBefore ?? 0, c.marginAfter ?? 0)), isoB, isoA, isoMin: Math.min(isoB, isoA), overlap: overlap > 0 ? 1 : 0, isUm: /^u+m+$/.test(c.token ?? "") ? 1 : 0, }; } // Score a candidate with a model written by fit-order.mjs. Higher = more likely // to earn a "good" by ear. export function scoreWith(model, c, words) { const f = featuresOf(c, words); let z = model.b; model.feats.forEach((name, j) => { z += model.w[j] * ((f[name] - model.mean[name]) / model.sd[name]); }); return z; }