|
34 | 34 | INPUT_AUDIO_CHANNELS = 1 |
35 | 35 | OUTPUT_AUDIO_SAMPLE_RATE = 24000 |
36 | 36 | OUTPUT_AUDIO_CHANNELS = 1 |
| 37 | +TURN_COMPLETE_FALLBACK_SECONDS = 1.0 |
37 | 38 |
|
38 | 39 | DEFAULT_IMAGE_ENCODE_OPTIONS = images.EncodeOptions( |
39 | 40 | format="JPEG", |
@@ -463,6 +464,7 @@ def __init__(self, realtime_model: RealtimeModel) -> None: |
463 | 464 | self._session_should_close = asyncio.Event() |
464 | 465 | self._response_created_futures: dict[str, asyncio.Future[llm.GenerationCreatedEvent]] = {} |
465 | 466 | self._pending_generation_fut: asyncio.Future[llm.GenerationCreatedEvent] | None = None |
| 467 | + self._turn_complete_fallback_task: asyncio.Task[None] | None = None |
466 | 468 |
|
467 | 469 | self._session_resumption_handle: str | None = ( |
468 | 470 | self._opts.session_resumption.handle |
@@ -728,6 +730,7 @@ def truncate( |
728 | 730 | pass |
729 | 731 |
|
730 | 732 | async def aclose(self) -> None: |
| 733 | + self._cancel_turn_complete_fallback() |
731 | 734 | self._msg_ch.close() |
732 | 735 | self._session_should_close.set() |
733 | 736 |
|
@@ -1037,6 +1040,8 @@ def _build_connect_config(self) -> types.LiveConnectConfig: |
1037 | 1040 | return conf |
1038 | 1041 |
|
1039 | 1042 | def _start_new_generation(self) -> None: |
| 1043 | + self._cancel_turn_complete_fallback() |
| 1044 | + |
1040 | 1045 | if self._current_generation and not self._current_generation._done: |
1041 | 1046 | logger.warning("starting new generation while another is active. Finalizing previous.") |
1042 | 1047 | self._mark_current_generation_done() |
@@ -1140,14 +1145,50 @@ def _handle_server_content(self, server_content: types.LiveServerContent) -> Non |
1140 | 1145 | if server_content.generation_complete or server_content.turn_complete: |
1141 | 1146 | current_gen._completed_timestamp = time.time() |
1142 | 1147 |
|
| 1148 | + if server_content.generation_complete and not server_content.turn_complete: |
| 1149 | + self._schedule_turn_complete_fallback(current_gen.response_id) |
| 1150 | + |
1143 | 1151 | if server_content.interrupted and not self._pending_generation_fut: |
1144 | 1152 | # interrupt agent if there is no pending user initiated generation |
1145 | 1153 | self._handle_input_speech_started() |
1146 | 1154 |
|
1147 | 1155 | if server_content.turn_complete: |
1148 | 1156 | self._mark_current_generation_done() |
1149 | 1157 |
|
| 1158 | + def _cancel_turn_complete_fallback(self) -> None: |
| 1159 | + if self._turn_complete_fallback_task and not self._turn_complete_fallback_task.done(): |
| 1160 | + self._turn_complete_fallback_task.cancel() |
| 1161 | + self._turn_complete_fallback_task = None |
| 1162 | + |
| 1163 | + def _schedule_turn_complete_fallback(self, response_id: str) -> None: |
| 1164 | + self._cancel_turn_complete_fallback() |
| 1165 | + self._turn_complete_fallback_task = asyncio.create_task( |
| 1166 | + self._wait_for_turn_complete_fallback( |
| 1167 | + response_id=response_id, |
| 1168 | + timeout=TURN_COMPLETE_FALLBACK_SECONDS, |
| 1169 | + ), |
| 1170 | + name=f"gemini_turn_complete_fallback_{response_id}", |
| 1171 | + ) |
| 1172 | + |
| 1173 | + async def _wait_for_turn_complete_fallback(self, *, response_id: str, timeout: float) -> None: |
| 1174 | + try: |
| 1175 | + await asyncio.sleep(timeout) |
| 1176 | + except asyncio.CancelledError: |
| 1177 | + return |
| 1178 | + |
| 1179 | + current_gen = self._current_generation |
| 1180 | + if not current_gen or current_gen._done or current_gen.response_id != response_id: |
| 1181 | + return |
| 1182 | + |
| 1183 | + logger.warning( |
| 1184 | + "Gemini Realtime did not emit turn_complete after generation_complete; " |
| 1185 | + f"finalizing generation (response_id={response_id}, timeout={timeout:.2f}s)" |
| 1186 | + ) |
| 1187 | + self._mark_current_generation_done() |
| 1188 | + |
1150 | 1189 | def _mark_current_generation_done(self) -> None: |
| 1190 | + self._cancel_turn_complete_fallback() |
| 1191 | + |
1151 | 1192 | if not self._current_generation or self._current_generation._done: |
1152 | 1193 | return |
1153 | 1194 |
|
|
0 commit comments