Skip to content

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.

Download the plugin
curl -O https://gandr.ai/integrations/livekit/gandr_tts.py
What it needs
One filegandr_tts.py, next to your agent. Delete it to uninstall.
No new dependenciesaiohttp only, which livekit-agents already requires.
Python3.10 or newer, same floor as livekit-agents itself.
A keyGANDR_API_KEY in your environment, or pass api_key=.
agent.py — the only line that changes
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 rateServer-side p50Through the plugin, p50
24000 (default)184 ms255 ms
22050164 ms260 ms
16000182 ms250 ms
8000394 ms429 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

ArgumentDefaultWhat it does
api_keyGANDR_API_KEYYour gnd_ key. Raises at construction if neither is set.
voicegandr-miaA stock id, or a gnd: clone id.
langenLanguage of the input text.
sample_rate240008000, 16000, 22050 or 24000. Leave it at 24000 — see below.
speedunset0.6 to 1.5. Pitch preserving, applied after synthesis.
volumeunset0.5 to 2.0. Soft-ceiling mastered, never clips.
extraunsetMerged into every request: pronunciation_dict, temperature, cfg_weight, expressiveness.
base_urltts.gandr.aiLeave it. Requests route to the nearest healthy region.
timeout30.0Socket read timeout on the audio stream, seconds.
prewarm_on_startTrueWakes a worker as soon as the plugin is built.

Changing voice mid-call

The next utterance picks it up. Nothing is torn down.

Mid-session
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.

Wake it on the invite
# 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.

Provider failover
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

IDCharacter
gandr-miawarm, natural (f) — the default
gandr-avawarm, friendly (f)
gandr-jennywarm, natural (f)
gandr-danesmooth, measured (m)
gandr-leoclear, professional (m)
gandr-lewiswarm, 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 writesThe caller hears
9:00 AMnine A M
$42.50forty-two dollars and fifty cents
order OX49order O X, four nine
(415) 555-0142digit by digit, grouped
zip, tracking, account numbersdigit by digit

Unlimited minutes on a flat stream, so agent talk time stops being a line item.

Apply for a key

Full API reference — gandr.ai/docs