fix(pipeline): repeat-vote review fixes
- Return an unchanged copy for a non-finite/zero grid (NaN downbeat used to
delete every guitar note).
- Skip an added note when a same-pitch note (bar, neighbouring bars or already
added) lies within tol; avoids near-duplicates across bar lines.
- bench run/real: --no-repeat-vote passes { repeatVote: false } to the pipeline.
- Tests: notes before a late downbeat, bpm 0 / NaN / Infinity, trailing partial
bar, half-tempo grid, neighbour-bar duplicate, pipeline-level on/off.
Eval unchanged: synthetic t1a 0.885 t1b 0.840 t1c 0.752 t1d 0.737, lnoe 0.507.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
79130c0aca
commit
48ae9780a6
@@ -41,9 +41,53 @@ test("add/drop can be disabled and empty input is handled", () => {
|
||||
expect(repeatVote([], grid)).toEqual([]);
|
||||
});
|
||||
|
||||
test("bars before a late first downbeat are handled", () => {
|
||||
const g: Grid = { ...grid, firstDownbeat: 0.5 };
|
||||
const notes = bars(6, (b, pos) => !(b === 2 && pos === 0.5)).map((n) => ({ ...n, onset: n.onset + 0.5, offset: n.offset + 0.5 }));
|
||||
test("notes before a late first downbeat are voted too", () => {
|
||||
// Grid downbeat at 4.3 s; the riff starts at 0.3 s, so bars -2 and -1 precede the first downbeat.
|
||||
const g: Grid = { ...grid, firstDownbeat: 4.3 };
|
||||
const notes = bars(6, (b, pos) => !(b === 0 && pos === 0.5)).map((n) => ({ ...n, onset: n.onset + 0.3, offset: n.offset + 0.3 }));
|
||||
expect(notes[0]!.onset).toBeLessThan(g.firstDownbeat);
|
||||
const out = repeatVote(notes, g);
|
||||
expect(out.some((n) => Math.abs(n.onset - 5) < 1e-6 && n.pitch === 55)).toBe(true);
|
||||
expect(out.length).toBe(30);
|
||||
expect(out.some((n) => Math.abs(n.onset - 0.8) < 1e-6 && n.pitch === 55)).toBe(true);
|
||||
});
|
||||
|
||||
test("non-finite or zero grid returns an unchanged copy", () => {
|
||||
const notes = bars(6, (b, pos) => !(b === 3 && pos === 1));
|
||||
for (const g of [
|
||||
{ ...grid, bpm: 0 }, { ...grid, bpm: NaN }, { ...grid, bpm: Infinity },
|
||||
{ ...grid, firstDownbeat: NaN }, { ...grid, firstDownbeat: -Infinity }, { ...grid, beatsPerBar: NaN },
|
||||
]) {
|
||||
const out = repeatVote(notes, g);
|
||||
expect(out).toEqual(notes);
|
||||
expect(out).not.toBe(notes);
|
||||
}
|
||||
});
|
||||
|
||||
test("notes after the last full bar are kept and voted", () => {
|
||||
// 6 full bars plus a trailing partial bar holding only the riff's first note.
|
||||
const notes = [...bars(6), { onset: 12, offset: 12.2, pitch: 52 }];
|
||||
const out = repeatVote(notes, grid);
|
||||
// Every input note survives (the trailing one is supported by the riff's downbeat note in all repeats) ...
|
||||
for (const n of notes) expect(out.some((m) => Math.abs(m.onset - n.onset) < 1e-6 && m.pitch === n.pitch)).toBe(true);
|
||||
// ... and anything added there stays inside that bar.
|
||||
expect(out.every((n) => n.onset < 14)).toBe(true);
|
||||
});
|
||||
|
||||
test("half-tempo grid (riff twice per bar) still fills a missing note", () => {
|
||||
const g: Grid = { ...grid, bpm: 60 }; // 4 s bars
|
||||
const notes = bars(8, (b, pos) => !(b === 5 && pos === 1));
|
||||
const out = repeatVote(notes, g);
|
||||
expect(out.some((n) => Math.abs(n.onset - 11) < 1e-6 && n.pitch === 57)).toBe(true);
|
||||
});
|
||||
|
||||
test("added notes do not duplicate a same-pitch note within tol in a neighbouring bar", () => {
|
||||
// The riff ends with a note 80 ms before the next downbeat. In bar 3 that note is 40 ms late, which moves it
|
||||
// across the bar line into bar 4: bar 3 looks like it misses the note, but adding it would duplicate the late one.
|
||||
const notes = bars(6).flatMap((n) => n.pitch === 52 ? [n, { onset: n.onset + 1.92, offset: n.onset + 2, pitch: 64 }] : [n])
|
||||
.map((n) => (n.pitch === 64 && Math.abs(n.onset - 7.92) < 1e-9 ? { ...n, onset: 7.96, offset: 8.04 } : n));
|
||||
const out = repeatVote(notes, grid, { drop: false });
|
||||
expect(out.filter((n) => n.pitch === 64 && Math.abs(n.onset - 7.94) < 0.1).length).toBe(1);
|
||||
// Without the late note, the gap is filled.
|
||||
const gap = notes.filter((n) => !(n.pitch === 64 && n.onset === 7.96));
|
||||
expect(repeatVote(gap, grid, { drop: false }).some((n) => n.pitch === 64 && Math.abs(n.onset - 7.92) < 1e-6)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@ import { test, expect, beforeAll } from "bun:test";
|
||||
import { join } from "node:path";
|
||||
import { buildSong, loadSongDir } from "../../src/bench/dataset.ts";
|
||||
import { createPipeline, copyFingering, sumAudio } from "../../src/pipeline/run.ts";
|
||||
import { createTranscriber } from "../../src/pipeline/transcribe.ts";
|
||||
import { repeatVote } from "../../src/pipeline/repeat.ts";
|
||||
import { snapTicks, quantize } from "../../src/pipeline/quantize.ts";
|
||||
import type { TimedNote } from "../../src/core/types.ts";
|
||||
import { truthFromGp } from "../../src/score/gp.ts";
|
||||
@@ -99,3 +101,21 @@ test("sumAudio adds buffers sample-wise", () => {
|
||||
expect([...s.channels[1]!]).toEqual([1, 2]);
|
||||
expect(s.sampleRate).toBe(10);
|
||||
});
|
||||
|
||||
test("repeatVote option: off = raw transcription, on (default) = repeat-voted", async () => {
|
||||
// Cheap fixture: oracle stems + oracle tempo (no separation), t1a/1 guitar stem.
|
||||
const bytes = async (p: string) => new Uint8Array(await Bun.file(join(root, p)).arrayBuffer());
|
||||
const off = await createPipeline({ demucs: await bytes("models/demucs/htdemucs_6s.onnx"), basicPitch: await bytes("models/basic-pitch/nmp.onnx") }, { repeatVote: false });
|
||||
const transcribe = await createTranscriber(await bytes("models/basic-pitch/nmp.onnx"));
|
||||
const d = await loadSongDir(join(root, "data/t1a/1"));
|
||||
const flags = { oracleStems: true, oracleTempo: true, oracleNotes: false };
|
||||
const input = { mix: d.mix, oracle: { stems: d.stems, truth: d.truth } };
|
||||
const gt = d.truth.tracks.find((t) => t.role === "guitar")!;
|
||||
const raw = await transcribe(d.stems.get(gt.name)!, { minPitch: 40, maxPitch: 88 });
|
||||
const strip = (ns: TimedNote[]) => ns.map((n) => [n.onset, n.offset, n.pitch]);
|
||||
const offNotes = (await off(input, flags)).output.tracks.find((t) => t.role === "guitar")!.notes;
|
||||
expect(strip(offNotes)).toEqual(strip(raw));
|
||||
const onRes = await run(input, flags);
|
||||
const onNotes = onRes.output.tracks.find((t) => t.role === "guitar")!.notes;
|
||||
expect(strip(onNotes)).toEqual(strip(repeatVote(raw, onRes.output.grid)));
|
||||
}, SLOW);
|
||||
|
||||
Reference in New Issue
Block a user