/** * Round-trip test of the dataset download: build a benchmark in-process, * write the offline archive's files to a scratch directory (unzipped; * the zip wrapper is byte bookkeeping tested separately), run the shipped * evaluate.m through the real numbl CLI, and require the per-run * accuracies it prints to match the in-process scores exactly. Float32 * WAVs round-trip the samples bit-exactly, so any disagreement is a bug * in the harness, not noise. */ import { execFileSync } from 'node:child_process'; import { mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { FS, type SynthConfig } from '../src/bench/types.ts'; import { synthesize } from '../src/bench/synthesize.ts'; import { buildTraining } from '../src/bench/training.ts'; import { runAlgorithm } from '../src/bench/execute.ts'; import { score } from '../src/bench/score.ts'; import { buildOfflineFiles, type OfflineExportInput } from '../src/export/offline.ts'; import { zipStore } from '../src/export/zip.ts'; import { makeSyntheticLibrary } from './synthetic-lib.ts'; import matchedFilterSrc from '../src/algorithms/template_matched_filter.m?raw'; import onsetKnnSrc from '../src/algorithms/onset_knn.m?raw'; import silenceGapKnnSrc from '../src/algorithms/silence_gap_knn.m?raw'; import overlapPursuitSrc from '../src/algorithms/overlap_pursuit.m?raw'; import naiveSrc from '../src/algorithms/naive_threshold.m?raw'; const lib = makeSyntheticLibrary(); const cfg: SynthConfig = { durationS: 20, eventsPerType: 15, gainMinDb: -12, overlap: 0.2, coincidence: 0, coincidenceMs: 5, trainFraction: 0.3, seed: 1, }; const NRUNS = 2; const TOL_MS = 20; const algos = [ { name: 'template_matched_filter', source: matchedFilterSrc }, { name: 'onset_knn', source: onsetKnnSrc }, { name: 'silence_gap_knn', source: silenceGapKnnSrc }, { name: 'overlap_pursuit', source: overlapPursuitSrc }, { name: 'naive_threshold', source: naiveSrc }, ]; // in-process benchmark, collecting expected per-run accuracies const runs: OfflineExportInput['runs'] = []; const expected = new Map(); for (let r = 0; r < NRUNS; r++) { const ds = synthesize(lib, cfg, r); const training = buildTraining(lib, ds.trainIndices); runs.push({ x: ds.x, truth: ds.truth, training: training.snips.map((s) => ({ data: Float32Array.from(s.data), count: s.count })), }); for (const a of algos) { const out = runAlgorithm(a.source, ds.x, FS, training); const s = score(ds.truth, out.detections, lib.names.length, TOL_MS / 1000); (expected.get(a.name) ?? expected.set(a.name, []).get(a.name)!).push(s.accuracy.toFixed(3)); } } // write the archive's files to a scratch directory const files = buildOfflineFiles({ names: lib.names, fs: FS, pre: lib.pre, post: lib.post, toleranceMs: TOL_MS, config: cfg, algorithms: algos.map((a) => ({ source: a.source })), runs, }); const zip = zipStore(files); console.log(`archive: ${files.length} files, ${(zip.length / 1e6).toFixed(1)} MB stored`); const dir = join(process.env.TMPDIR ?? '/tmp', `asstest-offline-${process.pid}`); rmSync(dir, { recursive: true, force: true }); for (const f of files) { mkdirSync(join(dir, dirname(f.name)), { recursive: true }); writeFileSync(join(dir, f.name), f.data); } // run the shipped harness through the numbl CLI console.log('running: npx numbl run evaluate.m'); const stdout = execFileSync('npx', ['numbl', 'run', 'evaluate.m'], { cwd: dir, encoding: 'utf8', timeout: 600000, }); console.log(stdout); // compare per-run accuracies, algorithm by algorithm let failures = 0; for (const a of algos) { const block = stdout.split(`=== ${a.name} ===`)[1]?.split('===')[0] ?? ''; const got = [...block.matchAll(/run\s+\d+: accuracy (\d\.\d{3})/g)].map((m) => m[1]); const want = expected.get(a.name)!; const ok = got.length === want.length && got.every((g, i) => g === want[i]); console.log(`${a.name.padEnd(26)} browser=[${want.join(', ')}] offline=[${got.join(', ')}] ${ok ? 'MATCH' : 'MISMATCH'}`); if (!ok) failures++; } rmSync(dir, { recursive: true, force: true }); if (failures) { console.error('OFFLINE FAIL'); process.exit(1); } console.log('OFFLINE PASS');