import type { Cue } from "./vtt"; import type { TranscriptOutputFormat } from "./transcriptionApps"; type WhisperSegment = { offsets?: { from?: number; to?: number }; text?: string; }; type WhisperDoc = { transcription?: WhisperSegment[]; }; type ChoughChunk = { start_time?: number; end_time?: number; text?: string; }; type ChoughDoc = { chunk_data?: ChoughChunk[]; }; // Append a cue, merging into the previous one when the text is identical // (whisper/chough both repeat a line across adjacent chunks). Shared by both // parsers so the merge behavior stays consistent. function pushCue(cues: Cue[], start: number, end: number, text: string): void { const trimmed = text.trim(); if (!trimmed) return; const prev = cues[cues.length - 1]; if (prev && prev.text === trimmed) { prev.end = end; return; } cues.push({ start, end, text: trimmed }); } function whisperCuesFromDoc(doc: WhisperDoc): Cue[] { const segments = doc.transcription ?? []; const cues: Cue[] = []; for (const seg of segments) { const fromMs = seg.offsets?.from; const toMs = seg.offsets?.to; if (typeof fromMs !== "number" || typeof toMs !== "number") continue; pushCue(cues, fromMs / 1000, toMs / 1000, seg.text ?? ""); } return cues; } function choughCuesFromDoc(doc: ChoughDoc): Cue[] { const chunks = doc.chunk_data ?? []; const cues: Cue[] = []; for (const chunk of chunks) { const start = chunk.start_time; const end = chunk.end_time; if (typeof start !== "number" || typeof end !== "number") continue; // chough times are already in seconds. pushCue(cues, start, end, chunk.text ?? ""); } return cues; } // whisper.cpp JSON: { transcription: [{ offsets: { from, to (ms) }, text }] }. export function parseWhisper(src: string): Cue[] { return whisperCuesFromDoc(JSON.parse(src) as WhisperDoc); } // chough native JSON: { chunk_data: [{ start_time, end_time (sec), text }], ... }. export function parseChough(src: string): Cue[] { return choughCuesFromDoc(JSON.parse(src) as ChoughDoc); } // Sniff a transcript.json shape. Returns null for an unrecognized shape so the // caller can fall back to whisper (the only app before chough). export function detectTranscriptFormat( src: string, ): "whisper-json" | "chough-json" | null { let doc: unknown; try { doc = JSON.parse(src); } catch { return null; } if (doc && typeof doc === "object") { const d = doc as Record; if (Array.isArray(d.chunk_data)) return "chough-json"; if (Array.isArray(d.transcription)) return "whisper-json"; } return null; } // Parse a transcript.json into cues. `hint` is the authoritative format when // known (e.g. the app that just produced the file, or a recorded per-video // tag); when absent or "vtt", the shape is sniffed, and anything unrecognized // falls back to whisper — the only transcription format in use before chough. export function parseTranscriptJson( src: string, hint?: TranscriptOutputFormat | null, ): Cue[] { const fmt = hint === "whisper-json" || hint === "chough-json" ? hint : detectTranscriptFormat(src) ?? "whisper-json"; return fmt === "chough-json" ? parseChough(src) : parseWhisper(src); }