Real-time Speech-to-Speech SDK for Indian Languages
| Home | Architecture | API Reference | Latency Guide |
This document explains the techniques used in the Sarvam S2S SDK to achieve ~500-1000ms end-to-end latency (user stops speaking → first audio byte plays).
| Stage | What Happens | Target | Optimization |
|---|---|---|---|
| Audio → STT | 32ms audio chunks streamed continuously | ~32ms | Small chunk size, no buffering |
| STT → Transcript | VAD detects speech end, final transcript produced | ~100-200ms | High VAD sensitivity |
| Transcript → LLM request | HTTP request initiated | ~50ms | Connection reuse (httpx) |
| LLM first token | Model generates first output token | ~200-400ms | reasoning_effort: null |
| LLM → TTS | First phrase sent to TTS | ~0ms | Pipelined (phrase detection) |
| TTS first audio | Audio bytes begin streaming | ~100-200ms | HTTP streaming, low buffer |
| Total | ~500-1000ms |
reasoning_effort: nullThe single biggest latency win. By setting reasoning_effort to null in the Sarvam LLM request, the model skips its internal chain-of-thought step and outputs tokens immediately.
payload = {
"model": "sarvam-105b",
"messages": [...],
"reasoning_effort": None, # Disables thinking, instant tokens
"stream": True,
}
Impact: Reduces LLM first-token latency from ~1-2s (with reasoning) to ~200-400ms.
Trade-off: Slightly lower quality on complex reasoning tasks. For voice assistants with short, conversational responses, this is an excellent trade-off.
Instead of waiting for the full LLM response, the SDK detects phrase boundaries and sends text to TTS immediately:
PHRASE_ENDERS = (".", "!", "?", "।", "॥", "\n", ",", ";", ":")
MIN_PHRASE_LENGTH = 10 # Avoid sending tiny fragments
# In the streaming loop:
if text.rstrip().endswith(PHRASE_ENDERS) and len(text.strip()) >= MIN_PHRASE_LENGTH:
send_to_tts(text)
text = ""
Impact: TTS synthesis begins while LLM is still generating the rest of the response. For a 3-sentence response, the user hears the first sentence ~500ms earlier than waiting for the full response.
Why commas and semicolons? For voice output, natural pause points (commas, semicolons) are valid places to split. The TTS model handles these fragments well and it reduces perceived latency.
The SDK uses POST https://api.sarvam.ai/text-to-speech/stream which returns audio bytes via chunked transfer encoding:
async with client.stream("POST", url, headers=headers, json=payload) as resp:
async for chunk in resp.aiter_bytes():
play(chunk) # Play immediately as bytes arrive
Impact: Audio playback begins before the full TTS response is generated. First audio byte arrives ~100-200ms after request (vs ~500ms+ for REST which returns the complete audio).
Fallback chain: WebSocket → HTTP Streaming → REST (each progressively more reliable but higher latency).
Audio is captured in 32ms chunks (512 samples at 16kHz):
audio_chunk_ms: int = 32 # 512 samples at 16kHz
Why 32ms? Balances between:
32ms is optimal for real-time conversation. Going smaller (16ms) adds overhead without meaningful latency improvement due to STT server-side buffering.
The STT WebSocket stays open for the entire conversation session:
# Connected once at session start
await stt.connect()
# Audio streams continuously (no reconnect overhead per utterance)
async for chunk in capture.stream():
await stt.send_audio(chunk)
Impact: Eliminates ~100-200ms WebSocket handshake per utterance. In a multi-turn conversation, this saves significant cumulative latency.
Instead of using fixed silence duration to detect speech end, the SDK relies on Sarvam’s server-side VAD:
stt_vad_sensitivity: bool = True # High sensitivity for conversational speech
stt_vad_signals: bool = True # Receive speech_start/speech_end events
Impact: Faster and more accurate endpoint detection than client-side silence counting. Server VAD uses model-aware signal processing.
max_tokens for VoiceVoice responses should be short and conversational:
llm_max_tokens: int = 200 # ~50-100 words
Impact: Model generates faster (fewer tokens = less time), and responses are naturally suited for spoken output. A 200-token response takes ~2-3 seconds to generate vs ~10+ seconds for 1000 tokens.
The SDK uses httpx.AsyncClient for LLM and TTS HTTP requests, which supports connection pooling:
async with httpx.AsyncClient(timeout=30.0) as client:
async with client.stream("POST", url, ...) as resp:
...
Impact: TCP connection reuse eliminates ~50-100ms of TLS handshake on subsequent requests within the same session.
When the user interrupts, the SDK takes immediate action:
Impact: The user’s new utterance is processed without waiting for the old response to finish. Perceived responsiveness matches natural conversation.
To measure end-to-end latency in your deployment:
import time
config = SarvamS2SConfig(...)
# Track pipeline timing
async with SarvamS2S(config) as s2s:
t_speech_end = None
t_first_audio = None
def on_transcript(text):
nonlocal t_speech_end
t_speech_end = time.monotonic()
def on_first_audio():
nonlocal t_first_audio
t_first_audio = time.monotonic()
if t_speech_end:
print(f"Latency: {(t_first_audio - t_speech_end)*1000:.0f}ms")
s2s.on_transcript(on_transcript)
# ... hook into player for first audio timing
| Setting | Lower Latency | Higher Quality |
|---|---|---|
reasoning_effort |
null |
"medium" or omit |
llm_max_tokens |
100-200 | 500-1000 |
llm_model |
sarvam-30b |
sarvam-105b |
tts_min_buffer_size |
20-50 | 100-200 |
| Phrase splitting | Aggressive (commas) | Conservative (periods only) |
For voice assistants, optimize for latency. For read-aloud or non-interactive use cases, optimize for quality.