Alternar tema

Como criar um assistente de IA de áudio e vídeo com a Gemini Live API

Easton editorial illustration: one live assistant core connected to audio and video stream cards

A Gemini Live API aceita entrada e saída de áudio nativamente. Sua arquitetura de ponta a ponta reduz a latência para menos de 500 ms, sem precisar usar ASR/TTS como intermediários. A seguir, mostro como usar essa API para criar um assistente de IA capaz de manter uma conversa realmente em tempo real.

O que é a Gemini Multimodal Live API?

Primeiro, vale esclarecer a diferença. A API tradicional do Gemini funciona de forma simples: você envia um texto e recebe outro texto. Para adicionar interação por voz, porém, é necessário integrar ASR (reconhecimento de fala) e TTS (síntese de voz). Cada etapa intermediária aumenta a latência.

A Gemini Multimodal Live API segue outra abordagem: ela oferece entrada e saída de áudio nativas. O áudio capturado pelo microfone pode ser enviado diretamente, e a resposta também chega como um fluxo de áudio. Não é necessário converter formatos no meio do caminho. Essa arquitetura de ponta a ponta reduz a latência para menos de 500 ms.

Testei esse recurso em um projeto de casa inteligente. Quando o usuário dizia “diminua um pouco a luz da sala”, a IA respondia praticamente assim que ele terminava de falar. A conversa ficava tão fluida que era fácil esquecer que havia um programa do outro lado.

O modelo compatível no momento é o gemini-2.0-flash-native-audio-preview. Preste atenção nessa versão: o Google ainda está fazendo atualizações rápidas, então convém acompanhar as mudanças periodicamente.

Arquitetura e escolha das tecnologias

Agora podemos montar o sistema. Recomendo uma arquitetura separada entre frontend e backend por um motivo simples: a chave da API não pode ficar exposta no frontend.

O fluxo de dados fica assim:

[Navegador] --WebSocket--> [Proxy backend em Python] --WebSocket--> [Gemini Live API]
     |                              |                              |
Captura do microfone       Encaminhamento + lógica             Processamento de IA
Reprodução no alto-falante VAD/controle de interrupção        Geração de áudio

Talvez você se pergunte por que o navegador não pode se conectar diretamente ao Gemini. Tecnicamente, pode. O problema é que a chave da API teria de ficar no JavaScript, e qualquer pessoa poderia abri-la nas ferramentas de desenvolvedor. Já cometi esse erro uma vez; no dia seguinte, a conta disparou. Foi uma lição cara.

Por isso, escolhi a seguinte stack:

CamadaTecnologiaFinalidade
FrontendJavaScript nativo + Web Audio APICaptura e reprodução de áudio, além de processamento em tempo real com AudioWorklet
BackendPython 3.9+ + biblioteca websocketsProxy WebSocket, detecção VAD e gerenciamento de sessão
ProtocoloWebSocket + JSONComunicação bidirecional com o Gemini

O AudioWorklet da Web Audio API é especialmente útil porque processa o áudio em uma thread separada, sem bloquear a thread principal. Mais adiante, veremos a implementação.

Conexão WebSocket e gerenciamento de sessão

Vamos ao código. O primeiro problema a resolver é a conexão com o serviço do Gemini.

O endpoint WebSocket da Live API tem este formato:

wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent?key=YOUR_API_KEY

Observe o v1alpha: ele indica que a interface ainda está em versão preview e pode mudar. Considere esse risco antes de usá-la em produção.

Depois de estabelecer a conexão, a primeira ação é enviar uma mensagem Setup para informar ao Gemini como a conversa deve funcionar:

import asyncio
import json
import websockets

GEMINI_API_KEY = "your-api-key-here"
GEMINI_WS_URL = (
    f"wss://generativelanguage.googleapis.com/ws/"
    f"google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent"
    f"?key={GEMINI_API_KEY}"
)

CONFIG = {
    "setup": {
        "model": "models/gemini-2.0-flash-native-audio-preview",
        "generation_config": {
            "response_modalities": ["AUDIO"],
            "speech_config": {
                "voice_config": {
                    "prebuilt_voice_config": {
                        "voice_name": "Charon"  # Opções: Charon, Aoede etc.
                    }
                }
            }
        },
        "system_instruction": {
            "parts": [{"text": "Você é um assistente de IA útil. Responda de forma breve e natural."}]
        }
    }
}

