VEXYL AI

Text to speech

Text to speech (TTS) turns the bot's reply into the audio the caller hears. This chapter lists every TTS provider the gateway supports, the variables each one reads, how to override the voice for a single call, and the cache and playback settings that shape what callers hear. Pick a provider, set its required key, and restart the gateway. Ready-made combinations are in Configuration recipes; the recogniser side is in Speech recognition.

Choosing

Set TTS_PROVIDER to one of the values below. The default is sarvam.

TTS_PROVIDER=sarvam
Provider (TTS_PROVIDER) Mode Best for
sarvam Streaming (WebSocket or HTTP) Indian languages, low latency with the v4-flash model
google Streaming with Chirp3-HD voices Multi-language production, broad language coverage
gemini Streaming on Vertex AI, batch on Cloud TTS Expressive voices, designed or replicated custom voices
elevenlabs Streaming over a persistent WebSocket Natural, expressive customer-facing voices
smallest Streaming (WebSocket) with native 8 kHz output Indian languages incl. Malayalam, lowest cost of those
deepgram Batch Speed on English and a few European and Asian languages
azure Streaming (chunked) Neural voices with speaking styles
murf Batch Human-like brand voices, English first
vexyl Batch over a WebSocket, self-hosted On-premise, 22 Indian languages
openrouter Batch only Trying many vendors behind one key

Streaming providers start playing while the sentence is still being rendered, so the caller hears audio sooner. Batch providers render the whole sentence first, so expect a longer pause before each reply. When a provider fails on a turn, the gateway falls back to another configured provider so the caller is not left in silence.

Costs are billed per provider and model with built-in rates. To change a rate, create config/pricing.json from the example in Webhooks and billing and edit it.

Sarvam

Variable Default Set it when
SARVAM_API_KEY (required) unset Always, for this provider
SARVAM_TTS_MODEL bulbul:v3 You want bulbul:v4-flash, the current fast model. bulbul:v2 is legacy
SARVAM_TTS_SPEAKER built-in: kavya (bulbul:v3), kavya_hi_conversational (bulbul:v4-flash) Choosing the voice. v3 takes a bare name such as kavya; v4-flash takes a persona id such as kavya_hi_conversational
SARVAM_TTS_SPEAKER_<LANG> unset One language needs a different speaker, for example SARVAM_TTS_SPEAKER_ML_IN. Beats the global speaker
SARVAM_TTS_PACE 1.0 Speaking speed. Clamped to 0.5–2.0 (v2 allows 0.3–3.0)
SARVAM_TTS_TEMPERATURE unset Variation in delivery, 0.01–1.0, v3 and v4-flash only. Unset uses Sarvam's own default
SARVAM_TTS_STREAMING true Streams each sentence as it is synthesised. Set false to use REST batch only
SARVAM_TTS_STREAM_TRANSPORT ws Set http when the model is v4-flash (see below)
SARVAM_TTS_WS_REUSE true Pooled sockets stay open between sentences, so the first chunk of each warm sentence arrives faster. Set false for a fresh socket per sentence
SARVAM_TTS_WS_IDLE_MAX unset Idle pooled sockets kept per configuration
SARVAM_TTS_WS_IDLE_TTL_MS unset How long an idle pooled socket lives
SARVAM_TTS_WS_PING_MS unset Keep-alive interval for pooled sockets
SARVAM_TTS_WS_TIMEOUT unset Cap on a streaming request, both transports
SARVAM_TTS_WS_CONNECT_TIMEOUT_MS unset Cap on opening a socket
SARVAM_TTS_MAX_CONCURRENT unset Limit on simultaneous Sarvam requests
SARVAM_TTS_QUEUE_TIMEOUT unset How long a request waits in that limit before failing
SARVAM_TTS_PREPROCESSING true Text normalisation of numbers and abbreviations; always on server-side for v3 and v4-flash. Set false to disable
SARVAM_TTS_STREAM_URL https://api.sarvam.ai/text-to-speech/stream You route through your own proxy
SARVAM_TTS_WS_URL wss://api.sarvam.ai/text-to-speech/ws You route through your own proxy

