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:
Lucas WintherandClaude Opus 5.5 committed 2026-10-30 21:21:00 +01:00
1 parent 79130c0aca
commit 48ae9780a6
6 files changed
+87 -12

No files matched your search

+20
View File
@@ -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);