async def connect():
    async with websockets.connect(GEMINI_WS_URL) as ws:
        # Envia a configuração de setup
        await ws.send(json.dumps(CONFIG))

        # Aguarda a resposta setup complete
        response = await ws.recv()
        data = json.loads(response)

        if "setupComplete" in data:
            print("✅ Conexão estabelecida. A conversa pode começar")
            return ws
        else:
            raise Exception(f"Falha no setup: {data}")

Alguns parâmetros merecem atenção:

  • response_modalities: use ["AUDIO"] quando quiser receber apenas respostas por voz. Para receber texto também, altere para ["AUDIO", "TEXT"].
  • voice_name: o Gemini oferece algumas vozes predefinidas. Eu prefiro Charon, que tem um tom mais sóbrio.

Para reconectar depois de uma queda, recomendo o backoff exponencial. Não faça várias tentativas em sequência logo de início, pois isso pode sobrecarregar o serviço:

async def connect_with_retry(max_retries=5):
    for attempt in range(max_retries):
        try:
            return await connect()
        except Exception as e:
            wait_time = min(2 ** attempt, 30)  # Aguarda no máximo 30 segundos
            print(f"Falha na conexão ({e}). Nova tentativa em {wait_time} segundos...")
            await asyncio.sleep(wait_time)
    raise Exception("Não foi possível conectar após várias tentativas")

Captura e transmissão de áudio PCM de 16 kHz

Com a conexão pronta, precisamos capturar o áudio e enviá-lo.

Primeiro, por que escolher 16 kHz? A faixa de frequência da voz humana costuma ficar entre 85 Hz e 255 Hz, com frequências mais baixas para vozes masculinas e mais altas para vozes femininas. Pelo teorema de amostragem de Nyquist, 8 kHz seriam suficientes em teoria. Na prática, 16 kHz preservam mais detalhes e oferecem um bom equilíbrio entre qualidade e volume de dados. Essa também é a taxa recomendada pelo Gemini.

O código de captura no frontend fica assim:

class AudioRecorder {
  constructor() {
    this.sampleRate = 16000;
    this.bufferSize = 1024;
    this.audioContext = null;
    this.workletNode = null;
    this.stream = null;
    this.onAudioData = null; // Função de callback
  }

  async start() {
    // Solicita permissão para usar o microfone
    this.stream = await navigator.mediaDevices.getUserMedia({
      audio: {
        sampleRate: 16000,
        channelCount: 1,
        echoCancellation: true,
        noiseSuppression: true
      }
    });

    // Cria o AudioContext com uma taxa de amostragem definida
    this.audioContext = new AudioContext({
      sampleRate: 16000
    });

    // Carrega o processador AudioWorklet
    await this.audioContext.audioWorklet.addModule('pcm-processor.js');

    const source = this.audioContext.createMediaStreamSource(this.stream);
    this.workletNode = new AudioWorkletNode(this.audioContext, 'pcm-processor');

    // Processa os dados de áudio
    this.workletNode.port.onmessage = (event) => {
      const float32Data = event.data;

      // Converte para PCM Int16
      const int16Data = this.float32ToInt16(float32Data);

      // Codifica em Base64 antes de enviar
      const base64Data = btoa(String.fromCharCode(...new Uint8Array(int16Data.buffer)));

      if (this.onAudioData) {
        this.onAudioData(base64Data);
      }
    };

    source.connect(this.workletNode);
    console.log('🎤 Captura de áudio iniciada');
  }

  float32ToInt16(float32Array) {
    const int16Array = new Int16Array(float32Array.length);
    for (let i = 0; i < float32Array.length; i++) {
      // Float32 (-1.0 ~ 1.0) -> Int16 (-32768 ~ 32767)
      const s = Math.max(-1, Math.min(1, float32Array[i]));
      int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
    }
    return int16Array;
  }

  stop() {
    if (this.workletNode) {
      this.workletNode.disconnect();
    }
    if (this.audioContext) {
      this.audioContext.close();
    }
    if (this.stream) {
      this.stream.getTracks().forEach(track => track.stop());
    }
    console.log('🛑 Captura de áudio encerrada');
  }
}

O AudioWorklet precisa de um arquivo separado chamado pcm-processor.js:

// pcm-processor.js
class PCMProcessor extends AudioWorkletProcessor {
  process(inputs, outputs, parameters) {
    const input = inputs[0];
    if (input && input[0]) {
      // Envia para a thread principal
      this.port.postMessage(input[0].slice());
    }
    return true; // Mantém o processador ativo
  }
}

