Skip to content
anecho.ai

audio_stream_end=True no termina un turno en la Gemini Live API

Medido en Vertex contra gemini-live-2.5-flash-native-audio con un solo clip de 5 segundos: el audio solo devuelve 0 bytes, el audio más audio_stream_end=True devuelve 0 bytes, y el audio más 1.5 segundos de silencio final devuelve 229,994 bytes. El servidor termina un turno cuando su propio VAD oye silencio, así que tu enlace ascendente tiene que transportar una pausa como muestras reales y nunca como chunks ausentes. La propia documentación de Google dice dos cosas distintas sobre este campo, y los docs de Vertex no lo mencionan en absoluto.

Anecho Engineering16 ago 202611 min de lectura
gemini-livevoice-agentsvadturn-takingstreaming

Si estás construyendo un agente de voz sobre la Gemini Live API y se conecta, transmite audio, muestra una sesión sana y nunca responde, este post son las dos horas que estás a punto de perder.

Primero la medición. Un clip de LibriSpeech de 5 segundos, enviado de tres maneras al mismo endpoint de Vertex, mismo modelo, misma configuración de sesión. Lo único que varía es cómo el cliente señala que el llamante ha dejado de hablar:

Lo que envió el clienteBytes devueltos
Solo audio, sin señal de fin0
Audio, luego send_realtime_input(audio_stream_end=True)0
Audio, luego 1.5 s de silencio final como muestras reales229,994

229,994 bytes de 24 kHz 16-bit mono son unos 4.8 segundos de respuesta. La fila del medio es la que merece mirarse fijamente. El campo se llama audio_stream_end, lo pones a True, el socket sigue abierto, no vuelve ningún error — y el modelo no responde. Nunca. Esperamos.

El servidor termina un turno cuando su propio detector de actividad de voz (VAD) oye silencio. Ese es el único mecanismo. Así que el silencio tiene que llegarle, como muestras, a velocidad de tiempo real. Un cliente que simplemente deja de enviar no es un cliente que ha terminado de hablar; es un cliente indistinguible de uno muerto.

Lo que dice la documentación, que son dos cosas distintas

Fuimos a buscar esto en los docs después de medirlo, y los docs están genuinamente divididos contra sí mismos. Las tres citas de abajo proceden de la referencia y la guía publicadas por el propio Google.

La referencia normativa del campo, en BidiGenerateContentRealtimeInput, describe un estado del stream, no un turno:

«Indica que el stream de audio ha terminado, p. ej. porque el micrófono se apagó. Esto solo debería enviarse cuando la detección automática de actividad está habilitada (que es el valor por defecto). El cliente puede reabrir el stream enviando un mensaje de audio.»

La guía de la Live API, en su sección de VAD automático, dice lo mismo en términos operativos — vaciar (flush), no finalizar:

«Cuando el stream de audio lleva pausado más de un segundo (por ejemplo, porque el usuario apagó el micrófono), debería enviarse un evento audioStreamEnd para vaciar cualquier audio en caché. El cliente puede reanudar el envío de datos de audio en cualquier momento.»

Y la misma guía, en su sección de VAD híbrido, dice algo bastante distinto:

«El servidor trata la señal audio_stream_end como un aviso de finalización inmediata, que se salta el retardo por defecto de detección de silencio del lado del servidor y devuelve la transcripción y la respuesta del modelo con latencia mínima.»

Esas dos cosas no pueden ser ambas el comportamiento general de un mismo campo, y la tercera es la única frase en todas partes que dice que produce una respuesta. Mientras tanto, el propio tipo del SDK para el mensaje contenedor declara el contrato real del modelo:

«El fin de turno no se especifica explícitamente, sino que se deriva de la actividad del usuario (por ejemplo, el fin del habla).»

Hay una cosa más que creemos que es la explicación real de por qué esto es tan fácil de hacer mal. La documentación de Vertex no menciona audioStreamEnd en absoluto. No pudimos encontrar el campo en una sola página de la Live API de Vertex — y Vertex es donde vive gemini-live-2.5-flash-native-audio, con disponibilidad general, como el modelo recomendado para agentes de voz de baja latencia. Así que en la plataforma donde medimos, el campo no está documentado como que haga nada, y el párrafo que dice que finaliza un turno está en la guía del otro producto.

