TTS inference humbled me. I came from LLM serving, where a time-to-first-token of 200 ms is considered good and 500 ms is acceptable. In voice agents, the user is waiting for the AI to "speak," and the entire pipeline from receiving text to the first audio chunk hitting the speaker needs to be under 300 to 500 ms. That is the time-to-first-audio (TTFA), and it is the metric that defines whether a voice agent feels responsive or awkward.
The challenge is that TTS is not one model. It is a pipeline, and every stage adds latency.
The modern TTS pipeline
Most production TTS systems have three stages, though some newer models collapse these:
- Text frontend. Normalizes the input text: expanding abbreviations ("Dr." to "Doctor"), converting numbers ("$4.50" to "four dollars and fifty cents"), handling heteronyms ("read" past tense vs. present), and producing phonemes. This stage is CPU-bound and often uses rule-based systems or small neural models.
- Acoustic model. Converts the phoneme or token sequence into a mel spectrogram or latent audio representation. This is the neural backbone of the system, often a transformer or flow-matching model. Examples include VITS, XTTS, and Matcha-TTS.
- Vocoder. Converts the mel spectrogram into a raw audio waveform. Models like HiFi-GAN, BigVGAN, and Vocos do this. Some end-to-end models (like VITS) have a built-in vocoder, eliminating this as a separate step.
For streaming, each stage needs to operate incrementally, producing output before it has consumed all its input. This is where things get architecturally interesting.
Streaming the acoustic model
The acoustic model is typically the latency bottleneck. Non-autoregressive models like VITS and Matcha-TTS are fast but traditionally require the full input text before producing any audio. Autoregressive models like VALL-E and XTTS generate audio tokens left-to-right, which naturally supports streaming but is slower overall.
The practical approach for streaming with non-autoregressive models is sentence-level chunking:
# Sentence-level streaming TTS
import re
def stream_tts(text, model, vocoder):
# Split on sentence boundaries
sentences = re.split(r'(?<=[.!?])\s+', text)
for sentence in sentences:
# Generate mel spectrogram for this sentence
mel = model.synthesize(sentence)
# Convert to waveform
audio_chunk = vocoder(mel)
# Yield immediately, do not wait for next sentence
yield audio_chunk
This is simple but effective. The first sentence is typically short (5 to 15 words), so the model can produce it quickly. While the client plays the first chunk, the server generates the second. As long as generation is faster than playback (faster than real-time), the listener hears continuous speech.
TTS performance is measured by the real-time factor (RTF): the ratio of generation time to audio duration. An RTF of 0.5 means you generate audio twice as fast as it plays back. For streaming, you need RTF well below 1.0 to build up a buffer. An RTF of 0.1 to 0.3 is typical for optimized GPU inference with modern models.
The vocoder bottleneck
Vocoders are surprisingly compute-intensive. HiFi-GAN generates audio at 22,050 Hz sample by sample (or in small frames). For one second of audio, that is 22,050 samples. Each sample requires a forward pass through a stack of transposed convolutions. On a GPU this is fast, but on a CPU it can be the slowest part of the pipeline.
Key optimizations for vocoders:
- Chunk-based vocoding. Instead of generating the entire waveform at once, process the mel spectrogram in overlapping chunks and crossfade the output. This lets you start streaming audio before the full mel is ready.
- Lightweight vocoders. Models like Vocos and MB-MelGAN are designed specifically for low-latency inference. Vocos replaces the transposed convolutions with iSTFT (inverse short-time Fourier transform), which is dramatically faster.
- GPU residency. Keep the vocoder on GPU even if you are tempted to offload it to CPU. The mel-to-waveform conversion involves enough compute that GPU acceleration gives 10x to 50x speedup.
# Chunked vocoding with crossfade
import numpy as np
def chunked_vocoder(mel, vocoder, chunk_size=64, overlap=8):
"""Generate audio in chunks with crossfade for seamless streaming."""
audio_chunks = []
hop = chunk_size - overlap
for i in range(0, mel.shape[-1], hop):
mel_chunk = mel[:, :, i:i + chunk_size]
if mel_chunk.shape[-1] < chunk_size:
# Pad the last chunk
mel_chunk = F.pad(mel_chunk, (0, chunk_size - mel_chunk.shape[-1]))
audio = vocoder(mel_chunk)
if audio_chunks and overlap > 0:
# Crossfade with previous chunk
fade_len = overlap * vocoder.hop_length
fade_out = np.linspace(1, 0, fade_len)
fade_in = np.linspace(0, 1, fade_len)
audio[:fade_len] = (audio[:fade_len] * fade_in +
audio_chunks[-1][-fade_len:] * fade_out)
audio_chunks[-1] = audio_chunks[-1][:-fade_len]
audio_chunks.append(audio)
yield audio # Stream each chunk as it is ready
End-to-end latency breakdown
For a voice agent responding to a user, the full chain looks like this:
- ASR (Whisper): 100 to 300 ms for the user's utterance
- LLM (first token): 100 to 400 ms for the response start
- TTS (first audio chunk): 50 to 200 ms for speech synthesis
- Network + buffering: 20 to 100 ms round trip
Total: 270 to 1000 ms. To stay under 500 ms end-to-end, you need every component to be at its best. This is why many voice agent systems pipeline aggressively: start TTS as soon as the first LLM sentence is complete, not after the full response is generated.
# Pipelined voice agent (conceptual)
async def voice_respond(user_audio):
# ASR and LLM can partially overlap
text = await asr.transcribe(user_audio)
llm_stream = llm.generate_stream(text)
async for sentence in extract_sentences(llm_stream):
# Start TTS immediately on each sentence
audio_chunks = tts.stream_synthesize(sentence)
async for chunk in audio_chunks:
await send_audio_to_client(chunk)
Audio codec models: the new paradigm
A newer approach bypasses the mel spectrogram entirely. Models like EnCodec (Meta), SoundStream (Google), and DAC learn to compress audio into discrete tokens, similar to how LLM tokenizers work for text. The TTS model then generates these audio tokens autoregressively, and a lightweight decoder converts tokens back to waveform.
This is the architecture behind models like VALL-E, Bark, and MusicGen. The inference pattern looks more like an LLM:
- Tokenize the text input
- Generate audio tokens autoregressively (8 codebook levels, each with its own sequence)
- Decode tokens to waveform with the codec decoder
The advantage is simplicity and quality. The disadvantage is speed: autoregressive generation of 75 audio tokens per second across 8 codebook levels means generating 600 tokens per second of audio. That is significantly more tokens per second than a typical LLM needs to produce, and it must all happen faster than real-time.
Serving considerations
TTS serving differs from LLM serving in a few important ways:
- Output size. A 10-second audio clip at 24 kHz mono 16-bit PCM is about 480 KB. Compressed to Opus at 32 kbps, it is about 40 KB. Either way, the response is much larger than a text response, and bandwidth matters.
- Streaming protocol. WebSocket or server-sent events with base64-encoded audio chunks, or raw binary WebSocket frames. The choice affects latency and complexity.
- Batching is harder. Different texts produce different-length audio. Unlike embeddings where inputs are easy to pad, TTS outputs vary wildly. Most production systems process one request at a time per GPU stream and rely on multiple streams or multiple GPUs for throughput.
- Voice cloning adds cold-start cost. Models like XTTS need a reference audio clip to clone a voice. Processing this reference (typically 6 to 30 seconds of audio) adds a one-time cost per voice per session. Cache the speaker embedding aggressively.
In LLM serving, you optimize for tokens per second. In TTS serving, you optimize for time-to-first-audio and real-time factor. The metrics are different, the bottlenecks are different, and the streaming semantics are different. But the underlying principle is the same: find the bottleneck, measure it, and attack it specifically.
Tomorrow we go visual: image generation with diffusion models, where the inference loop is iterative denoising and the kernel optimization story is completely different from anything we have seen so far.