Text-to-SpeechReal-time API

Real-time generation

Learn how to generate speech in real time with the Soniox Text-to-Speech WebSocket API, including configuration, streaming, responses, and errors.

Overview

Soniox Text-to-Speech AI generates native-speaker-quality speech in over 60 languages with ultra-low latency, hallucination-free output, and accurate pronunciation of alphanumerics like phone numbers, email addresses, and IDs. This is ideal for use cases like voice agents, conversational AI, live narration, and interactive assistants.

Real-time generation is provided through our WebSocket API, which streams audio back to you as text arrives. A single connection can host multiple concurrent streams for serving many users or voices at once.


How real-time generation works

Each stream is opened with a one-time config that picks the voice and audio output format. After that, Text-to-Speech is a two-way stream: you send text in small chunks as it becomes available, and Soniox streams audio back without waiting for the full input.

  • Text in (chunks) → You send text incrementally over the WebSocket. Each message carries a text fragment and a text_end flag marking the last chunk.
  • Audio out (chunks) → Soniox returns base64-encoded audio payloads as they are produced. You can decode and play or buffer each chunk on arrival.
  • Interleaved → Audio for the first words starts flowing back while you are still writing later chunks. You never have to wait for a full sentence before audio begins.

Pairing with an LLM

This is what makes real-time TTS the right fit for LLM-driven workloads. Pipe each LLM token (or token batch) straight into a Soniox text chunk. Because audio generation begins with the first words, the user hears speech while the LLM is still generating the rest of the response. No buffering the full reply before speaking, no awkward "thinking" pause between the model finishing and the voice starting.

Example streaming timeline

Here is what a single stream looks like on the wire. Outbound text chunks and inbound audio chunks are interleaved, they do not happen in two separate phases.

Client → Server (first text chunk):

{
  "text": "Hello there,",
  "text_end": false,
  "stream_id": "stream-001"
}

Server → Client (audio already streaming back for "Hello"):

{
  "audio": "<base64 chunk 1>",
  "audio_end": false,
  "stream_id": "stream-001"
}

Client → Server (next text chunk sent while audio for earlier text keeps arriving):

{
  "text": " this is Soniox",
  "text_end": false,
  "stream_id": "stream-001"
}

Server → Client (more audio chunks):

{
  "audio": "<base64 chunk 2>",
  "audio_end": false,
  "stream_id": "stream-001"
}

Client → Server (final text chunk, text_end: true):

{
  "text": " speaking live.",
  "text_end": true,
  "stream_id": "stream-001"
}

Server → Client (last audio chunk, audio_end: true):

{
  "audio": "<base64 chunk N>",
  "audio_end": true,
  "stream_id": "stream-001"
}

Server → Client (stream terminated):

{
  "terminated": true,
  "stream_id": "stream-001"
}

Bottom line: You don't wait for a full sentence before you hear anything. Audio for the first words starts streaming back while the rest of the text is still being written. Treat the stream as complete only after terminated: true.


Connection keepalive

Between streams, for example while waiting on an upstream LLM or between agent turns, send keepalive messages to prevent the WebSocket connection from timing out. Keepalive does not keep an individual stream open: a stream that receives no text for more than a few seconds without text_end: true is terminated. For details, see Connection keepalive.


API reference

For the full message schema, configuration parameters, cancellation, and error codes, see the WebSocket API reference.


Code example

Prerequisite: Complete the steps in Get started.

See on GitHub: soniox_sdk_realtime.py.

import argparse
import os
import threading
import time
from pathlib import Path
from uuid import uuid4

from soniox import SonioxClient
from soniox.errors import SonioxRealtimeError
from soniox.types import RealtimeTTSConfig
from soniox.utils import output_file_for_audio_format

VALID_SAMPLE_RATES = [8000, 16000, 24000, 44100, 48000]
VALID_BITRATES = [32000, 64000, 96000, 128000, 192000, 256000, 320000]
VALID_AUDIO_FORMATS = [
    "pcm_f32le",
    "pcm_s16le",
    "pcm_mulaw",
    "pcm_alaw",
    "wav",
    "aac",
    "mp3",
    "opus",
    "flac",
]

DEFAULT_LINES = [
    "Welcome to Soniox real-time Text-to-Speech. ",
    "As text is streamed in, audio streams back in parallel with high accuracy, ",
    "so your application can start playing speech ",
    "within milliseconds of the first word.",
]


