Streaming & Chunked Synthesis

9 min read Module 4 of 9 Topic 11 of 25

What you'll learn

  • Explain why waiting for a complete LLM response before starting TTS is unacceptable for conversational latency
  • Design a sentence-boundary chunker that balances naturalness against time-to-first-audio
  • Implement a WebSocket streaming synthesis pattern against ElevenLabs or Cartesia
  • Describe how an audio playback buffer prevents choppy output when chunk arrival is uneven
Building this at your company? For enterprise and company teams taking this to production: book a 30-minute session with our AI engineers for architecture guidance, code review, and a rollout plan for your use case.
Book a Team Session

The previous lesson picked a TTS engine on the assumption that “fast” is a property of the engine alone. It isn’t. Even the fastest engine sounds slow if you feed it a fully-formed paragraph and wait for the whole thing to synthesize before playing a single sample. This lesson is about the orchestration pattern that actually delivers low perceived latency in production: chunking the LLM’s streaming output at sentence boundaries and pipelining synthesis so the caller hears the first words while the model is still writing the last ones.

Why full-response synthesis is a latency bug, not a quality feature

Picture the naive pipeline: the LLM streams tokens, your code accumulates them into a buffer, waits for a stop token, then hands the complete string to the TTS engine, then plays the resulting audio file. Every stage here is serial, and the user experiences the sum of all three delays stacked end to end, even though two of the three (LLM generation and TTS synthesis) could be running concurrently on different parts of the response. For a 3-sentence reply that takes 1.5 seconds to generate and 400ms to synthesize as one whole clip, the caller sits in dead silence for close to 2 seconds. That’s an eternity in a phone conversation, well past the point where humans start repeating themselves or hanging up.

The fix is architecturally simple and operationally the single highest-leverage change you can make to a voice pipeline’s felt responsiveness: synthesize each sentence as soon as it’s complete, not after the whole response finishes.

flowchart TB
    LLM["LLM token stream"] --> CHUNK["Sentence-boundary chunker"]
    CHUNK --> S1["Sentence 1 ready"]
    CHUNK --> S2["Sentence 2 ready"]
    CHUNK --> S3["Sentence 3 ready"]
    S1 --> TTS1["TTS stream: sentence 1"]
    S2 --> TTS2["TTS stream: sentence 2"]
    S3 --> TTS3["TTS stream: sentence 3"]
    TTS1 --> BUF["Playback buffer"]
    TTS2 --> BUF
    TTS3 --> BUF
    BUF --> OUT["Caller hears continuous audio"]

    style LLM fill:#EEF0F7,stroke:#6366F1,color:#0F172A
    style CHUNK fill:#EEF0F7,stroke:#6366F1,color:#0F172A
    style S1 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style S2 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style S3 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style TTS1 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style TTS2 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style TTS3 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style BUF fill:#fff7ed,stroke:#f59e0b,color:#0F172A
    style OUT fill:#fff7ed,stroke:#f59e0b,color:#0F172A

Chunking at the right granularity

The chunker’s job is to find natural break points in a token stream that hasn’t finished yet, without waiting so long that you lose the latency win, and without cutting so aggressively that prosody breaks. Sentence-final punctuation (., ?, !) is the standard boundary because TTS models use sentence-level context to shape intonation; splitting mid-sentence on commas or clause boundaries routinely produces flat or oddly emphasized audio because the model no longer has the full clause to inform its pacing decisions. Most production chunkers also enforce a minimum chunk length (skip synthesizing a lone “Ok.” as its own chunk if a longer sentence is one token away) and a maximum wait timeout, so a pathologically long sentence doesn’t stall the whole pipeline waiting for a period that never arrives soon enough.

import re
import asyncio

SENTENCE_END = re.compile(r'(?<=[.!?])\s+')

async def stream_sentences(token_stream):
    """Yield complete sentences as they become available from an
    LLM token stream, without waiting for the full response."""
    buffer = ""
    async for token in token_stream:
        buffer += token
        parts = SENTENCE_END.split(buffer)
        # Everything but the last piece is a complete sentence;
        # the last piece is a possibly-incomplete tail we keep buffering.
        for sentence in parts[:-1]:
            if sentence.strip():
                yield sentence.strip()
        buffer = parts[-1]
    if buffer.strip():
        yield buffer.strip()

Streaming synthesis over a persistent connection

Chunking text is only half the pattern. The other half is keeping a persistent WebSocket open to the TTS engine so each sentence chunk can be pushed in without paying connection setup cost per sentence. Both ElevenLabs and Cartesia expose exactly this shape of API.

import asyncio
import json
import websockets

async def stream_tts(sentence_queue: asyncio.Queue, voice_id: str, api_key: str, playback):
    uri = f"wss://api.elevenlabs.io/v1/text-to-speech/{voice_id}/stream-input?model_id=eleven_flash_v2_5"
    async with websockets.connect(uri, extra_headers={"xi-api-key": api_key}) as ws:
        await ws.send(json.dumps({"text": " ", "voice_settings": {"stability": 0.5}}))

        async def sender():
            while True:
                sentence = await sentence_queue.get()
                if sentence is None:
                    await ws.send(json.dumps({"text": ""}))  # close signal
                    break
                await ws.send(json.dumps({"text": sentence + " "}))

        async def receiver():
            async for message in ws:
                data = json.loads(message)
                if data.get("audio"):
                    await playback.enqueue(data["audio"])  # base64 PCM/mp3 chunk

        await asyncio.gather(sender(), receiver())

Cartesia’s wss://api.cartesia.ai/tts/websocket follows the same shape: open once, push text as it becomes available, receive audio chunks as they’re synthesized, and close when the sentence queue is exhausted. The sonic-2 model is tuned specifically for this incremental-input pattern, often returning the first audio chunk before the input sentence has finished transmitting.

The playback buffer: absorbing jitter without gaps

Sentence chunks won’t arrive at perfectly even intervals. Synthesis time varies with sentence length and momentary API load, and network jitter adds its own variance. Playing each chunk the instant it arrives, with no buffer, produces audible gaps whenever a chunk is a beat late. The standard fix, borrowed directly from real-time video streaming, is a small jitter buffer: hold 200-400ms of synthesized audio in a queue before starting playback, and keep that lead as long as chunks keep arriving faster than playback consumes them. This buffer is a tuning knob, not a fixed constant: too small and you reintroduce gaps under load, too large and you’ve re-added the latency this entire pattern was built to eliminate.

Knowledge Check

3 questions to test your understanding

1 A team pipes the full LLM response to their TTS engine only after generation completes, then plays the resulting audio. Users report the assistant feels sluggish even though the LLM's total generation time is under 2 seconds. What's the fix?

2 A chunker naively splits LLM output on every comma to start synthesizing as early as possible. What problem is this likely to introduce?

3 During a live call, chunk 2 of a TTS stream arrives 400ms after chunk 1 finished playing, causing a noticeable gap in the audio the caller hears. What's the correct architectural fix?

Discussion

Questions and notes from learners on this topic

Loading discussion…

Go further with expert guidance

Ready to build production AI?
Talk to our R&D team.

These courses give you the foundation. Our embedded AI teams take you from prototype to production in 30–90 days, with your team, your codebase, your goals. Book a free strategy call to see how we can accelerate your AI initiative.

30 minutes · No obligation · Expert AI engineers, not sales reps

AI Architecture Review

Audit your current stack and identify high-impact improvements

Project Review

Get expert feedback on your AI implementation and codebase

Team Mentoring

Upskill your engineers with hands-on AI coaching sessions

AI Strategy

Define your AI roadmap, prioritization, and implementation plan