feat: generate transcript and event-driven particle processing

This generates the transcript and shows the caption experience on the
client side for media particles. It also simplifies other side effects
that we must perform such as updating the `last_child_created_at` field
for stream and container particles.
This commit is contained in:
talksik
2026-03-25 14:43:47 -07:00
parent 92bbaa11b3
commit 986a389606
17 changed files with 578 additions and 52 deletions
+40
View File
@@ -0,0 +1,40 @@
import { useMemo } from "react";
import type { Transcript } from "@/api/types";
interface TranscriptPlaybackState {
/** The paragraph currently being spoken, or null if before/after speech */
activeParagraph: Transcript["paragraphs"][number] | null;
/** Index of the active word within the transcript's flat words array */
activeWordIndex: number | null;
}
export function useTranscriptPlayback(
transcript: Transcript | undefined,
currentTime: number,
): TranscriptPlaybackState {
return useMemo(() => {
if (!transcript) return { activeParagraph: null, activeWordIndex: null };
const activeParagraph =
transcript.paragraphs.find(
(p) => currentTime >= p.start && currentTime <= p.end,
) ?? null;
// Binary search for active word
const words = transcript.words;
let activeWordIndex: number | null = null;
let lo = 0;
let hi = words.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >>> 1;
if (currentTime < words[mid].start) hi = mid - 1;
else if (currentTime > words[mid].end) lo = mid + 1;
else {
activeWordIndex = mid;
break;
}
}
return { activeParagraph, activeWordIndex };
}, [transcript, currentTime]);
}