Synced Transcript Viewer
A karaoke-style transcript viewer that highlights words in lockstep with audio playback using word-level timestamps — click a word to seek.
Synced Transcript Viewer
Synced Transcript Viewer plays local audio and highlights each word in lockstep with playback using word-level alignment timestamps. The playback-time → word-index mapping is pure client-side (binary search on the timeupdate position). Clicking a word seeks the audio to its start.
Feed words from useTranscribe with returnTimestamps: 'word' (segments arrive on data.segments) and audio from the same local recording / TTS Blob.
When to use it: a read-along transcript for a meeting recording, podcast, or voice note where users want to follow — and jump around in — the audio.
Preview
Installation
pnpm dlx shadcn@latest add @localmode/ui/audio/synced-transcript-viewernpx shadcn@latest add @localmode/ui/audio/synced-transcript-vieweryarn dlx shadcn@latest add @localmode/ui/audio/synced-transcript-viewerbunx --bun shadcn@latest add @localmode/ui/audio/synced-transcript-viewerData source & dependencies
Data source: renders the words (with timestamps) + audio you pass — works with any backend. Recommended producer: useTranscribe({ returnTimestamps: 'word' }) (with the Whisper STT model from @localmode/transformers) from @localmode/react (on-device, optional).
@localmode/ui/lib/browser-utils—useObjectUrlfor Blob lifecycle (installed automatically as a registry dependency)clsx+tailwind-merge— via the sharedcn()util
Files installed
synced-transcript-viewer.tsx— the componentlib/browser-utils.ts— generic browser helpers (useObjectUrl)lib/utils.ts— thecn()helper (if not already present)
Props
SyncedTranscriptViewer
Prop
Type
Examples
Read-along from a transcription
import { SyncedTranscriptViewer, type TimedWord } from '@/components/synced-transcript-viewer';
import { useTranscribe } from '@localmode/react';
import { transformers } from '@localmode/transformers';
export function ReadAlong({ audio }: { audio: Blob }) {
const { data, execute } = useTranscribe({
model: transformers.speechToText('onnx-community/whisper-base'),
returnTimestamps: 'word',
});
// Word-level segments arrive on data.segments — map them into TimedWord[].
const words: TimedWord[] = (data?.segments ?? []).map((s) => ({
text: s.text,
start: s.start,
end: s.end,
}));
return (
<>
<button onClick={() => execute(audio)}>Transcribe</button>
{words.length > 0 && <SyncedTranscriptViewer words={words} audio={audio} />}
</>
);
}returnTimestamps can also be overridden per call: execute(audio, { returnTimestamps: 'word' }).
Customization
The active-word lookup is a binary search over start times, so it stays cheap for long transcripts. Past / active / upcoming words get distinct classes (text-foreground, bg-primary text-primary-foreground, text-muted-foreground) — restyle them in the copied file. If your timestamps are segment-level rather than per-word, split each segment's text across its [start, end] window before passing it in.
Audio Scrub Player
A composable scrubbable audio player for local Blob/object-URL audio — play/pause, a draggable seek bar, and a duration readout. Ships a standalone ScrubBar.
Conversation
A scrollable chat surface with first-class scroll-anchoring — auto-pins while streaming, releases on scroll-up, and re-pins on return.