spoken-text

Wrap your text. Hear it read. Watch every word light up.

One React component reads your text aloud and lights each word as it is said.

Click any prose on this page to hear it

what it renders

The tide came in slowly that morning, and the boats leaned over in the mud until the water found them again. By noon the harbour was full, and nobody could remember what the bottom looked like.

0:00/0:00

Composing

Press play, or click any word to hear the passage from there. Show the code and edit the string to make it say something else.

React 18 or 19 is the only requirement. The speech, the transcript and the storage are all functions you pass in.

import { SpokenText } from "spoken-text";

<SpokenText>Any text you like.</SpokenText>

The route

server

<SpokenText> posts the passage to /api/transcription and expects audio and word timings back. Mounting that route is the other half, and it is one export.

app/api/transcription/route.ts
import {
  createAlignmentHandler,
  openaiSpeech,
  openaiTranscription,
  vercelBlobCache,
} from "spoken-text/server";

export const POST = createAlignmentHandler({
  speech: openaiSpeech({ model: "tts-1", voice: "nova" }),
  transcribe: openaiTranscription({
    model: "whisper-1",
    language: "en",
  }),
  cache: vercelBlobCache(),
});
It is a plain handler

A (Request) => Response that reads { content } and returns audio plus word-level timings. It mounts anywhere that speaks the web standard, not only Next.js.

The adapters are opt-in

openaiSpeech, openaiTranscription and vercelBlobCache ship with the package, but nothing depends on them. Pass your own speech, transcribe and cache and the package asks for nothing but React.

Generated once, ever

The cache key is a hash of the passage, so identical text hits the same entry. Without a cache the audio comes back inline as a data: URL, which is fine for a first run and wrong for anything after it.

Environment

Those two adapters read OPENAI_API_KEY and BLOB_READ_WRITE_TOKEN.

The API

exports

The whole surface, so you can see where the escape hatches are.

<SpokenText>
children
string

The passage to speak. Required unless you pass speech.

speech
SpokenTextController

A controller from useSpokenText, so a passage and a <Transport> share one audio element.

classNames
{ word, past, current, future }

Your classes per word state. Supplying one drops the built-in look for that slot, so your CSS is not fighting inline styles.

renderWord
(word: DisplayWord) => ReactNode

Take over word rendering entirely. Whitespace is still inserted for you.

onWordChange
(index, word) => void

Fires whenever the spoken word changes. Index -1 means nothing has been said yet.

endpoint
string = "/api/transcription"

Where the passage is posted.

fetchAlignment
(text) => Promise<Alignment>

Skip the route and resolve audio and timings yourself.

as
"p" | "div" | "span" | …

Element the passage renders into. Also takes className and style.

seekOnWordClick
boolean = true

Click a word to hear the passage from there.

autoPlay
boolean = false

Speak as soon as the audio is ready.

debounceMs
number = 0

How long to wait after the text stops changing before fetching.

useSpokenText(text, options)
What is being said

text · words · currentWordIndex · currentWord

Where playback is

status · isLoading · isPlaying · error · currentTime · duration · audioUrl

Moving it

play() · pause() · toggle() · seek(seconds) · seekToWord(index) · seekToFraction(0-1)

The escape hatch

getAudioElement()

<Transport>
speech
SpokenTextController

The controller to drive. A play button and a scrubber; optional, since the passage highlights on its own.

classNames
{ root, button, track, elapsed, thumb, time, status }

Restyle any part of it. Also takes showTime and showStatus.

three of those, together
<SpokenText
  endpoint="/api/speech"
  classNames={{ current: "bg-sky-200" }}
  onWordChange={(index, word) =>
    setCaption(word?.text ?? "")
  }
>
  {text}
</SpokenText>
Also exported
from “spoken-text

tokenize · alignTokens · tokenIndexAt · normalizeForAlignment · createEndpointAligner · clearAlignmentCache · DEFAULT_ENDPOINT

from “spoken-text/server

createAlignmentHandler · openaiSpeech · openaiTranscription · vercelBlobCache · sha256Hex