Model and speaker must match. v4-flash rejects a bare v3 name and v3 rejects a persona id. The gateway maps one to the other automatically (kavya becomes kavya_hi_conversational and back) and logs a warning once, but set the right form to keep logs quiet. v4-flash has no Malayalam or Odia persona, but it is multilingual: Malayalam text spoken through a Hindi persona is correct Malayalam. Speaker ids keep their own language tag, so a Hindi-tagged persona such as shilpa_hi_narration works for Malayalam.

With v4-flash set SARVAM_TTS_STREAM_TRANSPORT=http. The WebSocket transport cuts the tail off many sentences on that model; HTTP streaming has the same first-audio speed without the cut. Cached audio made over WebSocket carries the cut, so purge cached v4-flash audio after switching (see The TTS cache). If a stream fails, the gateway tries the other transport, then REST batch, then another provider.

Language codes are mapped to Sarvam's list before the request: en-US and en-GB become en-IN, and or-IN becomes od-IN. Cache keys and SARVAM_TTS_SPEAKER_<LANG> still use the code your session carries.

Google

Variable Default Set it when
GOOGLE_APPLICATION_CREDENTIALS (required) unset Always. Path to the service-account JSON file; also used by Gemini TTS and Google STT
GOOGLE_VOICE_<LANG> built-in Chirp3-HD voice per language Choosing the voice for one language, for example GOOGLE_VOICE_ML_IN. Beats the global name
GOOGLE_TTS_VOICE_NAME unset Single-language bots only (see below)
GOOGLE_TTS_REGION unset (global endpoint) Data residency or latency: asia, us or eu
GOOGLE_TTS_STREAMING true On. Set false to use REST batch only
GOOGLE_TTS_TIMEOUT unset A stalled request should fail over sooner

Set voices per language, not globally. Google rejects a voice whose language does not match the request, and the turn then plays silence. A single GOOGLE_TTS_VOICE_NAME applies to every language and silently breaks a multi-language bot, so use it only on a bot that speaks one language.

The request language is derived from the voice name: zh-CN uses cmn-CN voices and ar-AE uses ar-XA voices. A name that does not match its language is rejected.

Keep Chirp3-HD voice names. Other voice types drop back to REST batch and lose progressive streaming. All configured languages default to Chirp3-HD except Odia: Google publishes no Odia voice, so an Odia turn on Google falls to another configured provider.

Gemini

Variable Default Set it when
GOOGLE_APPLICATION_CREDENTIALS (required) unset Always. Same service account as Google TTS
GEMINI_TTS_MODEL gemini-2.5-flash-tts You want gemini-3.8-flash-tts or gemini-3.8-flash-lite-tts. Introductory pricing applies to the 3.8 models; check your provider console
GEMINI_TTS_BACKEND auto Force cloud-tts or vertex. Auto sends 3.8 models to Vertex AI and everything else to Cloud TTS
GEMINI_TTS_VERTEX_MODELS unset Change which model names auto routes to Vertex
GEMINI_TTS_VERTEX_LOCATION global Never; only global has the 3.8 models
GEMINI_TTS_VERTEX_PROJECT project of the credentials The Vertex project differs from the credentials' project
GEMINI_TTS_STREAMING true On Vertex the sentence streams, so first audio arrives before rendering finishes. Set false to use batch on Vertex. Cloud TTS models are never streamed
GEMINI_TTS_REGION unset (global) Cloud TTS backend only: us, eu or northamerica-northeast1. asia is not supported
GEMINI_TTS_VOICE_NAME Kore Global voice (Kore, Puck, Zephyr, Charon and others), or a custom voice_<id>
GEMINI_TTS_VOICE_<LANG> unset Voice for one language, for example GEMINI_TTS_VOICE_ML_IN
GEMINI_TTS_PROMPT unset A style prompt on the Cloud TTS backend. Ignored on Vertex
GEMINI_TTS_VERTEX_PROMPT false Set true to prepend the prompt on Vertex anyway. The 3.8 models read it aloud, so leave it false
GEMINI_TTS_TEMPERATURE 1.0 Variation in delivery

