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

+14 -7
View File
@@ -127,12 +127,18 @@ describe("API", () => {
expect(Math.max(...first.recordingPeaks.max)).toBeGreaterThan(0.05);
expect(first.source).toBe("auto");
expect(first.savedAt).toBeNull();
expect(first.anchors.length).toBeGreaterThan(2);
// the fixture's MIDI is the recording's own score (identity alignment): the constant-offset baseline wins
expect(first.auto.chosen).toBe("baseline");
expect(first.anchors).toHaveLength(1);
expect(Math.abs(first.auto.baseline.offsetSec)).toBeLessThan(0.05);
expect(first.anchors.every((a: Anchor) => a.kind === "auto")).toBe(true);
expect(first.stretches.length).toBeGreaterThanOrEqual(first.anchors.length); // +1, minus an empty edge stretch
expect(first.stretches[1].midiBpm).toBeGreaterThan(30);
expect(first.stretches[1].bpm).toBeCloseTo(first.stretches[1].midiBpm / first.stretches[1].ratio, 6);
expect(first.fit.matchRate).toBeGreaterThan(0.5);
expect(first.fit.precision).toBeGreaterThan(0.6);
expect(first.fit.stretches.total).toBe(first.stretches.length);
expect(["dtw", "baseline"]).toContain(first.auto.chosen);
expect(typeof first.auto.baseline.offsetSec).toBe("number");
expect(first.midiTempo[0].bpm).toBeGreaterThan(30);
expect(await Bun.file(join(root, "fix", "recording.wav")).exists()).toBe(true);
expect(await Bun.file(join(root, "fix", "peaks.json")).exists()).toBe(true);
@@ -164,7 +170,7 @@ describe("API", () => {
const r = await api("/api/song/fix/fit", json({ anchors: [{ midiSec: 1, audioSec: 1.4, kind: "manual" }] }));
const j = await r.json();
expect(j.fit.windows.length).toBeGreaterThan(0);
expect(j.fit.matchRate).toBeLessThan(first.fit.matchRate);
expect(j.fit.precision).toBeLessThan(first.fit.precision);
expect(j.stretches.length).toBe(2);
});
test("bad anchors are rejected with 400", async () => {
@@ -173,12 +179,13 @@ describe("API", () => {
expect((await api("/api/song/fix/fit", { method: "POST", body: "{not json" })).status).toBe(400);
});
test("auto: keeps manual anchors and re-solves autos around them", async () => {
const manual: Anchor = { ...first.anchors[3], kind: "manual" };
const r = await api("/api/song/fix/auto", json({ anchors: [manual, ...first.anchors.slice(5)] }));
const manual: Anchor = { midiSec: 10, audioSec: 10, kind: "manual" };
const r = await api("/api/song/fix/auto", json({ anchors: [manual, { midiSec: 15, audioSec: 15.5, kind: "auto" }] }));
const j = await r.json();
expect(j.anchors.filter((a: Anchor) => a.kind === "manual")).toEqual([manual]);
expect(j.anchors.filter((a: Anchor) => a.kind === "auto").length).toBeGreaterThan(2);
expect(j.fit.matchRate).toBeGreaterThan(0.5);
expect(j.fit.precision).toBeGreaterThan(0.6);
expect(j.auto.chosen).toBe("dtw"); // manual anchors present: never the baseline
expect(j.stretches.length).toBeGreaterThanOrEqual(j.anchors.length);
}, BIG);
test("reset: raw MIDI (no anchors)", async () => {
@@ -217,6 +224,6 @@ describe("API", () => {
const j = await g.json();
expect(j.source).toBe("auto");
expect(j.warning).toContain("alignment.json");
expect(j.anchors.length).toBeGreaterThan(2);
expect(j.anchors.length).toBeGreaterThanOrEqual(1);
});
});