How to Build an Indian Language Voiceover App Using Sarvam Bulbul v3 API

What Is Sarvam Bulbul v3 and Why Indian Language TTS Is a Hard Problem

Most text-to-speech APIs were built for English. Feed them Hindi, Tamil, Bengali, or Marathi and you get robotic output โ€” mispronounced words, wrong prosody, unnatural stress patterns that make the audio immediately feel machine-generated.

Sarvam AI built Bulbul specifically for Indian languages. Bulbul v3 is their flagship TTS model โ€” trained on native speech data across multiple Indian languages, with speaker voices that sound like real regional speakers rather than anglicised approximations.

This guide covers how Bulbul v3 actually works under the hood, how to build a production-ready TTS pipeline with a secure server proxy, and how to handle the Base64 audio response correctly in a browser frontend.


๐ŸŽฏ Quick Answer (30-Second Read)

  • What it does: Converts Indian language text to natural-sounding speech using regionally accurate voices
  • Endpoint: POST https://api.sarvam.ai/text-to-speech
  • Key model param: Must explicitly send "model": "bulbul:v3" โ€” omitting it falls back to v2 silently
  • Audio response: Returns Base64-encoded MP3 โ€” must decode in frontend before playback
  • Security: Never call Sarvam API directly from the browser โ€” use a server-side proxy to protect your API key
  • Best for: Regional content apps, accessibility tools, vernacular education platforms, voice assistants for Bharat

How Bulbul v3 Actually Works โ€” The Technical Foundation

Before writing code it helps to understand what makes Bulbul v3 different from generic TTS models and why the API is designed the way it is.

The Linguistic Challenge of Indian TTS

Indian languages share almost no phonological structure with English. Hindi uses retroflex consonants. Tamil has a three-way distinction between short, long, and very long vowels. Bengali has vowel harmony rules. Marathi has gender agreement that affects pronunciation of adjacent words.

A model trained on English speech data cannot generalise to these patterns โ€” the acoustic space is fundamentally different. Bulbul v3 is trained on native Indian language speech corpora, which means the model has learned the actual phoneme distributions, prosody patterns, and coarticulation rules of each language from the ground up.

How the API Pipeline Works Internally

When you send a text string to the Bulbul v3 endpoint, the following happens:

Step 1 โ€” Language identification and normalisation. The model normalises the input text โ€” expanding abbreviations, converting numerals to words in the target language, handling mixed-script input where Hindi words appear in a Devanagari sentence.

Step 2 โ€” Grapheme-to-phoneme conversion. The text is converted to a phoneme sequence using language-specific G2P rules. This is where Indian TTS models diverge most from English models โ€” the phoneme inventories are larger and the rules are more complex.

Step 3 โ€” Acoustic model inference. The phoneme sequence is passed through the acoustic model which generates mel spectrogram frames โ€” a time-frequency representation of the audio. The speaker embedding (which voice you selected) conditions this step, shaping the vocal characteristics of the output.

Step 4 โ€” Vocoder synthesis. The mel spectrogram is converted to a raw audio waveform using a neural vocoder. Bulbul v3 uses a high-fidelity vocoder that produces 24kHz audio โ€” significantly higher quality than older TTS systems which typically output at 16kHz or 22kHz.

Step 5 โ€” Encoding and response. The raw audio waveform is encoded as MP3 and Base64-encoded before being returned in the JSON response. This is why you need to decode it on the frontend before playback.

flowchart TD A([๐Ÿ“ Input Text\nHindi / Tamil / Bengali]) --> B[Text Normalisation\nNumerals, abbreviations] B --> C[Grapheme to Phoneme\nLanguage-specific G2P rules] C --> D[Acoustic Model\nMel spectrogram generation] D --> E[Speaker Embedding\nVoice conditioning] E --> F[Neural Vocoder\n24kHz waveform synthesis] F --> G[MP3 Encoding\nBase64 wrapping] G --> H([๐Ÿ”Š JSON Response\naudios array]) style A fill:#0f172a,color:#ffffff,stroke:#334155 style H fill:#166534,color:#ffffff,stroke:#16a34a style B fill:#1e293b,color:#ffffff,stroke:#475569 style C fill:#1e293b,color:#ffffff,stroke:#475569 style D fill:#312e81,color:#ffffff,stroke:#6366f1 style E fill:#312e81,color:#ffffff,stroke:#6366f1 style F fill:#1e3a5f,color:#ffffff,stroke:#3b82f6 style G fill:#1e293b,color:#ffffff,stroke:#475569