The 3.8 models run on Vertex AI, in location global only. The same service account is used. Cloud TTS and Vertex are separate backends, so a Cloud TTS-only model keeps its original request while a 3.8 model goes to Vertex. A per-call tts_model can send one call to Vertex while the rest of the host stays on Cloud TTS.

ElevenLabs

Variable Default Set it when
ELEVENLABS_API_KEY (required) unset Always, for this provider
ELEVENLABS_MODEL eleven_flash_v2_5 You want a different realtime model
ELEVENLABS_VOICE_ID built-in voice Choosing the global voice
ELEVENLABS_VOICE_<LANG> unset One language needs its own voice, for example ELEVENLABS_VOICE_ML_IN
ELEVENLABS_V3_ENABLED true Languages the flash model cannot speak route to v3 automatically. Set false to throw instead and fall back to another provider
ELEVENLABS_V3_MODEL eleven_v3 You want a different model for those languages
ELEVENLABS_FORCE_V3 unset Set true to send every language to v3: best quality, slower first audio
ELEVENLABS_AUTO_MODE true The socket skips chunk-schedule buffering because input is whole sentences. Set false to restore buffering
ELEVENLABS_WS_REUSE true Persistent socket, so warm turns skip the handshake. Set false for a socket per request
ELEVENLABS_STREAMING true On. Set false to use REST batch only
ELEVENLABS_STABILITY 0.5 Consistency of delivery, 0.0–1.0
ELEVENLABS_SIMILARITY_BOOST 0.75 Clarity, 0.0–1.0
ELEVENLABS_STYLE 0.0 Style exaggeration, 0.0–1.0
ELEVENLABS_USE_SPEAKER_BOOST true Disable speaker boost
ELEVENLABS_OPTIMIZE_LATENCY unset Realtime socket only; v3 rejects it
ELEVENLABS_INACTIVITY_TIMEOUT unset Idle timeout of the socket, in seconds (the API caps it)
ELEVENLABS_STREAM_FORMAT ulaw_8000 Streaming output format; decoded locally to 8 kHz
ELEVENLABS_OUTPUT_FORMAT pcm_22050 REST fallback format
ELEVENLABS_TIMEOUT unset Cap on a request
ELEVENLABS_API_URL https://api.elevenlabs.io You route through a proxy
ELEVENLABS_WSS_URL wss://api.elevenlabs.io You route through a proxy

The first call after an idle period pays a socket handshake; warm turns do not. v3 is slower to first audio than flash, so use it only for languages flash cannot speak. v3 rejects the latency optimisation parameter, which the gateway leaves out for it.

Smallest

Variable Default Set it when
SMALLEST_API_KEY (required) unset Always, for this provider
SMALLEST_TTS_VOICE_ID avni Choosing the global voice. Avoid divya: it truncates long text
SMALLEST_VOICE_<LANG> unset One language needs its own voice, for example SMALLEST_VOICE_ML_IN. Beats the global voice
SMALLEST_TTS_MODEL lightning_v3.1 You want another model such as lightning_v3.1_pro
SMALLEST_TTS_SAMPLE_RATE 8000 Never for telephony: 8 kHz is native, so no resampling is in the path
SMALLEST_TTS_SPEED 1.0 Speaking speed
SMALLEST_TTS_STREAMING true WebSocket streaming. Set false to use REST batch
SMALLEST_TTS_COMPLETE_BACKOFF_MS 0 Keep 0. The API's own default of 4000 adds a multi-second delay at the end of every sentence
SMALLEST_TTS_TIMEOUT 30000 Cap on a request, both transports
SMALLEST_TTS_MAX_CHARS 5000 Longer text is truncated with a warning
SMALLEST_TTS_WS_URL wss://api.smallest.ai/waves/v1/tts/live You route through a proxy
SMALLEST_TTS_REST_URL https://api.smallest.ai/waves/v1/tts You route through a proxy

