Speech player

Pro

A read-aloud player for AI replies. The Read aloud button in the message's action row turns into a compact player with play, a scrubbable progress bar, speed and a voice picker, and a highlight follows the spoken word through the reply, karaoke style.

Live · keyboard ready
Claude Sonnet 5.5

Your flight to Lisbon leaves Thursday, October 29 at 7:40 from Zurich, gate A54. Check-in closes 40 minutes before departure, so plan to reach the airport by 6:15.

Lisbon looks warm, 21 degrees with a light breeze off the river. Your hotel in Príncipe Real holds the room until midnight, and I added both confirmations to your calendar.

  • A chat or answer surface where people want a reply read out, with the spoken word followed in the text.
  • Accessibility and hands free reading for long AI answers, summaries or briefings.
  • Previewing TTS voices and speeds against real product copy.
  • Use dictation-button to turn speech into text in a composer.
  • Use a plain audio player for podcasts or long recordings without a transcript to highlight.

Installation

Pro source and install commands unlock with a Pro plan.

example.tsx
import { SpeechPlayer } from "@/components/arc/speech-player/speech-player"; export function AssistantMessage({ message }) {  return (    <SpeechPlayer      variant="plain"      text={message.text}      author={{ name: "Claude Sonnet 5.5", mark: <ClaudeMark style={{ color: "#D97757" }} /> }}      voices={[        { id: "ava", name: "Ava", description: "Warm and conversational" },        { id: "leo", name: "Leo", description: "Calm, lower register" },      ]}      synthesize={async ({ text, voice, signal }) => {        const res = await fetch("/api/tts", { method: "POST", body: JSON.stringify({ text, voice: voice.id }), signal });        if (!res.ok) throw new Error("The voice service is busy. Try again in a moment.");        const { audioUrl, words } = await res.json(); // words: [{ start, end }] in ms        return { src: audioUrl, timings: words };      }}      onPlay={e => track("tts_play", { voice: e.voice.id, speed: e.speed })}      actions={<><CopyButton value={message.text} iconOnly variant="plain" /><RegenerateButton /></>}    />  );} // With no props it reads the sample reply, simulated, with no audio.<SpeechPlayer />

API reference

2 parts. The first is the root.

SpeechPlayer

Read aloud for an AI reply. The reply sits above an action row with Read aloud, Copy, Good response and Bad response. Read aloud crossfades the row into a compact player of the same height: a play and pause button, the elapsed time, a thin scrubbable progress bar, the total time, a speed button (1×, 1.5×, 2×), a voice picker and Stop reading. As it plays, the spoken word is highlighted in the paragraph by one rounded rect that glides from word to word along a line and cuts cleanly to the next line, while words still to come stay dimmed. Clicking a word jumps there. It covers idle, preparing (a shimmering "Preparing Ava's voice"), playing, paused, switching voice mid-sentence (it resumes from the same word), finished (Play again) and failed (Retry). With no props it reads a sample travel reply from Claude Sonnet 5.5 once in view and loops through two voices until someone interacts.

PropTypeDefaultDescription
textstring–The reply to read. Blank lines start new paragraphs; each whitespace separated word is one highlight step. Leave it out for the self playing sample.
synthesize(request: { text: string; voice: SpeechVoice; signal: AbortSignal }) => Promise<SpeechAudio>–Fetches speech from your TTS provider. Resolve with { src, timings?, duration? }: src is an audio URL or object URL, and timings are word start and end times in milliseconds from the provider's alignment data. Without timings, they are estimated from the text and scaled to the audio length. Abort the request when the signal fires (a new voice or Stop reading). Leave synthesize out for simulated timing with no audio.
voicesSpeechVoice[]Ava, Leo, Maya, NoahVoices in the picker: { id, name, description? }. The description is one short line, such as "Warm and conversational".
voicestring–Selected voice id (controlled). Pair with onVoiceChange.
defaultVoicestringfirst voiceInitial voice id when uncontrolled.
onVoiceChange(voice: SpeechVoice) => void–Called when a voice is picked. While reading, the player fetches the new voice and resumes from the word being spoken.
speed1 | 1.5 | 2–Playback speed (controlled). Pair with onSpeedChange.
defaultSpeed1 | 1.5 | 21Initial speed when uncontrolled. The speed button cycles 1×, 1.5×, 2×.
onSpeedChange(speed: SpeechSpeed) => void–Called when the speed button is pressed.
onPlay(event: SpeechPlaybackEvent) => void–Playback started or resumed. The event carries time and duration in milliseconds on the 1× timeline, wordIndex, voice and speed.
onPause(event: SpeechPlaybackEvent) => void–Playback paused.
onEnd(event: SpeechPlaybackEvent) => void–The last word finished.
onSeek(event: SpeechPlaybackEvent) => void–A scrub, an arrow key or a click on a word moved playback. Fires once when a drag ends, not on every move.
onWordChange(event: SpeechWordEvent) => void–The spoken word changed. Adds word to the playback event; use it to sync a caption or analytics.
onClose() => void–Stop reading was pressed and the action row returned. Fetched audio is kept, so Read aloud starts again instantly.
author{ name: string; mark?: ReactNode } | null–Who wrote the reply, shown above it with a 16px mark. Defaults to Claude Sonnet 5.5 for the sample and to nothing when text is passed.
actionsReactNode–Buttons after Read aloud in the action row. Defaults to Copy, Good response and Bad response (feedback is local and announced as simulated); null shows Read aloud alone.
autoPlaybooleantrue for the sampleStarts reading once the player is in view. Browsers may block audio autoplay; the player then waits paused at 0:00.
simulatedbooleantrue without synthesizeNotes in the voice menu and in announcements that no audio plays.
variant"card" | "plain""card"card draws a bordered surface; plain drops border and padding to sit inside a chat column.
classNamestring–Class on the root element.

simulateSpeech

The stand in TTS request behind the preview: waits like a network round trip, then resolves with estimated word timings and no audio. Nothing is sent. Pass { fail: true } as the second argument to see the error state, or { delay } to tune the wait. estimateSpeechTimings(text, pace) and fitTimings(timings, durationMs) are exported too.

PropTypeDefaultDescription
request{ text: string; voice: SpeechVoice; signal: AbortSignal }–The same request synthesize receives.
options{ delay?: number; fail?: boolean }{ delay: 640 }Milliseconds before it resolves, and whether it rejects instead.
Tab
Moves through Read aloud and the actions, or, while reading, through play, the progress bar, speed, voice and Stop reading.
Enter, Space
On Read aloud, opens the player and starts reading with focus on the play button. On play, plays or pauses; on Stop reading, closes the player and returns focus to Read aloud.
Arrow Left, Arrow Right
On the progress bar, moves 2 seconds back or forward. Page Up and Page Down move 10 seconds; Home and End jump to the start or end.
Space, K
On the progress bar, plays or pauses.
Arrow Up, Arrow Down
In the voice menu, moves between voices; Enter picks one and Escape closes the menu.
  • The player is a labelled group. The play button's name follows its state: Play, Pause, Play again, Retry, or Preparing audio (aria-disabled while a request is in flight).
  • The progress bar is a slider with a value text such as "0:04 of 0:22", updated once a second rather than every frame.
  • A polite status region announces what changed: reading with Ava at 1× (simulated, no audio plays), paused at 0:12, switching to Leo, finished, the failure message, speed changes and feedback.
  • Status never relies on color: the error pairs an alert icon with "Couldn't generate audio" and the reason; dimmed upcoming words keep body contrast and are only a progress cue.
  • Read aloud is described by the reply text. The voice menu uses menuitemradio items with aria-checked and opens with focus on the selected voice.
  • The hidden layer of the dock is inert, so Tab never lands on a control that is not visible. Pointer focus draws no ring; keyboard focus uses Arc's shared focus ring.
  • Read aloud and Stop reading crossfade the action row and the player in the same 32px slot (out in 100ms, in 160ms with a 2px rise), so the card never changes size.
  • The word highlight is one rect that glides between words on a line in 150ms at 1× (about 100ms at 1.5× and 75ms at 2×, so it always lands before the next word) and cuts to the next line instead of sweeping across it. Spoken words fade from dimmed to full color.
  • The progress bar fill and thumb are written straight to transforms on each animation frame; the track thickens and the thumb scales in on hover, focus and drag.
  • Play, pause, replay, retry and the spinner swap in place with a bounce free scale and fade. Speed and voice labels crossfade over hidden copies of every option, so their buttons never resize.
  • The preparing label carries a calm shimmer. Reduced motion keeps every state and announcement, drops the glide, the shimmer and the sample loop, and moves the highlight instantly.
  • The card fills its container up to 640px; the reply wraps and the highlight re-measures on resize without animating.
  • Below 440px of width the total time steps aside so the progress bar keeps room; elapsed time, speed, voice and Stop reading stay.
  • On touch screens the row grows to 36px so every control meets the touch target size.
  • One requestAnimationFrame loop runs only while playing. It writes the bar to the DOM and commits React state only when the spoken word or the whole second changes.
  • Highlight positions come from offsetLeft and offsetTop of the current word, read once per word, never per frame.
  • Fetched audio is kept per voice, so replaying after Stop reading starts instantly; switching voices aborts the previous request.

Notes for AI

Give your coding assistant the Markdown reference instead of screenshots.

  • Pass synthesize for real audio. Most TTS providers can return word or character alignment; convert it to one { start, end } per whitespace separated word of text, in milliseconds. If yours cannot, return src alone and the timings are estimated and fitted to the audio length.
  • Keep text exactly what was spoken: the word count of text must match timings. Strip markdown before passing it.
  • Throw an Error with a short, human message from synthesize to show it next to "Couldn't generate audio"; the play button becomes Retry.
  • Use variant="plain" inside a chat transcript and pass your own actions so the row matches the rest of the message actions.
  • Without synthesize the player is a timed simulation that plays no sound; keep the simulated note so nobody thinks audio is broken.

The full library index for assistants is at /llms.txt.