import { existsSync } from "node:fs"; import { sourcePitch, F0_GATE } from "@/lib/corpus"; import { sourceWav, wordsFor } from "@/lib/clips"; import { suspectVideos } from "@/lib/phrases"; import { readWavWindow } from "@/lib/wav"; import { spectralFlatness } from "../../../../../song/flatness.mjs"; export const dynamic = "force-dynamic"; // The evidence that would have changed a judgement, measured on the window // actually on screen. // // Two of the three faults this project has shipped were invisible at the moment // of judgement and perfectly measurable: // // HISS a clip at flatness 0.10 went into a finished render and was then // normalised UP x4.29, because render-poly levels every clip to // TARGET_RMS -- so the hissiest candidate gets the largest gain. // Nothing in the pipeline scored it. // A WORD 27 of 615 notes in one plan had a real word inside the window. // The one a listener picked out was "has been", stored under the // token "um". The ASR knew; nothing asked it. // // Both are cheap here, and both are about THIS window rather than the // candidate's original one -- so dragging a handle changes the answer, which is // the point. /** plan-qa.mjs's threshold, so the tool and the audit agree about "hissy". */ const FLAT_MAX = 0.055; export async function GET(request: Request, ctx: { params: Promise<{ key: string }> }) { const { key } = await ctx.params; const decoded = decodeURIComponent(key); const at = decoded.lastIndexOf("@"); const video = at > 0 ? decoded.slice(0, at) : decoded; const url = new URL(request.url); const from = Number(url.searchParams.get("from")); const to = Number(url.searchParams.get("to")); if (!Number.isFinite(from) || !Number.isFinite(to) || to <= from) { return Response.json({ error: "bad window" }, { status: 400 }); } const file = sourceWav(video); let flatness: number | null = null; if (existsSync(file)) { try { const region = await readWavWindow(file, from, to); // spectralFlatness needs a 1024-sample frame; a 240ms window at 48k is // 11,520 samples, so only an absurdly short selection returns nothing. const f = spectralFlatness(region.x, region.sampleRate); flatness = f > 0 ? +f.toFixed(4) : null; } catch { flatness = null; } } // A real word inside THIS window, from the ASR. The same 30ms tolerance the // plan scan and swap-clip use, so all three agree about which clips are dirty. const words = await wordsFor(video); const inside = words .filter((w) => w.end > from + 0.03 && w.start < to - 0.03) .map((w) => w.w); const source = await sourcePitch(video); // The same reduction /browse/find applies, from the same module. It used to // be inline here; two copies of "which episodes are flagged" is two answers // waiting to drift. const suspectSource = (await suspectVideos()).has(video); return Response.json( { key: decoded, video, window: { from, to }, flatness, flatMax: FLAT_MAX, hissy: flatness != null && flatness > FLAT_MAX, wordsInside: inside, gate: F0_GATE, source: { clips: source.n, f0s: source.f0s, lo: source.lo, hi: source.hi, // Spread in semitones. Over an octave from one person's filler sounds // is not plausible, so it is the two-speaker signature -- and unlike a // cluster count it stays true however the values are distributed. spread: source.spread, wide: source.wide, suspect: suspectSource, }, }, { headers: { "cache-control": "no-store" } }, ); }