Skip to content

Gandr in LiveKit Agents.

1.6.7

verified against livekit-agents, 2026-07-31

One file, one constructor line. It implements the standard livekit-agents TTS interface, so your STT, LLM and turn handling do not change.

Ask Claude CodeAsk ChatGPT

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="<your-stt-model>"),
    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.

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 materially on every single utterance, the most expensive silence 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 rateRelative to the default
24000 (default)baseline
22050no measurable cost
16000no measurable cost
8000materially slower, every utterance

Measured 2026-07-31, gandr-mia and gandr-leo, five runs per rate per voice. This run answers exactly one question, what a requested rate costs against the other rates, so the column is relative on purpose. These rows are relative on purpose; the absolute figure is: first audio byte in 146 ms over the open internet, 116 ms p50 first audio, server side warm.

If your pipeline genuinely needs a narrowband source, 16000 costs nothing against 24000. 8000 is the only rate that does.

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, seed. Send the first two, omit temperature and the door picks one from the voice, omit cfg_weight and nothing is sent.
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)

The overflow lane

The fleet is always on, but overflow spills to a fallback lane that can take longer on its first request. On a healthcare line a silent first second is a dropped call, so the plugin does three things about it: it opens the path when it is constructed, it retries once through an overflow response instead of failing, and it exposes prewarm() so you can open it 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()

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
])

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-miaeasy, unhurried (f), the default
gandr-avawarm, friendly (f)
gandr-jennysteady, sincere (f)
gandr-danesmooth, measured (m)
gandr-leoclear, professional (m)
gandr-lewiswarm, even (m)

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.

Get a key

Full API reference , gandr.ai/docs