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 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
79130c0aca
commit
48ae9780a6
+3
-3
@@ -44,18 +44,18 @@ async function main() {
|
||||
const seeds = parseSeeds(arg(rest, "seeds", "1-5"));
|
||||
const modes = arg(rest, "modes", "full,oracle-stems,oracle-tempo,oracle-notes").split(",");
|
||||
const runId = arg(rest, "run-id", new Date().toISOString().replace(/[:.]/g, "-"));
|
||||
await runBench({ tiers, seeds, modes, runId, root: arg(rest, "root", "data"), out: arg(rest, "out", "runs") });
|
||||
await runBench({ tiers, seeds, modes, runId, root: arg(rest, "root", "data"), out: arg(rest, "out", "runs"), repeatVote: !rest.includes("--no-repeat-vote") });
|
||||
console.log(`results/${runId}.md`);
|
||||
break;
|
||||
}
|
||||
case "real": {
|
||||
const runId = arg(rest, "run-id", new Date().toISOString().replace(/[:.]/g, "-"));
|
||||
await runReal({ runId, auto: rest.includes("--auto"), root: arg(rest, "root", "reference"), out: arg(rest, "out", "runs") });
|
||||
await runReal({ runId, auto: rest.includes("--auto"), root: arg(rest, "root", "reference"), out: arg(rest, "out", "runs"), repeatVote: !rest.includes("--no-repeat-vote") });
|
||||
console.log(`results/real-${runId}.md`);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.error("usage: bun run bench gen [--tiers t1a,t1b] [--seeds 1-5] [--root data]\n bun run bench run [--tiers ..] [--seeds ..] [--modes full,oracle-stems,oracle-tempo,oracle-notes] [--run-id id] [--root data] [--out runs]\n bun run bench real [--auto] [--run-id id] [--root reference] [--out runs]");
|
||||
console.error("usage: bun run bench gen [--tiers t1a,t1b] [--seeds 1-5] [--root data]\n bun run bench run [--tiers ..] [--seeds ..] [--modes full,oracle-stems,oracle-tempo,oracle-notes] [--run-id id] [--root data] [--out runs] [--no-repeat-vote]\n bun run bench real [--auto] [--run-id id] [--root reference] [--out runs] [--no-repeat-vote]");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -36,13 +36,13 @@ const MODELS = { demucs: "models/demucs/htdemucs_6s.onnx", basicPitch: "models/b
|
||||
const SF2 = join(import.meta.dir, "../../node_modules/@coderline/alphatab/dist/soundfont/sonivox.sf2");
|
||||
const TOL = 0.05, LENIENT_TOL = 0.1;
|
||||
|
||||
export async function runReal(opts: { runId: string; auto: boolean; root?: string; out?: string; resultsDir?: string }): Promise<RealSongResult[]> {
|
||||
export async function runReal(opts: { runId: string; auto: boolean; root?: string; out?: string; resultsDir?: string; repeatVote?: boolean }): Promise<RealSongResult[]> {
|
||||
const root = opts.root ?? "reference", out = opts.out ?? "runs", rdir = opts.resultsDir ?? "results";
|
||||
const songs = await readManifest(root);
|
||||
const pipeline = await createPipeline({
|
||||
demucs: new Uint8Array(await Bun.file(MODELS.demucs).arrayBuffer()),
|
||||
basicPitch: new Uint8Array(await Bun.file(MODELS.basicPitch).arrayBuffer()),
|
||||
});
|
||||
}, { repeatVote: opts.repeatVote ?? true });
|
||||
const sf2 = opts.auto ? new Uint8Array(await Bun.file(SF2).arrayBuffer()) : null;
|
||||
const results: RealSongResult[] = [];
|
||||
const skipped: SkippedSong[] = [];
|
||||
|
||||
+3
-1
@@ -25,11 +25,13 @@ async function sha256(bytes: Uint8Array): Promise<string> {
|
||||
|
||||
export async function runBench(opts: {
|
||||
tiers: TierId[]; seeds: number[]; modes: string[]; runId: string; root: string; out: string; resultsDir?: string;
|
||||
/** Guitar repetition voting (default on); false = --no-repeat-vote. */
|
||||
repeatVote?: boolean;
|
||||
}): Promise<BenchRun> {
|
||||
for (const m of opts.modes) if (!Object.hasOwn(MODES, m)) throw new Error(`unknown mode: ${m}`);
|
||||
const demucs = new Uint8Array(await Bun.file(MODEL_PATHS.demucs).arrayBuffer());
|
||||
const basicPitch = new Uint8Array(await Bun.file(MODEL_PATHS.basicPitch).arrayBuffer());
|
||||
const pipeline = await createPipeline({ demucs, basicPitch }); // model load once per run
|
||||
const pipeline = await createPipeline({ demucs, basicPitch }, { repeatVote: opts.repeatVote ?? true }); // model load once per run
|
||||
const run: BenchRun = {
|
||||
runId: opts.runId,
|
||||
date: new Date().toISOString(),
|
||||
|
||||
+11
-2
@@ -88,7 +88,8 @@ export function barSimilarity(
|
||||
/** Vote notes across repeated bars. Returns a new, onset-sorted note list; input notes are not modified. */
|
||||
export function repeatVote(notes: TimedNote[], grid: Grid, opts: RepeatOpts = {}): TimedNote[] {
|
||||
const o = { ...REPEAT_DEFAULTS, ...opts };
|
||||
if (notes.length === 0 || !(grid.bpm > 0) || !(grid.beatsPerBar > 0)) return notes.slice();
|
||||
const finite = Number.isFinite(grid.bpm) && Number.isFinite(grid.firstDownbeat) && Number.isFinite(grid.beatsPerBar);
|
||||
if (notes.length === 0 || !finite || grid.bpm <= 0 || grid.beatsPerBar <= 0) return notes.slice();
|
||||
const { byBar, barSec, bar0 } = barsOf(notes, grid);
|
||||
const nB = byBar.length;
|
||||
const { coarse, exact } = barSimilarity(byBar.map((ps) => ps.map((p) => ({ pitch: p.n.pitch, rel: p.rel }))), barSec, o.tol, o.context);
|
||||
@@ -117,11 +118,19 @@ export function repeatVote(notes: TimedNote[], grid: Grid, opts: RepeatOpts = {}
|
||||
if (c.bars.has(j)) continue;
|
||||
c.bars.add(j); c.rels.push(q.rel); c.durs.push(q.n.offset - q.n.onset);
|
||||
}
|
||||
const added: TimedNote[] = [];
|
||||
// Near-duplicate guard: no added note within tol of a same-pitch note already in the bar (or its neighbours,
|
||||
// or already added) - an onset just outside tol in the bar itself would otherwise get a second copy.
|
||||
const near = (onset: number, pitch: number) => [...(byBar[i - 1] ?? []), ...bar, ...(byBar[i + 1] ?? [])]
|
||||
.some((q) => q.n.pitch === pitch && Math.abs(q.n.onset - onset) <= o.tol) ||
|
||||
added.some((a) => a.pitch === pitch && Math.abs(a.onset - onset) <= o.tol);
|
||||
for (const c of cands) {
|
||||
if (c.bars.size < o.minAddSupport || c.bars.size / reps.length < o.addFrac) continue;
|
||||
const onset = bar0 + i * barSec + median(c.rels);
|
||||
if (onset >= 0) out.push({ onset, offset: onset + median(c.durs), pitch: c.pitch });
|
||||
if (onset < 0 || near(onset, c.pitch)) continue;
|
||||
added.push({ onset, offset: onset + median(c.durs), pitch: c.pitch });
|
||||
}
|
||||
out.push(...added);
|
||||
}
|
||||
return out.sort((a, b) => a.onset - b.onset || a.pitch - b.pitch);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user