registerProcessor('pcm-processor', PCMProcessor);

Depois de receber os dados, o backend precisa encaminhá-los ao Gemini:

async def send_audio(ws, base64_pcm_data):
    """Envia os dados de áudio para o Gemini"""
    message = {
        "realtime_input": {
            "media_chunks": [{
                "mime_type": "audio/pcm;rate=16000",
                "data": base64_pcm_data
            }]
        }
    }
    await ws.send(json.dumps(message))

Há uma armadilha aqui: alguns navegadores ignoram o sampleRate especificado em getUserMedia e podem retornar áudio em 44,1 kHz ou 48 kHz. Por segurança, faça uma nova amostragem no AudioContext ou use uma biblioteca como audiobuffer-to-wav.

Implementação da detecção de atividade de voz com VAD

Agora surge outro problema. Se todo o áudio for enviado ao Gemini sem nenhum filtro, até os períodos de silêncio consumirão dados, largura de banda e dinheiro. É aí que entra o VAD (Voice Activity Detection, ou detecção de atividade de voz).

Sua função é simples: determinar se há alguém falando em um trecho de áudio. Quando há voz, o trecho é enviado; quando não há, o sistema espera.

Recomendo o WebRTC VAD, projeto de código aberto do Google. Ele é leve, rápido e produz bons resultados. O Python oferece o pacote webrtcvad:

import webrtcvad
import collections
import numpy as np

class VADProcessor:
    def __init__(self, aggressiveness=2, frame_duration_ms=20):
        """
        aggressiveness: 0-3; quanto maior, mais rigorosa é a detecção
                        (maior a chance de classificar voz como silêncio)
        frame_duration_ms: 10, 20 ou 30
        """
        self.vad = webrtcvad.Vad(aggressiveness)
        self.frame_duration_ms = frame_duration_ms
        self.sample_rate = 16000

        # Buffer circular usado para suavização
        self.ring_buffer = collections.deque(maxlen=30)  # 600 ms
        self.triggered = False

    def process_frame(self, pcm_bytes):
        """
        Processa um quadro de áudio e informa se ele deve ser enviado
        """
        is_speech = self.vad.is_speech(pcm_bytes, self.sample_rate)

        if not self.triggered:
            # Estado não acionado: acumula quadros com voz
            self.ring_buffer.append((pcm_bytes, is_speech))
            num_voiced = sum(1 for _, speech in self.ring_buffer if speech)

            # Aciona se 90% dos quadros contiverem voz
            if num_voiced > 0.9 * self.ring_buffer.maxlen:
                self.triggered = True
                # Envia também os dados acumulados no buffer
                return b''.join([f for f, _ in self.ring_buffer])
            return None
        else:
            # Estado acionado
            if is_speech:
                self.ring_buffer.append((pcm_bytes, True))
                return pcm_bytes
            else:
                self.ring_buffer.append((pcm_bytes, False))
                num_unvoiced = sum(1 for _, speech in self.ring_buffer if not speech)

                # Encerra o acionamento se 90% dos quadros forem silêncio
                if num_unvoiced > 0.9 * self.ring_buffer.maxlen:
                    self.triggered = False
                    self.ring_buffer.clear()
                return pcm_bytes

O uso fica aproximadamente assim:

vad = VADProcessor(aggressiveness=2)

async def handle_client_audio(websocket, gemini_ws):
    async for message in websocket:
        data = json.loads(message)

        if 'audio' in data:
            pcm_bytes = base64.b64decode(data['audio'])

            # Detecção VAD
            result = vad.process_frame(pcm_bytes)

            if result:
                # Há alguém falando; encaminha para o Gemini
                await send_audio(gemini_ws, base64.b64encode(result).decode())

O parâmetro aggressiveness exige algum ajuste. Se o valor for muito baixo, qualquer ruído de fundo poderá ser tratado como voz. Se for muito alto, falas em volume baixo poderão passar despercebidas. Minha prática é começar com 2 e ajustar de acordo com o ambiente real.

Caso o ambiente de implantação não permita instalar o webrtcvad, uma detecção simples por limiar de energia pode servir como alternativa:

// Alternativa no frontend: detecção simples com energia RMS
function detectVoiceActivity(audioData, threshold = 0.015) {
    const sum = audioData.reduce((acc, val) => acc + val * val, 0);
    const rms = Math.sqrt(sum / audioData.length);
    return rms > threshold;
}

