import { v4 as uuidv4 } from "uuid"; import type { Caption } from "@/types"; const BASE_WORD_MS = 180; const CHAR_MS = 40; const GAP_MS = 60; // ponytail: phrases of 6 read far better than one word at a time, and each cue // stays on screen ~6x longer without touching playback speed export const WORDS_PER_CAPTION = 6; function wordDuration(text: string): number { return BASE_WORD_MS + text.length * CHAR_MS; } function groupWords(text: string, perGroup = WORDS_PER_CAPTION): string[] { const words = text.trim().split(/\s+/).filter(Boolean); const groups: string[] = []; for (let i = 0; i < words.length; i += perGroup) { groups.push(words.slice(i, i + perGroup).join(" ")); } // ponytail: a 1-2 word orphan at the end flashes by — fold it into the previous phrase if (groups.length > 1 && groups[groups.length - 1].split(" ").length <= 2) { const orphan = groups.pop() as string; groups[groups.length - 1] = `${groups[groups.length - 1]} ${orphan}`; } return groups; } export function autoSyncCaptions(text: string, speed = 1): Caption[] { const phrases = groupWords(text); let cursor = 0; return phrases.map((phrase) => { const duration = phrase.split(" ").reduce((sum, w) => sum + wordDuration(w), 0) / speed; const caption: Caption = { id: uuidv4(), text: phrase, startMs: cursor, endMs: cursor + duration, }; cursor += duration + GAP_MS / speed; return caption; }); } export function syncCaptionsToDuration(text: string, totalDurationMs: number): Caption[] { const phrases = groupWords(text); if (phrases.length === 0) return []; // weight by character count so a long phrase holds the screen longer const weights = phrases.map((p) => Math.max(p.length, 1)); const totalWeight = weights.reduce((a, b) => a + b, 0); let cursor = 0; return phrases.map((phrase, index) => { const duration = (weights[index] / totalWeight) * totalDurationMs; const caption: Caption = { id: uuidv4(), text: phrase.replace(/[,;:]+$/, ""), startMs: Math.round(cursor), endMs: Math.round(cursor + duration), }; cursor += duration; return caption; }); } export function captionsToText(captions: Caption[]): string { return captions.map((c) => c.text).join(" "); }