Skip to content

Commit d12deff

Browse files
committed
fix(#2009): emit truncation on VAD interrupt
1 parent 553c1bf commit d12deff

File tree

2 files changed

+32
-4
lines changed

2 files changed

+32
-4
lines changed

src/agents/realtime/openai_realtime.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -621,10 +621,30 @@ async def _handle_ws_event(self, event: dict[str, Any]):
621621
last_audio = self._audio_state_tracker.get_last_audio_item()
622622
if last_audio is not None:
623623
item_id, content_index = last_audio
624+
playback_state = self._get_playback_state()
625+
playback_item_id = playback_state.get("current_item_id")
626+
playback_content_index = playback_state.get("current_item_content_index") or 0
627+
playback_elapsed_ms = playback_state.get("elapsed_ms")
624628
await self._emit_event(
625629
RealtimeModelAudioInterruptedEvent(item_id=item_id, content_index=content_index)
626630
)
627631

632+
elapsed_override = getattr(parsed, "audio_end_ms", None)
633+
if elapsed_override is None or elapsed_override <= 0:
634+
effective_elapsed_ms = playback_elapsed_ms
635+
else:
636+
effective_elapsed_ms = float(elapsed_override)
637+
638+
if playback_item_id and effective_elapsed_ms is not None:
639+
truncated_ms = max(int(round(effective_elapsed_ms)), 0)
640+
await self._send_raw_message(
641+
_ConversionHelper.convert_interrupt(
642+
playback_item_id,
643+
playback_content_index,
644+
truncated_ms,
645+
)
646+
)
647+
628648
# Reset trackers so subsequent playback state queries don't
629649
# reference audio that has been interrupted client‑side.
630650
self._audio_state_tracker.on_interrupted()
@@ -643,9 +663,6 @@ async def _handle_ws_event(self, event: dict[str, Any]):
643663
)
644664
if not automatic_response_cancellation_enabled:
645665
await self._cancel_response()
646-
# Avoid sending conversation.item.truncate here. When the session's
647-
# turn_detection.interrupt_response is enabled (GA default), the server emits
648-
# conversation.item.truncated after the VAD start and takes care of history updates.
649666
elif parsed.type == "response.created":
650667
self._ongoing_response = True
651668
await self._emit_event(RealtimeModelTurnStartedEvent())

tests/realtime/test_openai_realtime.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ async def test_transcription_related_and_timeouts_and_speech_started(self, model
443443

444444
# Prepare tracker state to simulate ongoing audio
445445
model._audio_state_tracker.set_audio_format("pcm16")
446-
model._audio_state_tracker.on_audio_delta("i1", 0, b"aaaa")
446+
model._audio_state_tracker.on_audio_delta("i1", 0, b"a" * 48)
447447
model._ongoing_response = True
448448

449449
# Patch sending to avoid websocket dependency
@@ -464,6 +464,17 @@ async def test_transcription_related_and_timeouts_and_speech_started(self, model
464464
}
465465
)
466466

467+
truncate_events = [
468+
call.args[0]
469+
for call in model._send_raw_message.await_args_list
470+
if getattr(call.args[0], "type", None) == "conversation.item.truncate"
471+
]
472+
assert truncate_events
473+
truncate_event = truncate_events[0]
474+
assert truncate_event.item_id == "i1"
475+
assert truncate_event.content_index == 0
476+
assert truncate_event.audio_end_ms == 1
477+
467478
# Output transcript delta
468479
await model._handle_ws_event(
469480
{

0 commit comments

Comments
 (0)