> 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.

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-reference-docs/integration/build-voice-agent-with-live-kit) or [Pipecat](/api-reference-docs/integration/build-voice-agent-with-pipecat) voice agent build guides instead: they aren't a 1:1 API swap and aren't covered here.

**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_key>`                 | `api-subscription-key: <api_key>` |
| Auth failure status | `401`                                       | `403`                             |

With that, replace the transcription call:

```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
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);
```

```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"}
    ]
  }'
```

```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:v3",
    language_code="en-IN",
    mode="transcribe",
)

print(response.transcript)
```

```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:v3",
  language_code: "en-IN",
  mode: "transcribe",
});

console.log(response.transcript);
```

```bash
curl -X POST https://api.sarvam.ai/speech-to-text \
  -H "api-subscription-key: YOUR_SARVAM_API_KEY" \
  -F model="saaras:v3" \
  -F language_code="en-IN" \
  -F mode="transcribe" \
  -F file=@audio.mp3
```

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

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.**

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

```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:v3",
    language_code="en-IN",
    mode="transcribe",
)

print(response.transcript)
```

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-reference-docs/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.**

```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,
        },
    ],
)
```

```python
from sarvamai import SarvamAI

client = SarvamAI(api_subscription_key="YOUR_SARVAM_API_KEY")

job = client.speech_to_text_job.create_job(
    model="saaras:v3",
    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-reference-docs/api-guides-tutorials/speech-to-text/batch-api) for webhook setup and its [Speaker Diarization section](/api-reference-docs/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:

```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
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;
}
```

```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\"}]}"
}
```

```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:v3",
                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
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:v3",
      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;
  }
}
```

```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:v3" \
    -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

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.

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.

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.

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-reference-docs/migrations/from-gemini/overview): base URL, auth, and SDK client changes
* [Migrating Chat Completion](/api-reference-docs/migrations/from-gemini/chat-completion)
* [Migrating Text-to-Speech](/api-reference-docs/migrations/from-gemini/text-to-speech)
* [Which Speech-to-Text API to Use](/api-reference-docs/api-guides-tutorials/speech-to-text/which-api-to-use)
* [Batch API](/api-reference-docs/api-guides-tutorials/speech-to-text/batch-api)
* [Streaming API](/api-reference-docs/api-guides-tutorials/speech-to-text/streaming-api)
* [Errors & Troubleshooting](/api-reference-docs/errors-troubleshooting)