Como implementar interrupção natural por fala (barge-in)

Alguns assistentes de voz não permitem interrupções: depois que começam uma resposta longa, só resta esperar. O recurso barge-in resolve esse problema. Enquanto a IA fala, o usuário pode começar a falar e interromper a saída imediatamente, fazendo o sistema voltar a escutá-lo.

A boa notícia é que a Gemini Live API oferece suporte nativo ao recurso. Basta ativar a detecção automática de atividade na configuração:

CONFIG = {
    "setup": {
        "model": "models/gemini-2.0-flash-native-audio-preview",
        "generation_config": {
            "response_modalities": ["AUDIO"],
        },
        "realtime_input_config": {
            "automatic_activity_detection": {
                "disabled": False,
                "start_of_speech_sensitivity": "START_SENSITIVITY_HIGH",
                "end_of_speech_sensitivity": "END_SENSITIVITY_LOW"
            }
        }
    }
}

A configuração de sensitivity merece atenção:

  • Definir start_of_speech_sensitivity como HIGH torna a IA mais sensível ao início da fala do usuário, facilitando a interrupção.
  • Definir end_of_speech_sensitivity como LOW faz a IA esperar um pouco mais para confirmar que o usuário realmente terminou, evitando falsos positivos.

No cliente, basta escutar o evento interrupted e parar a reprodução imediatamente:

class GeminiClient {
  constructor() {
    this.audioQueue = [];
    this.isPlaying = false;
    this.currentSource = null;
  }

  async handleMessage(event) {
    const message = JSON.parse(event.data);

    // Trata o sinal de interrupção
    if (message.server_content?.interrupted) {
      console.log('⚡ Interrupção do usuário: reprodução encerrada');
      this.stopPlayback();
      return;
    }

    // Trata o áudio retornado pela IA
    if (message.server_content?.model_turn) {
      const parts = message.server_content.model_turn.parts;

      for (const part of parts) {
        if (part.inline_data?.mime_type.startsWith('audio/')) {
          const audioData = base64ToArrayBuffer(part.inline_data.data);
          this.queueAudio(audioData);
        }
      }
    }
  }

  stopPlayback() {
    // Esvazia a fila de reprodução
    this.audioQueue = [];
    this.isPlaying = false;

    // Interrompe o áudio em reprodução
    if (this.currentSource) {
      try {
        this.currentSource.stop();
      } catch (e) {
        // O áudio pode já ter terminado
      }
      this.currentSource = null;
    }
  }

  async queueAudio(audioData) {
    this.audioQueue.push(audioData);
    if (!this.isPlaying) {
      this.playNext();
    }
  }

  async playNext() {
    if (this.audioQueue.length === 0) {
      this.isPlaying = false;
      return;
    }

    this.isPlaying = true;
    const audioData = this.audioQueue.shift();

    // Decodifica e reproduz
    const audioBuffer = await this.audioContext.decodeAudioData(audioData.slice());
    this.currentSource = this.audioContext.createBufferSource();
    this.currentSource.buffer = audioBuffer;
    this.currentSource.connect(this.audioContext.destination);

    this.currentSource.onended = () => {
      this.playNext();
    };

    this.currentSource.start();
  }
}

Há um detalhe importante: o método stop() pode lançar uma exceção se o áudio já tiver terminado naturalmente. Por isso, adicionei um bloco try-catch para evitar erros no console.

Otimização de desempenho e controle de latência

Por fim, vamos reduzir ao máximo a latência do sistema.

Primeiro, precisamos entender de onde ela vem:

  1. Transmissão pela rede: o tempo de ida e volta dos pacotes entre o navegador, o servidor e o Gemini.
  2. Codificação e decodificação de áudio: o tempo gasto para comprimir e descomprimir PCM. Como o próprio PCM não tem perdas, esse custo é pequeno.
  3. Acúmulo no buffer: a profundidade do buffer usado para manter a reprodução contínua.

Minha experiência com esses pontos levou às seguintes otimizações.

Reduza a profundidade do buffer

Não use um buffer de reprodução maior do que o necessário. Em geral, trabalho com 100 a 200 ms:

// Configura um buffer menor
const audioContext = new AudioContext({
  sampleRate: 16000,
  latencyHint: 'interactive'  // Modo de baixa latência
});

Taxa de bits adaptativa — na prática, buffer adaptativo

Se houver muita oscilação na rede, aumente um pouco o buffer. Quando a conexão estiver estável, reduza-o.