Most catalogue voices are multilingual, so one voice usually covers every language you serve. Streaming and batch return identical audio, so cached entries are interchangeable between them.

OpenRouter

One key reaches many voice vendors. Choose the vendor and model with OPENROUTER_TTS_MODEL. It is a batch path with no streaming, so replies start after synthesis finishes.

Variable Default Set it when
OPENROUTER_API_KEY (required) unset Always. Use a normal key; a management key fails with "User not found"
OPENROUTER_TTS_MODEL mistralai/voxtral-mini-tts-3-0 Choosing another model, written vendor/name
OPENROUTER_TTS_VOICE unset Choosing the voice. Ids are model-specific (Gemini models use names such as Kore)
OPENROUTER_TTS_VOICE_<LANG> unset One language needs its own voice, for example OPENROUTER_TTS_VOICE_ML_IN
OPENROUTER_TTS_INSTRUCTIONS unset Delivery directions. Some models honour them, others ignore them
OPENROUTER_TTS_SPEED unset Speaking speed. Only sent when set, because some models reject a non-default value
OPENROUTER_TTS_TIMEOUT 30000 Cap on a request
OPENROUTER_TTS_MAX_CHARS 3000 Longer text is truncated with a warning; one model caps at 3000
OPENROUTER_TTS_URL unset Override the endpoint, for example a regional one

Audio arrives at a model-specific rate; the gateway reads it and resamples as needed. Listen to a voice before committing: a round trip through speech recognition proves the words are intelligible, not that they sound natural.

Deepgram

Variable Default Set it when
DEEPGRAM_API_KEY (required) unset Always, for this provider
DEEPGRAM_MODEL aura-asteria-en Choosing the global voice
DEEPGRAM_VOICE_<LANG> per-language built-ins One language needs another voice, for example DEEPGRAM_VOICE_EN_IN
DEEPGRAM_API_URL https://api.deepgram.com You route through a proxy
DEEPGRAM_TIMEOUT unset Cap on a request

Built-in voices cover English (US, GB, India), German, Spanish, French, Italian, Japanese, Korean, Dutch, Polish, Portuguese (Brazil), Russian and Chinese. Indian-language callers are better served by another provider.

Azure

Variable Default Set it when
AZURE_TTS_API_KEY (required) unset Always, for this provider
AZURE_TTS_REGION centralindia Your Speech resource lives in another region
AZURE_TTS_VOICE_NAME en-US-JennyNeural Choosing the global voice
AZURE_TTS_VOICE_STYLE none You want a speaking style such as friendly or cheerful
AZURE_TTS_RATE 1.2 Speaking speed; 1.0 is normal
AZURE_TTS_PITCH 0% Shifting pitch
AZURE_TTS_STREAMING true On. Set false for REST batch, resampled from 24 kHz
AZURE_TTS_TIMEOUT unset Cap on a request
AZURE_TTS_VOICE_<LANG> unset One language needs its own voice, for example AZURE_TTS_VOICE_ML_IN. Beats the global voice
AZURE_TTS_RATE_<LANG> unset One language needs its own speed, for example AZURE_TTS_RATE_ML_IN
AZURE_TTS_PITCH_<LANG> unset One language needs its own pitch, for example AZURE_TTS_PITCH_ML_IN
AZURE_TTS_STYLE_<LANG> unset One language needs its own style, for example AZURE_TTS_STYLE_EN_US

Murf