Understanding this pipeline explains several API design decisions that otherwise seem arbitrary โ€” why the response is Base64 JSON rather than a binary audio stream, why sample rate is a configurable parameter, and why the model parameter matters so much between v2 and v3.


The Core API Specification

Endpoint: POST https://api.sarvam.ai/text-to-speech

Authentication: api-subscription-key header โ€” a server-side secret, never exposed to the browser.

Request payload:

{
  "model": "bulbul:v3",
  "text": "เค†เคชเค•เคพ เคธเฅเคตเคพเค—เคค เคนเฅˆ",
  "target_language_code": "hi-IN",
  "speaker": "shubh",
  "pace": 1.0,
  "response_format": "mp3",
  "sample_rate": 24000
}

Supported language codes:

Language Code
Hindi hi-IN
Tamil ta-IN
Telugu te-IN
Bengali bn-IN
Marathi mr-IN
Gujarati gu-IN
Kannada kn-IN
Malayalam ml-IN
Punjabi pa-IN
Odia od-IN

Pace parameter range:

Value Effect
0.5 Half speed โ€” clear, deliberate speech
0.75 Slightly slow โ€” good for learning apps
1.0 Natural speaking pace (default)
1.5 Faster โ€” good for notifications
2.0 Double speed โ€” maximum rate

Response structure:

{
  "audios": [
    "base64encodedMP3stringhere..."
  ],
  "request_id": "uuid"
}

The audios array contains one Base64-encoded MP3 string per text input. For single text inputs this array always has exactly one element.


Step-by-Step Build Guide

Step 1 โ€” Project Setup

mkdir sarvam-tts-studio
cd sarvam-tts-studio
npm init -y
npm install dotenv node-fetch
touch server.js .env
mkdir public
touch public/index.html public/app.js public/style.css

Create your .env file:

# .env
SARVAM_API=your_sarvam_api_key_here
PORT=3000

Add .env to .gitignore immediately:

echo ".env" >> .gitignore

Step 2 โ€” Build the Secure Server Proxy

The proxy pattern is non-negotiable here. Your Sarvam API key must never reach the browser โ€” any frontend JavaScript that contains an API key is public. The proxy sits between your frontend and Sarvam, adding the authentication header server-side.

// server.js
const http = require('http');
const fs = require('fs');
const path = require('path');
require('dotenv').config();

// Helper to collect POST body chunks
function getRequestBody(req) {
  return new Promise((resolve, reject) => {
    let body = '';
    req.on('data', chunk => body += chunk.toString());
    req.on('end', () => {
      try { resolve(JSON.parse(body)); }
      catch (e) { reject(new Error('Invalid JSON body')); }
    });
    req.on('error', reject);
  });
}

// Serve static files from /public
function serveStatic(res, filePath) {
  const extMap = {
    '.html': 'text/html',
    '.js': 'application/javascript',
    '.css': 'text/css'
  };
  const ext = path.extname(filePath);
  const contentType = extMap[ext] || 'text/plain';

  fs.readFile(filePath, (err, data) => {
    if (err) {
      res.writeHead(404);
      res.end('Not found');
      return;
    }
    res.writeHead(200, { 'Content-Type': contentType });
    res.end(data);
  });
}

const server = http.createServer(async (req, res) => {
  const { url, method } = req;

  // CORS headers for local development
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  if (method === 'OPTIONS') { res.writeHead(204); res.end(); return; }

  // TTS proxy route
  if (url === '/api/tts' && method === 'POST') {
    try {
      const body = await getRequestBody(req);

      // Validate required fields before forwarding
      if (!body.text || !body.target_language_code) {
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'text and target_language_code are required' }));
        return;
      }

      // Always enforce bulbul:v3 โ€” do not let client override to v2
      body.model = 'bulbul:v3';

      const apiResponse = await fetch('https://api.sarvam.ai/text-to-speech', {
        method: 'POST',
        headers: {
          'api-subscription-key': process.env.SARVAM_API,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(body)
      });

      const data = await apiResponse.json();
      res.writeHead(apiResponse.status, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(data));

    } catch (error) {
      console.error('TTS proxy error:', error);
      res.writeHead(500, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'TTS proxy failed', details: error.message }));
    }
    return;
  }

  // Static file serving
  if (method === 'GET') {
    const safePath = url === '/' ? '/index.html' : url;
    serveStatic(res, path.join(__dirname, 'public', safePath));
    return;
  }

  res.writeHead(404);
  res.end('Not found');
});

