[Bugfix] Fix Qwen3-Omni realtime playback interruption and conversation history (#7791)
Signed-off-by: psv666 <2693925048@qq.com>
This commit is contained in:
1 parent
d3396fc5fb
commit
2d0b03f717
13 files changed
+1292
-68
No files matched your search
@@ -21,6 +21,7 @@ from vllm_omni.engine.duplex.config import (
|
||||
from vllm_omni.engine.duplex.contracts import DuplexFence
|
||||
from vllm_omni.engine.duplex.events import TurnEvent
|
||||
from vllm_omni.engine.duplex.session.engine_session import DuplexEngineSession, DuplexFenceMismatchError
|
||||
from vllm_omni.engine.duplex.session.playback_ledger import apply_playback_ack
|
||||
from vllm_omni.model_executor.models.minicpmo_4_5.duplex.capabilities import (
|
||||
minicpmo45_native_capabilities,
|
||||
)
|
||||
@@ -241,6 +242,83 @@ def test_history_commit_with_ack_only_policy_defers_unacknowledged_text():
|
||||
assert f"item_{response_id}" in session.pending_history_item_ids
|
||||
|
||||
|
||||
@pytest.mark.parametrize("audio_complete", [False, True])
|
||||
def test_unaligned_response_keeps_empty_turn_before_later_user_input(audio_complete):
|
||||
session = _session(config=DuplexSessionConfig(playback_commit_policy="ack_only"))
|
||||
first = session.commit_audio_input(transcript="recite a poem")
|
||||
response_id = session.begin_response()
|
||||
session.reserve_history_item(f"item_{response_id}")
|
||||
session.append_assistant_text("An answer the user has only partly heard")
|
||||
session.mark_audio_sent(
|
||||
duration_ms=10_000,
|
||||
text_requires_complete_audio=True,
|
||||
audio_complete=audio_complete,
|
||||
)
|
||||
|
||||
assert session.end_response(commit_text=True) is None
|
||||
session.barge_in()
|
||||
session.clear_playback_cursor()
|
||||
second = session.commit_audio_input(transcript="stop")
|
||||
empty_answer = {"role": "assistant", "content": ""}
|
||||
assert session.history == (first.message, empty_answer, second.message)
|
||||
|
||||
ack = apply_playback_ack(session, {"response_id": response_id, "played_ms": 6_981})
|
||||
assert ack[0].to_realtime()["event"]["history_committed"] is False
|
||||
assert session.history == (first.message, empty_answer, second.message)
|
||||
|
||||
if audio_complete:
|
||||
ack = apply_playback_ack(session, {"response_id": response_id, "played_ms": 10_000})
|
||||
assert ack[0].to_realtime()["event"]["history_committed"] is True
|
||||
assert session.history == (
|
||||
first.message,
|
||||
{"role": "assistant", "content": "An answer the user has only partly heard"},
|
||||
second.message,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("reserve_slot", "assistant_text"), [(True, ""), (False, "unplayed answer")])
|
||||
def test_unaligned_response_only_materializes_an_existing_answer_slot(reserve_slot, assistant_text):
|
||||
session = _session(config=DuplexSessionConfig(playback_commit_policy="ack_only"))
|
||||
first = session.commit_audio_input(transcript="first input")
|
||||
response_id = session.begin_response()
|
||||
if reserve_slot:
|
||||
session.reserve_history_item(f"item_{response_id}")
|
||||
session.append_assistant_text(assistant_text)
|
||||
session.mark_audio_sent(duration_ms=10_000, text_requires_complete_audio=True, audio_complete=True)
|
||||
|
||||
assert session.end_response(commit_text=reserve_slot) is None
|
||||
assert session.history == (first.message,)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("operation", ["delete", "truncate"])
|
||||
def test_removing_one_empty_assistant_preserves_the_other_turn_boundary(operation):
|
||||
session = _session(config=DuplexSessionConfig(playback_commit_policy="ack_only"))
|
||||
user_inputs = []
|
||||
response_ids = []
|
||||
for transcript in ("first input", "second input"):
|
||||
user_inputs.append(session.commit_audio_input(transcript=transcript).message)
|
||||
response_id = session.begin_response()
|
||||
response_ids.append(response_id)
|
||||
session.reserve_history_item(f"item_{response_id}")
|
||||
session.append_assistant_text("unplayed answer")
|
||||
session.mark_audio_sent(duration_ms=10_000, text_requires_complete_audio=True, audio_complete=True)
|
||||
session.end_response(commit_text=True)
|
||||
|
||||
item_id = f"item_{response_ids[1]}"
|
||||
if operation == "delete":
|
||||
assert session.delete_history_item(item_id) is True
|
||||
else:
|
||||
# A full ACK releases the pending snapshot. Truncation then edits the
|
||||
# stored message into another empty dict before removing that item.
|
||||
ack = apply_playback_ack(session, {"response_id": response_ids[1], "played_ms": 10_000})
|
||||
assert ack[0].to_realtime()["event"]["history_committed"] is True
|
||||
assert session.truncate_history_item(item_id, audio_end_ms=0, hard=True) is True
|
||||
assert session.history == (user_inputs[0], {"role": "assistant", "content": ""}, user_inputs[1])
|
||||
ack = apply_playback_ack(session, {"response_id": response_ids[1], "played_ms": 10_000})
|
||||
assert ack[0].to_realtime()["error"]["code"] == "playback_item_not_found"
|
||||
assert session.history == (user_inputs[0], {"role": "assistant", "content": ""}, user_inputs[1])
|
||||
|
||||
|
||||
def test_truncate_history_item_uses_response_alignment_marks():
|
||||
session = _session()
|
||||
response_id = session.begin_response()
|
||||
|
||||
Reference in new issue
Block a user