Sub-200 milliseconds is roughly the threshold where translated speech stops feeling like a delay and starts feeling like a conversation. Gemini's Live Translation API reaches it through a WebSocket-based streaming architecture. The patterns below are the ones that hold up once such an app leaves the demo stage and meets real users.
Understanding Gemini Live Translation API
Core Features
Gemini Live Translation API delivers:
Ultra-Low Latency
- WebSocket-based streaming architecture
- Typical latency: 150-200ms
- SIP trunk compatibility for legacy PBX integration
Comprehensive Language Support
- 140+ language pairs
- Support for dialects and colloquial speech
- Context-aware translation
Advanced Audio Processing
- Speaker diarization for multi-speaker sessions
- Background noise suppression
- Speaker-labeled output
Cost Efficiency
- Pay-as-you-go pricing
- Token-based billing for predictable costs
- Built-in caching for duplicate requests
System Architecture
Client-Server Communication Flow
[User (Audio Input)]
↓
[WebRTC/WebSocket]
↓
[Node.js Server]
↓
[Gemini Live API]
↓
[Multilingual Text + TTS]
↓
[User (Audio Output)]
Session Context Management
Long conversations require maintaining context for translation quality.
interface TranslationSession {
sessionId: string;
sourceLanguage: string;
targetLanguage: string;
startTime: Date;
speakers: Map<string, {
name: string;
language: string;
translations: string[];
}>;
contextBuffer: string[];
metadata: {
domain?: "business" | "medical" | "legal" | "general";
terminology?: Record<string, string>;
};
}
class SessionManager {
private sessions: Map<string, TranslationSession> = new Map();
private sessionTTL = 4 * 60 * 60 * 1000; // 4 hours
createSession(
sourceLanguage: string,
targetLanguage: string
): TranslationSession {
const sessionId = `trans_${Date.now()}_${Math.random().toString(36)}`;
const session: TranslationSession = {
sessionId,
sourceLanguage,
targetLanguage,
startTime: new Date(),
speakers: new Map(),
contextBuffer: [],
metadata: { domain: "general" },
};
this.sessions.set(sessionId, session);
setTimeout(
() => this.sessions.delete(sessionId),
this.sessionTTL
);
return session;
}
getSession(sessionId: string): TranslationSession | undefined {
return this.sessions.get(sessionId);
}
updateContext(sessionId: string, text: string): void {
const session = this.sessions.get(sessionId);
if (session) {
session.contextBuffer.push(text);
if (session.contextBuffer.length > 100) {
session.contextBuffer.shift();
}
}
}
}WebSocket-Based Real-Time Streaming
Bidirectional Audio Streaming
import WebSocket from "ws";
import { GoogleGenerativeAI } from "@google/generative-ai";
class LiveTranslationServer {
private wss: WebSocket.Server;
private client: GoogleGenerativeAI;
private sessionManager: SessionManager;
constructor(port: number, apiKey: string) {
this.wss = new WebSocket.Server({ port });
this.client = new GoogleGenerativeAI(apiKey);
this.sessionManager = new SessionManager();
this.setupConnections();
}
private setupConnections(): void {
this.wss.on("connection", (ws: WebSocket) => {
console.log("Client connected");
ws.on("message", (data: Buffer) => {
this.handleMessage(ws, data).catch((error) =>
console.error("Error:", error)
);
});
ws.on("close", () => {
console.log("Client disconnected");
});
ws.on("error", (error) => {
console.error("WebSocket error:", error);
});
});
}
private async handleMessage(
ws: WebSocket,
data: Buffer
): Promise<void> {
try {
const message = JSON.parse(data.toString());
switch (message.type) {
case "init":
await this.handleInit(ws, message);
break;
case "audio_chunk":
await this.handleAudioChunk(ws, message);
break;
case "end_stream":
await this.handleEndStream(ws, message);
break;
default:
ws.send(
JSON.stringify({
type: "error",
error: "Unknown message type",
})
);
}
} catch (error) {
ws.send(
JSON.stringify({
type: "error",
error: (error as Error).message,
})
);
}
}
private async handleInit(
ws: WebSocket,
message: {
sourceLanguage: string;
targetLanguage: string;
domain?: string;
}
): Promise<void> {
const session = this.sessionManager.createSession(
message.sourceLanguage,
message.targetLanguage
);
if (message.domain) {
session.metadata.domain = message.domain;
}
ws.send(
JSON.stringify({
type: "init_response",
sessionId: session.sessionId,
status: "ready",
})
);
}
private async handleAudioChunk(
ws: WebSocket,
message: {
sessionId: string;
audioData: string;
speakerId?: string;
}
): Promise<void> {
const session = this.sessionManager.getSession(
message.sessionId
);
if (!session) {
ws.send(
JSON.stringify({
type: "error",
error: "Session not found",
})
);
return;
}
const audioBuffer = Buffer.from(
message.audioData,
"base64"
);
const model = this.client.getGenerativeModel({
model: "gemini-2.0-flash-exp",
});
const translationRequest = {
contents: [
{
role: "user",
parts: [
{
inlineData: {
mimeType: "audio/wav",
data: audioBuffer.toString("base64"),
},
},
{
text: `Translate the audio from ${session.sourceLanguage} to ${session.targetLanguage}. Include speaker identification if available. Context: ${session.contextBuffer.join(" ")}`,
},
],
},
],
};
const response = await model.generateContent(
translationRequest
);
const translatedText =
response.response.content.parts[0].text || "";
this.sessionManager.updateContext(
message.sessionId,
translatedText
);
ws.send(
JSON.stringify({
type: "translation",
sessionId: message.sessionId,
translatedText,
speakerId: message.speakerId || "unknown",
timestamp: new Date().toISOString(),
})
);
}
private async handleEndStream(
ws: WebSocket,
message: { sessionId: string }
): Promise<void> {
const session = this.sessionManager.getSession(
message.sessionId
);
if (!session) {
ws.send(
JSON.stringify({
type: "error",
error: "Session not found",
})
);
return;
}
ws.send(
JSON.stringify({
type: "stream_ended",
sessionId: message.sessionId,
duration: Date.now() - session.startTime.getTime(),
})
);
}
}Speaker Recognition and Context Management
Speaker Diarization
interface SpeakerProfile {
speakerId: string;
language: string;
voiceCharacteristics: {
pitch: number;
speed: number;
accent?: string;
};
translations: Array<{
original: string;
translated: string;
timestamp: Date;
}>;
}
class SpeakerIdentifier {
private speakers: Map<string, SpeakerProfile> = new Map();
private voicePrints: Map<string, Float32Array> = new Map();
async identifySpeaker(
audioBuffer: Buffer,
context: TranslationSession
): Promise<string> {
const voicePrint = this.extractVoicePrint(
audioBuffer
);
let bestMatch = "unknown";
let bestScore = 0.5;
for (const [speakerId, knownPrint] of this.voicePrints) {
const similarity = this.cosineSimilarity(
voicePrint,
knownPrint
);
if (similarity > bestScore) {
bestScore = similarity;
bestMatch = speakerId;
}
}
if (bestMatch === "unknown") {
bestMatch = `speaker_${context.speakers.size + 1}`;
this.voicePrints.set(bestMatch, voicePrint);
}
return bestMatch;
}
private extractVoicePrint(
audioBuffer: Buffer
): Float32Array {
const samples = audioBuffer.length / 2;
const voiceData = new Float32Array(samples);
for (let i = 0; i < samples; i++) {
const int16 = audioBuffer.readInt16LE(i * 2);
voiceData[i] = int16 / 32768;
}
const rms = Math.sqrt(
voiceData.reduce((sum, v) => sum + v * v, 0) / samples
);
const voicePrint = new Float32Array(128);
voicePrint[0] = rms;
return voicePrint;
}
private cosineSimilarity(
a: Float32Array,
b: Float32Array
): number {
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return (
dotProduct /
(Math.sqrt(normA) * Math.sqrt(normB) + 1e-8)
);
}
}Context and Terminology Management
class ContextManager {
private glossary: Map<string, Map<string, string>> = new Map();
addDomain(
domain: "medical" | "legal" | "business" | "technical",
glossaryTerms: Record<string, Record<string, string>>
): void {
for (const [term, translations] of Object.entries(
glossaryTerms
)) {
if (!this.glossary.has(domain)) {
this.glossary.set(domain, new Map());
}
this.glossary
.get(domain)!
.set(term, JSON.stringify(translations));
}
}
buildContextPrompt(
session: TranslationSession,
currentUtterance: string
): string {
const domainGlossary = this.glossary.get(
session.metadata.domain as any
);
let prompt = `
Translate "${currentUtterance}" from ${session.sourceLanguage} to ${session.targetLanguage}.
Recent context:
${session.contextBuffer.slice(-5).join("\n")}
Domain: ${session.metadata.domain || "general"}
`;
if (domainGlossary) {
prompt += `\nTerminology guide:\n`;
for (const [lang, translation] of Object.entries(
domainGlossary
)) {
prompt += `- ${lang}: ${translation}\n`;
}
}
return prompt;
}
}Text-to-Speech Integration
import * as tts from "@google-cloud/text-to-speech";
class SpeechSynthesizer {
private client: tts.TextToSpeechClient;
private voiceCache: Map<string, Buffer> = new Map();
constructor() {
this.client = new tts.TextToSpeechClient();
}
async synthesize(
text: string,
languageCode: string,
voiceName?: string
): Promise<Buffer> {
const cacheKey = `${text}_${languageCode}`;
if (this.voiceCache.has(cacheKey)) {
return this.voiceCache.get(cacheKey)!;
}
const request = {
input: { text },
voice: {
languageCode,
name: voiceName || this.getDefaultVoice(languageCode),
ssmlGender: "NEUTRAL",
},
audioConfig: {
audioEncoding: "LINEAR16",
sampleRateHertz: 16000,
speakingRate: 1.0,
pitch: 0.0,
},
};
const [response] = await this.client.synthesizeSpeech(
request
);
const audio = response.audioContent;
if (Buffer.isBuffer(audio)) {
this.voiceCache.set(cacheKey, audio);
return audio;
}
throw new Error("Failed to synthesize speech");
}
private getDefaultVoice(languageCode: string): string {
const voices: Record<string, string> = {
"en-US": "en-US-Neural2-A",
"ja-JP": "ja-JP-Neural2-B",
"de-DE": "de-DE-Neural2-A",
"fr-FR": "fr-FR-Neural2-A",
"es-ES": "es-ES-Neural2-A",
"zh-CN": "cmn-CN-Neural2-B",
};
return voices[languageCode] || "en-US-Neural2-A";
}
}React Native Client Implementation
import React, { useState, useCallback } from "react";
import {
View,
Text,
TouchableOpacity,
StyleSheet,
} from "react-native";
import {
AudioRecorder,
AudioUtils,
} from "react-native-audio";
interface TranslationState {
sessionId: string | null;
sourceLanguage: string;
targetLanguage: string;
isRecording: boolean;
transcript: Array<{
speaker: string;
original: string;
translated: string;
}>;
}
const TranslationApp: React.FC = () => {
const [state, setState] = useState<TranslationState>({
sessionId: null,
sourceLanguage: "en",
targetLanguage: "ja",
isRecording: false,
transcript: [],
});
const [ws, setWs] = useState<WebSocket | null>(null);
const connectWebSocket = useCallback(() => {
const websocket = new WebSocket(
"wss://your-translation-server.com"
);
websocket.onopen = () => {
websocket.send(
JSON.stringify({
type: "init",
sourceLanguage: state.sourceLanguage,
targetLanguage: state.targetLanguage,
domain: "business",
})
);
};
websocket.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === "init_response") {
setState((prev) => ({
...prev,
sessionId: message.sessionId,
}));
} else if (message.type === "translation") {
setState((prev) => ({
...prev,
transcript: [
...prev.transcript,
{
speaker: message.speakerId,
original: "",
translated: message.translatedText,
},
],
}));
}
};
setWs(websocket);
}, [state.sourceLanguage, state.targetLanguage]);
const startRecording = useCallback(async () => {
const audioPath = AudioUtils.DocumentDirectoryPath +
"/translation_" +
Date.now() +
".wav";
const audioRecorderOptions = {
SampleRate: 16000,
Channels: 1,
AudioQuality: "High",
AudioEncoding: "wav",
OutputFormat: "wav",
};
await AudioRecorder.prepareRecordingAtPath(
audioPath,
audioRecorderOptions
);
AudioRecorder.onProgress = (data) => {
const audioData = require("fs").readFileSync(
audioPath
);
if (ws && state.sessionId) {
ws.send(
JSON.stringify({
type: "audio_chunk",
sessionId: state.sessionId,
audioData: audioData.toString("base64"),
})
);
}
};
await AudioRecorder.startRecording();
setState((prev) => ({ ...prev, isRecording: true }));
}, [ws, state.sessionId]);
const stopRecording = useCallback(async () => {
await AudioRecorder.stopRecording();
setState((prev) => ({ ...prev, isRecording: false }));
if (ws && state.sessionId) {
ws.send(
JSON.stringify({
type: "end_stream",
sessionId: state.sessionId,
})
);
}
}, [ws, state.sessionId]);
return (
<View style={styles.container}>
<Text style={styles.title}>Real-Time Translation</Text>
<View style={styles.languageSelector}>
<Text>
{state.sourceLanguage} → {state.targetLanguage}
</Text>
</View>
<View style={styles.transcript}>
{state.transcript.map((item, index) => (
<Text key={index} style={styles.transcriptItem}>
[{item.speaker}] {item.translated}
</Text>
))}
</View>
<TouchableOpacity
style={[
styles.button,
state.isRecording && styles.buttonActive,
]}
onPress={
state.isRecording ? stopRecording : startRecording
}
onLongPress={connectWebSocket}
>
<Text style={styles.buttonText}>
{state.isRecording ? "Stop" : "Start"}
</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: "#fff",
},
title: {
fontSize: 24,
fontWeight: "bold",
marginBottom: 20,
},
languageSelector: {
padding: 10,
backgroundColor: "#f0f0f0",
borderRadius: 8,
marginBottom: 20,
},
transcript: {
flex: 1,
marginBottom: 20,
borderWidth: 1,
borderColor: "#ddd",
padding: 10,
borderRadius: 8,
},
transcriptItem: {
marginBottom: 8,
fontSize: 14,
},
button: {
padding: 15,
backgroundColor: "#007AFF",
borderRadius: 8,
alignItems: "center",
},
buttonActive: {
backgroundColor: "#FF3B30",
},
buttonText: {
color: "#fff",
fontSize: 16,
fontWeight: "bold",
},
});
export default TranslationApp;Production Considerations
Error Handling
enum TranslationError {
SESSION_NOT_FOUND = "Session not found",
UNSUPPORTED_LANGUAGE = "Unsupported language pair",
AUDIO_PROCESSING_FAILED = "Audio processing failed",
API_RATE_LIMIT = "API rate limit exceeded",
NETWORK_ERROR = "Network connection lost",
}
class ErrorHandler {
private retryConfig = {
maxAttempts: 3,
backoffMs: 1000,
};
async handleError(
error: TranslationError,
context: TranslationSession
): Promise<void> {
switch (error) {
case TranslationError.NETWORK_ERROR:
await this.reconnectWebSocket(context);
break;
case TranslationError.API_RATE_LIMIT:
await this.exponentialBackoff();
break;
case TranslationError.SESSION_NOT_FOUND:
await this.reinitializeSession(context);
break;
}
}
private async reconnectWebSocket(
context: TranslationSession
): Promise<void> {
for (let attempt = 0; attempt < this.retryConfig.maxAttempts; attempt++) {
try {
break;
} catch (error) {
const delay =
this.retryConfig.backoffMs * Math.pow(2, attempt);
await new Promise((resolve) =>
setTimeout(resolve, delay)
);
}
}
}
private async exponentialBackoff(): Promise<void> {
let delay = 1000;
for (let attempt = 0; attempt < 5; attempt++) {
delay *= 2;
await new Promise((resolve) =>
setTimeout(resolve, delay)
);
}
}
}SaaS Monetization
interface PricingTier {
name: string;
monthlyPrice: number;
minutesIncluded: number;
maxSpeakers: number;
languagePairs: number;
priority: "standard" | "premium" | "enterprise";
}
const PRICING_TIERS: PricingTier[] = [
{
name: "Basic",
monthlyPrice: 9.99,
minutesIncluded: 100,
maxSpeakers: 2,
languagePairs: 5,
priority: "standard",
},
{
name: "Pro",
monthlyPrice: 29.99,
minutesIncluded: 1000,
maxSpeakers: 10,
languagePairs: 50,
priority: "premium",
},
{
name: "Enterprise",
monthlyPrice: 299.99,
minutesIncluded: 10000,
maxSpeakers: 100,
languagePairs: 140,
priority: "enterprise",
},
];
class BillingManager {
async trackUsage(
userId: string,
minutes: number,
tier: PricingTier
): Promise<boolean> {
const usage = await this.getUserUsage(userId);
if (usage.minutesUsed + minutes > tier.minutesIncluded) {
const overage = usage.minutesUsed + minutes - tier.minutesIncluded;
const overageCharge = overage * 0.1;
await this.chargeOverage(userId, overageCharge);
}
return true;
}
private async getUserUsage(
userId: string
): Promise<{ minutesUsed: number }> {
return { minutesUsed: 0 };
}
private async chargeOverage(
userId: string,
amount: number
): Promise<void> {
// Stripe API call for overage charges
}
}Key Success Factors
Building production-grade real-time multilingual voice translation apps requires:
- Low-Latency Architecture: WebSocket streaming with sub-200ms delays
- Speaker Recognition: Accurate identification across diverse voices
- Context Preservation: Terminology and domain-specific accuracy
- Error Resilience: Graceful handling of network failures
- SaaS Monetization: Tiered pricing aligned with usage patterns
These patterns enable you to build scalable, revenue-generating products for global business, customer support, and international collaboration.
A note from the field
Building the Dolice Labs apps as an indie developer, real-time translation was a tug-of-war between latency and naturalness. I emit translations in small confirmed chunks and lightly correct afterward so the conversation never stalls, and I reinforce weaker language pairs with extra context rather than treating every pair the same.