No afirmamos que los docs de Google estén equivocados. Afirmamos que en Vertex, con el VAD automático en sus valores por defecto, en este modelo, en esta fecha, la señal no produjo nada y 1.5 segundos de silencio produjeron una respuesta completa — y que si lees solo la frase sobre la finalización inmediata, construirás algo que se cuelga.

No somos los primeros en toparnos con esto. El issue #1328 de python-genai informa exactamente de esto, paso por paso: envía audioStreamEnd cuando el micrófono se apaga, y luego «observa que el modelo no termina el turno del usuario ni reacciona, incluso después de esperar más de un minuto». Ese issue lo cerró un bot de inactividad en enero sin arreglo y sin confirmación, así que trátalo como corroboración de otro desarrollador y no como una declaración oficial — que es precisamente la razón por la que publicamos una medición en lugar de un enlace.

El invariante que lo arregla

Escribimos esto en el servicio como un contrato de una sola frase, porque todo lo demás en el fichero está aguas abajo de él:

Una vez que una llamada está viva, un stream PCM16 continuo de 16 kHz llega a la API a velocidad de tiempo real, y una pausa se transporta como muestras de silencio reales — nunca como chunks ausentes.

Eso no es una preferencia de estilo. Es el protocolo, reformulado como algo que puedes testear. Cada lugar de tu código que pueda negarse a reenviar un chunk es un lugar que puede colgar la llamada para siempre con el socket todavía abierto, y los dos lugares que nosotros teníamos eran bugs.

La segunda consecuencia es la que atrapa a los clientes de navegador, y no es obvia: tu cliente dejará de enviar audio durante una pausa, quieras o no.

  • Chrome le entrega a un AudioWorkletProcessor un array de entrada vacío siempre que el bus aguas arriba está marcado como silencioso. No un buffer de ceros — un array vacío. El código ingenuo no reenvía nada.
  • Una pista silenciada, o un cambio de dispositivo, detiene los callbacks por completo.

Así que «el usuario se quedó callado» y «el micrófono dejó de producir callbacks» llegan a tu servidor como el mismo evento, y lo único que el VAD del lado del servidor necesita para responder es exactamente lo que el navegador ha decidido no darte.

Nuestro arreglo es un keep-alive que sintetiza el silencio que el cliente debería haber enviado y lo empuja por la ruta idéntica — los mismos remuestreadores, el mismo modelo — de modo que el pipeline se mantiene coherente y el stream transporta muestras de silencio verdaderas en lugar de un agujero. Dos detalles importan más que la idea:

  • Dispáralo por la llegada del cliente, nunca por tu propio enlace ascendente. Nuestro enlace ascendente es legítimamente a ráfagas, porque el remuestreador de stream emite en tandas de aproximadamente 95 ms. Un keep-alive que vigilara su propia salida leería cada hueco entre ráfagas como un cliente parado e inyectaría silencio en medio de habla viva. El nuestro se dispara tras 150 ms sin nada del navegador, que son unos diecinueve quanta de 128 muestras perdidos: una parada, no jitter.
  • Limita el relleno por tick. Una parada larga se repara de forma constante, en lugar de como un único blob enorme y tardío que llega fuera de proporción con la conversación.

Y repórtalo. La cantidad de silencio sintetizado por llamada es un número en nuestro payload de salud, no una reparación oculta, porque una sesión que es 40% silencio sintético te está diciendo algo sobre el cliente que quieres saber.

El enclavamiento que nadie espera

La fusión de frames (coalescing) vino después, por una razón distinta — dos brazos de un A/B estaban siendo empaquetados de forma diferente, 10.6 contra 125.0 frames por segundo — y ambos brazos ahora se ensamblan en frames fijos de 100 ms antes de enviarse. La propia guía de Google es «envía chunks pequeños (de entre 20 ms y 40 ms) para minimizar la latencia», y 125 frames por segundo queda muy fuera de eso en la otra dirección.

Pero un frame construido a medias es audio que no se ha enviado. Así que la fusión solo es segura gracias al keep-alive: sin un productor detrás del buffer, el final de la última frase del llamante se queda en un frame medio lleno exactamente en el momento en que el servidor lo necesita para detectar que el llamante ha parado. El keep-alive garantiza que el buffer siempre tiene un productor, así que un frame parcial siempre lo vacía el silencio que lo sigue.

