fix(align): calibrated fit (precision, lift, beat-shift check, baseline) and aligner guards (A7, A8)

Fit flags on precision (<0.6) and chance-corrected lift (<0.25), null with <8
recording onsets; old match rate kept as audibleRate (info). Symbolic-chroma
beat/bar-shift check per window. Constant-offset baseline (-2..+2 s) replaces
DTW without manual anchors unless DTW precision is better by >=0.02. Stretch
warning counts in the fit summary. DTW: slope-limited steps, open begin/end
options; per-bar outlier rejection (1/4 beat, Theil-Sen-transported +-3 bar
median); no anchor in the final partial bar. bench real: saved/computed
source, baseline + method, corrupt alignment.json skipped. Parabolic onset
interpolation; warp extrapolation documented; spec amended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-30 19:50:48 +01:00
1 parent 3b002059e9
commit 3db87c0317
19 files changed
+1016 -252

No files matched your search

+51 -16
View File
@@ -7,12 +7,20 @@ import { matchNotes, type MatchResult } from "../eval/notes.ts";
import { DRUM_CLASSES, ROLE_ORDER, drumClass, toClassPitch, type DrumClass } from "../eval/score.ts";
import { readManifest, readAlignment, loadSong, trackKeys } from "../ref/manifest.ts";
import { makeWarp, type Anchor } from "../align/warp.ts";
import { autoAlign, alignmentFit } from "../align/auto.ts";
import { autoAlign, autoSummary, constantOffsetBaseline, fitFromCues, recordingCues, type Baseline } from "../align/auto.ts";
import { fitSummary, type FitReport } from "../align/fit.ts";
export interface RealSongResult {
id: string; title: string;
alignment: { source: "manual" | "auto"; manual: number; auto: number; savedAt: string | null };
/**
* source: "saved" = reference/<id>/alignment.json, "computed" = auto-aligned this run (--auto). chosen: how the auto
* alignment was made ("dtw" or the constant-offset "baseline"), null when unknown (saved from the page).
* baseline: the constant-offset baseline computed this run, for comparison.
*/
alignment: {
source: "saved" | "computed"; manual: number; auto: number; savedAt: string | null;
chosen: "baseline" | "dtw" | null; baseline: Baseline | { offsetSec: number; precision: number | null }; dtwPrecision: number | null;
};
fit: FitReport;
perRole: Partial<Record<Role, MatchResult>>;
/** F1 at the lenient +-100 ms tolerance, per role (null when the role has no notes on either side). */
@@ -37,19 +45,37 @@ export async function runReal(opts: { runId: string; auto: boolean; root?: strin
});
const sf2 = opts.auto ? new Uint8Array(await Bun.file(SF2).arrayBuffer()) : null;
const results: RealSongResult[] = [];
const skipped: string[] = [];
const skipped: SkippedSong[] = [];
const skip = (id: string, reason: string) => { skipped.push({ id, reason }); console.log(`${id}: skipped (${reason})`); };
for (const entry of songs) {
const saved = await readAlignment(entry.id, root);
if (!saved && !opts.auto) { skipped.push(entry.id); console.log(`${entry.id}: skipped (no alignment.json; pass --auto to compute one)`); continue; }
let saved: Awaited<ReturnType<typeof readAlignment>>;
try {
saved = await readAlignment(entry.id, root);
if (saved) makeWarp(saved.anchors); // validates the anchors
} catch (e) { skip(entry.id, (e as Error).message); continue; }
if (!saved && !opts.auto) { skip(entry.id, "no alignment.json; pass --auto to compute one"); continue; }
const t0 = performance.now();
const { smf, audio } = await loadSong(entry.id, root);
const keys = trackKeys(smf.tracks);
const roleOf = (i: number): Role | null => entry.roles[keys[i]!] ?? null;
const roles = Object.fromEntries(keys.map((k) => [k, entry.roles[k] ?? null]));
let anchors: Anchor[], savedAt: string | null, source: "manual" | "auto";
if (saved) { anchors = saved.anchors; savedAt = saved.savedAt ?? null; source = "manual"; }
else { anchors = autoAlign(audio, smf, entry.roles, [], sf2!).anchors; savedAt = null; source = "auto"; }
const fit = alignmentFit(audio, smf, entry.roles, anchors);
const cues = recordingCues(audio);
let anchors: Anchor[], alignment: RealSongResult["alignment"];
if (saved) {
anchors = saved.anchors;
alignment = {
source: "saved", manual: 0, auto: 0, savedAt: saved.savedAt ?? null,
chosen: saved.auto?.chosen ?? null, baseline: constantOffsetBaseline(cues, smf, roles), dtwPrecision: saved.auto?.dtwPrecision ?? null,
};
} else {
const r = autoAlign(audio, smf, roles, [], sf2!);
anchors = r.anchors;
alignment = { source: "computed", manual: 0, auto: 0, savedAt: null, chosen: r.chosen, baseline: r.baseline, dtwPrecision: r.dtwPrecision };
}
alignment.manual = anchors.filter((a) => a.kind === "manual").length;
alignment.auto = anchors.filter((a) => a.kind === "auto").length;
const fit = fitFromCues(cues, smf, roles, anchors);
const warp = makeWarp(anchors);
const res = await pipeline({ mix: audio }, { oracleStems: false, oracleTempo: false, oracleNotes: false });
@@ -77,7 +103,7 @@ export async function runReal(opts: { runId: string; auto: boolean; root?: strin
const seconds = (performance.now() - t0) / 1000;
const result: RealSongResult = {
id: entry.id, title: entry.title,
alignment: { source, manual: anchors.filter((a) => a.kind === "manual").length, auto: anchors.filter((a) => a.kind === "auto").length, savedAt },
alignment,
fit, perRole, lenient, drumsPerClass,
excludedTracks: smf.tracks.map((t, i) => (roleOf(i) ? null : keys[i]!)).filter((k): k is string => k !== null),
capo: entry.capo ?? {}, seconds,
@@ -100,16 +126,25 @@ const NA = "n/a";
const fmt = (x: number | null | undefined) => (x == null ? NA : x.toFixed(2));
const row = (cells: string[]) => `| ${cells.join(" | ")} |`;
export function renderRealReport(runId: string, results: RealSongResult[], skipped: string[] = []): string {
export interface SkippedSong { id: string; reason: string }
export function renderRealReport(runId: string, results: RealSongResult[], skipped: SkippedSong[] = []): string {
const o: string[] = [`# STSheets real-song benchmark: ${runId}`, ""];
o.push("> Reference = the team's Guitar Pro-exported MIDI warped onto the recording by the saved alignment; scores are note-onset matches (pitch must equal; drums compared by class). " +
"The fit line reports the *consistency of the alignment* (warped MIDI onsets vs onset-flux peaks of the recording), not an independent validation of it: " +
"a poor alignment lowers every score below. Capo is informational and not applied.", "");
"The fit line reports the *consistency of the alignment*, not an independent validation of it: precision = share of the recording's onset-flux peaks with a warped MIDI onset within ±70 ms " +
"(flag < 60% per 30 s window), lift = precision minus its value with the MIDI shifted off-grid (flag < 0.25), a symbolic-chroma check for whole-beat/bar shifts, " +
"and \"MIDI onsets audible as attacks\" (information only; a full mix exposes far fewer attacks than the score has notes). " +
"A poor alignment lowers every score below. Capo is informational and not applied.", "");
for (const r of results) {
const a = r.alignment;
const src = a.source === "auto" ? `auto (${a.manual} manual / ${a.auto} auto anchors, computed this run)` : `saved (${a.manual} manual / ${a.auto} auto anchors, saved ${a.savedAt ?? "unknown"})`;
const counts = `${a.manual} manual / ${a.auto} auto anchors`;
const src = a.source === "computed" ? `computed this run (${counts})` : `saved ${a.savedAt ?? "(time unknown)"} (${counts})`;
const pc = (x: number | null) => (x === null ? NA : `${(x * 100).toFixed(1)}%`);
const off = `${a.baseline.offsetSec >= 0 ? "+" : "−"}${Math.abs(a.baseline.offsetSec).toFixed(2)} s`;
const method = a.chosen ? autoSummary({ chosen: a.chosen, baseline: a.baseline, dtwPrecision: a.dtwPrecision })
: `method unknown (saved from the page) · constant-offset baseline ${off}: precision ${pc(a.baseline.precision)}`;
o.push(`## ${r.title} (${r.id})`, "");
o.push(`- Alignment: ${src}`, `- Consistency of the alignment: ${fitSummary(r.fit)}`);
o.push(`- Alignment: ${src}`, `- Method: ${method}`, `- Consistency of the alignment: ${fitSummary(r.fit)}`);
const capos = Object.entries(r.capo);
o.push(`- Capo: ${capos.length ? capos.map(([k, v]) => `${k}: ${v}`).join(", ") : "none listed"}`);
o.push(`- Excluded tracks (no role): ${r.excludedTracks.length ? r.excludedTracks.join(", ") : "none"}`, `- Pipeline + scoring: ${r.seconds.toFixed(1)} s`, "");
@@ -128,6 +163,6 @@ export function renderRealReport(runId: string, results: RealSongResult[], skipp
o.push("");
}
}
if (skipped.length) o.push(`Skipped (no alignment.json): ${skipped.join(", ")}`, "");
if (skipped.length) o.push(`Skipped: ${skipped.map((s) => `${s.id} (${s.reason})`).join("; ")}`, "");
return o.join("\n");
}