Build a Stable Caption UI

Consume StableStream events without manufacturing caption jitter.

How to consume realtime transcription events and build a caption UI that doesn't jitter.

The problem with naive approaches

A naive live caption UI might try to update text on every event. With partial-text systems, this causes visible jitter as words are repeatedly replaced.

LansonAI's StableStream eliminates this: each conversation.item.input_audio_transcription.completed event contains final, stable text.

Append-only display

const captions = document.getElementById('captions');

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);

  if (data.type === 'conversation.item.input_audio_transcription.completed') {
    // Append directly — text is final, no replacement needed
    const p = document.createElement('p');
    p.dataset.utterance = data.utterance_index;
    p.textContent = data.text;
    captions.appendChild(p);
    captions.scrollTop = captions.scrollHeight;
  }
};

With optional status indicators

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);

  if (data.type === 'input_audio_buffer.speech_started') {
    showIndicator(data.utterance_index, 'listening');
  }

  if (data.type === 'input_audio_buffer.speech_stopped') {
    showIndicator(data.utterance_index, 'processing');
  }

  if (data.type === 'conversation.item.input_audio_transcription.completed') {
    hideIndicator(data.utterance_index);
    appendCaption(data.utterance_index, data.text);
  }
};

What you do NOT need to do

  • ❌ No partial → final text replacement mapping
  • ❌ No waiting for "stabilization" (received text is already stable)
  • ❌ No text rollback or undo
  • ❌ No debounce or jitter elimination
  • ❌ No re-rendering of previously shown text

Scrolling behavior

For long sessions, manage scroll behavior:

function appendCaption(index, text) {
  const p = document.createElement('p');
  p.textContent = text;

  // Auto-scroll if user is near bottom
  const isNearBottom =
    captions.scrollHeight - captions.scrollTop - captions.clientHeight < 100;

  captions.appendChild(p);

  if (isNearBottom) {
    captions.scrollTop = captions.scrollHeight;
  }
}

Styling for readability

#captions p {
  margin: 0.25em 0;
  padding: 0.25em 0.5em;
  border-radius: 4px;
  transition: opacity 0.2s;
}