def get_config(
    model: str,
    language: str,
    voice: str,
    audio_format: str,
    sample_rate: int | None,
    bitrate: int | None,
    stream_id: str | None,
) -> RealtimeTTSConfig:
    config = RealtimeTTSConfig(
        # Stream id for this realtime TTS session.
        # If omitted, a random id is generated.
        stream_id=stream_id or f"tts-{uuid4()}",
        #
        # Select the model to use.
        # See: docs.soniox.com/tts/models
        model=model,
        #
        # Set the language of the input text.
        # See: docs.soniox.com/tts/concepts/supported-languages
        language=language,
        #
        # Select the voice to use.
        # See: docs.soniox.com/tts/concepts/voices
        voice=voice,
        #
        # Set output audio format and optional encoding parameters.
        # See: docs.soniox.com/api-reference/tts/websocket-api
        audio_format=audio_format,
        sample_rate=sample_rate,
        bitrate=bitrate,
    )

    return config


def run_session(
    client: SonioxClient,
    lines: list[str],
    model: str,
    language: str,
    voice: str,
    audio_format: str,
    sample_rate: int | None,
    bitrate: int | None,
    stream_id: str | None,
    output_path: str | None,
) -> None:
    # Build a realtime Text-to-Speech session configuration.
    config = get_config(
        model=model,
        language=language,
        voice=voice,
        audio_format=audio_format,
        sample_rate=sample_rate,
        bitrate=bitrate,
        stream_id=stream_id,
    )
    sanitized_lines = [line.strip() for line in lines if line.strip()]
    if not sanitized_lines:
        raise ValueError("Text is empty after parsing.")

    destination = (
        Path(output_path)
        if output_path
        else output_file_for_audio_format(audio_format, "tts_realtime")
    )
    print("Connecting to Soniox...")
    audio_chunks: list[bytes] = []
    try:
        with client.realtime.tts.connect(config=config) as session:
            print("Session started.")
            send_errors: list[Exception] = []

            def send_worker() -> None:
                try:
                    for line in sanitized_lines:
                        session.send_text_chunk(line, text_end=False)
                        time.sleep(0.1)
                    session.finish()
                except Exception as exc:
                    send_errors.append(exc)

            threading.Thread(target=send_worker, daemon=True).start()
            # Receive streamed audio chunks from the websocket.
            for audio_chunk in session.receive_audio_chunks():
                audio_chunks.append(audio_chunk)
            if send_errors:
                raise RuntimeError(f"Failed to send realtime text: {send_errors[0]}")
            print("Session finished.")
    finally:
        audio = b"".join(audio_chunks)
        if audio:
            destination.write_bytes(audio)
            print(f"Wrote {len(audio)} bytes to {destination.resolve()}")
        else:
            print("No audio file was written.")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--line",
        action="append",
        default=None,
        help="Line to send to realtime TTS (repeat --line for multiple lines).",
    )
    parser.add_argument("--model", default="tts-rt-v2")
    parser.add_argument("--language", default="en")
    parser.add_argument("--voice", default="Adrian")
    parser.add_argument("--audio_format", default="wav")
    parser.add_argument("--sample_rate", type=int)
    parser.add_argument("--bitrate", type=int)
    parser.add_argument("--stream_id", help="Optional stream id.")
    parser.add_argument(
        "--output_path",
        help="Optional output file path. If omitted, a timestamped path is generated.",
    )
    args = parser.parse_args()

    if args.audio_format not in VALID_AUDIO_FORMATS:
        raise ValueError(f"audio_format must be one of {VALID_AUDIO_FORMATS}")
    if args.sample_rate is not None and args.sample_rate not in VALID_SAMPLE_RATES:
        raise ValueError(f"sample_rate must be None or one of {VALID_SAMPLE_RATES}")
    if args.bitrate is not None and args.bitrate not in VALID_BITRATES:
        raise ValueError(f"bitrate must be None or one of {VALID_BITRATES}")

    api_key = os.environ.get("SONIOX_API_KEY")
    if not api_key:
        raise RuntimeError(
            "Missing SONIOX_API_KEY.\n"
            "1. Get your API key at https://console.soniox.com\n"
            "2. Run: export SONIOX_API_KEY=<YOUR_API_KEY>"
        )

    client = SonioxClient(api_key=api_key)

    try:
        run_session(
            client=client,
            lines=args.line or DEFAULT_LINES,
            model=args.model,
            language=args.language,
            voice=args.voice,
            audio_format=args.audio_format,
            sample_rate=args.sample_rate,
            bitrate=args.bitrate,
            stream_id=args.stream_id,
            output_path=args.output_path,
        )
    except SonioxRealtimeError as exc:
        print("Soniox realtime error:", exc)
    finally:
        client.close()


if __name__ == "__main__":
    main()

Terminal
# Generate speech with default settings (wav output)
python soniox_sdk_realtime.py --line "Hello from Soniox realtime Text-to-Speech."

# Generate raw PCM output
python soniox_sdk_realtime.py --audio_format pcm_s16le --sample_rate 24000 --output_path tts-output.pcm

On this page