Skip to content
anecho.ai

audio_stream_end=True não encerra um turno na Gemini Live API

Medido no Vertex contra o gemini-live-2.5-flash-native-audio com um único clipe de 5 segundos: áudio sozinho retorna 0 bytes, áudio mais audio_stream_end=True retorna 0 bytes, e áudio mais 1.5 segundos de silêncio ao final retorna 229,994 bytes. O servidor encerra um turno quando a própria detecção de atividade de voz (VAD) dele ouve silêncio, então o seu uplink precisa carregar uma pausa como amostras reais, e nunca como chunks ausentes. A própria documentação do Google diz duas coisas diferentes sobre esse campo, e os docs do Vertex nem o mencionam.

Anecho Engineering16 de ago. de 202611 min de leitura
gemini-livevoice-agentsvadturn-takingstreaming

Se você está construindo um agente de voz sobre a Gemini Live API e ele conecta, transmite áudio, mostra uma sessão saudável e nunca responde, este post é as duas horas que você está prestes a gastar.

Primeiro a medição. Um clipe de 5 segundos do LibriSpeech, enviado de três maneiras para o mesmo endpoint do Vertex, mesmo modelo, mesma configuração de sessão. A única coisa que varia é como o cliente sinaliza que quem liga parou de falar:

O que o cliente enviouBytes retornados
Apenas áudio, nenhum sinal de fim0
Áudio, depois send_realtime_input(audio_stream_end=True)0
Áudio, depois 1.5 s de silêncio ao final como amostras reais229,994

229,994 bytes de mono de 16 bits a 24 kHz são cerca de 4.8 segundos de resposta. A linha do meio é a que merece ser encarada. O campo se chama audio_stream_end, você o define como True, o socket permanece aberto, nenhum erro volta — e o modelo não responde. Nunca. Nós esperamos.

O servidor encerra um turno quando o próprio detector de atividade de voz dele ouve silêncio. Esse é o único mecanismo. Então o silêncio precisa chegar até ele, como amostras, em taxa de tempo real. Um cliente que simplesmente para de enviar não é um cliente que terminou de falar; é um cliente indistinguível de um cliente morto.

O que a documentação diz, que são duas coisas diferentes

Fomos procurar isso nos docs depois de medir, e os docs estão genuinamente divididos contra si mesmos. As três citações abaixo vêm da própria referência e do próprio guia publicados pelo Google.

A referência normativa do campo, em BidiGenerateContentRealtimeInput, descreve um estado do stream, não um turno:

"Indica que o stream de áudio terminou, por exemplo porque o microfone foi desligado. Isso só deve ser enviado quando a detecção automática de atividade está habilitada (o que é o default). O cliente pode reabrir o stream enviando uma mensagem de áudio."

O guia da Live API, na sua seção de VAD automático, diz a mesma coisa em termos operacionais — flush, não fim:

"Quando o stream de áudio fica pausado por mais de um segundo (por exemplo, porque o usuário desligou o microfone), um evento audioStreamEnd deve ser enviado para descarregar qualquer áudio em cache. O cliente pode retomar o envio de dados de áudio a qualquer momento."

E o mesmo guia, na sua seção de VAD híbrido, diz algo bem diferente:

"O servidor trata o sinal audio_stream_end como um comando de finalização imediata, contornando o atraso padrão de detecção de silêncio do lado do servidor e retornando a transcrição e a resposta do modelo com latência mínima."

As duas primeiras não podem ser, junto com a terceira, o comportamento geral de um único campo, e a terceira é a única frase em qualquer lugar que diz que ele produz uma resposta. Enquanto isso, o próprio tipo do SDK para a mensagem que o contém declara o contrato real do modelo:

"O fim do turno não é especificado explicitamente; em vez disso, é derivado da atividade do usuário (por exemplo, fim de fala)."

Há mais uma coisa que achamos ser a explicação real de por que isso é tão fácil de errar. A documentação do Vertex não menciona audioStreamEnd em lugar nenhum. Não conseguimos encontrar o campo em uma única página da Live API do Vertex — e o Vertex é onde vive o gemini-live-2.5-flash-native-audio, em disponibilidade geral, como o modelo recomendado para agentes de voz de baixa latência. Então, na plataforma em que medimos, o campo não está documentado como fazendo coisa alguma, e o parágrafo que diz que ele finaliza um turno está no guia do outro produto.

