import { writeFileSync } from 'node:fs'; export interface NarrationCue { text: string; videoTimeMs: number; durationMs: number; } /** * Narration manifest writer. * * The runner knows the exact video-time of each narration block from the * storyboard itself, so cues come in with an explicit `videoTimeMs` instead * of being stamped against a wall-clock origin. That keeps the manifest in * lockstep with the trimmed video even if step durations drift slightly. */ class NarrationLog { private cues: NarrationCue[] = []; reset(): void { this.cues = []; } add(cue: NarrationCue): void { if (cue.videoTimeMs < 0) return; this.cues.push(cue); } flush(path: string, totalDurationMs: number): NarrationCue[] { const sorted = [...this.cues].sort((a, b) => a.videoTimeMs - b.videoTimeMs); const manifest = { totalDurationMs, cues: sorted }; writeFileSync(path, JSON.stringify(manifest, null, 2)); return sorted; } } export const narrationLog = new NarrationLog(); function formatVttTimestamp(ms: number): string { const clamped = Math.max(0, Math.round(ms)); const totalSeconds = Math.floor(clamped / 1000); const millis = clamped % 1000; const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; const pad = (n: number, width: number): string => n.toString().padStart(width, '0'); return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)}.${pad(millis, 3)}`; } /** * Render narration cues as a WebVTT document. * * Pure helper: takes cues (already ordered by the caller) and produces a * caption sidecar string. Cue text is trimmed and any internal blank lines * are collapsed so a single cue stays a single VTT block. */ export function cuesToVtt(cues: NarrationCue[]): string { let out = 'WEBVTT\n\n'; for (const cue of cues) { const start = formatVttTimestamp(cue.videoTimeMs); const end = formatVttTimestamp(cue.videoTimeMs + cue.durationMs); const text = cue.text .split('\n') .map((line) => line.trim()) .filter((line) => line.length > 0) .join('\n') .trim(); out += `${start} --> ${end}\n${text}\n\n`; } return out; }