GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-03-29Advanced

Gemini Live Translation API — Building Real-Time Multilingual Voice Apps

A practical guide to building real-time multilingual voice translation applications using Gemini's Live Translation and Speech-to-Speech APIs

Live TranslationVoice APIMultilingual3Real-Time TranslationSpeech-to-Speech2Gemini API192

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:

  1. Low-Latency Architecture: WebSocket streaming with sub-200ms delays
  2. Speaker Recognition: Accurate identification across diverse voices
  3. Context Preservation: Terminology and domain-specific accuracy
  4. Error Resilience: Graceful handling of network failures
  5. 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.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API / SDK2026-06-19
Generate Japanese and English in One Structured Call to Stop Term Drift
Generating Japanese and English versions separately makes terminology drift article by article. Pair both languages in one Gemini 3.5 Flash structured-output call, pin a glossary, and detect drift mechanically — with measured results.
API / SDK2026-03-22
Automating Screenshot Localization with the Gemini API
Learn how to leverage Gemini API's multimodal capabilities to automatically localize app store screenshots across multiple languages
API / SDK2026-07-19
Still image or short clip? Deciding feature placement from the cost gap between Nano Banana 2 Lite and Omni Flash
When I froze over whether a wallpaper app's hero asset should be a still image or a short moving loop, the deciding factor was not taste but the order of magnitude of the cost. Here is how to normalize Nano Banana 2 Lite and Omni Flash onto the same footing, down to a working decision function.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →