Real-time Speech-to-Speech SDK for Indian Languages
| Home | Architecture | API Reference | Latency Guide |
A Python SDK that delivers real-time conversational AI by orchestrating Sarvam AI’s STT, LLM, and TTS APIs in a streaming pipeline. The user speaks and gets intelligent spoken responses with sub-second latency.
┌─────────────┐ ┌──────────────┐ ┌────────────────┐ ┌──────────────┐ ┌─────────────┐
│ Microphone │────▶│ STT Engine │────▶│ LLM Engine │────▶│ TTS Engine │────▶│ Speaker │
│ (16kHz PCM)│ │ (Saaras v3) │ │ (Sarvam-105B) │ │ (Bulbul v3) │ │ (Audio Out)│
└─────────────┘ │ WebSocket │ │ HTTP Stream │ │ WS + HTTP │ └─────────────┘
└──────────────┘ └────────────────┘ └──────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌────────────────┐ ┌──────────────┐
│ VAD Events │ │ Streaming │ │ Audio Chunks│
│ speech_start│ │ Token-by-Token│ │ Progressive │
│ speech_end │ │ Generation │ │ Playback │
└──────────────┘ └────────────────┘ └──────────────┘
reasoning_effort: null for instant token flow (no thinking overhead)Natural interruption model inspired by human conversation:
speech_start (VAD signal) while TTS is playingThe SDK prevents TTS audio from being picked up by the microphone and re-processed:
speech_start events during SPEAKING state trigger barge-in (not echo processing)Don’t wait for the full LLM response. As soon as a phrase boundary is detected in the LLM stream:
., !, ?, ,, ;, :, ।, ॥reasoning_effort: nullSetting reasoning_effort to null in the Sarvam LLM request disables the model’s internal “thinking” step. This produces instant token flow, critical for voice latency where every 100ms matters. The model skips chain-of-thought and outputs tokens directly.
The HTTP streaming endpoint (POST https://api.sarvam.ai/text-to-speech/stream) provides:
POST https://api.sarvam.ai/text-to-speech) if streaming failsclass SarvamS2S:
"""Orchestrates the full conversation lifecycle"""
- state: SessionState (IDLE | LISTENING | PROCESSING | SPEAKING)
- config: SarvamS2SConfig
async def start() # Begin listening
async def stop() # End session
async def wait_until_done() # Block until stopped
# Internal pipeline
async def _audio_capture_loop() # Mic → STT
async def _stt_receive_loop() # STT → Transcript events
async def _handle_barge_in() # Interrupt handling
async def _handle_transcript(text) # Transcript → LLM → TTS → Speaker
async def _generate_and_speak(text) # Core pipeline execution
class STTEngine:
"""Manages Sarvam STT WebSocket connection"""
- ws: WebSocket → wss://api.sarvam.ai/speech-to-text/ws
- model: "saaras:v3"
- vad_signals: True (for barge-in detection)
- high_vad_sensitivity: True (for conversational use)
async def connect()
async def send_audio(chunk: bytes)
async def events() -> AsyncIterator[STTEvent]
async def disconnect()
class LLMEngine:
"""Streaming LLM inference with full context management"""
- model: "sarvam-105b" (default) | "sarvam-30b" | custom
- memory: ConversationMemory (sliding window)
- context pipeline: system prompt → static context → RAG → few-shot → history
async def generate_stream() -> AsyncIterator[str]
def add_user_message(text: str)
def add_assistant_message(text: str)
def clear_history()
class TTSEngine:
"""Sarvam TTS with WebSocket + HTTP Streaming + REST fallback"""
- model: "bulbul:v3"
- speaker: "aditya" (default), 16 voices available
- transports: WS > HTTP Stream > REST (priority order)
async def connect() # Open WebSocket
async def synthesize_stream(text) -> AsyncIterator[bytes]
async def disconnect() # Close (for barge-in)
class AudioCapture:
"""Microphone input - 16-bit PCM, mono, 16kHz, 32ms chunks"""
async def stream() -> AsyncIterator[bytes]
class AudioPlayer:
"""Speaker output with instant stop for barge-in"""
async def play_chunk(audio_bytes: bytes)
async def stop() # Immediate interruption
┌──────────┐
│ IDLE │
└────┬─────┘
│ start()
▼
┌──────────┐
┌────▶│LISTENING │◀────────────────────────┐
│ └────┬─────┘ │
│ │ speech_end + transcript │
│ ▼ │
│ ┌──────────┐ │
│ │PROCESSING│ (LLM generating) │
│ └────┬─────┘ │
│ │ first phrase ready │
│ ▼ │
│ ┌──────────┐ barge-in │
│ │ SPEAKING │─────────────────────────┘
│ └────┬─────┘
│ │ TTS complete
└──────────┘
User Speaking STT Engine Session Manager LLM Engine TTS Engine Speaker
│ │ │ │ │ │
│──audio────────▶ │ │ │ │
│ │──speech_start─▶ │ │ │
│ │ │ │ │──STOP──────▶│
│ │ │──cancel_llm────▶ │ │
│ │ │──disconnect────────────────────▶ │
│ │ │ state → LISTENING │ │
│ │ │ │ │ │
│──more audio──▶│ │ │ │ │
│ │──speech_end───▶ │ │ │
│ │──transcript───▶ │ │ │
│ │ │──generate─────▶│ │ │
│ │ │ │──stream──────▶│ │
│ │ │ │ │──audio─────▶│
Messages sent to the LLM are constructed in this order:
1. System prompt (always present)
2. Static context [configurable position: before_system | after_system | before_user]
3. RAG-retrieved context (if retriever callback is set)
4. Few-shot examples (if configured)
5. Conversation history (sliding window by turns + tokens)
The ConversationMemory class manages trimming:
llm_max_history_turns: Max user turns to retain (default: 20)llm_max_history_tokens: Approximate token budget for history (default: 4000)# POST https://api.sarvam.ai/v1/chat/completions
{
"model": "sarvam-105b", # or "sarvam-30b"
"messages": [...],
"max_tokens": 200, # Low for voice (fast responses)
"temperature": 0.7,
"reasoning_effort": null, # CRITICAL: disables thinking for instant tokens
"stream": true
}
Headers:
api-subscription-key: <api_key>Authorization: Bearer <api_key>Content-Type: application/json| Stage | Technique | Target Latency |
|---|---|---|
| Audio Capture → STT | Continuous streaming, 32ms chunks | ~32ms per chunk |
| STT → Transcript | VAD-based endpoint detection | ~100-200ms after speech ends |
| Transcript → LLM | Immediate request on transcript | ~50ms |
| LLM First Token | reasoning_effort: null + streaming |
~200-400ms |
| LLM → TTS | Phrase boundary detection, immediate send | ~0ms (pipelined) |
| TTS First Audio | HTTP streaming, low min_buffer_size | ~100-200ms |
| Total: User stops → First audio plays | ~500-1000ms |
@dataclass
class SarvamS2SConfig:
# API Configuration
api_key: str
base_url: str = "https://api.sarvam.ai"
# STT Configuration
stt_model: str = "saaras:v3"
stt_language: str = "hi-IN"
stt_mode: str = "transcribe"
stt_vad_sensitivity: bool = True
stt_vad_signals: bool = True
# LLM Configuration
llm_provider: str = "sarvam" # "sarvam" | "openai" | "custom"
llm_model: str = "sarvam-105b" # "sarvam-30b", "sarvam-105b"
llm_max_tokens: int = 200 # Low for voice latency
llm_temperature: float = 0.7
llm_stream: bool = True
# LLM Context
llm_context: str = "" # Static context
llm_context_retriever: Callable | None # RAG callback
llm_few_shot_examples: list # Example conversations
llm_max_history_turns: int = 20
llm_max_history_tokens: int = 4000
# TTS Configuration
tts_model: str = "bulbul:v3"
tts_speaker: str = "aditya"
tts_language: str = "hi-IN"
tts_pace: float = 1.0
tts_audio_codec: str = "linear16"
tts_sample_rate: int = 16000
# Behavior
enable_barge_in: bool = True
sentence_buffer_chars: int = 50
max_response_chars: int = 500
idle_timeout_seconds: int = 300
| Language | Code | STT | TTS |
|---|---|---|---|
| Hindi | hi-IN | ✅ | ✅ |
| English (Indian) | en-IN | ✅ | ✅ |
| Bengali | bn-IN | ✅ | ✅ |
| Tamil | ta-IN | ✅ | ✅ |
| Telugu | te-IN | ✅ | ✅ |
| Kannada | kn-IN | ✅ | ✅ |
| Malayalam | ml-IN | ✅ | ✅ |
| Marathi | mr-IN | ✅ | ✅ |
| Gujarati | gu-IN | ✅ | ✅ |
| Punjabi | pa-IN | ✅ | ✅ |
| Odia | od-IN | ✅ | ✅ |
| Speaker | Gender |
|---|---|
| aditya | Male |
| priya | Female |
| rahul | Male |
| neha | Female |
| anushka | Female |
| kavitha | Female |
| karun | Male |
| hitesh | Male |
| ritu | Female |
| rohan | Male |
| simran | Female |
| kavya | Female |
| amit | Male |
| dev | Male |
| ishita | Female |
| shreya | Female |
| Component | Rate | Estimate (5-min convo) |
|---|---|---|
| STT (Saaras v3) | ₹30/hour | ~₹2.50 |
| TTS (Bulbul v3) | ₹30/10K chars | ~₹1.50 (500 chars response avg) |
| LLM (Sarvam-105B) | ₹2.5 input + ₹10 output / 1M tokens | ~₹0.50 |
| Total | ~₹4.50 per 5-min conversation |
The SDK is LLM-agnostic. While it defaults to Sarvam-105B, it supports:
Configuration:
# Sarvam (default)
config = SarvamS2SConfig(api_key="sarvam-key")
# OpenAI
config = SarvamS2SConfig(
api_key="sarvam-key",
llm_provider="openai",
llm_api_key="sk-...",
llm_model="gpt-4o-mini",
)
# Custom (Groq, Together, etc.)
config = SarvamS2SConfig(
api_key="sarvam-key",
llm_provider="custom",
llm_base_url="https://api.groq.com/openai/v1",
llm_api_key="gsk_...",
llm_model="llama-3.1-70b-versatile",
)
| Component | Technology |
|---|---|
| Language | Python 3.10+ |
| Async Runtime | asyncio |
| WebSocket Client | websockets |
| Audio I/O | sounddevice (PortAudio) |
| Audio Format | PCM 16-bit, 16kHz, mono |
| HTTP Client | httpx (async, streaming) |
| Package Manager | pip / uv |
| Testing | pytest + pytest-asyncio |
| Web Framework | FastAPI + uvicorn (demos) |
The run_web_demo.py server provides a browser-based interface:
Browser (JS) FastAPI Server
┌───────────────┐ ┌──────────────────────┐
│ Text Input │──WebSocket──▶│ /ws/chat │
│ Audio Playback│◀─────────────│ LLM Stream │
│ Interrupt Btn │──interrupt──▶│ TTS per sentence │
└───────────────┘ │ Interrupt support │
└──────────────────────┘
Key features:
{"type": "interrupt"} to cancel in-flight generation.,!?;: + min 8 chars)