Não estamos afirmando que os docs do Google estão errados. Estamos afirmando que no Vertex, com o VAD automático nos seus defaults, neste modelo, nesta data, o sinal não produziu nada e 1.5 segundos de silêncio produziram uma resposta completa — e que, se você ler apenas a frase sobre finalização imediata, vai construir algo que trava.

Não somos os primeiros a bater nisso. A issue #1328 do python-genai reporta exatamente isso, passo a passo: envie audioStreamEnd quando o microfone desliga e então "observe que o modelo não encerra o turno do usuário nem reage, mesmo depois de esperar mais de um minuto." Essa issue foi fechada por um bot de inatividade em janeiro, sem correção e sem confirmação, então trate-a como corroboração de outro desenvolvedor, não como uma declaração oficial — e é precisamente por isso que estamos publicando uma medição em vez de um link.

O invariante que conserta isso

Escrevemos isto no serviço como um contrato de uma frase, porque todo o resto do arquivo está rio abaixo dele:

Uma vez que a chamada está ao vivo, um stream contínuo de PCM16 a 16 kHz chega à API em taxa de tempo real, e uma pausa é carregada como amostras de silêncio reais — nunca como chunks ausentes.

Isso não é uma preferência de estilo. É o protocolo, reescrito como algo que você pode testar. Cada lugar do seu código que pode se recusar a encaminhar um chunk é um lugar que pode travar a chamada para sempre com o socket ainda aberto, e os dois lugares que nós tínhamos eram bugs.

A segunda consequência é a que pega clientes de navegador, e ela não é óbvia: o seu cliente vai parar de enviar áudio durante uma pausa, queira você ou não.

  • O Chrome entrega a um AudioWorkletProcessor um array de entrada vazio sempre que o barramento rio acima é marcado como silencioso. Não um buffer de zeros — um array vazio. Código ingênuo encaminha nada.
  • Uma track mutada, ou uma troca de dispositivo, interrompe os callbacks por completo.

Então "o usuário ficou quieto" e "o microfone parou de produzir callbacks" chegam ao seu servidor como o mesmo evento, e a única coisa de que o VAD do lado do servidor precisa para responder é exatamente a coisa que o navegador decidiu não te dar.

A nossa correção é um keep-alive que sintetiza o silêncio que o cliente deveria ter enviado e o empurra pelo caminho idêntico — os mesmos reamostradores, o mesmo modelo — para que o pipeline permaneça coerente e o stream carregue amostras de silêncio verdadeiras, em vez de um buraco. Dois detalhes importam mais do que a ideia:

  • Acione-o pela chegada de dados do cliente, nunca pelo seu próprio uplink. O nosso uplink é legitimamente bursty, porque o reamostrador de stream emite em levas de aproximadamente 95 ms. Um keep-alive que observasse a própria saída leria cada intervalo entre rajadas como um cliente travado e injetaria silêncio no meio de fala ao vivo. O nosso dispara depois de 150 ms sem nada vindo do navegador, o que dá cerca de dezenove quanta de 128 amostras perdidos: um travamento, não jitter.
  • Limite o preenchimento por tick. Um travamento longo é reparado de forma constante, em vez de como um único blob enorme e atrasado que chega fora de proporção com a conversa.

E reporte isso. A quantidade de silêncio sintetizado por chamada é um número no nosso payload de saúde, não um reparo escondido, porque uma sessão que é 40% silêncio sintético está te dizendo algo sobre o cliente que você quer saber.

O interlock que ninguém espera

A coalescência veio em seguida, por uma razão diferente — dois braços de um A/B estavam sendo empacotados de formas diferentes, 10.6 contra 125.0 frames por segundo — e os dois braços agora se montam em frames fixos de 100 ms antes de serem enviados. A orientação do próprio Google é "envie chunks pequenos (entre 20 ms e 40 ms) para minimizar a latência", e 125 frames por segundo está muito fora disso na outra direção.

