/** biome-ignore-all lint/suspicious/noConsole: CLI script output */ /** * CLI harness: replays a directory of extracted VoD frames through the full * detector registry driven by a DetectorScheduler, mirroring the worker's * chunk scan — the tool for "the browser scan missed an event that a fixture * parses fine": extract the surrounding footage with * ffmpeg -ss -i vod.mkv -t 30 -vf fps=6 frames/f%04d.png * then replay it and watch which checks the scheduler ran and what they saw. * * Usage: pnpm scanner:replay * (frames are ffmpeg-numbered f0001.png..; t = startT + (n-1)/fps) */ import { readdirSync } from "node:fs"; import { join } from "node:path"; import { loadOpenCV } from "../../app/features/scanner/core/cv"; import { MAP_START_EVENT_TYPE } from "../../app/features/scanner/core/detectors/map-start/index"; import { createAllDetectors, SCOREBOARD_EVENT_TYPES, } from "../../app/features/scanner/core/detectors/registry"; import { DetectorScheduler } from "../../app/features/scanner/core/detectors/scheduler"; import { normalizeFrame, toMat } from "../../app/features/scanner/core/image"; import { readImage } from "../../app/features/scanner/node/image-io"; import { loadScoreboardResources } from "../../app/features/scanner/node/resources"; const [framesDir, startTArg, fpsArg] = process.argv.slice(2); if (!framesDir || !startTArg || !fpsArg) { console.error("usage: pnpm scanner:replay "); process.exit(1); } const startT = Number(startTArg); const fps = Number(fpsArg); await loadOpenCV(); const detectors = createAllDetectors(await loadScoreboardResources()); const scheduler = new DetectorScheduler(detectors, { matchOpeningTypes: [MAP_START_EVENT_TYPE], matchClosingTypes: SCOREBOARD_EVENT_TYPES, }); scheduler.reset(startT); const files = readdirSync(framesDir) .filter((f) => f.endsWith(".png")) .sort(); for (const [i, file] of files.entries()) { const t = startT + i / fps; const due = scheduler.dueDetectors(t); if (due.length === 0) continue; const image = await readImage(join(framesDir, file)); const src = toMat(image); const frame = normalizeFrame(src); src.delete(); for (const detector of detectors) { if (!due.includes(detector.id)) continue; const gate = detector.gate(frame); scheduler.recordGate(detector.id, t, gate.pass, gate.signature); if (!gate.pass) { if (process.env.LOG_GATES?.includes(detector.id)) { console.log( `${t.toFixed(2)} ${file} ${detector.id} gate=${gate.score.toFixed(3)} FAIL`, ); } continue; } if (!scheduler.shouldParse(detector.id, t)) { console.log( `${t.toFixed(2)} ${file} ${detector.id} gate=${gate.score.toFixed(3)} PARSE-SUPPRESSED`, ); continue; } const events = detector.parse(frame, t, gate); scheduler.recordParse(detector.id, t, events); console.log( `${t.toFixed(2)} ${file} ${detector.id} gate=${gate.score.toFixed(3)} events=[${events .map((e) => `${e.type}@${e.confidence.toFixed(3)}`) .join(", ")}]`, ); } frame.delete(); }