Migrating Text-to-Speech from Gemini to Sarvam

View as Markdown

This guide covers migrating Gemini text-to-speech calls to Sarvam. If you’re building a full voice agent with live, bidirectional audio rather than generating speech from text one call at a time, start from the LiveKit or Pipecat voice agent build guides instead: they aren’t a 1:1 API swap and aren’t covered here.

In short: replace generation_config.speech_config (a list of {voice, speaker, language} objects) plus response_format: {"type": "audio"} with two flat params, speaker and target_language_code, and read the audio back from a plain audios array instead of unwrapping interaction.output_audio.data.

Quick start

Base URL and auth header change the same way for every Gemini migration, so they’re folded in here rather than in a separate page:

Gemini APISarvam
Base URLhttps://generativelanguage.googleapis.comhttps://api.sarvam.ai
Auth headerx-goog-api-key: <api_key>api-subscription-key: <api_key>
Auth failure status401403

With that, replace the synthesis call and add the required language code:

1import wave
2import base64
3from google import genai
4
5client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
6
7interaction = client.interactions.create(
8 model="gemini-3.1-flash-tts-preview",
9 input="Welcome to our platform!",
10 response_format={"type": "audio"},
11 generation_config={
12 "speech_config": [{"voice": "Kore"}],
13 },
14)
15
16def wave_file(filename, pcm, channels=1, rate=24000, sample_width=2):
17 with wave.open(filename, "wb") as wf:
18 wf.setnchannels(channels)
19 wf.setsampwidth(sample_width)
20 wf.setframerate(rate)
21 wf.writeframes(pcm)
22
23wave_file("output.wav", base64.b64decode(interaction.output_audio.data))

Gemini’s output_audio.data is raw PCM you wrap in a WAV header yourself. Sarvam’s convert() returns a JSON object; the audio lives in response.audios as a list of base64-encoded, already-WAV-formatted strings. Join and decode before writing to disk, or use the Python SDK’s sarvamai.play.save() helper, which does this for you.

The REST response is JSON with a base64 audios array, already WAV-formatted; no manual header construction needed. Decode .audios[0] before writing it out; see the REST API guide for the full decode snippet.

The rest of this page covers the full endpoint and parameter mapping, streaming, and common mistakes.

Endpoint map

The tables below aren’t a checklist to apply field-by-field: Quick Start above already shows the whole call swapped over in one go. Use these tables as a reference for what changed and why, not as a sequence of edits to make yourself.

Gemini APISarvam
Synchronous synthesisPOST /v1beta/interactions (with response_format: {"type": "audio"})POST /text-to-speech
HTTP streamingPOST /v1beta/interactions with stream: truePOST /text-to-speech/stream
WebSocket streamingNot applicable to this endpoint; live bidirectional audio goes through the Live API insteadGET /text-to-speech/ws

Parameter concept map

Gemini APISarvamNote
model (TTS-specific, e.g. gemini-3.1-flash-tts-preview)model (bulbul:v2 or bulbul:v3)Dedicated TTS endpoint with two models, not a general model configured for audio output
generation_config.speech_config (a list, one entry per voice)speaker (plain name, e.g. shubh)One flat param instead of a list-of-objects, even for a single voice
speech_config[].language (optional)target_language_code (BCP-47; required)Guarantees correct pronunciation, especially for code-mixed Indian-language text
speech_config with two entries, each tied to a speaker name in the prompt (max 2 speakers)Not applicableGenerate each speaker separately and stitch the audio; independent language and pace control per speaker
Response: interaction.output_audio.data (base64 raw PCM, wrap in a WAV header yourself)Response: response.audios (base64 WAV strings, ready to decode and write)No manual WAV header construction needed

Streaming

Gemini APISarvam
HTTP streamingstream=True on the same interactions.create() call, currently only on gemini-3.1-flash-tts-previewHTTP Stream API (POST /text-to-speech/stream), which streams on every model
Streamed chunk formatEvents; audio in event.delta.data (base64) when event.event_type == "step.delta" and event.delta.type == "audio"Audio returned progressively over the same request, no separate event-type check needed
Persistent multi-turn connectionNot applicable to this endpointWebSocket API: ws.configure(...) once, then repeated ws.convert(text) and ws.flush() on the same connection

What’s different by design

  • One flat call, no list to build. speaker and target_language_code are passed directly, rather than nested inside a generation_config.speech_config list, even for a single voice.
  • A dedicated TTS endpoint. POST /text-to-speech is purpose-built for speech synthesis, rather than a general interactions endpoint configured to return audio instead of text.
  • Ready-to-use WAV output. response.audios is already WAV-formatted; there’s no separate step to wrap raw PCM samples in a WAV header yourself.
  • Explicit language targeting. Every Sarvam request carries target_language_code, so pronunciation and prosody are correct by design on every call, especially valuable for Hindi, Hinglish, and other code-mixed Indian-language text.
  • Indian language depth is Sarvam’s specific strength. Sarvam covers Hindi plus 9 other Indian languages (Bengali, Tamil, Telugu, Kannada, Malayalam, Marathi, Gujarati, Punjabi, Odia), natively tuned for code-mixed input like Hinglish.

Full example

A small wrapper function, before and after, showing the complete set of changes together:

1import wave
2import base64
3from google import genai
4
5client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
6
7def wave_file(filename, pcm, channels=1, rate=24000, sample_width=2):
8 with wave.open(filename, "wb") as wf:
9 wf.setnchannels(channels)
10 wf.setsampwidth(sample_width)
11 wf.setframerate(rate)
12 wf.writeframes(pcm)
13
14def synthesize(text: str, out_path: str) -> None:
15 interaction = client.interactions.create(
16 model="gemini-3.1-flash-tts-preview",
17 input=text,
18 response_format={"type": "audio"},
19 generation_config={"speech_config": [{"voice": "Kore"}]},
20 )
21 wave_file(out_path, base64.b64decode(interaction.output_audio.data))

Common mistakes

Gemini’s TTS output is unwrapped PCM; without the wave module step, the resulting file has no valid header and won’t play. Sarvam’s response.audios entries are already complete WAV files once base64-decoded, so this extra wrapping step goes away entirely, not just changes shape.

It’s required on every request, by design: this is what guarantees Sarvam applies the correct pronunciation and prosody instead of relying on the optional, per-entry language field Gemini lets you set inside speech_config. Include it from the start rather than treating it as optional.

There isn’t a single-call two-speaker mode. Generate each speaker’s dialogue as a separate convert() call with its own speaker, then stitch the resulting audio together in your application code.

Gemini’s TTS uses a distinct model family (e.g. gemini-3.1-flash-tts-preview) from its chat models. Likewise, Sarvam’s model here is bulbul:v2/bulbul:v3, not sarvam-30b/sarvam-105b; the chat and speech models aren’t interchangeable on either platform.

Google’s own docs note that Gemini’s TTS models can occasionally emit text tokens instead of audio, which fails the request with a 500 error; they recommend building retry logic for it. Sarvam’s TTS doesn’t have that particular failure mode, so the retry logic in the examples on this page (403/429/503) is the only error handling you need to carry over.

See also