google-gemini / google-gemini/gemini-live-api-examples

`gemini-3.1-flash-live-preview`: a 1.0 s mid-sentence pause closes the turn while the caller is still speaking (+ empty generation when `thinking_level` is set)

Open
#37 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
496
Forks
175
Avg merge
6m
Merged PRs (30d)
1

Description

> Re-posted from [`google-gemini/cookbook#1328`](https://github.com/google-gemini/cookbook/issues/1328) at the maintainers' request ([comment](https://github.com/google-gemini/cookbook/issues/1328#issuecomment-5569468878)), so that it reaches the Live engineering team. The report below is unchanged except for the cross-reference to the companion report, which now points to its copy in this repository; the earlier thread is in the original issue.

### Description of the bug

With server-side (automatic) activity detection, a **1.0 s pause in the middle of one caller
sentence** makes `gemini-3.1-flash-live-preview` mis-segment the turn, reproducibly, in a
**minimal configuration**: a one-line system instruction (158 characters), no tools, no session
resumption, and no `automaticActivityDetection` block at all — the server's own default VAD is
what segments the speech. Every measurement below has a control arm in the same session that
differs only by the 1.0 s of silence.

1. **The turn is closed while the caller is still speaking.** The input transcription of the
first burst is delivered *during* the second burst — **24 TRIGGER trials out of 24**, across
four configurations, **0 out of 24** in the control arm of the same sessions. The model starts
generating an answer to half a sentence, and in **21 of those 24** trials a `turnComplete` for
that generation arrives **0.13–0.50 s before the last audio chunk of the utterance is sent**;
never in any control trial.
2. **The input transcription of the final burst is always slower than every control trial of the
same session** — **24/24**. 1.61–1.78 s against 1.14–1.39 s in the default configuration.
3. **With `thinkingConfig.thinkingLevel` set, the generation on that boundary turn completes
with zero output tokens** — `usageMetadata` with `responseTokenCount` absent (`out_tokens=None`
in the SDK). **12 of the 15 trials that requested a thinking level, 0 of the 9 that did not.**
The caller gets no answer at all for that turn.

The same script, the same synthesised audio bytes and the same default configuration on
`gemini-2.5-flash-native-audio-latest` produce **0 empty generations out of 9 TRIGGER trials**.
The 1.0 s pause costs about the same there — TRIGGER 0.56–0.64 s against CONTROL 0.03–0.17 s —
but it is added to a baseline an order of magnitude lower than 3.1's 1.14–1.39 s, and it never
empties a generation. The client is not what differs.

**What this report is not.** It is not a request to raise `silenceDurationMs`: we are aware the
value is capped at 2000 ms, and that committing a turn after a short silence has already been
described as expected behaviour
([forum 172306](https://discuss.ai.google.dev/t/live-api-realtime-audio/172306)). This report is
about what happens *after* that commit: a turn closed while input audio is still arriving on the
wire, and — with a thinking level set — a generation billed with zero output tokens while the
caller is waiting.

### Actual vs expected behavior

| | expected | observed on `gemini-3.1-flash-live-preview` |
|---|---|---|
| turn boundary | a turn is not committed, and no answer generated, while input audio for it is still being received | first burst transcribed *during* the second burst: 24/24 TRIGGER, 0/24 CONTROL. `turnComplete` 0.13–0.50 s *before* the last audio chunk: 21/24 TRIGGER, 0/24 CONTROL |
| transcription of the final burst | comparable to an uninterrupted utterance (1.14–1.39 s here) | 1.61–1.78 s, 24/24 slower than every control trial of the same session |
| boundary turn output | a completed generation produces output, or no generation is completed | `usageMetadata` with no `responseTokenCount`: 12/15 with `thinkingLevel` set, 0/9 without |
| control model, same bytes, same config | — | `gemini-2.5-flash-native-audio-latest`: 0/9 empty generations, TRIGGER delay 0.24–0.64 s |

### Environment

| | |
|---|---|
| Model | `gemini-3.1-flash-live-preview` (control: `gemini-2.5-flash-native-audio-latest`) |
| Transport | `google-genai` Python SDK **1.73.1**, `client.aio.live.connect()` |
| Endpoint / backend | Gemini API — `generativelanguage.googleapis.com`, API key auth. **Not** Vertex AI. |
| Python / platform | CPython 3.12.13, macOS 26.5.1 arm64 |
| Audio | PCM `s16le`, 16 000 Hz, 1 channel, 16-bit; `audio/pcm;rate=16000`; **20 ms chunks (320 samples / 640 bytes) sent every 20 ms, continuously, silence included** |
| Activity detection | **no `automaticActivityDetection` block is sent** — server default |
| System instruction | **158 characters**, one line (the `--system-chars 60000` runs are a documented cross-check, not the default) |
| Sessions (UTC) | 11 Live sessions, started between `2026-08-18T10:39:54Z` and `2026-08-18T11:16:11Z`; the exact start of each is in the ENVIRONMENT block of that run in the gist output |

`setup` payload actually sent, **verbatim and complete** — this is the whole configuration under
test, nothing is elided:

```json
{
"setup": {
"model": "models/gemini-3.1-flash-live-preview",
"response_modalities": ["AUDIO"],
"thinking_config": {"include_thoughts": false, "thinking_level": "MEDIUM"},
"system_instruction": {
"parts": [{"text": "Sei l'assistente telefonico di un servizio di consulenza. Parli solo italiano, con frasi brevi e cortesi. Rispondi sempre a voce a quello che dice il cliente."}],
"role": "user"
},
"input_audio_transcription": {},
"output_audio_transcription": {},
"realtime_input_config": {
"activity_handling": "START_OF_ACTIVITY_INTERRUPTS",
"turn_coverage": "TURN_INCLUDES_ONLY_ACTIVITY"
}
}
}
```

Per-trial UTC timestamps for server-side log lookup are in the gist output. Happy to provide
our Google Cloud project number privately through whichever intake form you prefer — it is
deliberately not in this public body.

### Reproducer

`gemini_live_repro.py` is in this gist: https://gist.github.com/IngLP/124873f0574ada091ab4324e5ff12432 — together with `gemini_live_repro_output.txt`, the unedited output of all eleven runs cited below. It is self-contained: no audio file is
shipped, the two speech pieces are synthesised at run time with the system TTS (`say` on macOS,
`espeak-ng` elsewhere), resampled to 16 kHz mono 16-bit PCM and level-matched, so the **speech
bytes are byte-identical between the two arms**. The only difference between arms is 1.0 s of
PCM silence in the middle:

```
CONTROL [head][tail] 1.50 s, spoken back to back
TRIGGER [head][1.0 s of PCM silence][tail] 2.50 s
```

Both arms end with the same final burst, so the delay measured from the last audio chunk of that
burst is attributable to the pause and to nothing else. Arms alternate inside one session, same
config, same connection.

```
export GEMINI_API_KEY=...
pip install google-genai==1.73.1

mkdir repro && cd repro # put the script in a directory of its own; Python
# puts the script's directory first on sys.path, so a
# neighbouring types.py/enum.py shadows the stdlib

python gemini_live_repro.py --reps 3 --max-sessions 1 # RUNS 1, 2, 11
python gemini_live_repro.py --thinking omit --reps 3 --max-sessions 1 # RUNS 3-4
python gemini_live_repro.py --system-chars 60000 --reps 3 --max-sessions 1 # RUNS 6-7
python gemini_live_repro.py --system-chars 60000 --thinking omit \
--reps 3 --max-sessions 1 # RUN 5
python gemini_live_repro.py --model gemini-2.5-flash-native-audio-latest \
--reps 3 --max-sessions 1 # RUNS 8-9
python gemini_live_repro.py --model gemini-2.5-flash-native-audio-latest \
--system-chars 60000 --reps 3 --max-sessions 1 # RUN 10
python gemini_live_repro.py --dry # offline: synthesises the audio and audits the send cadence, spends nothing
```

One run is one Live session of ~50 s and 6 trials. `--reps` and `--max-sessions` only bound the
spend; **no run in this report changes the configuration except where the flag says so**, and
RUN 11 in the attached output is the script exactly as published, with no configuration flags at
all.

### The audio stream is continuous — including during the pause

This is the first thing that gets asked (e.g.
[dotnet-genai#269](https://github.com/googleapis/dotnet-genai/issues/269)), so the script measures
it instead of asserting it. The "pause" is **not** a pause in sending: the caller leg pushes one
20 ms PCM frame every 20 ms for the whole session, and during the pause those frames are digital
zeros. Every trial prints chunks sent vs expected and the mean/max interval between consecutive
sends, for the whole utterance and separately for the pause window. From RUN 11:

```
TRIGGER #1 stream: 125 chunks sent during the utterance (expected ~125),
interval mean 20.0 ms / max 24.1 ms;
DURING THE 1.0 s PAUSE: 50 silence chunks (expected ~50), max interval 24.1 ms
[session1] stream continuity over the whole session: 2519 chunks in 50.6s (expected ~2529),
interval mean 20.0 ms / max 24.1 ms — the sender never stopped.
```

Worst inter-chunk interval anywhere in the eleven sessions in the gist: **25.0 ms** (RUN 4), against
a 20 ms nominal period; the other ten stay under 24.2 ms. There is no gap in the stream for the
server to interpret as end of input.

### Verbatim output

Full output of all eleven runs is in the gist as `gemini_live_repro_output.txt` (93 KB, unedited): https://gist.github.com/IngLP/124873f0574ada091ab4324e5ff12432.
The load-bearing part of one TRIGGER trial from **RUN 11** — the script as published, no flags
except `--reps 3 --max-sessions 1`, which only bound the spend (`t=` is seconds from connect,
`(+…)` is seconds relative to the last audio chunk of the final burst):

```
--- TRIGGER #1 -------------------------------------------------
11:16:25.362Z t= 14.16s (+ -2.49s) caller speech starts
11:16:27.853Z t= 16.65s (+ -0.00s) [!] 1 transcription fragment(s) arrived DURING the caller's speech: 'Buongiorno.'
11:16:27.854Z t= 16.65s (+ 0.00s) last audio chunk of the FINAL burst sent
11:16:27.415Z t= 16.21s (+ -0.44s) server: interrupted
11:16:27.492Z t= 16.29s (+ -0.36s) server: turn_complete
11:16:27.492Z t= 16.29s (+ -0.36s) server: usage out_tokens=None
11:16:29.466Z t= 18.26s (+ 1.61s) FIRST TRANSCRIPTION OF THE FINAL BURST: 'Sì, grazie.'
11:16:30.256Z t= 19.05s (+ 2.40s) model audio starts
stream: 125 chunks sent during the utterance (expected ~125), interval mean 20.0 ms / max 24.1 ms;
DURING THE 1.0 s PAUSE: 50 silence chunks (expected ~50), max interval 24.1 ms
=> delay 1.61s, 1 EMPTY generation(s) (out_tokens=None)
```

Read the ordering: the server has committed the turn on the first burst and started answering it,
so the transcription `'Buongiorno.'` is delivered at `t=16.65 s` — *inside* the second burst — the
half-sentence answer is `interrupted` by the rest of the same sentence at `t=16.21 s`, and its
`turnComplete` at `t=16.29 s`, still **0.36 s before the caller's last audio chunk**, carries
`usage out_tokens=None`.

The identical trial with **no `thinkingConfig` at all** (RUN 4) shows the same broken segmentation
and a *non-empty* interrupted generation:

```
--- TRIGGER #2 -------------------------------------------------
11:01:24.067Z t= 27.18s (+ -2.49s) caller speech starts
11:01:26.553Z t= 29.67s (+ -0.00s) [!] 1 transcription fragment(s) arrived DURING the caller's speech: 'Buongiorno.'
11:01:26.554Z t= 29.67s (+ 0.00s) last audio chunk of the FINAL burst sent
11:01:26.130Z t= 29.25s (+ -0.42s) server: interrupted
11:01:26.166Z t= 29.28s (+ -0.39s) server: turn_complete
11:01:26.166Z t= 29.28s (+ -0.39s) server: usage out_tokens=43
11:01:28.260Z t= 31.38s (+ 1.71s) FIRST TRANSCRIPTION OF THE FINAL BURST: 'Sì, grazie.'
11:01:28.265Z t= 31.38s (+ 1.71s) model audio starts
=> delay 1.71s
```

### Summary tables (as printed by the script)

**RUNS 1, 2 and 11 — `gemini-3.1-flash-live-preview`, default configuration** (one-line system
instruction, `thinkingLevel: MEDIUM`; RUN 11 is the script with no configuration flags at all,
only the two that bound the spend):

```
arm n min p50 max no transcript empty generations turn cut mid-speech
CONTROL 3 1.14 1.25 1.27 0/3 0/3 0/3 RUN 1
TRIGGER 3 1.61 1.61 1.61 0/3 3/3 3/3 RUN 1

CONTROL 3 1.21 1.31 1.35 0/3 0/3 0/3 RUN 2
TRIGGER 3 1.67 1.67 1.78 0/3 3/3 3/3 RUN 2

CONTROL 3 1.25 1.38 1.39 0/3 0/3 0/3 RUN 11
TRIGGER 3 1.61 1.72 1.77 0/3 2/3 3/3 RUN 11
```

**RUN 8 and RUN 9 — `gemini-2.5-flash-native-audio-latest`, same script, same audio bytes, same
default configuration:**

```
arm n min p50 max no transcript empty generations turn cut mid-speech
CONTROL 3 0.12 0.14 0.15 0/3 0/3 2/3 RUN 8
TRIGGER 3 0.56 0.57 0.62 0/3 0/3 3/3 RUN 8

CONTROL 3 0.03 0.15 0.17 0/3 0/3 2/3 RUN 9
TRIGGER 3 0.57 0.58 0.64 0/3 0/3 3/3 RUN 9
```

Read honestly, the 2.5 "turn cut mid-speech" column is 4/6 in the **control** arm too. Those four
are not closed turns: they are the incremental fragment `'Bu'` of a transcription still being
streamed, while every TRIGGER case is the complete `'Buongiorno.'` of a committed turn. On 2.5 the
pause costs ~0.45 s and never an empty generation; on 3.1 the same column is 0/9 control against
9/9 trigger in the same default configuration, and 0/24 against 24/24 over all configurations.

### It is not the size of the system instruction — it is the thinking level

The obvious objection to any report like this is "it is your configuration, not the model". So the
same script was run over the 2×2 of system-instruction size and thinking level, one to three Live
sessions per cell, 3 TRIGGER trials per session:

| system instruction | `thinkingConfig` | TRIGGER trials | turn cut mid-speech | empty generations | TRIGGER delay | runs |
|---|---|---|---|---|---|---|
| 158 chars (default) | `thinkingLevel: MEDIUM` | 9 | **9/9** | **8/9** | 1.61–1.78 s | 1, 2, 11 |
| 60 000 chars | `thinkingLevel: MEDIUM` | 6 | **6/6** | 4/6 | 1.50–1.70 s | 6, 7 |
| 158 chars | not sent | 6 | **6/6** | 0/6 | 1.61–2.08 s | 3, 4 |
| 60 000 chars | not sent | 3 | **3/3** | 0/3 | 1.54–1.71 s | 5 |

The broken segmentation and the delay are **the same in all four cells** — the size of the system
instruction changes nothing, and the delay is if anything largest in the leanest cell. Only effect
3, the zero-token generation, tracks the thinking level: 12/15 when a `thinkingLevel` is requested,
0/9 when the field is absent. In every control arm of all four cells the turn is never cut: 0/24.

One precision, because the API has no "thinking off": *not sent* means no `thinkingConfig` field in
the `setup` message, so whatever the server's default is applies. The claim is therefore "an
explicit `thinkingLevel` is what accompanies the empty generation", not "thinking causes it".

### Scope and limits of the claim

- What reproduces **every time**, in every configuration tested, is the *mechanism*: the turn is
closed while the caller is still speaking (24/24 TRIGGER trials, 0/24 CONTROL), and the
transcription of the final burst is slower than every control trial of the same session (24/24).
- The **empty generation** is 12/15 with an explicit `thinkingLevel` and 0/9 without. It is
reproducible in the minimal configuration — RUNS 1, 2 and 11 are 8/9 — but it is not
configuration-independent, and this report does not claim it is.
- What does **not** reproduce at a fixed size is the *magnitude of the delay*. An earlier in-house
campaign with the same trigger (23 trials, two bursts separated by 1.0 s, same audio path)
measured the final-burst transcription at **2.46–7.11 s** with a median of 3.45 s, and 11 of
those 23 above 5.8 s; in that campaign an empty generation accompanied **20 of the 27** turns
whose delay was ≥ 2 s and **0 of the 45** below 2 s. In the runs attached here the same trigger
produced 1.50–2.08 s. We are not claiming the large numbers are the norm — only that the
mechanism is deterministic and the delay varies with load.
- All measurements are clean 16 kHz PCM from a laptop straight to the API, not 8 kHz telephony.
- 3 trials per arm per session is a small n per cell. The per-trial separation is what makes it
readable: every one of the 24 TRIGGER trials is slower than every CONTROL trial of the same
session, and the trials are interleaved inside one connection, so arm and moment cannot be
confounded.

### Related issues

Same underlying VAD segmentation, different symptom — none of them reports the empty generation
or the delay of the final burst's transcription:

- [python-genai#2580](https://github.com/googleapis/python-genai/issues/2580) — `silence_duration_ms` not honoured on 3.1 (reproduced by triage 2026-06-11)
- [js-genai#1467](https://github.com/googleapis/js-genai/issues/1467) — same, with a public repro that splits `[speech][2.0 s silence][speech]` into two turns
- [cookbook#1263](https://github.com/google-gemini/cookbook/issues/1263) §2–§3 — effective 2000 ms cap, VAD cutting users mid-sentence
- [cookbook#1262](https://github.com/google-gemini/cookbook/issues/1262) — VAD turn thrashing on 3.1, absent on 2.5 native audio
- [forum 172306](https://discuss.ai.google.dev/t/live-api-realtime-audio/172306) — official answer describing the early commit as expected and the 2000 ms cap

We are filing two other reports on the same model. They may share a root cause with this one,
but we have not demonstrated that: [#38](https://github.com/google-gemini/gemini-live-api-examples/issues/38), and a comment with a `voice_activity`
repro on [python-genai#1285](https://github.com/googleapis/python-genai/issues/1285).

### Questions

1. Is a `turnComplete` emitted while input audio for that turn is still being received on the
socket the intended behaviour of the default VAD?
2. Is a completed generation with no `responseTokenCount` an expected outcome for a boundary turn
that gets interrupted, and is there a client-side signal that distinguishes it from a
generation that is still coming? Why does it appear only when `thinkingLevel` is set?
3. Is there a supported way to keep the pre-3.1 segmentation behaviour on `gemini-3.1-flash-live-preview`
short of disabling automatic activity detection entirely?

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the linked gemini_live_repro.py gist and its gemini_live_repro_output.txt; run the --dry mode first, then compare the documented CONTROL and TRIGGER runs using google-genai 1.73.1. Check the server event ordering, continuous 20 ms audio sends, and usage metadata. Done means confirming the reproducible Live API behavior and isolating it from client-side stream gaps.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.