> 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 Speech-to-Text from Gemini to Sarvam > Endpoint, parameter, and response-format differences between transcribing audio through the Gemini API's audio input and Sarvam's dedicated Speech-to-Text API, with before/after code. *Last updated: July 15, 2026* This guide covers migrating Gemini audio transcription calls to Sarvam. If you're building a full voice agent with live, bidirectional audio rather than transcribing recorded audio 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:** Gemini transcribes audio by passing it, along with a natural-language instruction, to a general model. Sarvam has a dedicated transcription endpoint. Swap the text-plus-audio input list for a `transcribe()` call with a `mode` parameter, and read the result from a guaranteed-clean `transcript` field instead of `interaction.output_text`. ## 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 transcription call: #### Before (Gemini) #### Python ```python import base64 from google import genai client = genai.Client(api_key="YOUR_GEMINI_API_KEY") with open("audio.mp3", "rb") as f: audio_bytes = f.read() interaction = client.interactions.create( model="gemini-3.5-flash", input=[ {"type": "text", "text": "Generate a transcript of the speech."}, { "type": "audio", "data": base64.b64encode(audio_bytes).decode("utf-8"), "mime_type": "audio/mp3", }, ], ) print(interaction.output_text) ``` #### JavaScript ```javascript import { GoogleGenAI } from "@google/genai"; import fs from "fs"; const ai = new GoogleGenAI({ apiKey: "YOUR_GEMINI_API_KEY" }); const audioBytes = fs.readFileSync("audio.mp3"); const interaction = await ai.interactions.create({ model: "gemini-3.5-flash", input: [ { type: "text", text: "Generate a transcript of the speech." }, { type: "audio", data: audioBytes.toString("base64"), mime_type: "audio/mp3" }, ], }); console.log(interaction.output_text); ``` #### 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.5-flash", "input": [ {"type": "text", "text": "Generate a transcript of the speech."}, {"type": "audio", "data": "BASE64_AUDIO_HERE", "mime_type": "audio/mp3"} ] }' ``` #### After (Sarvam) #### Python ```python from sarvamai import SarvamAI client = SarvamAI(api_subscription_key="YOUR_SARVAM_API_KEY") response = client.speech_to_text.transcribe( file=open("audio.mp3", "rb"), model="saaras:v4", language_code="en-IN", mode="transcribe", ) print(response.transcript) ``` #### JavaScript ```javascript import { SarvamAIClient } from "sarvamai"; import fs from "fs"; const client = new SarvamAIClient({ apiSubscriptionKey: "YOUR_SARVAM_API_KEY" }); const response = await client.speechToText.transcribe({ file: fs.createReadStream("audio.mp3"), model: "saaras:v4", language_code: "en-IN", mode: "transcribe", }); console.log(response.transcript); ``` #### cURL ```bash curl -X POST https://api.sarvam.ai/speech-to-text \ -H "api-subscription-key: YOUR_SARVAM_API_KEY" \ -F model="saaras:v4" \ -F language_code="en-IN" \ -F mode="transcribe" \ -F file=@audio.mp3 ``` > **Note** > > Gemini's reply is a model's response to your prompt, so its exact wording and formatting can vary between calls unless your prompt constrains it carefully. Sarvam's `response.transcript` is a dedicated field that always contains just the transcript. The rest of this page covers the full endpoint and parameter mapping, long audio, 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 | | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | Transcription | `POST /v1beta/interactions`, with the audio passed as an input content block alongside a text prompt asking for a transcript | `POST /speech-to-text` (up to 30 seconds per request; use Batch for longer audio) | | Long audio (up to 9.5 hours per prompt) | Upload via `client.files.upload(...)` first, then reference the file's `uri` in the input | Batch API: `POST /speech-to-text/job/v1` (create), upload, start, then poll or receive a webhook callback, up to 2 hours per file | | Real-time streaming transcription | Not applicable to this endpoint; live audio goes through the Live API instead | `GET /speech-to-text/ws` | ## Parameter concept map | Gemini API | Sarvam | Note | | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `model` (a general model, e.g. `gemini-3.5-flash`, shared with text generation) | `model` (`saaras:v3`, a dedicated speech-to-text model) | A transcription-specific endpoint and model, rather than a general model prompted to behave like one | | A natural-language prompt (e.g. `{"type": "text", "text": "Generate a transcript of the speech."}`) | `mode="transcribe"` (a dedicated parameter) | An explicit parameter rather than an instruction the model has to interpret correctly every time | | `{"type": "audio", "data": ..., "mime_type": ...}` input block (or `"uri"` for an uploaded file) | `file` (upload the audio file directly) | Same concept: the audio goes in as the file content either way | | No dedicated language parameter; Gemini infers language from the audio and the prompt | `language_code` (optional, BCP-47, e.g. `hi-IN`; pass `unknown` to auto-detect) | Same auto-detect behavior available explicitly, plus the option to hint the language for better accuracy | | Response: `interaction.output_text` (a model reply satisfying the prompt) | Response: `response.transcript` (a dedicated, transcript-only field) | Guaranteed to be just the transcript, not a model's phrasing of one | ## Long audio and diarization **For files over 20 MB, upload first instead of inlining the audio.** #### Before (Gemini) ```python from google import genai client = genai.Client(api_key="YOUR_GEMINI_API_KEY") uploaded_file = client.files.upload(file="long-meeting.mp3") interaction = client.interactions.create( model="gemini-3.5-flash", input=[ {"type": "text", "text": "Generate a transcript of the speech."}, { "type": "audio", "uri": uploaded_file.uri, "mime_type": uploaded_file.mime_type, }, ], ) print(interaction.output_text) ``` #### After (Sarvam) ```python from sarvamai import SarvamAI client = SarvamAI(api_subscription_key="YOUR_SARVAM_API_KEY") response = client.speech_to_text.transcribe( file=open("long-meeting.mp3", "rb"), model="saaras:v4", language_code="en-IN", mode="transcribe", ) print(response.transcript) ``` > **Note** > > Sarvam's REST endpoint caps at 30 seconds of audio regardless of file size. For a file like this one, use the [Batch API](/api/api-guides-tutorials/speech-to-text/batch-api) instead, shown next, rather than the direct `transcribe()` call above. **Move diarized or long-form audio to the Batch API.** #### Before (Gemini) ```python from google import genai client = genai.Client(api_key="YOUR_GEMINI_API_KEY") # Not available as a dedicated mode: Gemini has no speaker-labeling parameter. # You'd need to prompt the model to attempt speaker labeling in its reply, # with no guarantee of a consistent, structured format back. uploaded_file = client.files.upload(file="meeting.mp3") interaction = client.interactions.create( model="gemini-3.5-flash", input=[ {"type": "text", "text": "Transcribe this and label each speaker."}, { "type": "audio", "uri": uploaded_file.uri, "mime_type": uploaded_file.mime_type, }, ], ) ``` #### After (Sarvam) ```python from sarvamai import SarvamAI client = SarvamAI(api_subscription_key="YOUR_SARVAM_API_KEY") job = client.speech_to_text_job.create_job( model="saaras:v4", mode="transcribe", language_code="en-IN", with_diarization=True, num_speakers=3, ) job.upload_files(file_paths=["meeting.mp3"]) job.start() job.wait_until_complete() file_results = job.get_file_results() for f in file_results["failed"]: print(f"Failed: {f['file_name']}: {f['error_message']}") if file_results["successful"]: job.download_outputs(output_dir="./output") ``` See the [Batch API guide](/api/api-guides-tutorials/speech-to-text/batch-api) for webhook setup and its [Speaker Diarization section](/api/api-guides-tutorials/speech-to-text/batch-api#speaker-diarization) for the current per-job speaker limit. ## What's different by design * **A dedicated transcription endpoint.** `POST /speech-to-text` and `mode="transcribe"` are purpose-built for turning audio into text, rather than a general model interpreting a natural-language request. * **A guaranteed-clean transcript field.** `response.transcript` always holds just the transcript, with no prompt-engineering needed to keep a general-purpose model from adding commentary or reformatting the text. * **Flexible output modes on one model.** `saaras:v3` also supports `translate`, `verbatim`, `translit`, and `codemix` modes as explicit parameters, rather than needing a differently worded prompt for each. * **Diarization comes standard, not prompted for.** Sarvam's Batch API identifies and labels speakers with `with_diarization` and `num_speakers`, returning a structured `diarized_transcript`, rather than relying on a model's best-effort attempt inside free-text output. * **Indian language depth is Sarvam's specific strength.** Saaras v3 covers 22 Indian languages, including Assamese, Konkani, Kashmiri, Sindhi, Sanskrit, Santali, Manipuri, Bodo, Maithili, and Dogri, with native handling for code-mixed speech like Hinglish. ## Full example A small wrapper function, before and after, showing the complete set of changes together: #### Before (Gemini) #### Python ```python import base64 from google import genai client = genai.Client(api_key="YOUR_GEMINI_API_KEY") def transcribe(path: str) -> str: with open(path, "rb") as f: audio_bytes = f.read() interaction = client.interactions.create( model="gemini-3.5-flash", input=[ {"type": "text", "text": "Generate a transcript of the speech."}, { "type": "audio", "data": base64.b64encode(audio_bytes).decode("utf-8"), "mime_type": "audio/mp3", }, ], ) return interaction.output_text ``` #### JavaScript ```javascript import { GoogleGenAI } from "@google/genai"; import fs from "fs"; const ai = new GoogleGenAI({ apiKey: "YOUR_GEMINI_API_KEY" }); async function transcribe(path) { const audioBytes = fs.readFileSync(path); const interaction = await ai.interactions.create({ model: "gemini-3.5-flash", input: [ { type: "text", text: "Generate a transcript of the speech." }, { type: "audio", data: audioBytes.toString("base64"), mime_type: "audio/mp3" }, ], }); return interaction.output_text; } ``` #### cURL ```bash transcribe() { local path="$1" local audio_b64 audio_b64=$(base64 -w 0 "$path") 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.5-flash\", \"input\": [{\"type\": \"text\", \"text\": \"Generate a transcript of the speech.\"}, {\"type\": \"audio\", \"data\": \"$audio_b64\", \"mime_type\": \"audio/mp3\"}]}" } ``` #### After (Sarvam) #### Python ```python from sarvamai import SarvamAI from sarvamai.core.api_error import ApiError client = SarvamAI(api_subscription_key="YOUR_SARVAM_API_KEY") def transcribe(path: str, language_code: str = "en-IN") -> str: try: with open(path, "rb") as f: response = client.speech_to_text.transcribe( file=f, model="saaras:v4", language_code=language_code, mode="transcribe", ) 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 return response.transcript ``` #### JavaScript ```javascript import { SarvamAIClient, SarvamAIError } from "sarvamai"; import fs from "fs"; const client = new SarvamAIClient({ apiSubscriptionKey: "YOUR_SARVAM_API_KEY" }); async function transcribe(path, languageCode = "en-IN") { try { const response = await client.speechToText.transcribe({ file: fs.createReadStream(path), model: "saaras:v4", language_code: languageCode, mode: "transcribe", }); return response.transcript; } 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; } } ``` #### cURL ```bash transcribe() { local path="$1" language_code="${2:-en-IN}" local body status body=$(curl -sS -w '\n%{http_code}' -X POST https://api.sarvam.ai/speech-to-text \ -H "api-subscription-key: YOUR_SARVAM_API_KEY" \ -F model="saaras:v4" \ -F language_code="$language_code" \ -F mode="transcribe" \ -F file=@"$path") 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. echo "${body%$'\n'*}" | grep -o '"transcript":"[^"]*"' | cut -d'"' -f4 } ``` ## Common mistakes #### Trusting interaction.output\_text as a clean transcript It's a model's reply to your prompt, so it can include extra phrasing, partial answers, or formatting depending on how the prompt was worded. Read `response.transcript` on Sarvam's side instead, which is always transcript-only. #### Wording a prompt instead of setting a parameter Sarvam's `mode` parameter (`transcribe`, `translate`, `verbatim`, `translit`, `codemix`) replaces the need to phrase a natural-language instruction carefully and hope the model interprets it the same way every time. #### Expecting structured diarization from a prompted request Asking Gemini to "label each speaker" in the prompt returns free text, not a structured, per-segment breakdown. Sarvam's `with_diarization` and `num_speakers` are Batch API parameters that return a dedicated `diarized_transcript` field, so route diarized requests through the Batch workflow instead of relying on prompt wording. #### Sending Batch parameters as top-level fields Calling the Batch endpoints directly over HTTP (rather than through the SDK) requires nesting parameters under `job_parameters`, e.g. `{"job_parameters": {"model": "saaras:v3", ...}}`. A flat body won't be picked up. ## 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 Text-to-Speech](/api/migrations/from-gemini/text-to-speech) * [Which Speech-to-Text API to Use](/api/api-guides-tutorials/speech-to-text/which-api-to-use) * [Batch API](/api/api-guides-tutorials/speech-to-text/batch-api) * [Streaming API](/api/api-guides-tutorials/speech-to-text/streaming-api) * [Errors & Troubleshooting](/api/getting-started/errors-troubleshooting) > Endpoint, parameter, and response-format differences between transcribing audio through the Gemini API's audio input and Sarvam's dedicated Speech-to-Text API, with before/after code.