import { useMemo } from "react"; import type { Transcript } from "@/api/types"; type Sentence = Transcript["paragraphs"][number]["sentences"][number]; type Word = Transcript["words"][number]; const CHUNK_SIZE = 9; /** Split an array of words into fixed-size display chunks */ function chunkWords(words: Word[]): Word[][] { const chunks: Word[][] = []; for (let i = 0; i < words.length; i += CHUNK_SIZE) { chunks.push(words.slice(i, i + CHUNK_SIZE)); } return chunks; } interface TranscriptOverlayProps { transcript: Transcript; activeSentence: Sentence | null; activeWordIndex: number | null; /** Center captions vertically (e.g. for audio-only playback) */ centered?: boolean; } export function TranscriptOverlay({ transcript, activeSentence, activeWordIndex, centered = false, }: TranscriptOverlayProps) { const sentenceWords = useMemo(() => { if (!activeSentence) return []; return transcript.words.filter( (w) => w.start >= activeSentence.start && w.end <= activeSentence.end, ); }, [transcript.words, activeSentence]); const chunks = useMemo(() => chunkWords(sentenceWords), [sentenceWords]); const activeWord = activeWordIndex !== null ? transcript.words[activeWordIndex] : null; // Find which chunk contains the active word const activeChunk = useMemo(() => { if (!activeWord) return chunks[0] ?? null; for (const chunk of chunks) { if (chunk.some((w) => w.start === activeWord.start && w.end === activeWord.end)) { return chunk; } } return chunks[0] ?? null; }, [chunks, activeWord]); if (!activeSentence || !activeChunk || activeChunk.length === 0) return null; return (
{activeChunk.map((word, i) => { const isSpoken = activeWord !== null && word.start <= activeWord.end; return ( {i > 0 ? " " : ""} {word.word} ); })}