diff --git a/src/app/transcript/[id].tsx b/src/app/transcript/[id].tsx index e570851..909a7f0 100644 --- a/src/app/transcript/[id].tsx +++ b/src/app/transcript/[id].tsx @@ -17,6 +17,7 @@ import { MaxContentWidth, Spacing } from '@/constants/theme'; import { useTheme } from '@/hooks/use-theme'; import { getRepo, type Transcript } from '@/lib/db'; import { downloadText } from '@/lib/download'; +import { errorMessage } from '@/lib/errorMessage'; import { detectCitations, detectEvents, @@ -52,6 +53,7 @@ export default function TranscriptScreen() { }, [t]); const [transcript, setTranscript] = useState(undefined); + const [loadError, setLoadError] = useState(null); const [title, setTitle] = useState(''); const [segments, setSegments] = useState([]); const [dirty, setDirty] = useState(false); @@ -99,6 +101,9 @@ export default function TranscriptScreen() { if (!alive) return; url = u; setPersistedUrl(u); + }) + .catch(() => { + // Missing/failed media is non-fatal — playback just stays unavailable. }); return () => { alive = false; @@ -115,6 +120,12 @@ export default function TranscriptScreen() { setTranscript(t ?? null); setTitle(t?.title ?? ''); setSegments(t?.segments ?? []); + }) + .catch((e) => { + // Without this, a rejected load left the screen on its spinner forever. + if (!alive) return; + setLoadError(errorMessage(e)); + setTranscript(null); }); return () => { alive = false; @@ -277,7 +288,9 @@ export default function TranscriptScreen() { if (transcript === null) return ( - Transcript not found. + + {loadError ? `Couldn't load this transcript: ${loadError}` : 'Transcript not found.'} + ); diff --git a/src/stores/embeddingStore.ts b/src/stores/embeddingStore.ts index d93c404..b8e4c10 100644 --- a/src/stores/embeddingStore.ts +++ b/src/stores/embeddingStore.ts @@ -40,10 +40,21 @@ async function embedTranscript(transcriptId: string): Promise { return; } const eng = getEmbeddingEngine(); - const vecs = await eng.embed( - t.segments.map((s) => s.text), - 'passage', - ); + // Embed in small batches, yielding to the event loop between them. The WASM + // backend runs synchronously on the MAIN thread, so embedding a whole lecture + // (100s of segments) in one call froze the UI for seconds — which, right after + // a transcription, looked like the transcript screen was "stuck loading" and + // starved any follow-up transcription. Per-text vectors are identical + // regardless of batch size (e5 pools each text independently). + const texts = t.segments.map((s) => s.text); + const BATCH = 16; + const vecs: Float32Array[] = []; + for (let i = 0; i < texts.length; i += BATCH) { + const out = await eng.embed(texts.slice(i, i + BATCH), 'passage'); + for (const v of out) vecs.push(v); + // Hand the thread back so navigation, rendering and other work can run. + await new Promise((resolve) => setTimeout(resolve, 0)); + } await repo.upsertVectors( transcriptId, EMBED_MODEL,