// The phrase console's shapes, constants and its ONE normaliser, with no server // imports. // // This file exists for the reason lib/note-types.ts does, and the break it // prevents is the same one: PhraseConsole.tsx is a client component and needs // LEAD_PAD, PAGE_SIZE and normTerms() as VALUES at runtime. A value import is // not erased, so taking them from lib/phrases.ts would drag node:fs/promises // into the browser chunk and the build would refuse. `tsc --noEmit` cannot see // that; only the bundler can. // // lib/phrases.ts re-exports everything here, so the server side has one import. // --------------------------------------------------------------------------- // The corpus constants. Every one of these is a MEASURED property of the ASR, // not a preference, so each carries the measurement that set it. // --------------------------------------------------------------------------- /** * The chunk stride the transcription ran at: parakeet-chunked.mjs uses a 240s * chunk with 3s of overlap, so a new chunk starts every 237s. * * This matters because the merge only drops a word when * `|dstart| < 0.12 && x.w === w.w` -- casing and punctuation drift let twins * through. There are 3,472 adjacent same-word pairs within 1s in the corpus, * and 2,248 of them (65%) sit in a `start % 237 < 3.5` band. That band is an * exact discriminator rather than a heuristic: inside it a repeat is an * artefact of the seam, outside it the other 1,224 are genuine stutters * ("Very, very") and must survive. */ export const CHUNK_STEP = 237; export const CHUNK_OVERLAP = 3.5; /** * Lead-in for a preview, in seconds. * * Parakeet timestamps a word's START LATE by 25-290ms -- see the note in * song/clipwindow.mjs, where the same fact makes the window guards asymmetric. * A preview that seeks to `word.start` clips the first phoneme, which reads as * the archive being wrong rather than the timestamp. * * This is a LISTENING pad, not a cut boundary. Nothing here decides where an * edit goes; clipWindow() still owns that, and it measures rather than pads. */ export const LEAD_PAD = 0.35; export const TAIL_PAD = 0.35; /** The in-context audition: enough either side to hear what the line was. */ export const WIDE_PAD = 1.5; /** Words of context shown each side of a match. */ export const CONTEXT_WORDS = 7; export const PAGE_SIZE = 50; export const MAX_PER_PAGE = 200; /** * Terms in one query. `the` alone is 23,081 hits; eight terms is far past any * phrase anyone means, and the cap is here so a pathological needle cannot be * built by pasting a paragraph in. */ export const MAX_TERMS = 8; // --------------------------------------------------------------------------- // The shapes. // --------------------------------------------------------------------------- export type PhraseOrder = "time" | "conf"; export type PhraseQuery = { /** What was typed, verbatim, so the box can be refilled with it. */ q: string; /** What is actually searched for. normTerms(q). */ terms: string[]; /** One episode, or the whole corpus. */ video: string | null; /** Minimum confidence, or null for no filter -- which is the DEFAULT. */ minConf: number | null; /** Drop hits from episodes flagged in suspect-sources.json. */ clean: boolean; /** * Fold chunk twins. `dupes=1` (the default) folds them and reports the count * as `collapsed`; `dupes=0` shows every raw match. * * Read the flag as "the dupe-folding is on", not as "dupes are shown" -- it * is the folding that is being switched, and `total` goes DOWN when it is 1. */ dupes: boolean; order: PhraseOrder; /** 1-based. */ page: number; per: number; }; export type PhraseHit = { /** * `