Migrating Text-to-Speech from Deepgram to Sarvam

View as Markdown

This guide covers migrating Deepgram text-to-speech API calls to Sarvam. If you’re building a full voice agent rather than replacing individual TTS calls, 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: split Deepgram’s combined model string into a separate model and speaker, and change your response handling: both platforms return audio bytes, but Sarvam wraps them in JSON as a base64 audios array rather than handing back a raw stream.

Quick start

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

DeepgramSarvam
Base URLhttps://api.deepgram.comhttps://api.sarvam.ai
Auth headerAuthorization: Token <api_key>api-subscription-key: <api_key>
Auth failure status401403

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

1from deepgram import DeepgramClient
2
3client = DeepgramClient(api_key="YOUR_DEEPGRAM_API_KEY")
4
5audio = client.speak.v1.audio.generate(
6 text="Welcome to our platform!",
7 model="aura-2-asteria-en",
8)
9
10with open("output.mp3", "wb") as f:
11 for chunk in audio:
12 f.write(chunk)

Deepgram’s generate() yields raw audio chunks/bytes you can write directly. Sarvam’s convert() returns a JSON object; the audio lives in response.audios as a list of base64 strings, one per input text. 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, not a file. 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.

DeepgramSarvam
Synchronous synthesisPOST /v1/speakPOST /text-to-speech
HTTP streamingPOST /v1/speak (same endpoint, streamed as it generates)POST /text-to-speech/stream
WebSocket streamingPOST /v1/speak (WebSocket upgrade)GET /text-to-speech/ws

Parameter concept map

DeepgramSarvamNote
model (voice, model version, and language combined in one string, e.g. aura-2-asteria-en)model (bulbul:v2/bulbul:v3) + speaker (plain name, e.g. shubh)Sarvam splits model and voice into two params, so you don’t need a separate combined name for every voice
encoding + container + sample_rate (three separate output settings)output_audio_codec + speech_sample_rateSame idea, two settings instead of three; Sarvam’s REST response defaults to WAV
speed (float, model-dependent range)pace (float, 0.5-2.0 on v3, 0.3-3.0 on v2)Same concept, consistent range across REST, HTTP streaming, and WebSocket
bit_rate (optional, for compressed formats)Not applicableSarvam doesn’t expose a separate bitrate control; pick output_audio_codec for the compression level you need
callback (webhook URL for async delivery)Not applicableSarvam’s TTS endpoints respond synchronously or stream in real time; there’s no async/webhook mode to configure
Response: raw audio bytes (Iterator[bytes])Response: JSON, audio as base64 strings in audiosStructured response makes it straightforward to pair audio with per-request metadata; decode .audios[0] before writing to disk

Streaming

DeepgramSarvam
HTTP streamingSame POST /v1/speak request, streamed as it generatesHTTP Stream API (POST /text-to-speech/stream), same behavior over one request with no persistent connection
WebSocket{"type": "Speak", "text": "..."} per chunk of text, {"type": "Flush"} to force out buffered audio, closed with {"type": "Close"}WebSocket API: ws.configure(...) once, then repeated ws.convert(text) (equivalent to Speak) and ws.flush() (equivalent to Flush) on the same connection
Discarding buffered text{"type": "Clear"} discards buffered, not-yet-generated textNo separate discard operation, since ws.flush() already generates audio for whatever text has been sent rather than leaving it queued

What’s different by design

  • Plain speaker names, no combined model string. Sarvam’s speaker is a name you pick straight from the Voices page, rather than being baked into a single model value like aura-2-asteria-en that encodes voice, version, and language together.
  • Two output settings instead of three. speech_sample_rate and output_audio_codec cover what Deepgram splits across encoding, container, and sample_rate.
  • No async/webhook mode to configure. Sarvam’s TTS endpoints are always synchronous or streamed live, so there’s one less delivery mode to decide between.
  • Indian language depth is Sarvam’s specific strength. Deepgram’s Aura-2 voices are mostly English, with a handful in Spanish; 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:

1from deepgram import DeepgramClient
2
3client = DeepgramClient(api_key="YOUR_DEEPGRAM_API_KEY")
4
5def synthesize(text: str, out_path: str) -> None:
6 audio = client.speak.v1.audio.generate(
7 text=text,
8 model="aura-2-asteria-en",
9 )
10 with open(out_path, "wb") as f:
11 for chunk in audio:
12 f.write(chunk)

Common mistakes

The single most common migration bug. Deepgram’s REST response is the audio stream itself; Sarvam’s is JSON. Code that does f.write(response.content) unmodified will produce a file full of JSON text, not audio. Decode response.audios[0] from base64 first.

It’s required on every request, by design: this is what guarantees Sarvam applies the correct pronunciation and prosody instead of inferring language from a combined model string like Deepgram’s. Include it from the start rather than treating it as optional.

There isn’t one, and that’s fine: pick output_audio_codec for the compression and quality tradeoff you need instead of tuning a separate bitrate value.

A value like aura-2-asteria-en doesn’t map to a single Sarvam field. Split it into model (bulbul:v3) and speaker (a name from the Voices page) instead of trying to find a one-to-one string replacement.

See also