/transcribe WebSocket emits two kinds of transcript messages: interim results (isFinal: false) that preview in-progress recognition, and final results (isFinal: true) that are ready to commit to your document. How you display interims and commit finals in your UI affects perceived latency, accuracy, and the stability of the editing experience.
For enabling interim results and understanding the feature, see Interim Results. For spacing and casing rules at the insertion boundary, see Transcript Text Handling.
The committedText / interimText model
Maintain two pieces of state to separate what is committed from what is still a preview:committedText: text already inserted into the document and visible to the user.interimText: the current interim preview, shown outside the document or in a preview layer.
- If
isFinal: false: replaceinterimTextwith the new segment (after applying boundary spacing). Keep only the newest interim for the current unfinished span. - If
isFinal: true: append the final segment tocommittedTextand clearinterimText.
committed-text-interim-text-model.ts
Maintain a single active interim span and replace it as newer interim results arrive. Do not accumulate multiple interim results into committed text.
Dedicated preview area (recommended starting point)
The simplest approach is to show interim text in a separate element below or beside the editor, toggled via show/hide. The editor’s content is only modified when a final transcript is committed.Recommendation
—
- Show interim text in a dedicated
<p>or<span>outside the editor. - Toggle its visibility: show when interim text arrives, hide when the interim is committed or cleared.
- On
isFinal: true, commit the final segment into the document using your insertion logic and clear the interim preview. - On command recognition, clear the interim preview before dispatching the command action. Clearing after dispatch or conditionally on whether the command was handled causes the command phrase to linger.
Clear interim before command dispatch
When a command is recognized, the transcript text for the command phrase may still be in the interim buffer. Clear it the instant the command is detected, before any dispatch or action execution:The
clearInterimForCommand function is included in the state model snippet above. If you clear after dispatch or conditionally, the spoken command phrase can appear briefly in the preview before the command takes effect.Out-of-order messages after stop/flush
After a stop or flush, transcript messages may arrive out of order. A final result may have already been committed for a span, and a late interim for that same span arrives afterward. To handle this, usedata.start and data.end as identity keys:
startis the primary identity key.endis the secondary stale-overlap check during stop/flush races.
end predates a finalized transcript (an out-of-order message from a stop/flush race).
stale-interim-guard.ts
Cursor movement during active dictation
If the user moves the cursor or changes selection while dictating, treat it as a new insertion context:- Clear the interim span.
- Send a
flushmessage so remaining buffered audio returns transcripts promptly, then continue dictation in the new location. The API responds withtype: "flushed"when done returning text from audio received before the flush message was sent.
Inline at caret position (advanced)
For a more polished experience, interim text can be shown as an overlay at the caret’s pixel coordinates, appearing inline with the document text without modifying the editor’s content. This approach is more complex and requires care to avoid common pitfalls.This section describes editor-agnostic principles and editor-type-specific considerations. A full implementation depends on your editor framework.
Editor-agnostic principles
These apply regardless of editor type (textarea, contenteditable, Tiptap, Lexical, ProseMirror, native controls):- Overlay, not DOM injection. Do not pollute the editor’s document state or undo history with temporary text. Render the interim as a sibling element positioned over the editor. The editor’s content is only modified when a final transcript is committed.
- Mirror the insert target. The overlay position and the actual insert must resolve to the same logical location. If your adapter clamps to end-of-content when focus is outside the editor, the overlay must use the same clamped position.
- Stale interim guards. Track the command’s
start/endtime span and the most recently finalized transcript’sendtime. Skip any interim whose time span overlaps a command (the command handler already cleared the preview) or whoseendpredates a finalized transcript. These guards use transcript message fields (data.start,data.end), not DOM APIs. - Clear interim before command dispatch. Clear the preview the instant a command is recognized, before any dispatch or action execution. Clearing after dispatch or conditionally on whether the command was handled causes the command phrase to linger.
- Performance. Throttle caret measurements (coalesce bursts via
requestAnimationFrameor equivalent). Cache style reads (padding, line-height) instead of reading them on every measurement. Skip measurement when the selection or focus change is outside the editor.
Caret coordinate resolution by editor type
The overlay needs pixel coordinates for the caret position. The approach depends on the editor:- Textarea / input. The caret’s pixel position is not directly accessible. Use a mirror-div technique: a hidden
<div>with identical font, padding, and width; render the text up to the caret offset and measure the resulting span’s bounding rect. - Contenteditable (raw DOM). Create a collapsed
Rangeat the caret point and callgetClientRects(). Do not usegetBoundingClientRect(); it returns zeros for collapsed ranges. At<br>or wrapping boundaries,getClientRects()returns multiple rects; pick the one with the smallestleft(the start of the next line where new text will land). - Rich editors (Tiptap, Lexical, ProseMirror). Use the editor’s coordinate API directly (e.g. Tiptap’s
view.coordsAtPos(), ProseMirror’scoordsAtPos()). These return reliable pixel coordinates without Range API workarounds. - Native controls (Windows, macOS). Use the platform’s accessibility API or caret-rect query. Coordinate systems differ from web.
Contenteditable-specific implementation notes
The following issues are specific to contenteditable and do not apply to textarea or rich editor frameworks:- Structural trailing
<br>. Every contenteditable has one (leftover from the empty editor’s<div><br></div>). It must be excluded from trailing-break counting because it never moves the caret to a new line; every insert goes before it. - Half-leading compensation. The overlay span inherits the editor’s
line-height(e.g. 22.75px) but the caret rect is the font’s em-box (e.g. 17px). The line-box is taller by half-leading on each side, so anchoring at the caret’s top places the overlay’s glyphs below the caret’s. Lift the overlay by(lineHeight - caretRect.height) / 2. - Empty text node anchoring after trailing
<br>. Chrome paints a caret anchored after an element node (<br>) one visual line too high, a well-known contenteditable quirk. Anchor the caret in an empty text node after the<br>instead; the empty node contributes nothing totextContent, so text-offset-based models are unaffected.
Contact us if you need further assistance working with interim results.