Elements

Components / Audio

Transcript Viewer

Word-by-word transcript display synced to Audio Player. Pass word timings from your transcription pipeline; the current word highlights in lime as playback progresses. Click any word to seek the shared player.

Word-by-word playback

Timings are loaded from audio_test.words.json alongside the sample MP3. Compose TranscriptViewerContainer inside the same AudioPlayerProvider as AudioPlayerBar.

Transcript viewer with docked player
Loading word timings...

Timeline mode

Set variant="timeline" and pass breaks to group words into jumpable segments. Each break ends a segment after the given word index; optional label, speaker, and role metadata title each row in the list. Click a segment to jump; the active segment highlights as playback progresses.

Timeline segments with docked player
Loading word timings...
tsx
import {
  TranscriptViewerContainer,
  TranscriptViewerTimeline,
} from "@/components/ui/transcript-viewer"

const breaks = [
  {
    id: "opening",
    label: "Call to order",
    speaker: "David Okonkwo",
    role: "Chair",
    afterWordIndex: 7,
  },
]

<AudioPlayerProvider item={recording}>
  <TranscriptViewerContainer words={wordTimings} breaks={breaks} variant="timeline">
    <TranscriptViewerTimeline />
  </TranscriptViewerContainer>
  <AudioPlayerBar position="docked" label="Welcome recording" />
</AudioPlayerProvider>

Word timing format

Each word needs a start and end time. By default timings are in milliseconds; pass timeUnit="s" if your ASR returns seconds.

json
{
  "words": [
    { "word": "Welcome", "start": 0, "end": 347 },
    { "word": "to", "start": 347, "end": 440 },
    { "word": "here.", "start": 2032, "end": 2560 }
  ],
  "breaks": [
    {
      "id": "opening",
      "label": "Call to order",
      "speaker": "David Okonkwo",
      "role": "Chair",
      "afterWordIndex": 7
    },
    {
      "id": "overview",
      "label": "Platform overview",
      "speaker": "Sarah Mitchell",
      "role": "Strata manager",
      "afterWordIndex": 28
    }
  ]
}

Installation

Requires audio-player to be installed first.

bash
npx shadcn add https://raw.githubusercontent.com/levi-putna/elements/main/registry/transcript-viewer/registry.json

Usage

tsx
import { AudioPlayerBar, AudioPlayerProvider } from "@/components/ui/audio-player"
import {
  TranscriptViewerContainer,
  TranscriptViewerWords,
} from "@/components/ui/transcript-viewer"

const recording = { id: "welcome", src: "/audio/welcome.mp3" }

const wordTimings = [
  { word: "Welcome", start: 0, end: 347 },
  { word: "to", start: 347, end: 440 },
  // ...
]

<AudioPlayerProvider item={recording}>
  <TranscriptViewerContainer words={wordTimings}>
    <TranscriptViewerWords />
  </TranscriptViewerContainer>
  <AudioPlayerBar position="docked" label="Welcome recording" />
</AudioPlayerProvider>

Reference

TranscriptViewerContainer

PropTypeDefaultDescription
wordsTranscriptWordTiming[]-Word text with start and end timings from transcription or forced alignment.
breaksTranscriptTimelineBreakpoint[][]Timeline segment boundaries. Each break ends a segment after the given word index. Optional speaker and role metadata display above the passage text.
finalSegmentTranscriptFinalSegmentMeta-Optional label, speaker, and role for the trailing segment after the last break.
variant"words" | "timeline""words"Inline word highlighting or grouped timeline segments.
timeUnit"ms" | "s""ms"Unit for each word start and end value.

TranscriptViewerWords

PropTypeDefaultDescription
renderWord({ word, status }) => ReactNode-Optional custom renderer per word.

TranscriptViewerTimeline

PropTypeDefaultDescription
renderSegment({ segment, status }) => ReactNode-Optional custom renderer per timeline segment.