Text to Speech
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Written on 2026-10-01 against Spring AI 2.0.1 on Spring Boot 4.1.x and Java 21, the elevenlabs Python package 2.70.0, the
openai Python package 3.22.1, Kokoro-82M (Apache-2.0), OHF-Voice/piper1-gpl 1.8.0 (GPL-3.0), the idiap/coqui-ai-TTS
fork 0.27.5 with XTTS-v2 weights under the Coqui Public Model Licence, Sesame CSM-1B (Apache-2.0) and Kyutai TTS
(CC-BY-4.0); the full list is the version baseline. The code was
written from the documentation linked after each block and was not run against live providers. Licence statements were
read from each project’s licence file or model card on that date and are not legal advice; read the licence yourself
before shipping. Hosted voice and model names change quickly, so check the provider’s voices page.
Text to speech (TTS) is the last stage of a cascaded voice agent and the one the caller judges most directly. The model list of open voices with sizes and cards is on the Hugging Face speech models page; this page covers what a voice agent needs from a synthesiser.
Streaming synthesis
A non-streaming TTS call returns a whole clip, so the caller waits for the full sentence to be synthesised. A streaming call returns audio chunks while synthesis continues, which gives a short time to first byte (the term in the latency budget). There are two kinds of streaming:
-
Streaming output. You send the complete text of a sentence and receive audio in chunks. Every hosted provider and most local engines support this, and it combines with the sentence chunking from the architecture page.
-
Streaming input. You send text in pieces as the LLM produces tokens and the provider starts speaking before the sentence is complete. ElevenLabs offers this over a WebSocket, and some local models are designed for it. It lowers latency further but gives the engine less context for intonation, so measure the quality.
Whichever you use, keep a cancel path: when the caller interrupts, the request must be cancelled and the queued audio dropped (see Turn-Taking and Interruptions).
Voices, cloning and consent
A hosted provider offers a catalogue of stock voices, voice design from a text description, and cloning from recordings. Cloning needs ethical and legal care that the code cannot enforce for you:
-
Clone only voices of people who have given informed, recorded consent for that use, and keep the consent record.
-
Do not make a voice that imitates a real person without permission, and do not use an agent voice to impersonate staff or deceive callers about talking to a machine; many jurisdictions require the caller to be told it is an AI voice (see Production Voice Agents).
-
Open models that clone from a few seconds of audio carry a misuse risk you take on as the deployer; the model cards often forbid impersonation, and some projects deliberately limit cloning to pre-computed voice embeddings.
-
Store voice identifiers, not raw reference audio, in application configuration, and restrict who can create voices.
For the DocsAssistant, a single stock voice chosen for clarity on telephone lines is the right default.
SSML and expressiveness
Speech Synthesis Markup Language (SSML, a W3C recommendation) controls pauses, emphasis, pronunciation and speaking rate
in synthesisers that support it, notably Azure AI Speech and Google Cloud Text-to-Speech. ElevenLabs and OpenAI take
instructions in other forms: pronunciation dictionaries and voice settings for ElevenLabs, and a free-text instruction
prompt for OpenAI’s gpt-4o-mini-tts model. Local models vary: Kokoro and Piper take plain text and phonemes, Piper-style
engines accept pronunciation overrides, and many ignore SSML altogether.
For a voice agent the practical needs are small: spell out identifiers ("W-three-C"), read version numbers digit by digit, pronounce product names correctly, and insert short pauses between list items. Do this in a text preprocessing step that you control (a function that converts the LLM’s sentence into speakable text), because it works the same with every engine, and use SSML or the provider’s dictionary only for the cases the preprocessing cannot reach.
Hosted providers
| Provider | Characteristics | Where to check |
|---|---|---|
ElevenLabs |
Expressive voices, voice design and cloning, low-latency streaming over REST and WebSocket, PCM and mu-law outputs for telephony. A Spring AI provider. |
|
OpenAI |
|
|
Azure AI Speech |
Neural voices in many languages, SSML, visemes, and custom neural voice under a gated process. |
|
Google, Deepgram, Cartesia and others |
Streaming synthesisers aimed at agents, some with word timestamps useful for interruption handling. |
The vendors' documentation |
Choose by time to first byte on your network path, voice quality on telephone-rate audio, the languages you need, streaming input support, timestamps (to know how much was actually spoken), pronunciation control and data handling. Azure’s voices, SSML extensions, batch synthesis and avatars are covered on Foundry Tools: Text to Speech and Avatars.
Local models and their licences
| Model | Licence | Notes for agents |
|---|---|---|
Kokoro-82M |
Apache-2.0 |
A small model (82 million parameters) with good quality and fast CPU or GPU synthesis; 24 kHz output; several stock voices; yields audio per text segment. A strong default for a fully local stack. |
Piper ( |
GPL-3.0 |
Very fast on CPUs and small devices, many voices and languages. The maintained repository is the GPL-licensed
|
XTTS-v2 (idiap |
Code MPL-2.0; model weights under the Coqui Public Model Licence (non-commercial) |
Multilingual voice cloning from a short reference clip. The weights' licence forbids commercial use, so it fits research and internal experiments, not a product. |
Sesame CSM-1B |
Apache-2.0 |
A conversational speech generation model that conditions on prior turns for more natural prosody. It generates speech only (it is not an end-to-end agent), is heavier than Kokoro and needs a GPU for real-time use. |
Kyutai TTS |
CC-BY-4.0 |
Streaming-text input, designed for low latency in conversational settings; requires attribution and a GPU server. |
Licence terms and voice licences change, and model repositories can be relicensed or gated. Re-read the licence file at the commit you deploy, record it in your software bill of materials, and check each voice file’s own terms, since a model under a permissive licence can still ship voices trained on data with restrictions.
Audio formats and resampling
The synthesiser, the transport and the listener each have a preferred audio format, and a wrong guess shows up as noise, chipmunk speech or silence.
| Format | Rate | Used by | Notes |
|---|---|---|---|
mu-law (G.711 mu-law), 8-bit |
8 kHz |
Twilio Media Streams, North American PSTN |
64 kbit/s; one byte per sample; the format Twilio expects in |
A-law (G.711 A-law), 8-bit |
8 kHz |
European PSTN and many SIP trunks |
Same size as mu-law with a different companding curve. |
PCM16 (signed 16-bit little-endian, linear) |
16 kHz |
Most STT services, WebRTC wideband paths, ElevenLabs |
256 kbit/s mono. |
PCM16 |
24 kHz |
Kokoro, OpenAI realtime PCM, many TTS outputs |
384 kbit/s mono. |
Opus |
48 kHz in WebRTC |
Browsers and WebRTC |
Compressed; the transport encodes and decodes, your code usually sees PCM. |
A typical telephone path therefore resamples the synthesiser’s 24 kHz PCM16 down to 8 kHz and companding it to mu-law, and the reverse for the caller’s audio into the STT. Resample with a proper low-pass filter (a polyphase resampler) rather than dropping samples, which aliases and makes speech harsh. For a conversion by a ratio of the polyphase resampler needs the output length ; 24 kHz to 8 kHz is .
import audioop # removed in Python 3.13: use the audioop-lts package there
import numpy as np
import soxr
class Pcm24kToUlaw8k:
"""Converts a stream of TTS chunks; one instance per utterance or call, fed every chunk in order."""
def __init__(self) -> None:
self._resampler = soxr.ResampleStream(24000, 8000, 1, dtype="int16") # keeps filter state across chunks
self._pending = b"" # odd trailing byte of the last chunk
def convert(self, chunk: bytes, last: bool = False) -> bytes:
data = self._pending + chunk
usable = len(data) - (len(data) % 2) # PCM16 needs whole 2-byte samples
self._pending = data[usable:]
samples = np.frombuffer(data[:usable], dtype=np.int16)
pcm8k = self._resampler.resample_chunk(samples, last=last) # 24 kHz -> 8 kHz, no seams between chunks
return audioop.lin2ulaw(pcm8k.tobytes(), 2) # 16-bit linear -> mu-law
Source: python-soxr: ResampleStream and the
Python audioop module. Synthesisers stream audio in chunks of arbitrary
size, so the converter keeps two pieces of state. The resampler’s filter history removes the click that per-chunk
resampling would add at every chunk boundary. The odd trailing byte of a chunk is held back so that a sample split across
two chunks is not dropped. A one-shot scipy.signal.resample_poly call is fine for a whole file, but not for a stream.
Pass last=True with the final chunk to flush the filter. Frameworks do this for you; see
Pipecat and Telephony with Twilio.
Examples
Spring AI: TextToSpeechModel and streaming
Spring AI 2.0 names the speech interfaces TextToSpeechModel and StreamingTextToSpeechModel; the SpeechModel of
Spring AI 1.0 and of the books written against it is gone, as recorded on the
Audio Transcription & Speech page, which also covers the starters,
options and the OpenAI and ElevenLabs configuration in depth. The class below returns the audio of an answer as a stream of
chunks that a WebSocket handler can forward as they arrive.
import org.springframework.ai.audio.tts.TextToSpeechModel;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Service
class SpokenAnswerService {
private final TextToSpeechModel speech;
SpokenAnswerService(TextToSpeechModel speech) {
this.speech = speech;
}
Flux<byte[]> speak(String sentence) {
return this.speech.stream(sentence); // audio chunks as they are produced
}
}
Source: Spring AI: Text-To-Speech (TTS) API. Which provider
backs the bean is chosen with spring.ai.model.audio.speech (openai or elevenlabs); with ElevenLabs the voice is
selected by identifier, as described in
Spring AI: ElevenLabs Text-To-Speech.
Check the output format of the provider you configure against what your transport expects (the table above), and
cancel the subscription when the caller interrupts.
Python: ElevenLabs streaming
from elevenlabs.client import ElevenLabs
client = ElevenLabs() # reads ELEVENLABS_API_KEY from the environment
def speak(sentence: str, voice_id: str):
return client.text_to_speech.stream(
voice_id=voice_id,
text=sentence,
model_id="eleven_flash_v2_5", # check the models page for the current low-latency model
output_format="pcm_16000", # raw 16 kHz PCM16, ready for a pipeline or resampling
) # iterate the result to receive audio chunks
Source: ElevenLabs: Text to speech (the model names and output formats are listed there).
Python: OpenAI streaming speech
from openai import OpenAI
client = OpenAI()
def speak(sentence: str):
with client.audio.speech.with_streaming_response.create(
model="gpt-4o-mini-tts",
voice="coral",
input=sentence,
instructions="Speak clearly and calmly, like a helpful support engineer.",
response_format="pcm", # raw 24 kHz PCM16 mono
) as response:
for chunk in response.iter_bytes(chunk_size=4096):
yield chunk
Source: OpenAI: Text to speech (the text-to-speech guide
lists voices, formats and the instructions parameter). The same LangChain wiring around an agent is shown on
Multimodality & Voice.
Python: Kokoro, fully local
import numpy as np
from kokoro import KPipeline
pipeline = KPipeline(lang_code="a") # American English; Apache-2.0 model and code
def speak(text: str):
for _graphemes, _phonemes, audio in pipeline(text, voice="af_heart"):
samples = audio.numpy() # KPipeline yields a torch.FloatTensor
yield (np.clip(samples, -1, 1) * 32767).astype(np.int16).tobytes() # 24 kHz PCM16
Source: Kokoro-82M model card (installation, language codes and voices). The pipeline yields the audio as a PyTorch tensor at 24 kHz, so it is converted to a NumPy array before the PCM16 conversion.
Choosing
Start with a hosted voice and the sentence-chunking pattern, measure time to first byte from your own servers, and make the engine replaceable behind one function. Move to Kokoro when you need on-premises audio under a permissive licence, treat Piper as GPL software, and keep XTTS out of commercial products. Always run the speakable-text preprocessing before the engine.