Mas um frame parcialmente montado é áudio que não foi enviado. Então a coalescência só é segura por causa do keep-alive: sem um produtor atrás do buffer, o fim da última frase de quem liga fica parado em um frame pela metade exatamente no momento em que o servidor precisa dele para detectar que quem liga parou. O keep-alive garante que o buffer sempre tem um produtor, então um frame parcial é sempre descarregado pelo silêncio que vem depois dele.

Duas mudanças de aparência independente que não são separáveis. Se você adotar a coalescência de frames sem o keep-alive, construiu uma versão mais sutil do mesmo travamento.

Torne as configurações de detecção de turnos explícitas

A última coisa que mudamos não foi tanto código quanto visibilidade. Este serviço deixava cada parâmetro de VAD implícito, e foi assim que "o agente nunca responde" conseguiu parecer um mistério em vez de uma configuração.

A superfície documentada, para referência:

CampoO que os docs dizem
automaticActivityDetection.disabledO default é false — o VAD do lado do servidor está ligado
silenceDurationMs"O default interno do servidor é de aproximadamente 800ms"; o guia recomenda 500–800 ms
prefixPaddingMsNenhum default documentado que tenhamos conseguido encontrar
startOfSpeechSensitivity / endOfSpeechSensitivityO SDK declara defaults diferentes por plataforma — LOW no branch enterprise, HIGH no branch da Gemini API
activityStart / activityEnd"só podem ser enviados se a detecção automática de atividade (isto é, do lado do servidor) estiver desabilitada"

Duas armadilhas nessa tabela. Primeiro, o exemplo de configuração do próprio guia usa prefix_padding_ms: 20 e silence_duration_ms: 100 — valores ilustrativos, e 100 ms fica dentro da faixa que a mesma página chama de "baixa demais", em que "o sistema encerra turnos de fala durante pausas naturais, dividindo um único enunciado em vários fragmentos pequenos de áudio." Não copie o exemplo para produção.

Segundo, detecção manual de atividade é um mundo diferente, não um complemento. Com disabled: true, você mesmo envia activityStart e activityEnd, e — segundo o guia — "um audioStreamEnd não é enviado nessa configuração." Se o VAD é seu, o limite do turno é seu, e o problema do silêncio desaparece; você simplesmente comprou a metade mais difícil do problema, que é detectar turnos em uma linha ruidosa.

A nossa própria escolha é expor os cinco como variáveis de ambiente e parâmetros de query por chamada, e deixá-los não definidos por padrão, para que o endpoint de status reporte "vertex default" em vez de um número que o serviço inventou. As duas configurações que medimos funcionando foram medidas nos defaults do Vertex, e movê-las em silêncio mudaria um resultado que agora está provado.

O resto do contrato de áudio, já que você vai precisar dele de qualquer jeito

Da documentação, e vale a pena acertar tudo de uma vez:

  • Entrada: PCM bruto de 16 bits, little-endian. "O áudio de entrada é nativamente 16kHz, mas a Live API reamostra se necessário, então qualquer taxa de amostragem pode ser enviada." Declare a taxa no tipo MIME de cada blob, por exemplo audio/pcm;rate=16000. A página de troubleshooting do Vertex acrescenta a contagem de canais: um único canal mono.
  • Saída: PCM bruto de 16 bits, little-endian, sempre 24 kHz. Note a assimetria — você envia 16 kHz, você recebe 24 kHz. Qualquer mixagem ou gravação rio abaixo tem que reamostrar um dos dois.
  • Chunking: 20 a 40 ms; não acumule em buffer por volta de um segundo antes de enviar.
  • Duração da sessão: sem compressão de janela de contexto, sessões só de áudio estão documentadas em 15 minutos e as de áudio e vídeo em 2. Separadamente, "o tempo de vida de uma conexão é limitado a cerca de 10 minutos devido a restrições de conexão WebSocket", com um aviso goAway enviado 60 segundos antes do fim. São dois limites diferentes, e o mais curto é o da conexão, então reconectar é parte normal de uma chamada longa, não um caminho de erro. Nós o tratamos assim: fila de microfone limitada e com descarte do mais antigo através da lacuna, para que uma reconexão não "se recupere" reproduzindo um backlog crescente e respondendo a perguntas de dez segundos atrás.

