audio_stream_end=True beendet in der Gemini Live API keinen Turn
Gemessen auf Vertex gegen gemini-live-2.5-flash-native-audio mit einem einzelnen 5-Sekunden-Clip: Audio allein liefert 0 Bytes zurück, Audio plus audio_stream_end=True liefert 0 Bytes, und Audio plus 1.5 Sekunden nachlaufende Stille liefert 229,994 Bytes. Der Server beendet einen Turn, wenn seine eigene VAD Stille hört — Ihr Uplink muss eine Pause also als echte Samples transportieren und nie als ausbleibende Chunks. Googles eigene Dokumentation sagt zwei verschiedene Dinge über dieses Feld, und die Vertex-Doku erwähnt es überhaupt nicht.
Wenn Sie einen Voice Agent auf der Gemini Live API bauen und er verbindet sich, streamt Audio, zeigt eine gesunde Session und antwortet nie — dann ist dieser Beitrag die zwei Stunden, die Sie gleich verlieren würden.
Zuerst die Messung. Ein einzelner 5-Sekunden-Clip aus LibriSpeech, auf drei Arten an denselben Vertex-Endpoint geschickt, gleiches Modell, gleiche Session-Konfiguration. Das Einzige, was variiert, ist, wie der Client signalisiert, dass der Anrufer aufgehört hat zu sprechen:
| Was der Client gesendet hat | Zurückgelieferte Bytes |
|---|---|
| Nur Audio, kein Endsignal | 0 |
Audio, dann send_realtime_input(audio_stream_end=True) | 0 |
| Audio, dann 1.5 s nachlaufende Stille als echte Samples | 229,994 |
229,994 Bytes mit 24 kHz, 16 Bit, mono sind etwa 4.8 Sekunden Antwort. Die mittlere Zeile ist die, auf die man starren sollte. Das Feld heißt audio_stream_end, man setzt es auf True, der Socket bleibt offen, kein Fehler kommt zurück — und das Modell antwortet nicht. Nie. Wir haben gewartet.
Der Server beendet einen Turn, wenn seine eigene Sprachaktivitätserkennung (VAD) Stille hört. Das ist der einzige Mechanismus. Die Stille muss ihn also erreichen, als Samples, in Echtzeitrate. Ein Client, der einfach aufhört zu senden, ist kein Client, der fertig gesprochen hat; er ist ein Client, der von einem toten nicht zu unterscheiden ist.
Was die Dokumentation sagt, und das sind zwei verschiedene Dinge
Wir haben in den Docs danach gesucht, nachdem wir es gemessen hatten, und die Docs sind in sich tatsächlich uneins. Alle drei Zitate unten stammen aus Googles eigener veröffentlichter Referenz und dem eigenen Guide; wir geben sie unübersetzt wieder, weil es hier auf den exakten Wortlaut ankommt.
Die normative Feldreferenz, in BidiGenerateContentRealtimeInput, beschreibt einen Stream-Zustand, keinen Turn:
"Indicates that the audio stream has ended, e.g. because the microphone was turned off. This should only be sent when automatic activity detection is enabled (which is the default). The client can reopen the stream by sending an audio message."
Der Live-API-Guide sagt in seinem Abschnitt zur automatischen VAD dasselbe in operativen Begriffen — flush, nicht finish:
"When the audio stream is paused for more than a second (for example, because the user switched off the microphone), an
audioStreamEndevent should be sent to flush any cached audio. The client can resume sending audio data at any time."
Und derselbe Guide sagt in seinem Abschnitt zur hybriden VAD etwas ganz anderes:
"The server treats the
audio_stream_endsignal as an immediate finalization prompt, bypassing the default server-side silence detection delay and returning the transcript and model response with minimal latency."
Das kann nicht beides das allgemeine Verhalten eines einzigen Feldes sein, und das dritte Zitat ist der einzige Satz irgendwo, der behauptet, es produziere eine Antwort. Der eigene Typ des SDK für die umschließende Nachricht nennt derweil den tatsächlichen Vertrag des Modells:
"End of turn is not explicitly specified, but is rather derived from user activity (for example, end of speech)."
Es gibt noch etwas, das wir für die eigentliche Erklärung halten, warum man hier so leicht danebengreift. Die Vertex-Dokumentation erwähnt audioStreamEnd überhaupt nicht. Wir konnten das Feld auf keiner einzigen Vertex-Live-API-Seite finden — und Vertex ist der Ort, an dem gemini-live-2.5-flash-native-audio lebt, allgemein verfügbar, als das empfohlene Modell für Voice Agents mit niedriger Latenz. Auf der Plattform, auf der wir gemessen haben, ist das Feld also nicht dokumentiert, irgendetwas zu tun — und der Absatz, der sagt, es finalisiere einen Turn, steht im Guide des anderen Produkts.
Wir behaupten nicht, dass Googles Docs falsch sind. Wir behaupten, dass auf Vertex, mit automatischer VAD in den Defaults, auf diesem Modell, an diesem Datum, das Signal nichts produzierte und 1.5 Sekunden Stille eine vollständige Antwort produzierten — und dass Sie, wenn Sie nur den Satz über die sofortige Finalisierung lesen, etwas bauen werden, das hängt.
Wir sind nicht die Ersten, die das trifft. python-genai Issue #1328 berichtet exakt das, Schritt für Schritt: audioStreamEnd senden, wenn das Mikrofon ausgeht, dann "observe that the model does not end the user turn or react, even after waiting over a minute." Dieses Issue wurde im Januar von einem Stale-Bot geschlossen, ohne Fix und ohne Bestätigung — behandeln Sie es also als Bestätigung durch einen anderen Entwickler, nicht als offizielle Aussage. Genau deshalb veröffentlichen wir eine Messung statt eines Links.
Die Invariante, die es behebt
Wir haben das als Ein-Satz-Vertrag in den Service geschrieben, weil alles andere in der Datei stromabwärts davon liegt:
Sobald ein Anruf live ist, erreicht ein kontinuierlicher 16-kHz-PCM16-Stream die API in Echtzeitrate, und eine Pause wird als echte stille Samples transportiert — nie als ausbleibende Chunks.
Das ist keine Stilfrage. Es ist das Protokoll, umformuliert als etwas, das man testen kann. Jede Stelle in Ihrem Code, die das Weiterleiten eines Chunks verweigern könnte, ist eine Stelle, die den Anruf bei weiterhin offenem Socket für immer hängen lassen kann — und beide Stellen, die wir hatten, waren Bugs.
Die zweite Konsequenz ist die, die Browser-Clients erwischt, und sie ist nicht offensichtlich: Ihr Client wird während einer Pause aufhören, Audio zu senden — ob Sie das wollen oder nicht.
- Chrome reicht einem
AudioWorkletProcessorein leeres Input-Array, wann immer der vorgelagerte Bus als still markiert ist. Kein Puffer aus Nullen — ein leeres Array. Naiver Code leitet nichts weiter. - Ein stummgeschalteter Track oder ein Gerätewechsel stoppt die Callbacks ganz.
„Der Nutzer ist verstummt" und „das Mikrofon produziert keine Callbacks mehr" kommen bei Ihrem Server also als dasselbe Ereignis an, und das Eine, das die serverseitige VAD braucht, um zu antworten, ist exakt das, was der Browser beschlossen hat, Ihnen nicht zu geben.
Unser Fix ist ein Keep-alive, das die Stille, die der Client hätte senden sollen, synthetisiert und durch den identischen Pfad schiebt — dieselben Resampler, dasselbe Modell —, sodass die Pipeline kohärent bleibt und der Stream echte stille Samples trägt statt eines Lochs. Zwei Details zählen mehr als die Idee:
- Verankern Sie es am Eintreffen des Client-Audios, nie am eigenen Uplink. Unser Uplink ist legitim stoßweise, weil der Stream-Resampler in Bündeln von rund 95 ms emittiert. Ein Keep-alive, das seine eigene Ausgabe beobachtete, würde jede Lücke zwischen Bündeln als eingefrorenen Client lesen und Stille mitten in lebende Sprache injizieren. Unseres löst nach 150 ms ohne irgendetwas vom Browser aus, das sind etwa neunzehn verpasste 128-Sample-Quanten: ein Stillstand, kein Jitter.
- Deckeln Sie die Füllung pro Tick. Ein langer Stillstand wird stetig repariert statt als ein einziger riesiger, verspäteter Klumpen, der außer Verhältnis zum Gespräch ankommt.
Und berichten Sie es. Die Menge pro Anruf synthetisierter Stille ist eine Zahl in unserem Health-Payload, keine versteckte Reparatur — denn eine Session, die zu 40% aus synthetischer Stille besteht, sagt Ihnen etwas über den Client, das Sie wissen wollen.
Die Verriegelung, die niemand erwartet
Das Koaleszieren kam als Nächstes, aus einem anderen Grund — zwei Arme eines A/B-Tests wurden unterschiedlich paketiert, 10.6 gegen 125.0 Frames pro Sekunde —, und beide Arme setzen jetzt feste Frames zu 100 ms zusammen, bevor gesendet wird. Googles eigene Empfehlung lautet "send small chunks (between 20 ms and 40 ms) to minimize latency", und 125 Frames pro Sekunde liegen weit außerhalb davon, in der anderen Richtung.
Aber ein halb gebauter Frame ist Audio, das nicht gesendet wurde. Das Koaleszieren ist also nur wegen des Keep-alives sicher: Ohne einen Produzenten hinter dem Puffer sitzt das Ende des letzten Satzes des Anrufers in einem halbvollen Frame — genau in dem Moment, in dem der Server es braucht, um zu erkennen, dass der Anrufer aufgehört hat. Das Keep-alive garantiert, dass der Puffer immer einen Produzenten hat, ein partieller Frame wird also immer von der Stille geflusht, die ihm folgt.
Zwei unabhängig aussehende Änderungen, die nicht trennbar sind. Wer das Frame-Koaleszieren ohne das Keep-alive übernimmt, hat eine subtilere Version desselben Hängers gebaut.
Machen Sie die Turn-Detection-Einstellungen explizit
Das Letzte, was wir geändert haben, war weniger Code als Sichtbarkeit. Dieser Service ließ früher jeden VAD-Parameter implizit, und so konnte „der Agent antwortet nie" wie ein Mysterium aussehen statt wie eine Einstellung.
Die dokumentierte Oberfläche, zur Referenz:
| Feld | Was die Docs sagen |
|---|---|
automaticActivityDetection.disabled | Default ist false — die serverseitige VAD ist an |
silenceDurationMs | "The server's internal default is approximately 800ms"; der Guide empfiehlt 500–800 ms |
prefixPaddingMs | Kein dokumentierter Default, den wir finden konnten |
startOfSpeechSensitivity / endOfSpeechSensitivity | Das SDK nennt je Plattform verschiedene Defaults — LOW auf dem Enterprise-Zweig, HIGH auf dem Gemini-API-Zweig |
activityStart / activityEnd | "can only be sent if automatic (i.e. server-side) activity detection is disabled" |
Zwei Fallen in dieser Tabelle. Erstens: Das Konfigurationsbeispiel des Guides selbst verwendet prefix_padding_ms: 20 und silence_duration_ms: 100 — illustrative Werte, und 100 ms liegen innerhalb des Bereichs, den dieselbe Seite "Too low" nennt, wo "the system ends speech turns during natural pauses, splitting a single utterance into multiple small audio fragments." Kopieren Sie das Beispiel nicht in die Produktion.
Zweitens: Manuelle Activity Detection ist eine andere Welt, keine Ergänzung. Mit disabled: true senden Sie activityStart und activityEnd selbst, und — laut Guide — "an audioStreamEnd isn't sent in this configuration." Wer die VAD besitzt, besitzt die Turn-Grenze, und das Stille-Problem verschwindet; man hat sich lediglich die härtere Hälfte des Problems eingekauft, nämlich Turns auf einer verrauschten Leitung zu erkennen.
Unsere eigene Wahl ist, alle fünf als Umgebungsvariablen und Query-Parameter pro Anruf zu exponieren und sie standardmäßig ungesetzt zu lassen, sodass der Status-Endpoint „vertex default" meldet statt einer Zahl, die sich der Service ausgedacht hat. Die zwei Konfigurationen, die wir funktionierend gemessen haben, wurden mit Vertex' Defaults gemessen, und sie stillschweigend zu verschieben würde ein Ergebnis verändern, das jetzt belegt ist.
Der Rest des Audio-Vertrags, denn Sie werden ihn ohnehin brauchen
Aus der Dokumentation, und es lohnt sich, das in einem Durchgang richtig zu machen:
- Input: rohes, little-endian, 16-Bit-PCM. "Input audio is natively 16kHz, but the Live API will resample if needed so any sample rate can be sent." Deklarieren Sie die Rate im MIME-Type jedes Blobs, z. B.
audio/pcm;rate=16000. Die Vertex-Troubleshooting-Seite ergänzt die Kanalzahl: ein einzelner Mono-Kanal. - Output: rohes, little-endian, 16-Bit-PCM, immer 24 kHz. Beachten Sie die Asymmetrie — Sie senden 16 kHz, Sie empfangen 24 kHz. Jedes nachgelagerte Mischen oder Aufzeichnen muss eines von beiden resampeln.
- Chunking: 20 bis 40 ms; puffern Sie nicht rund eine Sekunde, bevor Sie senden.
- Session-Länge: Ohne Kompression des Kontextfensters sind Audio-only-Sessions mit 15 Minuten dokumentiert, Audio-Video mit 2. Separat gilt: "the lifetime of a connection is limited to around 10 minutes due to WebSocket connection constraints", mit einer
goAway-Mitteilung 60 Sekunden vor dem Ende. Das sind zwei verschiedene Limits, und das kürzere ist die Verbindung — Reconnects sind also ein normaler Teil eines langen Anrufs, kein Fehlerpfad. Wir behandeln sie als einen: begrenzte Mikrofon-Queue mit Drop-oldest über die Lücke hinweg, damit sich ein Reconnect nicht „erholt", indem er einen wachsenden Rückstau abspielt und Fragen von vor zehn Sekunden beantwortet.
Ein Vorbehalt dazu, wo diese Docs leben: Die Vertex-Live-API-Seiten wurden umbenannt und verschoben, und der alte Pfad cloud.google.com/vertex-ai/generative-ai/docs/live-api leitet inzwischen um. Wenn ein Link in Ihren Notizen tot ist — das ist der Grund.
Vorbehalte
- Das ist n = 1 beim Stimulus. Ein 5-Sekunden-Clip, drei Bedingungen, ein Modell (
gemini-live-2.5-flash-native-audio), eine Region, ein Datum. Es ist eine saubere, reproduzierbare Demonstration eines Verhaltens, keine Erhebung. Wir würden die Byte-Zahl nicht berichten, als wäre sie ein Benchmark. - Das Ergebnis 0 / 0 / 229,994 ist eine Demonstration, dass die Stille funktioniert und das Flag es nicht tat — in dieser Konfiguration. Es belegt nicht, dass
audio_stream_endnie etwas tut, auf keinem Modell, bei keiner Einstellung. Wenn Sie zeigen können, wie es auf Vertex einen Turn finalisiert, wollen wir die Konfiguration sehen. - Das Verhalten ist auf Vertex gemessen. Der Gemini-API-Zweig dokumentiert andere Sensitivitäts-Defaults, und der Hybrid-VAD-Absatz, der die Finalisierung beschreibt, steht im Guide dieses Zweigs.
- Das Community-Issue ist Bestätigung durch Dritte, keine offizielle. Es wurde von Automatik geschlossen, ohne Auflösung.
- Wir messen hier keine Qualität. Nichts in diesem Beitrag sagt irgendetwas darüber, wie gut das Modell den Anrufer gehört hat. Das ist ein separates Experiment, und der Audiopfad davor ist das, woran wir tatsächlich arbeiten.
Die Kurzfassung
- Die eigene VAD des Servers ist das Einzige, was einen Turn beendet. Entwerfen Sie dafür und für nichts anderes.
- Transportieren Sie Pausen als Samples. Wenn Ihr Client verstummt, synthetisieren Sie die Stille und schicken Sie sie durch denselben Pfad wie echtes Audio.
- Verankern Sie ein Keep-alive nie am eigenen Uplink — ein stoßweiser Sendepfad sieht aus wie ein eingefrorener Client und zerfetzt lebende Sprache.
- Koaleszieren Sie auf einen festen Frame, aber erst, wenn etwas garantiert, dass der Puffer einen Produzenten hat. Die zwei Änderungen sind eine Änderung.
- Veröffentlichen Sie pro Session eine Delivered-Audio-Ratio. Uplink-Sekunden geteilt durch Wanduhr-Sekunden hat einen bekannten korrekten Wert nahe 1.0, und das macht einen falschen Wert lesbar. Unsere stand tagelang auf 0.08, während die UI fröhlich eine Live-Session meldete — das ist ein eigener Beitrag.
- Kopieren Sie die VAD-Beispielwerte nicht. Lassen Sie sie ungesetzt, berichten Sie, was in Kraft ist, und ändern Sie sie bewusst.