Cloud|

Live dictation

Stream speech into an application's draft with shared authorization, limits, and cancellation.

5 min read Updated 2026-11-01 #ai#audio#dictation#browser

Live dictation turns microphone audio into text while a person speaks. Core provides one endpoint for built-in and third-party applications. The application chooses where to insert the text and when to save its draft.

Check availability on the server

ts
import { getLiveDictationStatus } from "@k2b/cloud/ai";

const status = await getLiveDictationStatus(c.get("accessSubject"));
// Pass status.available to the application's island with its initial data.

The helper returns { available: boolean, reason?: "not_configured" | "no_access" | "disabled" }. It checks AI enablement, the liveAudio slot, the enabled provider/model, and the person's access to the owning model ID. It opens no microphone or provider connection and reserves no capacity. GET /api/ai/dictation/status returns the same shape for browser sessions.

An application renders its microphone from this authorized SSR snapshot. The socket repeats the check when starting and every 30 seconds. Availability does not promise remaining daily allowance or free capacity.

Start a browser session

ts
import { createLiveDictationSource } from "@k2b/cloud/ai/browser";

const source = createLiveDictationSource({ app: "my-app" });
const controller = new AbortController();
const session = source.start({ signal: controller.signal });

for await (const event of session.events) {
  if (event.type === "text") appendToDraft(event.text);
  if (event.type === "error") showDictationError(event);
}

start({ deviceId?, signal }) returns immediately with:

  • events: AsyncIterable<DictationEvent>;
  • stop(): Promise<void>, which waits for final words and a terminal event.

The event types are exported alongside DictationSource, DictationSession, DictationDevice, and DictationErrorCode from the browser entry point:

Event Meaning
state: connecting Requesting microphone access and opening the connection
state: live, maxMs, device? Ready; device contains the actual track ID and label
level, level: number RMS level between 0 and 1, about every 100 ms
text, text: string Append this delta; previously delivered text never changes
state: finishing Microphone stopped; waiting for final words
state: finished, reason? Complete; reason can be max_duration, silence, or daily_limit
error, code, retryable, resetsAt? Session ended; preserve text already received and offer an explicit recovery action

Call await session.stop() for a regular stop. It ends audio normally so the model can commit its final words. Cloud waits at most five seconds, then returns the text received so far. If the model's final text revises the streamed text, Cloud finishes successfully with the text already delivered. Abort the start signal to discard or leave: microphone and connection close immediately and the iterable ends silently. pagehide also cancels. There is no automatic reconnect, replay, or correction pass.

await source.devices() lists microphone IDs and labels; labels may be empty before permission. Pass a selected deviceId to start. A missing or vanished selection falls back to the system default. The host owns device preferences. Audio uses one channel, echo cancellation, noise suppression, and the browser's default AudioContext sample rate. An AudioWorklet resamples linearly to 16 kHz and emits 100 ms PCM16 frames.

Microphone permission failures map to microphone_blocked (retryable); unavailable or unreadable microphones map to microphone_unavailable. A socket closing without a final result maps to retryable connection_lost, except close code 1008: its recognized error reason becomes a non-retryable error of that code; an unknown reason becomes non-retryable connection_lost.

WebSocket protocol

Connect to /api/ai/dictation/live on Cloud's origin using a browser session. An Origin header equal to Cloud's public origin is required. Model grants and Core application access apply. The app identifier records usage attribution; it does not grant access.

Send exactly one {"type":"start","app":"my-app"} within five seconds of opening. Wait for {"type":"ready","maxMs":600000}, then send binary mono PCM16 little-endian audio at 16 kHz. A 100 ms frame is 3200 bytes; each frame must contain whole samples and be at most 16 KiB. End with {"type":"stop"}. Unknown controls, duplicate starts, or audio before ready end the socket with close code 1008. This includes audio sent after start while the model is still connecting.

Server messages are:

ts
type ServerEvent =
  | { type: "ready"; maxMs: 600000 }
  | { type: "delta"; text: string }
  | { type: "warning"; code: "time_left"; ms: number }
  | { type: "finishing" }
  | { type: "done"; text: string; audioMs: number; reason?: "max_duration" | "silence" | "daily_limit" }
  | { type: "error"; code: string; retryable: boolean; resetsAt?: string };

Errors use not_configured, no_access, daily_limit, overloaded, busy_elsewhere, max_duration, silence, provider_failed, or connection_lost. A daily-limit rejection includes resetsAt, an ISO timestamp for the installation's next local midnight. Duration, silence, and daily limits reached during dictation finish normally with a reason and preserve the text. Terminal refusals send an error and close with 1008 and that error code.

Limits and recovery

Each person may run one session across all apps, tabs, and Core replicas. A second start receives busy_elsewhere; it cannot take over the session. Installation-wide concurrency defaults to four. A full installation or more than two seconds of queued provider audio returns overloaded.

A session lasts at most ten minutes, with one time-left warning at minute nine. Two minutes of silence below -50 dBFS, including an idle microphone connection, stop it. Incoming audio is limited to 32,000 bytes per second with a two-second burst. Cloud bounds its in-memory PCM queue to two seconds too. The model must establish its realtime session within five seconds.

The separate daily allowance defaults to 60 minutes per person, resets at local midnight in the installation's time zone, and is checked before start and on every charge at the five-second heartbeat while running. It measures active session seconds, including pauses. It does not consume the chat token allowance. The usage administration shows audio minutes grouped by person and app.

Session revocation, model access loss, lease loss, disconnect, or Core shutdown abort the model connection. After an interruption, start another dictation explicitly. Words still in transit may be missing.

Privacy

Cloud holds audio only in memory on its way from the browser to the speech model. It stores neither audio nor transcript and logs no content. Accounting keeps metadata: person, app, model, duration, and audio duration. The host app may save inserted text as ordinary draft text. The speech provider's own retention policy remains the operator's responsibility.

Discarding a session records a normal end with cancellation metadata. Dictation runs do not add turns to the chat usage timeline.

Public notebook pages do not receive live dictation. This API does not replace or alter Assistant's existing recording storage path.

Operators configure the model and limits in Runtime configuration.

Connect a coding agent

The Cloud skill provides compact working instructions. MCP supplies exact current documentation when details matter.

CLI command
bunx skills add https://docs.example.com

This command installs the working instructions published by this website in the selected coding agent.

For exact, current details, also connect the MCP server through one of the agent tabs.

Installation uses the open-source Vercel Skills CLI.

The skill and MCP complement each other: the skill describes workflows, while MCP supplies current documentation.