Skip to main content

Streaming Speech-to-Text

By default the pipeline transcribes each utterance after the user stops speaking: server-side VAD buffers the speech, then sends the whole segment to the STT provider. With streaming STT, audio flows to the provider live while the user is talking — interim transcripts appear in the client within a couple hundred milliseconds, and the final transcript (and therefore the LLM turn) starts earlier because most of the audio was already processed before the user finished.

Enable it​

Streaming STT is opt-in and requires an STT provider that implements transcribeStream. Two are included:

ProviderModelNotes
ElevenLabsScribeProviderScribe v2 RealtimeSub-150ms partials; batch requests use Scribe v2
OpenAIRealtimeSTTProvidergpt-realtime-whisperNative streaming over the Realtime API; audio-duration pricing

Library mode​

import {
LLMRTCServer,
ElevenLabsScribeProvider,
AnthropicLLMProvider,
OpenAITTSProvider
} from '@llmrtc/llmrtc-backend';

const server = new LLMRTCServer({
providers: {
llm: new AnthropicLLMProvider({ apiKey: process.env.ANTHROPIC_API_KEY! }),
stt: new ElevenLabsScribeProvider({ apiKey: process.env.ELEVENLABS_API_KEY! }),
tts: new OpenAITTSProvider({ apiKey: process.env.OPENAI_API_KEY! })
},
streamingSTT: true, // stream mic audio to STT during speech
streamingTTS: true
});

await server.start();

Or with OpenAI's realtime transcription:

import { OpenAIRealtimeSTTProvider } from '@llmrtc/llmrtc-backend';

const stt = new OpenAIRealtimeSTTProvider({
apiKey: process.env.OPENAI_API_KEY!,
model: 'gpt-realtime-whisper', // default
delay: 'low' // optional latency/accuracy knob
});

CLI mode​

STREAMING_STT=true
STT_PROVIDER=elevenlabs-scribe # or: openai-realtime
ELEVENLABS_API_KEY=xi-... # for elevenlabs-scribe

If streamingSTT is enabled but the configured STT provider has no transcribeStream, the server falls back to the buffered pipeline — nothing breaks.

Interim transcripts in the client​

No client changes are required. The web client has always emitted the transcript event with an isFinal flag; buffered STT only ever produced finals, streaming STT now delivers partials too:

client.on('transcript', (text, isFinal) => {
if (isFinal) {
commitMessage(text); // the final utterance
} else {
showLivePreview(text); // updates while the user speaks
}
});

Partial transcripts carry the accumulated text so far (not deltas), so each event can directly replace the previous preview.

Most providers emit exactly one final per utterance. If a provider emits several final segments, each is relayed to the client individually while the LLM receives them joined in order as a single turn.

How it works​

  1. Server-side VAD (Silero) detects speech start. A short pre-speech buffer (~300ms) is flushed first so the first word isn't clipped.
  2. Mic frames are resampled to the provider's preferred rate (streamingInputSampleRate: 16kHz for Scribe, 24kHz for OpenAI Realtime) and streamed over the provider's WebSocket.
  3. Each provider partial is relayed to the client as a transcript message with isFinal: false.
  4. At VAD speech end the audio stream is closed; the provider commits and returns the final transcript, which drives the normal LLM → TTS turn. Barge-in and turn cancellation behave exactly as in buffered mode.

Custom streaming providers​

Implement transcribeStream (and optionally declare the input rate) on any STTProvider:

import type { STTProvider, STTResult } from '@llmrtc/llmrtc-core';

class MyStreamingSTT implements STTProvider {
name = 'my-stt';
streamingInputSampleRate = 16000; // 16-bit mono PCM frames

async transcribe(audio: Buffer): Promise<STTResult> { /* batch */ }

async *transcribeStream(audio: AsyncIterable<Buffer>): AsyncIterable<STTResult> {
// send frames to your engine as they arrive...
yield { text: 'partial text', isFinal: false };
// ...and finish with a final result
yield { text: 'full utterance', isFinal: true };
}
}

Yield partials with the accumulated text and exactly one final per utterance (multiple finals are concatenated in order).