import { useMemo, useState } 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 [lastSpokenWord, setLastSpokenWord] = useState(null); if (activeWord && activeWord !== lastSpokenWord) { setLastSpokenWord(activeWord); } const highlightWord = activeWord ?? lastSpokenWord; // The chunk currently being spoken (null during a pause or if not found). const spokenChunk = useMemo(() => { if (!activeWord) return null; return ( chunks.find((chunk) => chunk.some( (w) => w.start === activeWord.start && w.end === activeWord.end, ), ) ?? null ); }, [chunks, activeWord]); // Resolve which chunk to display: the spoken one, else hold the last one while // it's still part of the current sentence, else fall back to the first chunk. const [lastChunk, setLastChunk] = useState(null); let activeChunk: Word[] | null; if (spokenChunk) { activeChunk = spokenChunk; } else if (lastChunk && chunks.includes(lastChunk)) { activeChunk = lastChunk; } else { activeChunk = chunks[0] ?? null; } if (activeChunk !== lastChunk) { setLastChunk(activeChunk); } 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} ); })}

); }