Variable Default Set it when
MURF_API_KEY (required) unset Always, for this provider
MURF_VOICE_ID en-US-natalie Choosing the global voice
MURF_MODEL GEN2 Choosing another Murf model
MURF_STYLE Conversational Choosing another speaking style
MURF_SAMPLE_RATE 24000 Choosing 8000; the audio is downsampled to the call's rate either way
MURF_VOICE_<LANG> built-in per language One language needs its own voice, for example MURF_VOICE_HI_IN. Beats the global voice

Murf has no streaming path: the whole sentence is rendered before playback. Dedicated voices cover English, Hindi, Tamil and Bengali; other Indian languages go through a multilingual voice.

Vexyl

A self-hosted Indian-language engine reached over a WebSocket, with named speakers and style control.

Variable Default Set it when
VEXYL_TTS_URL (required) ws://localhost:8092 The engine runs on another host or port
VEXYL_TTS_API_KEY unset The engine requires a key
VEXYL_TTS_STYLE default You want warm or formal. Default and warm pick a female speaker, formal a male one
VEXYL_TTS_STYLE_<LANG> default One language needs a different style, for example VEXYL_TTS_STYLE_ML_IN
VEXYL_TTS_SPEAKER unset Pinning a named speaker (for example Anjali or Harish). Beats style selection
VEXYL_TTS_SPEAKER_<LANG> unset One language needs its own speaker, for example VEXYL_TTS_SPEAKER_ML_IN. Beats the global speaker
VEXYL_TTS_TIMEOUT unset Cap on a request

Per-call voice and model

A single call can use a different provider, model or voice from the host default. Set these keys in the session metadata before the call is answered (from the dialplan), as SIP headers on the INVITE, or in an outbound call request. Every accepted voice and model id is listed in Per-call settings reference. The metadata endpoints are in HTTP API.

Metadata key Meaning
tts_provider The provider for this call. An unrecognised value is rejected with a log warning and the host default is used
tts_model Honoured by openrouter (vendor/name) and gemini (a bare id such as gemini-3.8-flash-lite-tts; that call goes to Vertex)
tts_voice openrouter: voice id. smallest: voice id, including cloned voices. sarvam: speaker. google: voice name, whose locale drives the language. gemini: a prebuilt name or a voice_<uuid> custom voice
curl -X POST http://127.0.0.1:8081/session/UUID/metadata \
  -H "Content-Type: application/json" \
  -d '{"tts_provider": "smallest", "tts_voice": "avni"}'

Only the provider named by tts_provider reads tts_model and tts_voice, so a fallback to another provider never receives a voice id it cannot use. Values are sanitised on entry: a model is vendor/name or a bare id, and a voice is at most 160 characters of letters, digits, and _ . : -. Browser widget sessions cannot set these keys. The cache key and the billing record follow the resolved model and voice.

Custom voices

Vexyl designs a Gemini voice from a text description, or replicates a voice from a consent-recorded sample, on request, and hands you a voice_<id>. Set it as GEMINI_TTS_VOICE_NAME or GEMINI_TTS_VOICE_<LANG>, or pass it per call as tts_voice. The gateway routes such calls to Vertex automatically, whatever the model. A description sets persona and timbre, not a regional dialect; only a replicated voice captures a real accent.

The TTS cache

The cache stores rendered audio on disk so a repeated sentence costs no synthesis and plays at once. It is off unless you enable it. It suits greetings, prompts, menus and FAQ answers; it does little for replies that are unique every time.

Variable Default Set it when
TTS_CACHE_ENABLED false Always, to turn caching on
TTS_CACHE_DIR cache/tts under the gateway's working directory You want it elsewhere (/app/cache/tts in Docker). Mount the parent so the cache survives restarts
TTS_CACHE_MAX_SIZE_MB 5000 The disk is smaller, or the phrase set larger
TTS_CACHE_MAX_AGE_DAYS 90 Entries not read for this long are removed. Age counts from the last read, so a greeting played on every call never expires
TTS_CACHE_CLEANUP_STRATEGY lru Choose lru, size, ttl or none for what is deleted when the cache is full
TTS_CACHE_STATS_LOGGING unset Set true to log hit rate periodically
TTS_CACHE_WARMUP_ON_START unset Set true to pre-render a phrase file at boot
TTS_CACHE_WARMUP_FILE unset Path to the phrase file for the boot warm-up

