import { useMemo, useRef } 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; // Remember the last spoken word so highlights hold during pauses const lastSpokenWordRef = useRef(null); if (activeWord) { lastSpokenWordRef.current = activeWord; } const highlightWord = activeWord ?? lastSpokenWordRef.current; const lastChunkRef = useRef(null); // Find which chunk contains the active word, holding the last one during pauses const activeChunk = useMemo(() => { if (activeWord) { for (const chunk of chunks) { if (chunk.some((w) => w.start === activeWord.start && w.end === activeWord.end)) { lastChunkRef.current = chunk; return chunk; } } } // No active word (speaker pausing) — hold the last chunk if (lastChunkRef.current && chunks.some((c) => c === lastChunkRef.current)) { return lastChunkRef.current; } // Sentence changed, last chunk no longer valid — use first chunk const fallback = chunks[0] ?? null; lastChunkRef.current = fallback; return fallback; }, [chunks, activeWord]); if (!activeSentence || !activeChunk || activeChunk.length === 0) return null; return (

{activeChunk.map((word, i) => { const isSpoken = highlightWord !== null && word.start <= highlightWord.end; return ( {i > 0 ? " " : ""} {word.word} ); })}

); }