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.
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.cssCreate your .env file:
# .env
SARVAM_API=your_sarvam_api_key_here
PORT=3000Add .env to .gitignore immediately:
echo ".env" >> .gitignoreStep 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:3000Open 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