> For clean Markdown of any page, append `.md` to the page URL. > For a complete documentation index, see https://docs.sarvam.ai/llms.txt. > For full documentation content in one file, see https://docs.sarvam.ai/llms-full.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.sarvam.ai/_mcp/server. # Migrating Text-to-Speech from Gemini to Sarvam > Endpoint, parameter, and response-format differences between Gemini's native audio text-to-speech generation and Sarvam's Text-to-Speech API, with before/after code. *Last updated: July 15, 2026* 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](/api/integration/build-voice-agent-with-live-kit) or [Pipecat](/api/integration/build-voice-agent-with-pipecat) voice agent build guides instead: they aren't a 1:1 API swap and aren't covered here. > **Tip** > > **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 `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 API | Sarvam | | ------------------- | ------------------------------------------- | --------------------------------- | | Base URL | `https://generativelanguage.googleapis.com` | `https://api.sarvam.ai` | | Auth header | `x-goog-api-key: ` | `api-subscription-key: ` | | Auth failure status | `401` | `403` | With that, replace the synthesis call and add the required language code: #### Before (Gemini) #### Python ```python import wave import base64 from google import genai client = genai.Client(api_key="YOUR_GEMINI_API_KEY") interaction = client.interactions.create( model="gemini-3.1-flash-tts-preview", input="Welcome to our platform!", response_format={"type": "audio"}, generation_config={ "speech_config": [{"voice": "Kore"}], }, ) def wave_file(filename, pcm, channels=1, rate=24000, sample_width=2): with wave.open(filename, "wb") as wf: wf.setnchannels(channels) wf.setsampwidth(sample_width) wf.setframerate(rate) wf.writeframes(pcm) wave_file("output.wav", base64.b64decode(interaction.output_audio.data)) ``` #### JavaScript ```javascript import { GoogleGenAI } from "@google/genai"; import fs from "fs"; const ai = new GoogleGenAI({ apiKey: "YOUR_GEMINI_API_KEY" }); const interaction = await ai.interactions.create({ model: "gemini-3.1-flash-tts-preview", input: "Welcome to our platform!", response_format: { type: "audio" }, generation_config: { speech_config: [{ voice: "Kore" }] }, }); function writeWavFile(filename, pcm, channels = 1, rate = 24000, sampleWidth = 2) { const header = Buffer.alloc(44); header.write("RIFF", 0); header.writeUInt32LE(36 + pcm.length, 4); header.write("WAVEfmt ", 8); header.writeUInt32LE(16, 16); header.writeUInt16LE(1, 20); header.writeUInt16LE(channels, 22); header.writeUInt32LE(rate, 24); header.writeUInt32LE(rate * channels * sampleWidth, 28); header.writeUInt16LE(channels * sampleWidth, 32); header.writeUInt16LE(sampleWidth * 8, 34); header.write("data", 36); header.writeUInt32LE(pcm.length, 40); fs.writeFileSync(filename, Buffer.concat([header, pcm])); } writeWavFile("output.wav", Buffer.from(interaction.output_audio.data, "base64")); ``` #### cURL ```bash curl -X POST "https://generativelanguage.googleapis.com/v1beta/interactions" \ -H "x-goog-api-key: YOUR_GEMINI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gemini-3.1-flash-tts-preview", "input": "Welcome to our platform!", "response_format": {"type": "audio"}, "generation_config": {"speech_config": [{"voice": "Kore"}]} }' ``` #### After (Sarvam) #### Python ```python import base64 from sarvamai import SarvamAI client = SarvamAI(api_subscription_key="YOUR_SARVAM_API_KEY") response = client.text_to_speech.convert( text="Welcome to our platform!", language_code="en-IN", speaker="shubh", model="bulbul:v3", pace=1.0, ) with open("output.wav", "wb") as f: f.write(base64.b64decode("".join(response.audios))) ``` #### JavaScript ```javascript import { SarvamAIClient } from "sarvamai"; import fs from "fs"; const client = new SarvamAIClient({ apiSubscriptionKey: "YOUR_SARVAM_API_KEY" }); const response = await client.textToSpeech.convert({ text: "Welcome to our platform!", language_code: "en-IN", speaker: "shubh", model: "bulbul:v3", pace: 1.0, }); fs.writeFileSync("output.wav", Buffer.from(response.audios.join(""), "base64")); ``` #### cURL ```bash curl -X POST https://api.sarvam.ai/text-to-speech \ -H "api-subscription-key: YOUR_SARVAM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Welcome to our platform!", "language_code": "en-IN", "speaker": "shubh", "model": "bulbul:v3" }' ``` > **Note** > > 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](/api/api-guides-tutorials/text-to-speech/rest-api) for the full decode snippet. The rest of this page covers the full endpoint and parameter mapping, streaming, and common mistakes. ## Endpoint map > **Tip** > > 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 API | Sarvam | | --------------------- | ------------------------------------------------------------------------------------------- | ----------------------------- | | Synchronous synthesis | `POST /v1beta/interactions` (with `response_format: {"type": "audio"}`) | `POST /text-to-speech` | | HTTP streaming | `POST /v1beta/interactions` with `stream: true` | `POST /text-to-speech/stream` | | WebSocket streaming | Not applicable to this endpoint; live bidirectional audio goes through the Live API instead | `GET /text-to-speech/ws` | ## Parameter concept map | Gemini API | Sarvam | Note | | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `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) | `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 applicable | Generate 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 API | Sarvam | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | HTTP streaming | `stream=True` on the same `interactions.create()` call, currently only on `gemini-3.1-flash-tts-preview` | [HTTP Stream API](/api/api-guides-tutorials/text-to-speech/streaming-api/http-stream) (`POST /text-to-speech/stream`), which streams on every model | | Streamed chunk format | Events; 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 connection | Not applicable to this endpoint | [WebSocket API](/api/api-guides-tutorials/text-to-speech/streaming-api/web-socket): `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 `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 `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: #### Before (Gemini) #### Python ```python import wave import base64 from google import genai client = genai.Client(api_key="YOUR_GEMINI_API_KEY") def wave_file(filename, pcm, channels=1, rate=24000, sample_width=2): with wave.open(filename, "wb") as wf: wf.setnchannels(channels) wf.setsampwidth(sample_width) wf.setframerate(rate) wf.writeframes(pcm) def synthesize(text: str, out_path: str) -> None: interaction = client.interactions.create( model="gemini-3.1-flash-tts-preview", input=text, response_format={"type": "audio"}, generation_config={"speech_config": [{"voice": "Kore"}]}, ) wave_file(out_path, base64.b64decode(interaction.output_audio.data)) ``` #### JavaScript ```javascript import { GoogleGenAI } from "@google/genai"; import fs from "fs"; const ai = new GoogleGenAI({ apiKey: "YOUR_GEMINI_API_KEY" }); function writeWavFile(filename, pcm, channels = 1, rate = 24000, sampleWidth = 2) { const header = Buffer.alloc(44); header.write("RIFF", 0); header.writeUInt32LE(36 + pcm.length, 4); header.write("WAVEfmt ", 8); header.writeUInt32LE(16, 16); header.writeUInt16LE(1, 20); header.writeUInt16LE(channels, 22); header.writeUInt32LE(rate, 24); header.writeUInt32LE(rate * channels * sampleWidth, 28); header.writeUInt16LE(channels * sampleWidth, 32); header.writeUInt16LE(sampleWidth * 8, 34); header.write("data", 36); header.writeUInt32LE(pcm.length, 40); fs.writeFileSync(filename, Buffer.concat([header, pcm])); } async function synthesize(text, outPath) { const interaction = await ai.interactions.create({ model: "gemini-3.1-flash-tts-preview", input: text, response_format: { type: "audio" }, generation_config: { speech_config: [{ voice: "Kore" }] }, }); writeWavFile(outPath, Buffer.from(interaction.output_audio.data, "base64")); } ``` #### cURL ```bash synthesize() { local text="$1" out_path="$2" local audio_b64 audio_b64=$(curl -sS -X POST "https://generativelanguage.googleapis.com/v1beta/interactions" \ -H "x-goog-api-key: YOUR_GEMINI_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"model\": \"gemini-3.1-flash-tts-preview\", \"input\": \"$text\", \"response_format\": {\"type\": \"audio\"}, \"generation_config\": {\"speech_config\": [{\"voice\": \"Kore\"}]}}" \ | grep -o '"data":"[^"]*"' | cut -d'"' -f4) # Raw PCM; wrap in a WAV header (see the Python/JavaScript examples) to make it playable directly. echo "$audio_b64" | base64 -d > "$out_path.pcm" } ``` #### After (Sarvam) #### Python ```python import base64 from sarvamai import SarvamAI from sarvamai.core.api_error import ApiError client = SarvamAI(api_subscription_key="YOUR_SARVAM_API_KEY") def synthesize(text: str, out_path: str, language_code: str = "en-IN") -> None: try: response = client.text_to_speech.convert( text=text, language_code=language_code, speaker="shubh", model="bulbul:v3", ) except ApiError as e: if e.status_code == 403: raise RuntimeError("Invalid API key. Check your credentials.") from e elif e.status_code == 429: raise RuntimeError("Rate limit exceeded. Wait and retry.") from e elif e.status_code == 503: raise RuntimeError("Service overloaded. Retry with backoff.") from e raise with open(out_path, "wb") as f: f.write(base64.b64decode("".join(response.audios))) ``` #### JavaScript ```javascript import { SarvamAIClient, SarvamAIError } from "sarvamai"; import fs from "fs"; const client = new SarvamAIClient({ apiSubscriptionKey: "YOUR_SARVAM_API_KEY" }); async function synthesize(text, outPath, languageCode = "en-IN") { let response; try { response = await client.textToSpeech.convert({ text, language_code: languageCode, speaker: "shubh", model: "bulbul:v3", }); } catch (e) { if (e instanceof SarvamAIError) { if (e.statusCode === 403) throw new Error("Invalid API key. Check your credentials."); if (e.statusCode === 429) throw new Error("Rate limit exceeded. Wait and retry."); if (e.statusCode === 503) throw new Error("Service overloaded. Retry with backoff."); } throw e; } fs.writeFileSync(outPath, Buffer.from(response.audios.join(""), "base64")); } ``` #### cURL ```bash synthesize() { local text="$1" out_path="$2" language_code="${3:-en-IN}" local body status audio_b64 body=$(curl -sS -w '\n%{http_code}' -X POST https://api.sarvam.ai/text-to-speech \ -H "api-subscription-key: YOUR_SARVAM_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"text\": \"$text\", \"language_code\": \"$language_code\", \"speaker\": \"shubh\", \"model\": \"bulbul:v3\"}") status="${body##*$'\n'}" case "$status" in 403) echo "Invalid API key. Check your credentials." >&2; return 1 ;; 429) echo "Rate limit exceeded. Wait and retry." >&2; return 1 ;; 503) echo "Service overloaded. Retry with backoff." >&2; return 1 ;; esac # Minimal single-value extraction; use a real JSON parser for production code. audio_b64=$(echo "${body%$'\n'*}" | grep -o '"audios":\["[^"]*"' | cut -d'"' -f4) echo "$audio_b64" | base64 -d > "$out_path" } ``` ## Common mistakes #### Writing raw PCM bytes as if they were a finished WAV file 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. #### Forgetting language\_code 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. #### Looking for a multi-speaker speech\_config equivalent 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. #### Reusing a chat model name for TTS 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-105b`; the chat and speech models aren't interchangeable on either platform. #### Not retrying on Gemini's audio-token failure mode 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 * [Migration overview](/api/migrations/from-gemini/overview): base URL, auth, and SDK client changes * [Migrating Chat Completion](/api/migrations/from-gemini/chat-completion) * [Migrating Speech-to-Text](/api/migrations/from-gemini/speech-to-text) * [Which Text-to-Speech API to Use](/api/api-guides-tutorials/text-to-speech/which-api-to-use) * [TTS Best Practices](/api/api-guides-tutorials/text-to-speech/best-practices) * [Voices](/api/api-guides-tutorials/text-to-speech/voices) * [Errors & Troubleshooting](/api/getting-started/errors-troubleshooting) > Endpoint, parameter, and response-format differences between Gemini's native audio text-to-speech generation and Sarvam's Text-to-Speech API, with before/after code.