Cancelamento de eco local

Se o usuário estiver usando alto-falantes em vez de fones de ouvido, o microfone poderá captar a voz da IA e criar um ciclo de áudio. O getUserMedia já oferece cancelamento de eco:

navigator.mediaDevices.getUserMedia({
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true
  }
})

Monitore as métricas

Para verificar se as otimizações funcionaram, registre os tempos com a Performance API:

// Registra métricas de latência
class LatencyMonitor {
  constructor() {
    this.metrics = [];
  }

  recordSendTime() {
    this.lastSendTime = performance.now();
  }

  recordReceiveTime() {
    const latency = performance.now() - this.lastSendTime;
    this.metrics.push(latency);

    // Mantém os 100 registros mais recentes
    if (this.metrics.length > 100) {
      this.metrics.shift();
    }

    // Calcula a latência média
    const avg = this.metrics.reduce((a, b) => a + b, 0) / this.metrics.length;
    console.log(`📊 Latência média: ${avg.toFixed(2)} ms`);
  }
}

Em meu ambiente de teste, obtive aproximadamente estes resultados:

  • Latência de ponta a ponta: 300 a 500 ms, dependendo das condições da rede.
  • Tempo de resposta do primeiro pacote: 200 a 400 ms.
  • Latência em conversa contínua: 150 a 300 ms.

Se a sua latência for muito maior, confira esta lista:

  • A conexão WebSocket está usando HTTPS/WSS? HTTP gera custo adicional.
  • Onde o servidor está implantado? Quanto mais perto dos data centers do Google, melhor.
  • A detecção VAD está acrescentando latência demais? Tente reduzir a duração dos quadros.
  • O buffer de reprodução do frontend está grande demais?

Outra armadilha envolve o contexto de áudio: o Chrome só permite reproduzir som depois de uma interação do usuário. Portanto, adicione à página um botão “Iniciar conversa” em vez de tentar reproduzir áudio automaticamente.

Conclusão

Com isso, concluímos um fluxo completo de desenvolvimento com a Gemini Live API: conceito inicial, arquitetura, conexão WebSocket, captura de áudio, detecção VAD, interrupção por fala e otimização de desempenho. Em cada etapa, procurei destacar também as dificuldades que encontrei na prática.

A interação por voz em tempo real ainda evolui rapidamente, e a própria Gemini Live API continua recebendo atualizações. Mesmo assim, essa arquitetura básica tem se mostrado confiável: meu projeto já a utiliza há vários meses com boa estabilidade.

Se você encontrar algum problema durante a implementação, compartilhe a experiência. Discutir soluções costuma ser mais rápido do que investigar tudo sozinho.

FAQ

Por que é necessário separar frontend e backend?
A chave da API precisa ficar no backend e não pode ser exposta no JavaScript do frontend. Se o navegador se conectar diretamente ao Gemini, qualquer pessoa poderá abrir as ferramentas de desenvolvedor e obter a chave, o que pode causar abuso e uma conta muito alta. O frontend se conecta a um proxy no backend em Python por WebSocket, e o backend encaminha as solicitações para a Gemini Live API.
Por que usar uma taxa de amostragem de 16 kHz?
A faixa de frequência da voz humana fica aproximadamente entre 85 e 255 Hz e, pelo teorema de Nyquist, 8 kHz seriam suficientes em teoria. No entanto, 16 kHz preservam mais detalhes e oferecem um bom equilíbrio entre qualidade e volume de dados. O Gemini também recomenda 16 kHz, o que ajuda a manter a precisão do reconhecimento de voz sem aumentar demais o custo de largura de banda.
Como ajustar o parâmetro aggressiveness do VAD?
O valor de aggressiveness varia de 0 a 3; quanto maior, mais rigorosa é a detecção e maior a chance de classificar voz como silêncio. Comece testando com 2: um valor baixo pode confundir ruído de fundo com voz e aumentar o consumo de largura de banda, enquanto um valor alto pode deixar passar falas em volume baixo. Ajuste de acordo com o nível de ruído do ambiente real.
A interrupção por fala exige desenvolvimento adicional?
A Gemini Live API oferece suporte nativo a barge-in; basta ativar automatic_activity_detection na configuração. O cliente precisa escutar o evento interrupted e interromper a reprodução imediatamente. O ponto principal é tratar corretamente a parada do player, inclusive esvaziando a fila e encerrando o áudio em reprodução.

12 min de leitura · Publicado em: 27 fev 2026 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog