[Bugfix] Fix Qwen3-Omni realtime playback interruption and conversation history (#7791)

Signed-off-by: psv666 <2693925048@qq.com>
This commit is contained in:
psv666 authored and GitHub committed 2026-09-20 14:42:17 +00:00
1 parent d3396fc5fb
commit 2d0b03f717
13 files changed
+1292 -68

No files matched your search

+68 -1
View File
@@ -11,7 +11,7 @@ import inspect
import pytest
from vllm_omni.engine.duplex import events as events_module
from vllm_omni.engine.duplex.commands import TruncateItem
from vllm_omni.engine.duplex.commands import ClearOutputAudio, TruncateItem
from vllm_omni.engine.duplex.events import (
REALTIME_ERROR_TYPES_BY_CODE,
AudioDelta,
@@ -41,6 +41,7 @@ from vllm_omni.engine.duplex.events import (
from vllm_omni.engine.duplex.realtime_events import (
RealtimeProjectionState,
project_internal_event,
resolve_clear_output_audio,
resolve_truncate_item,
retrieve_item_events,
)
@@ -376,6 +377,72 @@ def test_projection_of_output_audio_buffer_clear_emits_cleared_before_terminals(
assert state.item_truncation_cursors["item_resp_1"] == (0, 250)
@pytest.mark.parametrize("reason", ["turn_detected", "barge_in", "output_audio_buffer_clear"])
@pytest.mark.parametrize("committed_ms", [1000, 10000])
def test_completed_response_cancellation_only_clears_pending_playback(reason, committed_ms):
state = RealtimeProjectionState(session_id="duplex-playback")
project_internal_event(state, {"type": "response.created", "response_id": "resp_1"})
project_internal_event(state, {"type": "response.done", "response_id": "resp_1"})
cancelled = project_internal_event(
state,
{
"type": "audio.cancelled",
"response_id": "resp_1",
"reason": reason,
"committed_ms": committed_ms,
"playback": {"sent_ms": 10000, "committed_ms": committed_ms},
},
)
if reason == "output_audio_buffer_clear" or committed_ms < 10000:
assert _types(cancelled) == ["output_audio_buffer.cleared"]
assert cancelled[0].response_id == "resp_1"
else:
assert cancelled == []
@pytest.mark.parametrize("explicit_response_id", [False, True])
def test_clear_latest_completed_response_reaches_playback_owner(explicit_response_id):
state = RealtimeProjectionState(session_id="duplex-clear")
project_internal_event(state, {"type": "response.created", "response_id": "resp_1"})
project_internal_event(state, {"type": "response.done", "response_id": "resp_1"})
control = resolve_clear_output_audio(
state, ClearOutputAudio(response_id="resp_1" if explicit_response_id else None, event_id="clear_1")
)
# Generation completion does not prove the client has drained its audio.
# A cleared event must follow the engine clearing its playback cursor.
assert control.events == []
assert control.payloads == [
{
"type": "output_audio_buffer.clear",
"reason": "output_audio_buffer.clear",
"response_id": "resp_1",
"realtime_event_id": "clear_1",
}
]
@pytest.mark.parametrize("new_response_finished", [False, True])
def test_late_clear_of_completed_response_does_not_touch_new_response(new_response_finished):
state = RealtimeProjectionState(session_id="duplex-clear")
project_internal_event(state, {"type": "response.created", "response_id": "resp_1"})
project_internal_event(state, {"type": "response.done", "response_id": "resp_1"})
project_internal_event(state, {"type": "response.created", "response_id": "resp_2"})
if new_response_finished:
project_internal_event(state, {"type": "response.done", "response_id": "resp_2"})
control = resolve_clear_output_audio(state, ClearOutputAudio(response_id="resp_1"))
assert control.payloads == []
assert _types(control.events) == ["output_audio_buffer.cleared"]
assert control.events[0].response_id == "resp_1"
assert state.active_response_id == (None if new_response_finished else "resp_2")
assert state.last_response_id == "resp_2"
def test_one_truncate_command_truncates_retrieved_transcript_once():
state = RealtimeProjectionState(session_id="duplex-truncate")
state.conversation_items["item_resp_1"] = {