> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corti.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Use word-level transcripts

> Enable word-level timestamps and drive per-word highlighting during audio playback

By default, [`/transcribe`](https://docs.corti.ai/api-reference/transcribe) returns final transcripts as segments: multi-word phrases with one start and end time for the whole phrase. With `wordLevel` enabled, every recognized word comes back as its own item with its own timestamps. Per-word timings let you highlight the word being spoken while the audio plays back, the karaoke-style pattern this guide builds for a dictation verification UI.

This guide walks through enabling `wordLevel` on a live `/transcribe` session, handling the word-level messages, and driving highlight-on-playback. The same per-word timestamps are available for pre-recorded files through `/transcripts`; see [Transcribe a pre-recorded file](#transcribe-a-pre-recorded-file) at the end.

> [card] Feature availability:
>
> > [card]
> >
> > /transcribe
> >
> > [Open](https://docs.corti.ai/stt/transcribe.md)
>
> > [card]
> >
> > /streams
> >
> > [Open](https://docs.corti.ai/stt/streams.md)
>
> > [card]
> >
> > /transcripts
> >
> > [Open](https://docs.corti.ai/stt/transcripts.md)

***

## Enable word-level transcripts

Enable `wordLevel` in the [configuration message](https://docs.corti.ai/api-reference/transcribe.md#configuration) of your `/transcribe` session:

```json title="Configuration message" theme={null}
{
  "type": "config",
  "configuration": {
    "primaryLanguage": "en",
    "wordLevel": { "enabled": true }
  }
}
```

The same option exists on the other speech to text endpoints. On `/streams`, include `wordLevel` in the configuration: the transcript `data` is already a list, and each item is a word in the [`/streams` segment schema](https://docs.corti.ai/api-reference/streams#transcripts). On `/transcripts`, set it on the create request, described in [Transcribe a pre-recorded file](#transcribe-a-pre-recorded-file).

> [!NOTE]
>
> Interim previews are not split into words. When `interimResults` is also enabled, previews arrive as a one-element list carrying the in-progress segment, and the first finalized batch replaces the preview. See [Interim Results](https://docs.corti.ai/stt/interim-results.md) for the interim feature and [word-level transcripts](https://docs.corti.ai/api-reference/transcribe.md#word-level-transcripts) for the response schema.

***

## Handle word-level messages

During dictation your client receives two kinds of `transcript` messages with `wordLevel` enabled:

* Final batches: `data` is a list of word items with `isFinal` of `true`, one message per finalized utterance.
* Interim previews: `data` is a one-element list whose item carries the in-progress segment with `isFinal` of `false` (only when `interimResults` is enabled).

Accumulate the final word items in order and keep the latest preview text separately:

```ts title="JavaScript" theme={null}
// A word item from a final /transcribe batch with wordLevel enabled.
interface WordItem {
  // The word as returned in the final transcript.
  text: string;
  // The word as dictated, before post-processing.
  rawTranscriptText: string;
  // Word timing in seconds, relative to the session start.
  start: number;
  end: number;
  isFinal: boolean;
}

// Dictation state: accumulated final word items plus the preview text.
interface WordLevelState {
  words: WordItem[];
  interimText: string;
}

function handleTranscriptMessage(
  state: WordLevelState,
  data: WordItem[],
): WordLevelState {
  // Interim preview: a one-element list whose item is still in
  // progress. The preview is segment level, not per word.
  if (data.length === 1 && data[0].isFinal === false) {
    return { ...state, interimText: data[0].text };
  }

  // Final batch: append the word items, then clear the preview.
  return { ...state, words: [...state.words, ...data], interimText: "" };
}

// From your WebSocket message handler, once the session is connected:
// `state` holds the WordLevelState, `socket` is the /transcribe socket.
// With wordLevel enabled, msg.data is always a list.
socket.on("message", (msg) => {
  if (msg.type === "transcript" && Array.isArray(msg.data)) {
    state = handleTranscriptMessage(state, msg.data);
  }
});
```

> [!NOTE]
>
> Both shapes are intentional: without `wordLevel`, `data` keeps the singular segment object sessions have always used; with `wordLevel`, every finalized word arrives in one list message. The handler therefore narrows with `Array.isArray` before reading the message fields.

To display the transcript, join the word items using the spacing and casing rules described in [Transcript Text Handling](https://docs.corti.ai/stt/best-practices-transcribe.md), and show the interim preview as ghost text following the committed words; see [Interim & final result handling](https://docs.corti.ai/stt/best-practices-interim-handling.md) for the preview patterns.

> [!TIP]
>
> Your `words` array now carries one item per spoken word, with second-precision timings: that list is the input for the player in the next section. Record the audio in parallel to the dictation session, for example with the browser `MediaRecorder` API, to replay it afterwards.

***

## Highlight the spoken word during playback

As the audio plays, highlight the word whose time span contains the playback position, and seek the audio when the user clicks a word. The words are ordered by start time, so a binary search finds the active word cheaply, and the loop runs on `requestAnimationFrame` for smooth highlighting at display frame rate; the `timeupdate` event fires only a few times per second, which makes the highlight visibly stutter.

```ts title="JavaScript" theme={null}
// A word item reduced to what the player needs.
interface PlayerWord {
  text: string;
  // Start of the word in seconds.
  start: number;
  // End of the word in seconds.
  end: number;
}

// Index of the active word at a playback position, or -1 before the
// first word. Words are ordered by start time: binary search finds the
// last word whose start is at or before the position, so the active
// word stays highlighted through any trailing silence until the next
// word begins.
function activeWordIndex(words: PlayerWord[], position: number): number {
  let low = 0;
  let high = words.length - 1;
  let found = -1;
  while (low <= high) {
    const mid = (low + high) >>> 1;
    if (words[mid].start <= position) {
      found = mid;
      low = mid + 1;
    } else {
      high = mid - 1;
    }
  }
  return found;
}

// Drive highlighting at display frame rate. Returns a function that
// stops the loop, for example when the player is unmounted.
function startPlaybackSync(
  audio: HTMLAudioElement,
  words: PlayerWord[],
  onActiveWordChange: (index: number) => void,
) {
  let raf = 0;
  let last = -1;
  const tick = () => {
    const index = activeWordIndex(words, audio.currentTime);
    if (index !== last) {
      last = index;
      onActiveWordChange(index);
    }
    raf = requestAnimationFrame(tick);
  };
  raf = requestAnimationFrame(tick);
  return () => cancelAnimationFrame(raf);
}
```

Render one element per word and toggle the active styling in `onActiveWordChange`; to let users seek, set `audio.currentTime = words[index].start` when the word element is clicked. Keep the layout stable while highlighting: bold text is wider than regular text, so give each word a fixed horizontal padding that absorbs the difference, or the line reflows at every word change.

```ts title="JavaScript" theme={null}
// Map the dictation word items to the player's word list.
const playerWords: PlayerWord[] = state.words.map((w) => ({
  text: w.text,
  start: w.start,
  end: w.end,
}));
```

***

## Transcribe a pre-recorded file

When the audio is a pre-recorded file instead of a live session, run the `/transcripts` REST pipeline from the [File transcription](https://docs.corti.ai/stt/guides/dictation-file-transcription.md) guide and set `wordLevel` on the create request:

```json title="Create-transcript request (excerpt)" theme={null}
{
  "recordingId": "<your-recording-id>",
  "primaryLanguage": "en",
  "wordLevel": { "enabled": true }
}
```

The finalized transcript then holds one segment per word or entity, each with `text`, `start`, and `end` in milliseconds; note the unit difference from the live `/transcribe` word items, which use seconds. Divide the start and end times by 1000, map them into `PlayerWord` objects, and the player from [Highlight the spoken word during playback](#highlight-the-spoken-word-during-playback) works unchanged. See [Create Transcript](https://docs.corti.ai/api-reference/transcripts/create-transcript.md) and [Get Transcript](https://docs.corti.ai/api-reference/transcripts/get-transcript.md) for the request and response schemas.

***

## Next steps

> [card] [Word-level transcripts](https://docs.corti.ai/api-reference/transcribe.md#word-level-transcripts)
>
> The response schema for `/transcribe` sessions with `wordLevel` enabled.

> [card] [Transcript Text Handling](https://docs.corti.ai/stt/best-practices-transcribe.md)
>
> Spacing and casing rules for joining words and segments into display text.

> [card] [Interim Results](https://docs.corti.ai/stt/interim-results.md)
>
> How interim previews behave on `/transcribe`.

> [card] [File transcription](https://docs.corti.ai/stt/guides/dictation-file-transcription.md)
>
> The full `/transcripts` pipeline for pre-recorded audio.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.