[Model] Integrate AURA into Full-Duplex Runtime (#7633)

This commit is contained in:
NumberWan authored and GitHub committed 2026-09-21 09:26:54 -07:00
1 parent d397a2f547
commit 62b15142bd
77 files changed
+7695 -457

No files matched your search

@@ -0,0 +1,165 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""CPU tests for AuraDataPlaneSession (unwrap runner envelope; do not drop audio)."""
from __future__ import annotations
from types import SimpleNamespace
import numpy as np
from vllm_omni.model_executor.models.aura_omni.duplex.data_plane import AuraDataPlaneSession
def _encode_audio(audio: object, sample_rate: int, fmt: str, speed: float | None) -> str | None:
del sample_rate, fmt, speed
if audio is None:
return None
return "encoded-audio"
def test_project_unwraps_data_plane_outputs_and_emits_audio_then_done() -> None:
plane = AuraDataPlaneSession(_encode_audio)
request_id = "duplex-s.abc.e.0.r.stage0-turn1"
plane.begin_request(request_id)
audio = np.zeros(16, dtype=np.float32)
output = SimpleNamespace(
request_id=request_id,
finished=True,
stage_id=3,
outputs=[
SimpleNamespace(
text="",
cumulative_text="",
finished=True,
multimodal_output={"audio": audio, "sr": 24000},
)
],
multimodal_output={"audio": audio, "sr": 24000},
)
events = list(plane.project({"data_plane_outputs": [output]}))
assert events, "project must not silently drop the runner envelope"
assert any(event.get("audio") == "encoded-audio" for event in events)
assert events[-1].get("end_of_turn") is True
assert events[0].get("data_plane_request_id") == request_id
def test_project_sends_only_the_new_tail_of_a_growing_waveform() -> None:
seen: list[int] = []
def encode(audio: object, sample_rate: int, fmt: str, speed: float | None) -> str:
del sample_rate, fmt, speed
count = int(np.asarray(audio).reshape(-1).size)
seen.append(count)
return f"pcm-{count}"
plane = AuraDataPlaneSession(encode)
request_id = "duplex-s.abc.e.0.r.stage3-turn1"
plane.begin_request(request_id)
def chunk(n: int, *, finished: bool) -> SimpleNamespace:
audio = np.arange(n, dtype=np.float32)
return SimpleNamespace(
request_id=request_id,
finished=finished,
stage_id=3,
outputs=[
SimpleNamespace(
text="",
cumulative_text="",
finished=False,
multimodal_output={"audio": audio, "sr": 24000},
)
],
)
first = list(plane.project_output(chunk(8, finished=False)))
second = list(plane.project_output(chunk(12, finished=True)))
assert seen == [8, 4]
assert first[0]["audio_duration_ms"] == round(8 * 1000 / 24000)
assert second[0]["audio_duration_ms"] == round(4 * 1000 / 24000)
assert second[-1].get("end_of_turn") is True
def test_project_holds_silent_token_prefix() -> None:
plane = AuraDataPlaneSession(_encode_audio)
request_id = "duplex-s.abc.e.0.r.stage1-turn1"
plane.begin_request(request_id)
partial = SimpleNamespace(
request_id=request_id,
finished=False,
stage_id=1,
outputs=[SimpleNamespace(text="<|sil", cumulative_text="<|sil", finished=False)],
)
assert list(plane.project({"data_plane_outputs": [partial]})) == []
def test_project_emits_chinese_brackets_immediately() -> None:
"""[沉默] is ordinary text — do not hold as a <|silent|> prefix."""
plane = AuraDataPlaneSession(_encode_audio)
request_id = "duplex-s.abc.e.0.r.stage1_t2"
plane.begin_request(request_id)
partial = SimpleNamespace(
request_id=request_id,
finished=False,
stage_id=1,
outputs=[SimpleNamespace(text="[", cumulative_text="[", finished=False)],
)
events = list(plane.project({"data_plane_outputs": [partial]}))
assert len(events) == 1
assert events[0].get("text") == "["
def test_project_flushes_held_silent_prefix_when_text_becomes_speech() -> None:
plane = AuraDataPlaneSession(_encode_audio)
request_id = "duplex-s.abc.e.0.r.stage1-turn3"
plane.begin_request(request_id)
partial = SimpleNamespace(
request_id=request_id,
finished=False,
stage_id=1,
outputs=[SimpleNamespace(text="<|", cumulative_text="<|", finished=False)],
)
assert list(plane.project({"data_plane_outputs": [partial]})) == []
spoken = SimpleNamespace(
request_id=request_id,
finished=False,
stage_id=1,
outputs=[SimpleNamespace(text="<|hello", cumulative_text="<|hello", finished=False)],
)
events = list(plane.project({"data_plane_outputs": [spoken]}))
assert len(events) == 1
assert events[0].get("text") == "<|hello"
def test_project_marks_finished_silent_text_as_listen() -> None:
plane = AuraDataPlaneSession(_encode_audio)
request_id = "duplex-s.abc.e.0.r.stage1_t4"
plane.begin_request(request_id)
final = SimpleNamespace(
request_id=request_id,
finished=True,
stage_id=1,
outputs=[SimpleNamespace(text="<|silent|>", cumulative_text="<|silent|>", finished=True)],
)
events = list(plane.project({"data_plane_outputs": [final]}))
assert any(e.get("silent") and e.get("is_listen") for e in events)
assert any(e.get("abort_data_plane_request") is True for e in events)
def test_project_finished_spoken_turn_carries_model_context() -> None:
plane = AuraDataPlaneSession(_encode_audio)
request_id = "duplex-s.abc.e.0.r.stage1-turn5"
plane.begin_request(request_id)
final = SimpleNamespace(
request_id=request_id,
finished=True,
stage_id=1,
outputs=[SimpleNamespace(text="你好。", cumulative_text="你好。", finished=True)],
)
events = list(plane.project({"data_plane_outputs": [final]}))
context = [event for event in events if event.get("model_context_text")]
assert context and context[-1]["model_context_text"] == "你好。"
assert context[-1].get("is_listen") is False
assert context[-1].get("text") == ""
@@ -0,0 +1,115 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Aura duplex data-plane must surface Code2Wav MultimodalPayload audio."""
from __future__ import annotations
from types import SimpleNamespace
import torch
from vllm_omni.model_executor.models.aura_omni.duplex.data_plane import (
AuraDataPlaneSession,
_audio_value,
_multimodal,
)
from vllm_omni.outputs.mm_outputs import MultimodalPayload
def _encode(audio: object, sample_rate: int, fmt: str, speed: float | None) -> str | None:
del sample_rate, fmt, speed
if audio is None:
return None
return "encoded-audio"
def test_audio_value_accepts_from_raw_audio_key() -> None:
raw = {"model_outputs": [torch.zeros(8)], "sr": [torch.tensor(24000)]}
mm = MultimodalPayload.from_raw(raw, "audio")
assert mm is not None
assert "audio" in mm
audio = _audio_value(dict(mm))
assert isinstance(audio, torch.Tensor)
assert int(audio.numel()) == 8
def test_audio_value_accepts_wrongly_tagged_text_key() -> None:
# If stage output_modality is wrong, from_raw remaps model_outputs → "text".
raw = {"model_outputs": torch.zeros(4), "sr": torch.tensor(24000)}
mm = MultimodalPayload.from_raw(raw, "text")
assert mm is not None
audio = _audio_value(dict(mm))
assert isinstance(audio, torch.Tensor)
def test_multimodal_skips_empty_outer_dict_and_reads_completion() -> None:
mm = MultimodalPayload.from_dict({"audio": torch.zeros(3), "sr": torch.tensor(24000)})
completion = SimpleNamespace(multimodal_output=mm)
# Empty Mapping on the outer object must not hide completion payload.
outer = SimpleNamespace(multimodal_output={})
got = _multimodal(outer, completion)
assert "audio" in got
def test_project_emits_audio_from_multimodal_completion() -> None:
plane = AuraDataPlaneSession(_encode)
request_id = "duplex-s.test.e.0.r.stage0-turn0"
plane.begin_request(request_id)
mm = MultimodalPayload.from_raw(
{"model_outputs": [torch.zeros(16)], "sr": [torch.tensor(24000)]},
"audio",
)
completion = SimpleNamespace(
text="",
cumulative_text="",
finished=False,
multimodal_output=mm,
)
output = SimpleNamespace(
request_id=request_id,
finished=False,
stage_id=3,
outputs=[completion],
multimodal_output={},
)
events = list(plane.project({"data_plane_outputs": [output]}))
assert events, "project must emit TTS audio for MultimodalPayload"
assert any(event.get("audio") == "encoded-audio" for event in events)
def test_streaming_chunk_completion_finished_is_not_turn_eos() -> None:
"""Code2Wav chunks set CompletionOutput.finished; that must not EOT.
Runner._send_one_model_output_event drops is_terminal request ids before
emit. Marking terminal (or end_of_turn) on the first 8-frame chunk made
WS response.* never leave the engine.
"""
plane = AuraDataPlaneSession(_encode)
request_id = "duplex-s.test.e.0.r.stage0-turn0"
plane.begin_request(request_id)
mm = MultimodalPayload.from_raw(
{"model_outputs": [torch.zeros(16)], "sr": [torch.tensor(24000)]},
"audio",
)
completion = SimpleNamespace(
text="",
cumulative_text="",
finished=True,
multimodal_output=mm,
)
output = SimpleNamespace(
request_id=request_id,
finished=False,
stage_id=3,
outputs=[completion],
multimodal_output={},
)
events: list[dict[str, object]] = []
for event in plane.project({"data_plane_outputs": [output]}):
assert plane.is_terminal(request_id) is False
events.append(event)
assert events
assert events[0].get("audio") == "encoded-audio"
assert events[0].get("end_of_turn") is False
assert plane.is_terminal(request_id) is False
@@ -0,0 +1,598 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""CPU contract tests for AURA duplex plugin seams."""
from __future__ import annotations
import base64
from types import SimpleNamespace
import numpy as np
import pytest
from vllm.sampling_params import SamplingParams
from vllm_omni.engine.duplex.config import DuplexSessionConfig
from vllm_omni.engine.duplex.contracts import (
DuplexFence,
DuplexStageRequestContext,
DuplexStageSubmission,
duplex_ephemeral_stage_request_id,
duplex_resource_request_id,
)
from vllm_omni.engine.duplex.plugin import load_duplex_plugin
from vllm_omni.model_executor.models.aura_omni.duplex.data_plane import (
AuraDataPlaneContext,
AuraDataPlaneSession,
)
from vllm_omni.model_executor.models.aura_omni.duplex.history import (
drop_session_history,
get_or_create_session_history,
)
from vllm_omni.model_executor.models.aura_omni.duplex.input import AuraPcmAppendBuffer
from vllm_omni.model_executor.models.aura_omni.duplex.plugin import (
AURA_SILENT_TOKEN_ID,
AuraDuplexPlugin,
)
from vllm_omni.model_executor.stage_input_processors.aura_omni import SILENT_TEXT
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _encode_audio(audio: object, sample_rate: int, fmt: str, speed: float | None) -> str | None:
del audio, sample_rate, fmt, speed
return "ZmFrZQ=="
def test_load_aura_duplex_plugin_and_sampling_arity() -> None:
plugin = load_duplex_plugin(
"vllm_omni.model_executor.models.aura_omni.duplex.plugin.AuraDuplexPlugin",
_encode_audio,
)
assert isinstance(plugin, AuraDuplexPlugin)
assert plugin.plugin_id == "aura"
defaults = (
SamplingParams(max_tokens=16),
SamplingParams(max_tokens=16),
SamplingParams(max_tokens=4096),
SamplingParams(max_tokens=16),
)
configured = plugin.configure_sampling_params(runtime_config={}, defaults=defaults)
assert len(configured) == 4
assert configured[2].stop_token_ids == [2150]
assert configured[2].max_tokens == 4096
stage1_stops = set(configured[1].stop_token_ids or [])
assert {151669, 151645}.issubset(stage1_stops)
assert 248070 not in stage1_stops
caps = plugin.capabilities(max_sessions=1)
assert caps.supports_turn_commit_only is True
assert caps.supports_core_resumable_request is False
assert caps.supports_concurrent_turn_requests is True
assert caps.required_input_modalities == frozenset({"video"})
assert caps.optional_input_modalities == frozenset({"audio"})
assert caps.allows_video_without_audio() is True
assert caps.validate_append_modalities(has_audio=True, has_video=False) == (
"This duplex model requires video_frames on input_audio_buffer.append"
)
assert caps.validate_append_modalities(has_audio=True, has_video=True) is None
assert caps.validate_append_modalities(has_audio=False, has_video=True) is None
def test_commit_only_buffer_emits_on_commit() -> None:
buf = AuraPcmAppendBuffer()
samples = np.zeros(1600, dtype="<f4")
payload = {
"type": "audio",
"format": "pcm_f32le",
"sample_rate_hz": 16000,
"audio": base64.b64encode(samples.tobytes()).decode("ascii"),
"is_speech": True,
"video_frames": ["aGVsbG8="],
}
reservation = buf.prepare_append(
payload,
operation_id="op1",
chunk_period_ms=1000,
allow_emit=True,
)
assert reservation is None
assert buf.has_pending()
commit = buf.prepare_commit(operation_id="c1", chunk_period_ms=1000)
assert commit.payload is not None
assert commit.payload.get("final") is True
assert commit.payload.get("aura_turn_commit") is True
assert commit.payload.get("video_frames") == ["aGVsbG8="]
commit.commit()
def test_commit_reservation_includes_video_bytes() -> None:
buf = AuraPcmAppendBuffer()
pcm = np.zeros(1600, dtype="<f4").tobytes()
frames = ["aGVsbG8=", "d29ybGQ="]
payload = {
"type": "audio",
"format": "pcm_f32le",
"sample_rate_hz": 16000,
"audio": base64.b64encode(pcm).decode("ascii"),
"is_speech": True,
"video_frames": frames,
}
assert (
buf.prepare_append(
payload,
operation_id="op1",
chunk_period_ms=1000,
allow_emit=True,
)
is None
)
pending = buf.pending_byte_count
assert pending == len(pcm) + sum(len(frame) for frame in frames)
commit = buf.prepare_commit(operation_id="c1", chunk_period_ms=1000)
assert buf.pending_byte_count == 0
assert commit.byte_count == pending
commit.commit()
assert buf.pending_byte_count == 0
assert not buf.has_reserved()
def test_plan_append_commit_builds_stage0_prompt() -> None:
plugin = AuraDuplexPlugin(_encode_audio)
samples = np.zeros(800, dtype="<f4")
payload = {
"type": "audio",
"format": "pcm_f32le",
"sample_rate_hz": 16000,
"audio": base64.b64encode(samples.tobytes()).decode("ascii"),
"final": True,
"aura_turn_commit": True,
"is_speech": True,
}
fence = DuplexFence("sess-1", epoch=2, turn_id=5)
plan = plugin.plan_append(
request_id="req",
fence=fence,
session_config={},
runtime_config={"aura_system_prompt": "sys"},
seq=1,
turn_seq=1,
payload=payload,
final=True,
sampling_params=SamplingParams(max_tokens=8),
)
info = plan.prompt["additional_information"]
assert info["aura_duplex"] is True
assert info["session_id"] == "sess-1"
assert "audio" in plan.prompt["multi_modal_data"]
assert "prompt_token_ids" not in plan.prompt
def test_video_frames_to_mm_packs_one_video() -> None:
"""Two JPEG frames are one video clip, not two image items."""
from io import BytesIO
from PIL import Image
from vllm_omni.model_executor.models.aura_omni.duplex.plugin import _video_frames_to_mm
frames: list[str] = []
for color in ((255, 0, 0), (0, 255, 0)):
buf = BytesIO()
Image.new("RGB", (8, 8), color).save(buf, format="JPEG")
frames.append(base64.b64encode(buf.getvalue()).decode("ascii"))
mm = _video_frames_to_mm(frames)
assert list(mm.keys()) == ["video"]
assert len(mm["video"]) == 1
video, metadata = mm["video"][0]
assert np.asarray(video).shape == (2, 8, 8, 3)
assert metadata["total_num_frames"] == 2
assert metadata["do_sample_frames"] is False
def test_plan_append_vision_empty_audio_never_leaves_empty_prompt() -> None:
"""Frames + empty/near-silent audio must still get a Stage0 ASR pad."""
from io import BytesIO
from PIL import Image
plugin = AuraDuplexPlugin(_encode_audio)
img = Image.new("RGB", (32, 32), (8, 16, 24))
buf = BytesIO()
img.save(buf, format="JPEG")
frame = base64.b64encode(buf.getvalue()).decode("ascii")
fence = DuplexFence("sess-v", epoch=0, turn_id=1)
near = np.linspace(1e-5, -1e-5, 80, dtype="<f4")
cases = [
("empty_speech_true", True, ""),
("empty_speech_false", False, ""),
("near_silent", False, base64.b64encode(near.tobytes()).decode("ascii")),
]
for name, is_speech, audio in cases:
payload = {
"type": "audio",
"format": "pcm_f32le",
"sample_rate_hz": 16000,
"audio": audio,
"final": True,
"aura_turn_commit": True,
"is_speech": is_speech,
"video_frames": [frame],
}
plan = plugin.plan_append(
request_id="req",
fence=fence,
session_config={},
runtime_config={},
seq=1,
turn_seq=1,
payload=payload,
final=True,
sampling_params=SamplingParams(max_tokens=8),
)
assert plan.prompt.get("prompt"), f"{name}: Stage0 prompt must be non-empty"
assert "audio" in plan.prompt["multi_modal_data"], f"{name}: padded audio required"
assert "deferred_multi_modal_data" in plan.prompt["additional_information"]
def test_project_intermediate_output_targets_stage1_only() -> None:
plugin = AuraDuplexPlugin(_encode_audio)
assert plugin.project_intermediate_output(stage_id=1, output=object(), context=object()) is True
assert plugin.project_intermediate_output(stage_id=0, output=object(), context=object()) is False
assert plugin.project_intermediate_output(stage_id=3, output=object(), context=object()) is False
def test_release_concurrent_turn_requests_on_stage1_final() -> None:
plugin = AuraDuplexPlugin(_encode_audio)
assert plugin.release_concurrent_turn_requests(stage_id=1, segment_finished=True, output=object(), context=object())
assert not plugin.release_concurrent_turn_requests(
stage_id=1, segment_finished=False, output=object(), context=object()
)
finished = type("Out", (), {"finished": True})()
assert plugin.release_concurrent_turn_requests(
stage_id=1, segment_finished=False, output=finished, context=object()
)
assert not plugin.release_concurrent_turn_requests(
stage_id=2, segment_finished=True, output=object(), context=object()
)
def test_configure_sampling_keeps_silent_stop_visible() -> None:
plugin = AuraDuplexPlugin(_encode_audio)
defaults = (
SamplingParams(max_tokens=16),
SamplingParams(max_tokens=16),
SamplingParams(max_tokens=4096),
SamplingParams(max_tokens=16),
)
configured = plugin.configure_sampling_params(runtime_config={}, defaults=defaults)
stage1 = configured[1]
assert isinstance(stage1, SamplingParams)
assert stage1.include_stop_str_in_output is True
assert stage1.skip_special_tokens is False
assert AURA_SILENT_TOKEN_ID in (stage1.stop_token_ids or [])
def test_configure_sampling_keeps_explicit_talker_limits() -> None:
plugin = AuraDuplexPlugin(_encode_audio)
defaults = (
SamplingParams(max_tokens=16),
SamplingParams(max_tokens=16),
SamplingParams(max_tokens=500, stop_token_ids=[7]),
SamplingParams(max_tokens=16),
)
configured = plugin.configure_sampling_params(runtime_config={}, defaults=defaults)
stage2 = configured[2]
assert isinstance(stage2, SamplingParams)
assert stage2.max_tokens == 500
assert stage2.stop_token_ids == [7]
unset = plugin.configure_sampling_params(
runtime_config={},
defaults=(
SamplingParams(max_tokens=16),
SamplingParams(max_tokens=16),
SamplingParams(max_tokens=None),
SamplingParams(max_tokens=16),
),
)
assert unset[2].max_tokens == 240
assert unset[2].stop_token_ids == [2150]
def test_decide_output_silent_short_circuits() -> None:
plugin = AuraDuplexPlugin(_encode_audio)
class _Completion:
text = SILENT_TEXT
token_ids = [AURA_SILENT_TOKEN_ID]
finished = True
class _Output:
outputs = [_Completion()]
decision = plugin.decide_output(
stage_id=1,
final_stage_id=3,
segment_finished=True,
segment_token_ids=(AURA_SILENT_TOKEN_ID,),
segment_output_metadata={},
output=_Output(),
)
assert decision is not None
assert decision.metadata.get("model_listen") is True
def test_decide_output_chinese_silence_is_not_special_token() -> None:
plugin = AuraDuplexPlugin(_encode_audio)
class _Completion:
text = "[沉默]"
token_ids = [58, 107107, 60]
finished = True
class _Output:
outputs = [_Completion()]
assert (
plugin.decide_output(
stage_id=1,
final_stage_id=3,
segment_finished=True,
segment_token_ids=(58, 107107, 60),
segment_output_metadata={},
output=_Output(),
)
is None
)
def test_ephemeral_request_id_includes_turn() -> None:
fence = DuplexFence("s", epoch=1, turn_id=7)
resumable = duplex_resource_request_id(fence, "stage0")
ephemeral = duplex_ephemeral_stage_request_id(fence, stage_id=0)
assert ephemeral.endswith("stage0-turn7")
assert resumable != ephemeral
fence2 = DuplexFence("s", epoch=1, turn_id=8)
assert duplex_ephemeral_stage_request_id(fence, stage_id=0) != duplex_ephemeral_stage_request_id(fence2, stage_id=0)
def test_stage_submission_defaults_resumable_true() -> None:
ctx = DuplexStageRequestContext(
request_id="r",
session_id="s",
fence=DuplexFence("s"),
stage_id=0,
final_stage_id=3,
config_generation=0,
sampling_params=(SamplingParams(max_tokens=1),),
)
submission = DuplexStageSubmission(
context=ctx,
prompt={"prompt_token_ids": [0]},
already_submitted=False,
)
assert submission.resumable is True
def test_data_plane_close_session_drops_history() -> None:
from vllm_omni.model_executor.models.aura_omni.duplex.data_plane import AuraDataPlaneSession
from vllm_omni.model_executor.models.aura_omni.duplex.history import get_or_create_session_history
drop_session_history("close-hist")
get_or_create_session_history("close-hist").begin_user_turn("hi")
AuraDataPlaneSession(encode_audio=_encode_audio).close_session("close-hist")
from vllm_omni.model_executor.models.aura_omni.duplex.history import _STORE
assert "close-hist" not in _STORE
def test_session_history_strips_oldest_silent_videos() -> None:
drop_session_history("hist-ut")
history = get_or_create_session_history("hist-ut")
history.max_video_rounds = 2
history.video_rounds_to_remove = 1
for index in range(3):
history.begin_user_turn("", video=(f"clip-{index}",))
history.commit_turn(SILENT_TEXT)
kept = [message["video"] for message in history.messages if message.get("video") is not None]
assert kept == [("clip-1",), ("clip-2",)]
assert not any(message.get("video") == ("clip-0",) for message in history.messages)
drop_session_history("hist-ut")
def test_pipeline_declares_aura_duplex_plugin() -> None:
from vllm_omni.model_executor.models.aura_omni.pipeline import AURA_OMNI_PIPELINE
assert AURA_OMNI_PIPELINE.duplex_plugin == (
"vllm_omni.model_executor.models.aura_omni.duplex.plugin.AuraDuplexPlugin"
)
def test_project_output_uses_requested_format_and_chunk_duration() -> None:
seen: dict[str, object] = {}
def encode(audio: object, sample_rate: int, fmt: str, speed: float | None) -> str:
seen["fmt"] = fmt
seen["rate"] = sample_rate
seen["speed"] = speed
del audio
return "AAAA"
plane = AuraDataPlaneSession(encode)
class _Output:
request_id = "aura-turn"
finished = False
stage_id = 3
multimodal_output = {"audio": np.zeros(4800, dtype=np.float32), "sr": 24000}
events = list(plane.project_output(_Output(), context=AuraDataPlaneContext(response_format="pcm16", speed=1.0)))
assert seen["fmt"] == "pcm16"
assert events[0]["audio_format"] == "pcm16"
assert events[0]["audio_duration_ms"] == 200
assert events[0]["sample_rate_hz"] == 24000
def encode_wav(audio: object, sample_rate: int, fmt: str, speed: float | None) -> str:
del audio, sample_rate, speed
assert fmt == "wav"
return "V0FW"
plane_wav = AuraDataPlaneSession(encode_wav)
wav_events = list(plane_wav.project_output(_Output(), context=AuraDataPlaneContext(response_format="wav")))
assert wav_events[0]["audio_format"] == "wav"
def test_instructions_update_replaces_effective_prompt() -> None:
plugin = AuraDuplexPlugin(_encode_audio)
current = {"aura_system_prompt": "creation default", "instructions": "creation default"}
updated = plugin.runtime_config_for_update(
DuplexSessionConfig(model="aurateam/AURA", instructions="be brief"),
current,
)
assert updated["aura_system_prompt"] == "be brief"
assert updated["instructions"] == "be brief"
kept = plugin.runtime_config_for_update(
DuplexSessionConfig(
model="aurateam/AURA",
extra_body={"aura_system_prompt": "explicit"},
instructions="ignored when explicit",
),
updated,
)
assert kept["aura_system_prompt"] == "explicit"
def test_plan_partial_stage_output_hands_a_sentence_to_talker() -> None:
def aura2tts() -> None:
return None
class _Pool:
stage_client = SimpleNamespace(custom_process_input_func=aura2tts)
orchestrator = SimpleNamespace(
stage_pools={2: _Pool()},
_stage_receives_async_chunks=lambda stage_id: False,
)
req_state = SimpleNamespace(
session_owned=True,
final_stage_id=3,
request_id="req-1",
prompt={"additional_information": {}},
streaming=SimpleNamespace(bridge_states={}),
)
sentence = "这是一段用来直接测试句子级语音交接而且长度已经超过三十个字的内容。"
output = SimpleNamespace(
finished=False,
request_id="req-1",
outputs=[SimpleNamespace(cumulative_text=sentence)],
)
plan = AuraDuplexPlugin(_encode_audio).plan_partial_stage_output(orchestrator, 1, 0, output, req_state)
assert plan is not None
assert plan.close_only is False
assert plan.is_final_update is False
assert plan.output.text == sentence.strip()
assert req_state.prompt["additional_information"]["aura_tts_partial"] is True
assert AuraDuplexPlugin(_encode_audio).plan_partial_stage_output(orchestrator, 0, 0, output, req_state) is None
def _sentence_plan_fixture() -> tuple[SimpleNamespace, SimpleNamespace]:
def aura2tts() -> None:
return None
class _Pool:
stage_client = SimpleNamespace(custom_process_input_func=aura2tts)
orchestrator = SimpleNamespace(
stage_pools={2: _Pool()},
_stage_receives_async_chunks=lambda stage_id: False,
)
req_state = SimpleNamespace(
session_owned=True,
final_stage_id=3,
request_id="req-1",
prompt={"additional_information": {}},
streaming=SimpleNamespace(bridge_states={}),
)
return orchestrator, req_state
def _stage1_output(text: str, *, finished: bool) -> SimpleNamespace:
return SimpleNamespace(
finished=finished,
request_id="req-1",
outputs=[SimpleNamespace(cumulative_text=text)],
)
def test_finished_remainder_stays_resumable_until_close_sentinel() -> None:
"""A second sentence at Stage1 finish must not be the Talker end sentinel.
The first sentence is already a resumable Talker request. Marking the
remainder ``is_final_update`` makes ``StreamingUpdate.from_request``
return None, which aborts that in-flight sentence before Code2Wav has a
full codec group.
"""
orchestrator, req_state = _sentence_plan_fixture()
plugin = AuraDuplexPlugin(_encode_audio)
first = "我看到你身后是一个很明亮的办公室环境,天花板上能看到白色的管道和通风口,"
full = first + "远处还有办公桌和绿植。"
opened = plugin.plan_partial_stage_output(orchestrator, 1, 0, _stage1_output(first, finished=False), req_state)
assert opened is not None
assert opened.is_final_update is False
assert opened.queue_close_after is False
assert req_state.prompt["additional_information"]["aura_tts_close_only"] is False
tail = plugin.plan_partial_stage_output(orchestrator, 1, 0, _stage1_output(full, finished=True), req_state)
assert tail is not None
assert tail.close_only is False
assert tail.queue_close_after is True
assert tail.is_final_update is False
assert tail.output.text == "远处还有办公桌和绿植。"
assert req_state.prompt["additional_information"]["aura_tts_close_only"] is False
followup = plugin.partial_stage_followup(tail, req_state)
assert followup is not None
assert followup.is_final_update is True
assert followup.close_only is True
assert followup.output.text == ""
assert req_state.prompt["additional_information"]["aura_tts_close_only"] is True
def test_single_finished_sentence_is_one_non_resumable_submit() -> None:
orchestrator, req_state = _sentence_plan_fixture()
plugin = AuraDuplexPlugin(_encode_audio)
sentence = "你好,我看到一个戴眼镜、穿黑T恤的男生正对着镜头说话呢。"
plan = plugin.plan_partial_stage_output(orchestrator, 1, 0, _stage1_output(sentence, finished=True), req_state)
assert plan is not None
assert plan.queue_close_after is False
assert plan.is_final_update is True
assert plan.close_only is False
assert plugin.partial_stage_followup(plan, req_state) is None
def test_aura_draining_stages_are_declared_by_the_plugin() -> None:
plugin = AuraDuplexPlugin(_encode_audio)
assert plugin.draining_stage_ids(stage_count=4) == frozenset({2, 3})
assert plugin.draining_stage_ids(stage_count=2) == frozenset()
def test_spoken_stage0_text_is_the_user_transcript() -> None:
plugin = AuraDuplexPlugin(_encode_audio)
output = SimpleNamespace(
finished=True,
outputs=[
SimpleNamespace(text="language Chinese<asr_text>你好", cumulative_text="language Chinese<asr_text>你好")
],
)
prompt = {"additional_information": {"is_speech": True}}
assert plugin.user_transcript(stage_id=0, output=output, prompt=prompt, finished=True) == "你好"
def test_vision_follow_stage0_text_is_not_shown_as_user_speech() -> None:
plugin = AuraDuplexPlugin(_encode_audio)
output = SimpleNamespace(finished=True, text="嗯")
prompt = {"additional_information": {"is_speech": False}}
assert plugin.user_transcript(stage_id=0, output=output, prompt=prompt, finished=True) is None
assert plugin.user_transcript(stage_id=1, output=output, prompt=prompt, finished=True) is None
assert plugin.user_transcript(stage_id=0, output=output, prompt=prompt, finished=False) is None
@@ -0,0 +1,62 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Growing Stage1 token ids must decode to the same text as one full decode."""
from __future__ import annotations
from vllm_omni.model_executor.models.aura_omni.duplex.sentence_tts import decode_growing_token_ids
def _assert_matches_full(decode, ids: list[int]) -> None:
cache: dict[str, object] = {}
for end in range(1, len(ids) + 1):
got = decode_growing_token_ids(decode, ids[:end], cache)
assert got == decode(ids[:end])
def test_growing_decode_matches_full_decode_for_concat_pieces() -> None:
vocab = {1: "你", 2: "好", 3: "。", 4: "世", 5: "界"}
def decode(token_ids: list[int], skip_special_tokens: bool = False) -> str:
del skip_special_tokens
return "".join(vocab[token_id] for token_id in token_ids)
_assert_matches_full(decode, [1, 2, 3, 4, 5])
def test_growing_decode_matches_full_decode_when_the_previous_piece_changes() -> None:
"""Token 9 rewrites the previous character. The tail window must reject a bad splice."""
def decode(token_ids: list[int], skip_special_tokens: bool = False) -> str:
del skip_special_tokens
chars: list[str] = []
for token_id in token_ids:
if token_id == 9:
if chars:
chars.pop()
chars.append("X")
else:
chars.append(chr(96 + token_id))
return "".join(chars)
_assert_matches_full(decode, [1, 2, 9, 3])
def test_growing_decode_matches_wordpiece_spacing() -> None:
vocab = {1: "hello", 2: "##world", 3: "。"}
def decode(token_ids: list[int], skip_special_tokens: bool = False) -> str:
del skip_special_tokens
parts: list[str] = []
for index, token_id in enumerate(token_ids):
piece = vocab[token_id]
if piece.startswith("##"):
parts.append(piece[2:])
else:
if index:
parts.append(" ")
parts.append(piece)
return "".join(parts)
_assert_matches_full(decode, [1, 2, 3, 1])
@@ -389,6 +389,38 @@ def test_decode_batch_preprocess_matches_decode_state_updates(mrv2):
assert updates[1]["hidden_states"]["trailing_text"].numel() == 0
def test_decode_batch_zero_fills_missing_last_hidden(monkeypatch):
tts_pad = torch.full((1, 4), -1.0, dtype=torch.bfloat16)
model = _make_minimal_talker(tts_pad_embed=tts_pad)
def fake_embed_input_ids(input_ids):
return input_ids.to(torch.float32).reshape(-1, 1, 1).expand(-1, 1, 4)
model.embed_input_ids = fake_embed_input_ids
warnings: list[tuple[object, ...]] = []
monkeypatch.setattr(
"vllm_omni.model_executor.models.qwen3_tts.qwen3_tts_talker.logger.warning_once",
lambda *args, **kwargs: warnings.append(args),
)
trailing = torch.arange(8, dtype=torch.float32).reshape(2, 4)
_, _, past_hidden, _, _ = model.preprocess_decode_batch(
input_ids=torch.tensor([101], dtype=torch.long),
req_infos=[
{
"text": ["hello"],
"task_type": ["CustomVoice"],
"hidden_states": {"trailing_text": trailing},
"meta": {"talker_text_offset": 0},
}
],
)
assert torch.equal(past_hidden.cpu(), torch.zeros((1, 4), dtype=torch.bfloat16))
assert len(warnings) == 1
assert "zero-filling" in str(warnings[0][0])
def _stub_text_embedding(device_param: torch.nn.Parameter):
"""Build a lambda that emulates ``nn.Embedding`` for the ``_device()`` helper.
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from types import SimpleNamespace
@@ -10,6 +10,9 @@ from vllm_omni.model_executor.models.qwen3_tts.prompt_embeds_builder import (
)
from vllm_omni.model_executor.stage_input_processors.aura_omni import (
SILENT_TEXT,
_estimate_tts_prompt_len_from_token_ids,
_estimate_tts_prompt_len_official,
_normalize_asr_transcript,
asr2aura,
aura2tts,
)
@@ -166,7 +169,7 @@ def test_aura2tts_supports_custom_voice_mode():
assert tts_input["additional_information"]["task_type"] == ["CustomVoice"]
assert tts_input["additional_information"]["speaker"] == ["Vivian"]
assert "ref_audio" not in tts_input["additional_information"]
assert len(tts_input["prompt_token_ids"]) == 14
assert len(tts_input["prompt_token_ids"]) > 0
def test_aura2tts_passes_token_ids_to_qwen3_tts_when_enabled():
@@ -196,3 +199,109 @@ def test_aura2tts_passes_token_ids_to_qwen3_tts_when_enabled():
def test_aura2tts_drops_silent_response():
assert aura2tts([_source_output(SILENT_TEXT)]) == []
assert aura2tts([_source_output(f"{SILENT_TEXT}<|im_end|>")]) == []
def test_aura2tts_customvoice_uses_official_prompt_len_not_aura_token_count():
text = "当然可以,我正看着你呢。"
prompt = {
"additional_information": {
"tts_task_type": ["CustomVoice"],
"tts_speaker": ["Vivian"],
"tts_language": ["Chinese"],
}
}
aura_token_ids = list(range(80))
tts_info = {
"task_type": ["CustomVoice"],
"language": ["Chinese"],
"instruct": [""],
"text": [text],
"speaker": ["Vivian"],
}
official = _estimate_tts_prompt_len_official(tts_info, task_type="CustomVoice")
if official is None:
pytest.skip("Qwen3-TTS tokenizer unavailable for official prompt_len")
old_heuristic = _estimate_tts_prompt_len_from_token_ids(
aura_token_ids,
task_type="CustomVoice",
language="Chinese",
instruct="",
)
assert official != old_heuristic
[tts_input] = aura2tts(
[_source_output(text, token_ids=aura_token_ids)],
prompt=[prompt],
)
assert len(tts_input["prompt_token_ids"]) == official
assert tts_input["additional_information"]["instruct"] == [""]
def test_aura2tts_strips_im_end_from_spoken_text():
prompt = {
"additional_information": {
"tts_ref_audio": ["ref.wav"],
"tts_ref_text": ["Reference transcript sample."],
}
}
[tts_input] = aura2tts([_source_output("你好。<|im_end|>")], prompt=[prompt])
assert tts_input["additional_information"]["text"] == ["你好。"]
def test_aura2tts_does_not_treat_chinese_silence_as_special_token():
# [沉默] is not <|silent|>; prompt/session must instruct the real token.
prompt = {
"additional_information": {
"tts_ref_audio": ["ref.wav"],
"tts_ref_text": ["Reference transcript sample."],
}
}
assert len(aura2tts([_source_output("[沉默]")], prompt=[prompt])) == 1
def test_normalize_asr_transcript_strips_qwen3_asr_markup() -> None:
raw = "language Chinese<asr_text>出现《古韵》这本书的时候,提醒我。"
assert _normalize_asr_transcript(raw) == "出现《古韵》这本书的时候,提醒我。"
assert _normalize_asr_transcript("出现古韵这本书的时候提醒我。") == "出现古韵这本书的时候提醒我。"
def test_next_duplex_sentence_chunk_batches_until_min_chars(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("VLLM_AURA_SENTENCE_TTS", raising=False)
monkeypatch.delenv("VLLM_AURA_SENTENCE_TTS_MIN_CHARS", raising=False)
from vllm_omni.model_executor.stage_input_processors.aura_omni import (
next_duplex_sentence_chunk,
)
state: dict[str, object] = {}
first = "甲" * 12 + "。"
assert next_duplex_sentence_chunk(state, first, finished=False) is None
second = first + "乙" * 20 + "。"
chunk = next_duplex_sentence_chunk(state, second, finished=False)
assert chunk is not None
assert chunk.startswith("甲" * 12)
assert "乙" * 20 in chunk
def test_next_duplex_sentence_chunk_holds_think_silent_and_flushes(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("VLLM_AURA_SENTENCE_TTS", "1")
from vllm_omni.model_executor.stage_input_processors.aura_omni import (
next_duplex_sentence_chunk,
)
assert next_duplex_sentence_chunk({}, "<think>still thinking", finished=False) is None
assert next_duplex_sentence_chunk({}, "<|silent|>", finished=True) is None
assert next_duplex_sentence_chunk({}, '<tool_call>{"a":1}</tool_call>', finished=False) is None
state: dict[str, object] = {}
assert next_duplex_sentence_chunk(state, "你好。", finished=False) is None
assert next_duplex_sentence_chunk(state, "你好。", finished=True) == "你好。"
def test_next_duplex_sentence_chunk_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("VLLM_AURA_SENTENCE_TTS", "0")
from vllm_omni.model_executor.stage_input_processors.aura_omni import (
next_duplex_sentence_chunk,
)
assert next_duplex_sentence_chunk({}, "甲" * 40 + "。", finished=False) is None
@@ -0,0 +1,273 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Logic-equivalent checks adapted from AURA_026_p0exp stage processor tests."""
from __future__ import annotations
from types import SimpleNamespace
import pytest
from vllm_omni.model_executor.models.aura_omni.duplex.history import (
drop_session_history,
get_or_create_session_history,
)
from vllm_omni.model_executor.stage_input_processors.aura_omni import (
SILENT_TEXT,
asr2aura,
aura2tts,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _source_output(text: str, request_id: str = "r0") -> SimpleNamespace:
return SimpleNamespace(
request_id=request_id,
outputs=[SimpleNamespace(text=text, token_ids=[1, 2, 3])],
)
def test_asr2aura_carries_video_and_transcript() -> None:
prompt = {
"additional_information": {
"aura_system_prompt": "sys",
"deferred_multi_modal_data": {"image": ["frame"]},
},
"multi_modal_data": {},
}
[next_input] = asr2aura([_source_output("What is happening now?")], prompt=[prompt])
assert "What is happening now?" in next_input["prompt"]
assert next_input["multi_modal_data"].get("image") == ["frame"]
def test_asr2aura_supports_video_only_observation() -> None:
prompt = {
"additional_information": {
"deferred_multi_modal_data": {"image": ["frame"]},
},
"multi_modal_data": {},
}
[next_input] = asr2aura([_source_output("")], prompt=[prompt])
assert next_input["multi_modal_data"].get("image") == ["frame"]
def test_asr2aura_duplex_empty_transcript_is_not_a_vision_user_turn() -> None:
drop_session_history("duplex-vision")
prompt = {
"additional_information": {
"aura_duplex": True,
"session_id": "duplex-vision",
"is_speech": False,
"deferred_multi_modal_data": {"image": ["frame"]},
},
"multi_modal_data": {},
}
[next_input] = asr2aura([_source_output("noise")], prompt=[prompt])
assert "[vision]" not in next_input["prompt"]
history = get_or_create_session_history("duplex-vision")
history.commit_turn("saw a book")
assert all(message.get("content") != "[vision]" for message in history.messages)
assert [message["role"] for message in history.messages] == ["assistant"]
drop_session_history("duplex-vision")
def test_asr2aura_duplex_history_videos_match_prompt_pads() -> None:
"""Retained clips then this clip: one <|video_pad|> each, same order as mm data."""
drop_session_history("duplex-pads")
history = get_or_create_session_history("duplex-pads")
history.begin_user_turn("看着手", video=("clip-a", {"fps": 2.0}))
history.commit_turn("好的")
prompt = {
"additional_information": {
"aura_duplex": True,
"session_id": "duplex-pads",
"is_speech": False,
"deferred_multi_modal_data": {"video": [("clip-b", {"fps": 2.0})]},
},
"multi_modal_data": {},
}
[next_input] = asr2aura([_source_output("")], prompt=[prompt])
text = next_input["prompt"]
assert text.count("<|video_pad|>") == 2
assert "clip-a" not in text and "clip-b" not in text
assert next_input["multi_modal_data"]["video"] == [
("clip-a", {"fps": 2.0}),
("clip-b", {"fps": 2.0}),
]
assert text.index("<|video_pad|>") < text.rindex("<|im_start|>user")
drop_session_history("duplex-pads")
def test_asr2aura_duplex_uses_session_history_prefix() -> None:
drop_session_history("duplex-hist")
history = get_or_create_session_history("duplex-hist")
history.begin_user_turn("prev")
history.commit_turn("prev-answer")
prompt = {
"additional_information": {
"aura_duplex": True,
"session_id": "duplex-hist",
"deferred_multi_modal_data": {"image": ["frame"]},
},
"multi_modal_data": {},
}
[next_input] = asr2aura([_source_output("next question")], prompt=[prompt])
assert "prev-answer" in next_input["prompt"]
assert "next question" in next_input["prompt"]
drop_session_history("duplex-hist")
def test_asr2aura_vision_follow_keeps_previous_assistant_in_prefix() -> None:
drop_session_history("duplex-watch")
history = get_or_create_session_history("duplex-watch")
history.begin_user_turn("盯着滑鼠")
history.commit_turn("好的,出现的时候我会说")
prompt = {
"additional_information": {
"aura_duplex": True,
"session_id": "duplex-watch",
"is_speech": False,
"deferred_multi_modal_data": {"image": ["frame"]},
},
"multi_modal_data": {},
}
[next_input] = asr2aura([_source_output("noise")], prompt=[prompt])
assert "好的,出现的时候我会说" in next_input["prompt"]
drop_session_history("duplex-watch")
def test_aura2tts_drops_silent_response() -> None:
prompt = {"additional_information": {"tts_task_type": "CustomVoice"}}
assert aura2tts([_source_output(SILENT_TEXT)], prompt=[prompt]) == []
def test_aura2tts_duplex_does_not_commit_history() -> None:
"""Stage1 history has one owner: commit_model_context. aura2tts must not write a second row."""
drop_session_history("duplex-silent")
history = get_or_create_session_history("duplex-silent")
history.begin_user_turn("look")
prompt = {
"additional_information": {
"aura_duplex": True,
"session_id": "duplex-silent",
"tts_task_type": "CustomVoice",
}
}
assert aura2tts([_source_output(SILENT_TEXT)], prompt=[prompt]) == []
assert all(message.get("role") != "assistant" for message in history.messages)
from vllm_omni.model_executor.models.aura_omni.duplex.plugin import AuraDuplexPlugin
AuraDuplexPlugin(encode_audio=lambda *_a, **_k: None).commit_model_context(
session_id="duplex-silent",
assistant_text="只記一次",
)
assert [message["role"] for message in history.messages] == ["user", "assistant"]
assert history.messages[-1]["content"] == "只記一次"
drop_session_history("duplex-silent")
def test_aura2tts_skips_history_on_sentence_partial() -> None:
drop_session_history("duplex-partial")
history = get_or_create_session_history("duplex-partial")
history.begin_user_turn("look")
prompt = {
"additional_information": {
"aura_duplex": True,
"aura_tts_partial": True,
"session_id": "duplex-partial",
"tts_task_type": "CustomVoice",
"tts_speaker": "Vivian",
}
}
assert len(aura2tts([_source_output("你好,这是一句测试。")], prompt=[prompt])) == 1
assert all(message.get("role") != "assistant" for message in history.messages)
drop_session_history("duplex-partial")
def test_aura2tts_close_only_does_not_repeat_the_sentence() -> None:
prompt = {
"additional_information": {
"aura_tts_partial": True,
"aura_tts_close_only": True,
"tts_task_type": "CustomVoice",
"tts_speaker": "Vivian",
}
}
[request] = aura2tts([_source_output("上一句不該再送。")], prompt=[prompt])
info = request["additional_information"]
assert info["text"] == [""]
assert info["max_new_tokens"] == [1]
assert request["prompt_token_ids"] == [0]
def test_duplex_interception_commits_silent_history_without_aura2tts() -> None:
"""Silent Stage1 uses DIRECT_RESPONSE; history must commit on the data-plane path."""
from vllm.outputs import CompletionOutput
from vllm_omni.engine.duplex.contracts import (
DuplexFence,
DuplexOutputAction,
DuplexOutputDecision,
duplex_ephemeral_stage_request_id,
)
from vllm_omni.model_executor.models.aura_omni.duplex.data_plane import AuraDataPlaneSession
from vllm_omni.outputs import OmniRequestOutput
from vllm_omni.outputs.duplex import attach_duplex_output_decision
drop_session_history("duplex-silent-dp")
history = get_or_create_session_history("duplex-silent-dp")
history.begin_user_turn("look")
fence = DuplexFence("duplex-silent-dp", epoch=0, turn_id=1)
request_id = duplex_ephemeral_stage_request_id(fence, stage_id=1)
output = OmniRequestOutput(
request_id=request_id,
finished=True,
stage_id=1,
outputs=[
CompletionOutput(
index=0,
text=SILENT_TEXT,
token_ids=[151669],
cumulative_logprob=None,
logprobs=None,
)
],
)
attach_duplex_output_decision(
output,
DuplexOutputDecision(
action=DuplexOutputAction.DIRECT_RESPONSE,
metadata={"model_listen": True, "duplex_direct_response": True},
),
)
plane = AuraDataPlaneSession(encode_audio=lambda *_a, **_k: None)
events = list(plane.project_output(output))
assert events and events[0].get("silent") is True
assert events[0].get("model_context_text") == SILENT_TEXT
assert events[0].get("abort_data_plane_request") is True
assert history.pending_user == "look"
from vllm_omni.model_executor.models.aura_omni.duplex.plugin import AuraDuplexPlugin
AuraDuplexPlugin(encode_audio=lambda *_a, **_k: None).commit_model_context(
session_id="duplex-silent-dp",
assistant_text=str(events[0]["model_context_text"]),
)
assert history.pending_user is None
assert history.messages[-1]["content"] == SILENT_TEXT
drop_session_history("duplex-silent-dp")
def test_proactive_silent_ticks_do_not_evict_a_real_turn() -> None:
drop_session_history("duplex-silent-ticks")
history = get_or_create_session_history("duplex-silent-ticks")
history.begin_user_turn("真正的問題")
history.commit_turn("真正的回答")
for _ in range(20):
history.begin_user_turn("")
history.commit_turn("<|silent|>")
assert history.messages[0]["content"] == "真正的問題"
assert history.messages[1]["content"] == "真正的回答"
assert len(history.messages) == 2
drop_session_history("duplex-silent-ticks")