// THE TRACKS OF A TRANSCRIPT — one notion of "track" for every reader. // // A record's transcript is read from ONE track, its primary, chosen by the // caption-track rule (lib/videoStatus.ts: en-orig first, the operator's pin // above all, a local transcription above captions). A record may hold other // English tracks beside it: the served `en`, a regional en-GB, an en→en // auto-translation, or the captions a local transcription replaced. Human // captions are not always a transcript of what was said, so those stay // readable and searchable as ALTERNATES — a viewer's choice, never a change to // the primary (the pin, transcript-pin.json, is how the primary changes). // // An alternate is kept only where its words differ from the primary's and from // every alternate kept before it: most served `en` tracks are byte-identical to // en-orig, and shipping or indexing those would double the corpus for nothing. // // Track ids are the caption's language code as its file names it // (transcript..vtt), plus two that no file names: `transcription` (a local // whisper/parakeet transcript) and `pinned` (transcript.en.vtt while the // operator's pin stands — a copy of whichever track was picked). Labels are // derived from the id, here and nowhere else, so the editor, the export site, // a search hit and the MCP all say the same words. // // Pure: no node built-ins, safe in a client bundle. import type { Cue } from "./vtt"; export const TRANSCRIPTION_TRACK = "transcription"; export const PINNED_TRACK = "pinned"; export type AltTrack = { track: string; cues: Cue[] }; // What a transcript record carries about its tracks. Both fields are OMITTED // when the record has no alternate that differs (the common case), so those // records' pages stay byte-identical to the ones already on disk. export type TrackFields = { // The primary's track id. track?: string; // The English tracks whose words differ from the primary's, in preference // order. altTracks?: AltTrack[]; }; const REGIONS: Record = { US: "US", GB: "UK", UK: "UK", CA: "Canadian", AU: "Australian", IE: "Irish", IN: "Indian", NZ: "New Zealand", ZA: "South African", }; // The kind of a track, from its id. export type TrackKind = | "original" | "uploaded" | "regional" | "translated" | "transcription" | "pinned" | "other"; // A region subtag as YouTube writes one: two letters (en-US) or three digits // (en-419). Anything else after `en-` is a track YouTube named itself — an // uploader's extra caption track (en-uYU-mmqFLq8). const REGION_RE = /^(?:[A-Za-z]{2}|\d{3})$/; export function trackKind(track: string): TrackKind { if (track === TRANSCRIPTION_TRACK) return "transcription"; if (track === PINNED_TRACK) return "pinned"; if (track === "en-orig" || /^en-[^-]+-orig$/.test(track)) return "original"; if (track === "en") return "uploaded"; if (/^en-en(?:-|$)/.test(track)) return "translated"; const m = track.match(/^en-(.+)$/); if (m) return REGION_RE.test(m[1]) ? "regional" : "uploaded"; return "other"; } // "UK" for en-GB, the code itself for a region this table does not name. function regionName(code: string): string { return REGIONS[code.toUpperCase()] ?? code; } // A plain label for a track — what a person reads in the switcher and on a // search hit ("in uploaded captions"). export function trackLabel(track: string): string { switch (trackKind(track)) { case "original": { // en-US-orig: the original audio's captions, under a region. const m = track.match(/^en-([^-]+)-orig$/); return m ? `original audio captions (${regionName(m[1])})` : "original audio captions"; } case "uploaded": // A track YouTube named (en-uYU-mmqFLq8) is another uploaded one. return track === "en" ? "uploaded captions" : "other uploaded captions"; case "translated": return "auto-translated captions"; case "transcription": return "transcription"; case "pinned": return "chosen captions"; case "regional": { const name = REGIONS[track.slice(3).toUpperCase()]; return name ? `${name} English captions` : `regional captions (${track})`; } default: return `captions (${track})`; } } // The labels of a switcher's tracks, in order: trackLabel, with the id added // where two tracks would otherwise read the same (two tracks YouTube named). export function trackLabels(tracks: readonly string[]): string[] { const labels = tracks.map(trackLabel); return labels.map((l, i) => labels.indexOf(l) !== labels.lastIndexOf(l) ? `${l} (${tracks[i]})` : l, ); } // The track id of a caption file name (transcript..vtt), or null. export function trackOfVttFile(filename: string): string | null { const m = filename.match(/^transcript\.([^.]+)\.vtt$/); return m ? m[1] : null; } // A cue list's words, for "do these two tracks say the same thing" — timing // alone is not a different transcript. export function cueWords(cues: readonly Cue[]): string { return cues.map((c) => c.text).join("\n"); } // The alternates worth keeping: tracks with at least one cue whose words differ // from the primary's and from every track kept before them. `candidates` in // preference order; the primary is not among them. export function distinctAltTracks( primary: readonly Cue[] | undefined, candidates: readonly AltTrack[], ): AltTrack[] { const seen = new Set([cueWords(primary ?? [])]); const out: AltTrack[] = []; for (const c of candidates) { if (c.cues.length === 0) continue; const words = cueWords(c.cues); if (seen.has(words)) continue; seen.add(words); out.push(c); } return out; } // The tracks a record offers, primary first: what a switcher lists. A record // with no alternates offers just its primary (or nothing, with no track id). export function recordTracks(rec: TrackFields): string[] { if (!rec.altTracks || rec.altTracks.length === 0) return rec.track ? [rec.track] : []; return [rec.track ?? "", ...rec.altTracks.map((t) => t.track)].filter((t) => t !== ""); } // The cues of one track of a record: the primary when `track` is absent or // names it, an alternate when it names one, undefined when the record has no // such track. export function cuesOfTrack( rec: R, track: string | null | undefined, ): Cue[] | undefined { if (!track || track === rec.track) return rec.cues; return rec.altTracks?.find((t) => t.track === track)?.cues; } // How far (seconds) a primary hit covers an alternate's. An alternate hit with // a primary hit this close is the same moment found twice — the primary's is // the one shown. Only a match the primary has nowhere near is the alternate's // to report. export const ALT_HIT_COVERED_SEC = 20; // Drop the alternate hits a primary hit already covers (ALT_HIT_COVERED_SEC). // Both lists carry `start` in seconds; the primary's needs no order. export function uncoveredAltHits( primary: readonly { start: number }[], alt: readonly H[], ): H[] { if (primary.length === 0) return [...alt]; const starts = primary.map((h) => h.start).sort((a, b) => a - b); return alt.filter((h) => { // Binary search for the nearest primary start. let lo = 0; let hi = starts.length - 1; while (lo < hi) { const mid = (lo + hi) >> 1; if (starts[mid] < h.start) lo = mid + 1; else hi = mid; } const near = [starts[lo], starts[lo - 1]].filter((s) => s !== undefined); return !near.some((s) => Math.abs(s - h.start) <= ALT_HIT_COVERED_SEC); }); } // THE ONE RULE for matching a record's words across its tracks, used by every // search (the viewer's leaf pipeline, the MCP, the query-tree evaluator): // `find` is run over the primary's cues, then over each alternate's, and an // alternate's hit is kept only where no hit already kept is within // ALT_HIT_COVERED_SEC — so a word every track says is found once, in the // primary, and a word only an alternate says is found there, wearing that // alternate's track id. Ordered by start when an alternate adds anything. export function hitsAcrossTracks( rec: TrackFields & { cues?: readonly Cue[] | undefined }, find: (cues: readonly Cue[]) => H[], ): (H & { track?: string })[] { const out: (H & { track?: string })[] = rec.cues ? find(rec.cues) : []; let added = false; for (const alt of rec.altTracks ?? []) { for (const h of uncoveredAltHits(out, find(alt.cues))) { out.push({ ...h, track: alt.track }); added = true; } } if (added) out.sort((a, b) => a.start - b.start); return out; } // "in uploaded captions" — what a hit from an alternate says about itself. export function inTrackLabel(track: string): string { return `in ${trackLabel(track)}`; }