Skip to content

For clean Markdown of any page, append .md to the page URL. For a complete documentation index, see For full documentation content, see For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at

Real-time live captioner

Stream audio from your microphone with keyterms prompting for domain-specific accuracy, ideal for live events and accessibility.

For the complete documentation index, see llms.txt

Stream audio from your microphone with keyterms prompting for domain-specific accuracy, ideal for live events, accessibility, and broadcast captioning.

Products used: Streaming STT + Universal-3 Pro + keyterms prompting

Model selection: Uses u3-rt-pro for sub-300ms latency with format_turns enabled for clean, readable captions.

python
# pip install pyaudio websocket-client
import pyaudio
import websocket
import json
import threading
import time
from urllib.parse import urlencode

# ── Config ────────────────────────────────────────────────────
YOUR_API_KEY = "YOUR_API_KEY"

# Add domain-specific terms to boost recognition accuracy
KEYTERMS = ["AssemblyAI", "Universal-3 Pro", "LLM Gateway", "speech-to-text"]

CONNECTION_PARAMS = {
    "sample_rate": 16000,
    "speech_model": "u3-rt-pro",
    "format_turns": True,
    "keyterms_prompt": KEYTERMS,
}

API_ENDPOINT = (
    f"wss://streaming.assemblyai.com/v3/ws?{urlencode(CONNECTION_PARAMS, doseq=True)}"
)

# Audio settings
FRAMES_PER_BUFFER = 800
SAMPLE_RATE = 16000
stop_event = threading.Event()
caption_count = 0

def on_open(ws):
    print(f"Live captioning started — keyterms: {', '.join(KEYTERMS)}")
    print("Speak into your microphone. Press Ctrl+C to stop.\n")
    print("-" * 60)

    def stream_audio():
        audio = pyaudio.PyAudio()
        stream = audio.open(
            input=True, frames_per_buffer=FRAMES_PER_BUFFER,
            channels=1, format=pyaudio.paInt16, rate=SAMPLE_RATE,
        )
        while not stop_event.is_set():
            try:
                data = stream.read(FRAMES_PER_BUFFER, exception_on_overflow=False)
                ws.send(data, websocket.ABNF.OPCODE_BINARY)
            except Exception:
                break
        stream.stop_stream()
        stream.close()
        audio.terminate()

    threading.Thread(target=stream_audio, daemon=True).start()

def on_message(ws, message):
    global caption_count
    data = json.loads(message)

    if data.get("type") == "Turn":
        transcript = data.get("transcript", "")
        if data.get("end_of_turn") and transcript:
            caption_count += 1
            print(f"\r[{caption_count:03d}] {transcript}")
        elif transcript:
            # Show partial (live) caption
            print(f"\r  >> {transcript[-70:]}", end="", flush=True)

    elif data.get("type") == "Termination":
        duration = data.get("audio_duration_seconds", 0)
        print(f"\n{'=' * 60}")
        print(f"Session ended — {caption_count} captions, {duration}s of audio")

def on_error(ws, error):
    print(f"\nError: {error}")
    stop_event.set()

def on_close(ws, code, msg):
    stop_event.set()

ws_app = websocket.WebSocketApp(
    API_ENDPOINT,
    header={"Authorization": YOUR_API_KEY},
    on_open=on_open, on_message=on_message,
    on_error=on_error, on_close=on_close,
)

ws_thread = threading.Thread(target=ws_app.run_forever, daemon=True)
ws_thread.start()

try:
    while ws_thread.is_alive():
        time.sleep(0.1)
except KeyboardInterrupt:
    print("\n\nStopping...")
    stop_event.set()
    if ws_app.sock and ws_app.sock.connected:
        ws_app.send(json.dumps({"type": "Terminate"}))
        time.sleep(2)
    ws_app.close()
javascript
// npm install ws mic
const WebSocket = require("ws");
const mic = require("mic");
const querystring = require("querystring");

// ── Config ────────────────────────────────────────────────────
const YOUR_API_KEY = "YOUR_API_KEY";

// Add domain-specific terms to boost recognition accuracy
const KEYTERMS = ["AssemblyAI", "Universal-3 Pro", "LLM Gateway", "speech-to-text"];

const CONNECTION_PARAMS = {
  sample_rate: 16000,
  speech_model: "u3-rt-pro",
  format_turns: true,
  keyterms_prompt: KEYTERMS,
};

const API_ENDPOINT = `wss://streaming.assemblyai.com/v3/ws?${querystring.stringify(
  CONNECTION_PARAMS
)}`;

let micInstance;
let captionCount = 0;

const ws = new WebSocket(API_ENDPOINT, {
  headers: { Authorization: YOUR_API_KEY },
});

ws.on("open", () => {
  console.log(`Live captioning started — keyterms: ${KEYTERMS.join(", ")}`);
  console.log("Speak into your microphone. Press Ctrl+C to stop.\n");
  console.log("-".repeat(60));

  micInstance = mic({ rate: "16000", channels: "1", debug: false });
  const micStream = micInstance.getAudioStream();

  micStream.on("data", (data) => {
    if (ws.readyState === WebSocket.OPEN) ws.send(data);
  });

  micInstance.start();
});

ws.on("message", (message) => {
  const data = JSON.parse(message);

  if (data.type === "Turn") {
    const transcript = data.transcript || "";
    if (data.end_of_turn && transcript) {
      captionCount++;
      process.stdout.write(
        `\r${String(captionCount).padStart(3, "0")}  ${transcript}\n`
      );
    } else if (transcript) {
      process.stdout.write(`\r  >> ${transcript.slice(-70)}`);
    }
  } else if (data.type === "Termination") {
    console.log(`\n${"=".repeat(60)}`);
    console.log(
      `Session ended — ${captionCount} captions, ${data.audio_duration_seconds || 0}s of audio`
    );
  }
});

ws.on("error", (err) => console.error(`Error: ${err}`));
ws.on("close", () => {
  if (micInstance) micInstance.stop();
});

process.on("SIGINT", () => {
  console.log("\n\nStopping...");
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: "Terminate" }));
  }
  setTimeout(() => {
    if (micInstance) micInstance.stop();
    ws.close();
    process.exit(0);
  }, 2000);
});
text
Live captioning started — keyterms: AssemblyAI, Universal-3 Pro, LLM Gateway, speech-to-text
Speak into your microphone. Press Ctrl+C to stop.

------------------------------------------------------------
[001] Welcome everyone to today's demo of AssemblyAI's speech-to-text platform.
[002] We'll be showing you how Universal-3 Pro handles real-time transcription.
[003] The LLM Gateway integration lets you add AI analysis on top of your
      transcripts without switching providers.

============================================================
Session ended — 3 captions, 24s of audio

See the End-to-end examples overview for all available pipelines.