Integration
verified against livekit-agents, 2026-07-31
Gandr in LiveKit Agents.
One file, one constructor line. It implements the standard livekit-agents TTS interface, so your STT, LLM and turn handling do not change.
01
The whole integration
Drop the file next to your agent, change one line.
curl -O https://gandr.ai/integrations/livekit/gandr_tts.py
| What it needs | |
|---|---|
| One file | gandr_tts.py, next to your agent. Delete it to uninstall. |
| No new dependencies | aiohttp only, which livekit-agents already requires. |
| Python | 3.10 or newer, same floor as livekit-agents itself. |
| A key | GANDR_API_KEY in your environment, or pass api_key=. |
from gandr_tts import GandrTTS
session = AgentSession(
stt=inference.STT(model="deepgram/nova-3"),
llm=inference.LLM(model="google/gemma-4-31b-it"),
tts=GandrTTS(voice="gandr-mia"), # key from GANDR_API_KEY
)
That is the integration. There is no package to install — transport is aiohttp, which livekit-agents already depends on. Python 3.10 or newer.
02
Telephony: do not ask for 8 kHz
The obvious move on a SIP call is to construct the plugin at the call’s native rate so nothing resamples. It is the wrong move, and we had it wrong in our own guide until we measured it.
The engine renders at 24 kHz. Asking for 8 kHz makes the server resample before it releases the first chunk, and that costs about 220 ms on every single utterance — the most expensive 220 ms in a conversation, because it lands between the caller finishing and the agent starting.
LiveKit already resamples into the SIP leg, locally, for nothing. So take the default and let it.
Time to first audio by requested rate — production API, five runs per rate per voice
| Requested rate | Server-side p50 | Through the plugin, p50 |
|---|---|---|
| 24000 (default) | 184 ms | 255 ms |
| 22050 | 164 ms | 260 ms |
| 16000 | 182 ms | 250 ms |
| 8000 | 394 ms | 429 ms |
Measured 2026-07-31, gandr-mia and gandr-leo, warm workers. The plugin column includes network round trip from a laptop, which is why every row is higher.
If your pipeline genuinely needs a narrowband source, 16000 costs nothing against 24000. 8000 is the only rate that does.
03
Constructor
Everything the plugin takes
| Argument | Default | What it does |
|---|---|---|
| api_key | GANDR_API_KEY | Your gnd_ key. Raises at construction if neither is set. |
| voice | gandr-mia | A stock id, or a gnd: clone id. |
| lang | en | Language of the input text. |
| sample_rate | 24000 | 8000, 16000, 22050 or 24000. Leave it at 24000 — see below. |
| speed | unset | 0.6 to 1.5. Pitch preserving, applied after synthesis. |
| volume | unset | 0.5 to 2.0. Soft-ceiling mastered, never clips. |
| extra | unset | Merged into every request: pronunciation_dict, temperature, cfg_weight, expressiveness. |
| base_url | tts.gandr.ai | Leave it. Requests route to the nearest healthy region. |
| timeout | 30.0 | Socket read timeout on the audio stream, seconds. |
| prewarm_on_start | True | Wakes a worker as soon as the plugin is built. |
Changing voice mid-call
The next utterance picks it up. Nothing is torn down.
session.tts.update_options(voice="gandr-leo")
session.tts.update_options(lang="es", speed=1.1)
04
Cold starts
Workers scale to zero when idle, and a cold one takes about 25 seconds to serve. On a healthcare line that is a dropped call, so the plugin does three things about it: it wakes a worker when it is constructed, it retries once through a cold response instead of failing, and it exposes prewarm() so you can wake one earlier still.
Fire it on the SIP invite and first audio is at full speed by the time your model has a sentence.
# on the invite, before the caller has finished saying hello
await session.tts.prewarm()
05
Failover
Errors map to livekit-agents’ own exception classes, so the framework’s retry applies with no extra code. A stream that ends early now raises rather than playing a half sentence — which is what you want when the sentence is a dose or an address.
For provider failover during an evaluation, wrap it in LiveKit’s stock adapter:
Server-side we already route across two regions with failover between them before your adapter would engage.
from livekit.agents.tts import FallbackAdapter
tts = FallbackAdapter([
GandrTTS(voice="gandr-mia"),
inference.TTS(model="..."), # any secondary
])
06
Voices
Six stock voices on every key, all multilingual — set lang per session or per update_options call. Or clone: five to thirty seconds of clean reference audio gives you a gnd: id that works anywhere a stock id does, at the same latency after first use.
Stock voices
| ID | Character |
|---|---|
| gandr-mia | warm, natural (f) — the default |
| gandr-ava | warm, friendly (f) |
| gandr-jenny | warm, natural (f) |
| gandr-dane | smooth, measured (m) |
| gandr-leo | clear, professional (m) |
| gandr-lewis | warm, even (m) |
07
What your model does not have to spell out
Times, money, codes and phone numbers are normalized server-side on English requests, with nothing to configure. It matters more than it sounds: this is the category of mistake a caller actually notices.
Your own wording always wins. When you need to force it:
- <spell>TKT4829XB</spell>character by character, for codes and serials
- <break time="800ms"/>a pause at exactly that point
- extra={"pronunciation_dict": [...]}sounds-like replacements for brand and domain words
Automatic readback
| Your model writes | The caller hears |
|---|---|
| 9:00 AM | nine A M |
| $42.50 | forty-two dollars and fifty cents |
| order OX49 | order O X, four nine |
| (415) 555-0142 | digit by digit, grouped |
| zip, tracking, account numbers | digit by digit |
Unlimited minutes on a flat stream, so agent talk time stops being a line item.
Apply for a keyFull API reference — gandr.ai/docs