Uma ressalva sobre onde esses docs vivem: as páginas da Live API do Vertex foram rebatizadas e movidas, e o caminho antigo cloud.google.com/vertex-ai/generative-ai/docs/live-api agora redireciona. Se um link nas suas anotações está morto, é por isso.

Ressalvas

  • Isto é n = 1 no estímulo. Um clipe de 5 segundos, três condições, um modelo (gemini-live-2.5-flash-native-audio), uma região, uma data. É uma demonstração limpa e reproduzível de um comportamento, não um levantamento. Não reportaríamos a contagem de bytes como se fosse um benchmark.
  • O resultado 0 / 0 / 229,994 é uma demonstração de que o silêncio funciona e a flag não funcionou, naquela configuração. Ele não estabelece que o audio_stream_end nunca faz nada, em nenhum modelo, em nenhuma configuração. Se você conseguir mostrá-lo finalizando um turno no Vertex, queremos ver a configuração.
  • O comportamento foi medido no Vertex. O branch da Gemini API documenta defaults de sensibilidade diferentes, e o parágrafo de VAD híbrido que descreve a finalização está no guia desse branch.
  • A issue da comunidade é corroboração, não confirmação. Ela foi fechada por automação, sem resolução.
  • Não estamos medindo qualidade aqui. Nada neste post diz coisa alguma sobre quão bem o modelo ouviu quem ligou. Esse é um experimento separado, e o caminho de áudio na frente dele é aquilo em que realmente trabalhamos.

A versão curta

  1. O próprio VAD do servidor é a única coisa que encerra um turno. Projete para isso e para nada mais.
  2. Carregue pausas como amostras. Se o seu cliente fica quieto, sintetize o silêncio e envie-o pelo mesmo caminho do áudio real.
  3. Nunca acione um keep-alive pelo seu próprio uplink — um caminho de transmissão bursty vai parecer um cliente travado e retalhar fala ao vivo.
  4. Coalesça para um frame fixo, mas só quando algo garantir que o buffer tem um produtor. As duas mudanças são uma mudança só.
  5. Publique uma razão de áudio entregue por sessão. Segundos de uplink sobre segundos de relógio de parede tem um valor correto conhecido perto de 1.0, o que torna um valor errado legível. A nossa marcou 0.08 por dias enquanto a UI alegremente reportava uma sessão ao vivo, e isso é um post próprio.
  6. Não copie os valores de exemplo do VAD. Deixe-os não definidos, reporte o que está em vigor e mude-os deliberadamente.
A seguir
  • 24 de out. de 2026

    Alternativas à ai-coustics para agentes de voz e call centers (2026)

    Por que equipes olham além do ai-coustics Quail Voice Focus, e uma lista curta com fontes conforme listado em outubro de 2026: Krisp VIVA, AssemblyAI Voice Focus, isolamento de voz do LiveKit Cloud, NVIDIA Speaker Focus, Anecho e referências de código aberto. Comparamos a ai-coustics com o nosso próprio modelo em conversas ao vivo: empate com o faster-whisper, vantagem do v8_2 com o Gemini Live, menos barge-ins falsos para a ai-coustics, e a condição decide quem lidera.

  • 22 de out. de 2026

    Alternativas à Krisp para agentes de voz e call centers (2026)

    Por que equipes olham além do Krisp VIVA, e uma lista curta com fontes conforme listado em outubro de 2026: ai-coustics Quail Voice Focus, AssemblyAI Voice Focus, isolamento de voz do LiveKit Cloud, NVIDIA Speaker Focus, Anecho e referências de código aberto, com onde cada um roda, seu preço público e se o medimos. Não medimos a Krisp.

  • 20 de out. de 2026

    SDKs de isolamento de locutor comparados (2026): Krisp, ai-coustics, AssemblyAI, LiveKit e Anecho

    Uma comparação com fontes do isolamento de locutor para agentes de voz e call centers conforme listado em outubro de 2026: onde cada produto roda, latência declarada, taxas de amostragem, integração, preço público e licenciamento. Krisp VIVA e Anecho declaram 15 ms em CPU; a ai-coustics lista menos de 30 ms; AssemblyAI e LiveKit vendem isolamento dentro de um serviço em nuvem. Números de qualidade apenas do nosso próprio benchmark.