Back to home
Lanson Flow Docs

Build a Stable Caption UI

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.

Recommended render strategy

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;
    }
    

    Related

  • StableStream — the stability contract
  • Stable vs. Partial Text — concept
  • Browser Live Captions — full browser setup