Dos cambios de aspecto independiente que no son separables. Si te llevas la fusión de frames sin el keep-alive, has construido una versión más sutil del mismo cuelgue.

Haz explícitos los ajustes de detección de turnos

Lo último que cambiamos no fue tanto código como visibilidad. Este servicio solía dejar implícito cada parámetro del VAD, que es como «el agente nunca responde» pudo parecer un misterio en lugar de un ajuste.

La superficie documentada, como referencia:

CampoLo que dicen los docs
automaticActivityDetection.disabledPor defecto false — el VAD del lado del servidor está activado
silenceDurationMs«El valor por defecto interno del servidor es de aproximadamente 800ms»; la guía recomienda 500–800 ms
prefixPaddingMsNingún valor por defecto documentado que pudiéramos encontrar
startOfSpeechSensitivity / endOfSpeechSensitivityEl SDK declara valores por defecto distintos por plataforma — LOW en la rama enterprise, HIGH en la rama de la Gemini API
activityStart / activityEnd«solo pueden enviarse si la detección automática de actividad (es decir, del lado del servidor) está deshabilitada»

Dos trampas en esa tabla. Primera, el propio ejemplo de configuración de la guía usa prefix_padding_ms: 20 y silence_duration_ms: 100 — valores ilustrativos, y 100 ms cae dentro del rango que la misma página llama «demasiado bajo», donde «el sistema termina los turnos de habla durante las pausas naturales, partiendo un único enunciado en múltiples fragmentos de audio pequeños». No copies el ejemplo a producción.

Segunda, la detección manual de actividad es un mundo distinto, no un complemento. Con disabled: true envías tú mismo activityStart y activityEnd, y — según la guía — «en esta configuración no se envía un audioStreamEnd». Si eres dueño del VAD, eres dueño del límite del turno y el problema del silencio desaparece; simplemente has comprado la mitad más difícil del problema, que es detectar turnos en una línea ruidosa.

Nuestra propia elección es exponer los cinco como variables de entorno y parámetros de consulta por llamada, y dejarlos sin fijar por defecto, de modo que el endpoint de estado reporte «vertex default» en lugar de un número que el servicio se inventó. Las dos configuraciones que hemos medido funcionando se midieron con los valores por defecto de Vertex, y moverlos en silencio cambiaría un resultado que ahora está probado.

El resto del contrato de audio, ya que lo vas a necesitar de todos modos

De la documentación, y vale la pena dejarlo bien en una sola pasada:

  • Entrada: PCM crudo, little-endian, de 16 bits. «El audio de entrada es nativamente de 16kHz, pero la Live API remuestreará si hace falta, así que puede enviarse cualquier frecuencia de muestreo.» Declara la frecuencia en el tipo MIME de cada blob, p. ej. audio/pcm;rate=16000. La página de troubleshooting de Vertex añade el número de canales: un solo canal mono.
  • Salida: PCM crudo, little-endian, de 16 bits, siempre 24 kHz. Fíjate en la asimetría — envías 16 kHz, recibes 24 kHz. Cualquier mezcla o grabación aguas abajo tiene que remuestrear uno de los dos.
  • Troceado (chunking): de 20 a 40 ms; no acumules alrededor de un segundo antes de enviar.
  • Duración de sesión: sin compresión de la ventana de contexto, las sesiones de solo audio están documentadas en 15 minutos y las de audio y vídeo en 2. Por separado, «la vida de una conexión está limitada a unos 10 minutos debido a restricciones de las conexiones WebSocket», con un aviso goAway enviado 60 segundos antes del final. Son dos límites distintos y el más corto es el de la conexión, así que la reconexión es una parte normal de una llamada larga, no una ruta de error. Nosotros la tratamos como tal: cola de micrófono acotada, con descarte de lo más antiguo, a través del hueco, para que una reconexión no «se recupere» reproduciendo un backlog creciente y respondiendo preguntas de hace diez segundos.

