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
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user