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:
1 parent
3b002059e9
commit
3db87c0317
19 files changed
+1016
-252
No files matched your search
+32
-11
@@ -9,31 +9,46 @@ import { writeAlignment, type SongEntry } from "../../src/ref/manifest.ts";
|
||||
import { createPipeline } from "../../src/pipeline/run.ts";
|
||||
import { scoreSong } from "../../src/eval/score.ts";
|
||||
import type { Role } from "../../src/core/types.ts";
|
||||
import type { FitReport, FitWindow } from "../../src/align/fit.ts";
|
||||
|
||||
const hasReference = await Bun.file("reference/songs.json").exists();
|
||||
const mr = (f1: number | null, tp = 8, fp = 2, fn = 2) => ({ tp, fp, fn, precision: f1, recall: f1, f1, octaveErrors: 0, matches: [] as [number, number][] });
|
||||
const fit = (rate: number) => ({ matchRate: rate, medianResidualMs: 12.3, windows: [{ startSec: 0, matchRate: rate, medianResidualMs: 12.3, flagged: false }, { startSec: 30, matchRate: 0.2, medianResidualMs: 80, flagged: true }] });
|
||||
const win = (startSec: number, precision: number, reasons: string[]): FitWindow => ({ startSec, recOnsets: 40, precision, lift: 0.6, audibleRate: 0.4, medianResidualMs: 12.3, beatShift: null, flagged: reasons.length > 0, reasons });
|
||||
const fit = (p: number): FitReport => ({
|
||||
precision: p, lift: 0.62, audibleRate: 0.38, medianResidualMs: 12.3,
|
||||
windows: [win(0, p, []), win(30, 0.5, ["precision 50%"])], stretches: { total: 41, warned: 4, over15: 2 },
|
||||
});
|
||||
const base = { offsetSec: 0.06, precision: 0.88 };
|
||||
const fixture = (): RealSongResult[] => [
|
||||
{
|
||||
id: "a", title: "Song A", alignment: { source: "auto", manual: 0, auto: 40, savedAt: null }, fit: fit(0.8),
|
||||
id: "a", title: "Song A", alignment: { source: "computed", manual: 0, auto: 40, savedAt: null, chosen: "dtw", baseline: base, dtwPrecision: 0.8 }, fit: fit(0.8),
|
||||
perRole: { bass: mr(0.5), guitar: mr(0.9), keys: mr(null, 0, 0, 0) }, lenient: { bass: 0.6, guitar: 0.95, keys: null },
|
||||
drumsPerClass: null, excludedTracks: ["Click", "Pad"], capo: { Lead: 2 }, seconds: 3,
|
||||
},
|
||||
{
|
||||
id: "b", title: "Song B", alignment: { source: "manual", manual: 5, auto: 30, savedAt: "2026-10-29T10:00:00Z" }, fit: fit(0.6),
|
||||
id: "b", title: "Song B", alignment: { source: "saved", manual: 0, auto: 1, savedAt: "2026-10-29T10:00:00Z", chosen: "baseline", baseline: base, dtwPrecision: 0.85 }, fit: fit(0.6),
|
||||
perRole: { guitar: mr(0.3) }, lenient: { guitar: 0.4 }, drumsPerClass: null, excludedTracks: [], capo: {}, seconds: 4,
|
||||
},
|
||||
];
|
||||
|
||||
describe("renderRealReport", () => {
|
||||
const md = renderRealReport("fx", fixture());
|
||||
test("guitar row precedes the other roles; auto source and fit shown", () => {
|
||||
const md = renderRealReport("fx", fixture(), [{ id: "c", reason: "reference/c/alignment.json: not valid JSON" }, { id: "d", reason: "no alignment.json" }]);
|
||||
test("guitar row precedes the other roles; source, method, baseline, fit and stretch warnings shown", () => {
|
||||
expect(md).toContain("Song A");
|
||||
expect(md).toContain("computed this run");
|
||||
expect(md).toContain("2026-10-29T10:00:00Z");
|
||||
expect(md).toContain("saved 2026-10-29T10:00:00Z");
|
||||
expect(md).toContain("0 manual / 40 auto");
|
||||
expect(md).toContain("80.0% of MIDI onsets matched");
|
||||
expect(md).not.toMatch(/manual \(/);
|
||||
expect(md).toContain("chosen: DTW (precision 80.0%)");
|
||||
expect(md).toContain("chosen: baseline (DTW precision 85.0% not better");
|
||||
expect(md).toContain("constant-offset baseline +0.06 s: precision 88.0%");
|
||||
expect(md).toContain("precision 80.0% (lift +0.62)");
|
||||
expect(md).toContain("MIDI onsets audible as attacks 38.0%");
|
||||
expect(md).toContain("1/2 windows flagged (0:30 precision 50%)");
|
||||
expect(md).toContain("stretches: 4/41 warned, 2 > 15%");
|
||||
expect(md).toContain("consistency of the alignment");
|
||||
expect(md).toContain("c (reference/c/alignment.json: not valid JSON)");
|
||||
expect(md).toContain("d (no alignment.json)");
|
||||
const a = md.slice(md.indexOf("Song A"), md.indexOf("Song B"));
|
||||
expect(a.indexOf("| guitar")).toBeGreaterThan(-1);
|
||||
expect(a.indexOf("| guitar")).toBeLessThan(a.indexOf("| bass"));
|
||||
@@ -59,15 +74,18 @@ describe("runReal", () => {
|
||||
id: "t1a-1", title: "Synthetic t1a/1", audio: join(dir, "mix.wav"), midi: join(ref, "t1a-1", "song.mid"),
|
||||
roles: Object.fromEntries(truth.tracks.map((t) => [t.name, t.role])), capo: {},
|
||||
};
|
||||
await Bun.write(join(ref, "songs.json"), JSON.stringify([entry, { ...entry, id: "noalign", title: "No alignment" }]));
|
||||
await Bun.write(join(ref, "songs.json"), JSON.stringify([entry, { ...entry, id: "noalign", title: "No alignment" }, { ...entry, id: "corrupt", title: "Corrupt alignment" }]));
|
||||
await Bun.write(join(ref, "corrupt", "alignment.json"), "{ not json");
|
||||
const dur = 600;
|
||||
await writeAlignment("t1a-1", { anchors: [{ midiSec: 0, audioSec: 0, kind: "manual" }, { midiSec: dur, audioSec: dur, kind: "manual" }], savedAt: "2026-10-30T00:00:00Z", fit: null }, ref);
|
||||
|
||||
const results = await runReal({ runId: "t", auto: false, root: ref, out: join(root, "runs"), resultsDir: join(root, "results") });
|
||||
expect(results.map((r) => r.id)).toEqual(["t1a-1"]);
|
||||
const r = results[0]!;
|
||||
expect(r.alignment).toEqual({ source: "manual", manual: 2, auto: 0, savedAt: "2026-10-30T00:00:00Z" });
|
||||
expect(r.fit.matchRate).not.toBeNull();
|
||||
expect(r.alignment).toEqual({ source: "saved", manual: 2, auto: 0, savedAt: "2026-10-30T00:00:00Z", chosen: null, baseline: expect.any(Object), dtwPrecision: null });
|
||||
expect(Math.abs(r.alignment.baseline.offsetSec)).toBeLessThan(0.05); // identity alignment
|
||||
expect(r.fit.precision).not.toBeNull();
|
||||
expect(r.fit.stretches).toBeDefined();
|
||||
|
||||
const pipe = await createPipeline({
|
||||
demucs: new Uint8Array(await Bun.file("models/demucs/htdemucs_6s.onnx").arrayBuffer()),
|
||||
@@ -81,7 +99,10 @@ describe("runReal", () => {
|
||||
for (const f of ["notes.json", "out.gp", "score.json"]) expect(await Bun.file(join(root, "runs", "real-t", "t1a-1", f)).exists()).toBe(true);
|
||||
const json = await Bun.file(join(root, "results", "real-t.json")).text();
|
||||
expect(json).not.toContain('"matches"');
|
||||
expect(await Bun.file(join(root, "results", "real-t.md")).text()).toContain("Synthetic t1a/1");
|
||||
const md = await Bun.file(join(root, "results", "real-t.md")).text();
|
||||
expect(md).toContain("Synthetic t1a/1");
|
||||
expect(md).toMatch(/corrupt \(.*alignment\.json.*not valid JSON/);
|
||||
expect(md).toContain("noalign (no alignment.json; pass --auto to compute one)");
|
||||
} finally { await rm(root, { recursive: true, force: true }); }
|
||||
}, 600_000);
|
||||
|
||||
|
||||
Reference in new issue
Block a user