The cache key includes the text, language, provider, model and voice settings, so a changed voice makes new entries instead of serving the wrong audio. The old entries still sit on disk, though, and cached audio keeps the voice it was made with. Purge the cache after you change a voice, speaker, model or transport (for example moving Sarvam v4-flash from WebSocket to HTTP). Stop the gateway first, and start it again afterwards.

  • Binary install: stop the service, run rm -rf cache/tts/* in the gateway's working directory, start the service.
  • Docker with a bind mount: stop the container, run rm -rf ./cache/tts/* on the host, start it.
  • Docker with a named volume: run the commands below.
docker compose down
docker run --rm -v PROJECT_vexyl-cache:/c busybox sh -c 'rm -rf /c/tts/*'
docker compose up -d

Check the hit rate:

curl -s http://127.0.0.1:8081/cache/stats

The response covers the time since the process started. hitRateChars is the figure that tracks your bill, because a hit on a three-character "Yes" and a miss on a long sentence are not an even trade. hitRateRequests counts requests instead. byProvider gives hits, misses, hit characters and miss characters per provider, and cache gives disk entries, size and the last cleanup. Per-call figures are in the billing record.

Pre-render phrases before callers need them:

curl -X POST http://127.0.0.1:8081/cache/warmup \
  -H "Content-Type: application/json" \
  -d '{"phrases": [{"text": "Hello, how can I help you?", "languageCode": "en-IN"}]}'

Warm-up renders through the same path live sentences use, so entries land under the key calls will read. The response reports cached (newly rendered), skipped (already cached), errors and total. When two calls need the same uncached sentence at once, they share one synthesis.

Symptom Fix
No hits at all Check TTS_CACHE_ENABLED=true and that the cache directory is writable
Old voice after a change Purge the cache directory and restart
Disk filling Lower TTS_CACHE_MAX_SIZE_MB or TTS_CACHE_MAX_AGE_DAYS
Low hit rate Replies are unique; keep dynamic parts short and warm up the fixed phrases

Each miss on the sentence-streaming path is logged as CACHE MISS (streaming) with the text, which is the list to mine when choosing what to warm up. Cache behaviour during outages is covered in Troubleshooting.

Sentence streaming and seams

When the bot streams its reply, the gateway cuts it into sentences, renders them in order and plays them back to back. These settings control how smooth that is.

Variable Default Set it when
LITEBOT_SENTENCE_STREAMING true On. Set false to stop sentence-by-sentence playback and render each reply whole
LITEBOT_STREAM_MIN_SENTENCE_CHARS 12 Short pieces are merged into the next sentence so providers never get a tiny request, unless the short piece is already cached
LITEBOT_SENTENCE_PREBUFFER_MS 600 Audio buffered before playback starts. Raise it if slow providers leave gaps; half of it is rebuffered after a starvation
LITEBOT_TTS_SYNTH_CONCURRENCY 1 Keep 1. Parallel synthesis starves the sentence being played on providers that render near real time
TTS_SENTENCE_MIN_GAP_MS 200 Silence padded between sentences when the audio itself has too little. Providers that already pad get none. 0 disables
TTS_SENTENCE_EDGE_FADE_MS 5 A short fade where a sentence starts or ends mid-waveform, to remove clicks. 0 disables
TTS_TAIL_DELAY_MS 40 Delay after playback ends so the last audio drains before the line is treated as quiet
PROCESSING_SOUND_HARD_CUT true The thinking sound is cut the moment speech is ready. Set false to wait for its phrase to end

Barge-in, interruptions and the thinking sound are described in Conversation behaviour; day-to-day checks are in Operations.