> ## Documentation Index
> Fetch the complete documentation index at: https://assemblyai.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Real-time live captioner

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

**Products used:** [Real-time STT](/streaming/getting-started/transcribe-streaming-audio) + [Universal-3 Pro](/streaming/getting-started/transcribe-streaming-audio) + [keyterms prompting](/streaming/prompting-and-keyterms)

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

<Tabs groupId="language">
  <Tab language="python" title="Python" default>
    ```python expandable theme={null}
    # 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()
    ```
  </Tab>

  <Tab language="javascript" title="JavaScript">
    ```javascript expandable theme={null}
    // 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);
    });
    ```
  </Tab>
</Tabs>

<Accordion title="Example output">
  ```text theme={null}
  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
  ```
</Accordion>

***

See the [End-to-end examples overview](/getting-started/end-to-end-examples) for all available pipelines.