Una salvedad sobre dónde viven estos docs: las páginas de la Live API de Vertex han sido renombradas y movidas, y la antigua ruta cloud.google.com/vertex-ai/generative-ai/docs/live-api ahora redirige. Si un enlace de tus notas está muerto, esa es la razón.

Salvedades

  • Esto es n = 1 en el estímulo. Un clip de 5 segundos, tres condiciones, un modelo (gemini-live-2.5-flash-native-audio), una región, una fecha. Es una demostración limpia y reproducible de un comportamiento, no un estudio. No reportaríamos el recuento de bytes como si fuera un benchmark.
  • El resultado 0 / 0 / 229,994 es una demostración de que el silencio funciona y la bandera no, en esa configuración. No establece que audio_stream_end no haga nunca nada, en ningún modelo, con ningún ajuste. Si puedes mostrarlo finalizando un turno en Vertex, queremos ver la configuración.
  • El comportamiento está medido en Vertex. La rama de la Gemini API documenta valores por defecto de sensibilidad distintos, y el párrafo del VAD híbrido que describe la finalización está en la guía de esa rama.
  • El issue de la comunidad es corroboración, no confirmación. Lo cerró la automatización sin resolución.
  • Aquí no estamos midiendo calidad. Nada en este post dice nada sobre lo bien que el modelo oyó al llamante. Eso es un experimento aparte, y la ruta de audio que tiene delante es en lo que realmente trabajamos.

La versión corta

  1. El propio VAD del servidor es lo único que termina un turno. Diseña para eso y para nada más.
  2. Transporta las pausas como muestras. Si tu cliente se queda callado, sintetiza el silencio y envíalo por la misma ruta que el audio real.
  3. Nunca dispares un keep-alive por tu propio enlace ascendente — una ruta de transmisión a ráfagas parecerá un cliente parado y hará trizas el habla viva.
  4. Fusiona en un frame fijo, pero solo cuando algo garantice que el buffer tiene un productor. Los dos cambios son un solo cambio.
  5. Publica una ratio de audio entregado por sesión. Los segundos de enlace ascendente sobre los segundos de reloj tienen un valor correcto conocido cerca de 1.0, lo que hace legible un valor equivocado. El nuestro marcó 0.08 durante días mientras la UI reportaba alegremente una sesión viva, y eso es su propio post.
  6. No copies los valores de ejemplo del VAD. Déjalos sin fijar, reporta lo que está en vigor, y cámbialos deliberadamente.
Siguiente
  • 24 oct 2026

    Alternativas a ai-coustics para agentes de voz y centros de llamadas (2026)

    Por qué hay equipos que buscan más allá de ai-coustics Quail Voice Focus, y una lista corta con fuentes según figuraba en octubre de 2026: Krisp VIVA, AssemblyAI Voice Focus, el aislamiento de voz de LiveKit Cloud, NVIDIA Speaker Focus, Anecho y referencias de código abierto. Hemos comparado ai-coustics con nuestro propio modelo en conversaciones en vivo: empate con faster-whisper, ventaja de v8_2 con Gemini Live, menos interrupciones falsas para ai-coustics, y la condición decide quién va delante.

  • 22 oct 2026

    Alternativas a Krisp para agentes de voz y centros de llamadas (2026)

    Por qué hay equipos que buscan más allá de Krisp VIVA, y una lista corta con fuentes según figuraba en octubre de 2026: ai-coustics Quail Voice Focus, AssemblyAI Voice Focus, el aislamiento de voz de LiveKit Cloud, NVIDIA Speaker Focus, Anecho y referencias de código abierto, con dónde se ejecuta cada uno, su precio público y si lo hemos medido. No hemos medido Krisp.

  • 20 oct 2026

    Comparativa de SDK de aislamiento de hablante (2026): Krisp, ai-coustics, AssemblyAI, LiveKit y Anecho

    Una comparación con fuentes del aislamiento de hablante para agentes de voz y centros de llamadas, tal como figuraba en octubre de 2026: dónde se ejecuta cada producto, latencia declarada, frecuencias de muestreo, integración, precio público y licencias. Krisp VIVA y Anecho declaran 15 ms en CPU; ai-coustics indica menos de 30 ms; AssemblyAI y LiveKit venden el aislamiento dentro de un servicio en la nube. Los números de calidad vienen solo de nuestro propio benchmark.