server.listen(process.env.PORT || 3000, () => {
  console.log(`TTS Studio running at http://localhost:${process.env.PORT || 3000}`);
});

Why force bulbul:v3 on the server side? Because if a client sends bulbul:v2 (accidentally or maliciously), the server would forward it and the newer speaker voices would return errors. Enforcing the model on the proxy ensures consistent behaviour regardless of what the client sends.

Step 3 โ€” Build the Frontend HTML





  
  
  Sarvam TTS Studio
  


  

Regional Voiceover Studio

Powered by Sarvam Bulbul v3

Step 4 โ€” Build the Frontend Synthesizer

// public/app.js

async function generateTTS() {
  const text = document.getElementById('tts-input').value.trim();
  const lang = document.getElementById('tts-lang').value;
  const voice = document.getElementById('tts-voice').value;
  const pace = parseFloat(document.getElementById('tts-pace').value);
  const resultPanel = document.getElementById('tts-result');

  if (!text) {
    resultPanel.innerHTML = '

Please enter some text first.

'; return; } // Show loading state resultPanel.innerHTML = '

Generating audio โ€” this takes 2โ€“5 seconds...

'; try { const response = await fetch('/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, target_language_code: lang, speaker: voice, pace, response_format: 'mp3', sample_rate: 24000 // Note: model is enforced server-side as bulbul:v3 }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || data.error || 'API request failed'); } if (!data.audios || data.audios.length === 0) { throw new Error('No audio returned from API'); } // Decode Base64 to in-memory audio URL // The API returns raw Base64 โ€” no data URL prefix included const base64Audio = data.audios[0]; const audioUrl = `data:audio/mp3;base64,${base64Audio}`; resultPanel.innerHTML = `

Audio generated successfully

Download MP3
`; } catch (error) { resultPanel.innerHTML = `

Error: ${error.message}

`; console.error('TTS generation failed:', error); } }

Why construct the data URL manually? The Sarvam API returns a raw Base64 string โ€” not a data URL. The browser's <audio> element cannot play raw Base64. Prepending data:audio/mp3;base64, creates an in-memory URL the browser can use directly, without writing the audio to disk or making a second network request.

Step 5 โ€” Run the App

node server.js
# TTS Studio running at http://localhost:3000

Open http://localhost:3000, select a language and voice, paste Indian language text, and click Generate.


Bulbul v3 vs v2 โ€” What Actually Changed

The version difference matters more than most API version bumps.

Feature Bulbul v2 Bulbul v3
Speaker voices Limited set Expanded catalog including shubh, meera, arjun, diya
Audio quality 16โ€“22kHz 24kHz โ€” noticeably clearer
Prosody naturalness Functional Significantly improved โ€” more natural sentence rhythm
Pace control range Limited 0.5x to 2.0x full range
Mixed script handling Basic Improved โ€” handles Hinglish and code-switching better
Model param required Optional Required โ€” omitting defaults to v2 silently

The most important practical difference: v3 speaker names are not backward compatible with v2. Sending speaker: "shubh" to a v2 endpoint returns an error because that voice does not exist in v2. This is why the proxy enforces bulbul:v3 โ€” a silent fallback to v2 would produce confusing speaker errors.


My Take โ€” Why Indian Language TTS Is Still an Underrated Problem

Most developers I talk to treat TTS as a solved problem. They are thinking about English. The moment you step into Indian language territory the complexity multiplies in ways that are not obvious from the outside.

The real challenge is not just phonetics โ€” it is the cultural layer underneath the language. How a Hindi speaker in Lucknow stresses a word is different from how a Delhi speaker does it. Tamil has diglossia โ€” the written language and the spoken language are genuinely different registers, and a TTS model trained on written text sounds formal and unnatural when read aloud in casual contexts.

What excites me about models like Bulbul v3 is that they are attacking the problem from the right direction โ€” training on native speech rather than translating English TTS techniques into Indian languages. The results are audibly different. You can tell within two sentences whether audio was generated by a model that understands the language or one that is phonetically approximating it.

The future here is personalised regional voices โ€” not just Hindi or Tamil, but Bhojpuri, Rajasthani, Chhattisgarhi. The 700 million Indians who are more comfortable in their regional language than in Hindi or English represent the largest underserved market in the global voice AI space. The infrastructure to serve them is just starting to exist.

The worst outcome is that this space gets dominated by generic multilingual models that treat Indian languages as low-resource edge cases. The best outcome is specialised models like Bulbul that treat Indian languages as first-class citizens. Right now both futures are possible.


Common Errors and Fixes

Error Cause Fix
speaker not found Using v3 speaker name with v2 model Ensure model: "bulbul:v3" is in every request
audio plays silence Base64 decoded incorrectly Verify data URL prefix is data:audio/mp3;base64, not data:audio/wav;base64,
401 Unauthorized API key missing or wrong header name Header must be api-subscription-key not Authorization
413 Payload Too Large Text input too long Split long text into chunks under 500 characters each
audio sounds robotic Wrong target_language_code for input script Match language code to the actual script โ€” Hindi text needs hi-IN not en-IN
pace has no effect Sending pace as string not number Parse pace with parseFloat() before sending

Frequently Asked Questions

Can Bulbul v3 handle mixed Hindi-English text (Hinglish)?
Yes, with limitations. Bulbul v3 handles common Hinglish patterns better than v2 โ€” English words embedded in Hindi sentences are generally pronounced correctly. For best results, use Devanagari script for Hindi words and Roman script for English words. Avoid sending entire sentences in Roman-transliterated Hindi โ€” use the native script for the target language wherever possible.

What is the maximum text length per API call?
Sarvam does not publish a hard character limit, but practical testing shows quality degrades and latency increases significantly beyond 500 characters per request. For longer content โ€” articles, scripts, full paragraphs โ€” split into sentence-level chunks, generate each separately, and concatenate the audio client-side or server-side using an audio processing library.

Why does the API return Base64 instead of a binary audio stream?
Base64 encoding allows the audio to be embedded in a standard JSON response, which is easier to handle in most HTTP client libraries than a binary stream response. The trade-off is approximately 33% larger payload size due to Base64 encoding overhead. For high-volume production use, request binary streaming if Sarvam supports it on your plan tier โ€” it reduces bandwidth costs meaningfully at scale.

How do I choose the right speaker voice for a language?
Not all speaker voices are available in all languages. The Sarvam API returns an error if you request a speaker that does not have a trained voice for the target language. Check Sarvam's documentation for the speaker-language compatibility matrix. For production apps, build a speaker catalog that filters available voices based on the selected language rather than showing all voices for all languages.

Can I use this for production at scale without per-request latency issues?
At low to medium volume, yes โ€” latency is typically 2โ€“5 seconds per request at 24kHz. For high-volume production (thousands of requests per hour), pre-generate audio for known content and cache it. TTS is expensive to generate per-request at scale. For dynamic content, queue requests asynchronously and notify users when audio is ready rather than blocking the UI on generation time.


Conclusion

Sarvam Bulbul v3 is the most production-ready Indian language TTS API available to developers right now. The technical foundations โ€” native-trained acoustic models, 24kHz neural vocoder, regional speaker voices โ€” produce audio quality that generic multilingual models cannot match for Indian languages.

The architecture described in this guide โ€” secure server proxy, Base64 decoding, in-memory audio playback โ€” is the correct production pattern for any browser-based TTS application. Never expose API keys in frontend code. Always enforce the model version server-side. Always validate Base64 responses before rendering.

Build regional voice experiences that actually sound regional. The 700 million Indians using the internet in their native language deserve audio that sounds like them โ€” not a phonetically approximated translation of an English voice model.


Related reads: How Developers Use AI to Build Apps Faster ยท How to Build a Chrome Extension with AI ยท Claude Code Setup Guide ยท Best AI Coding Tools for Developers in 2026