# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project import base64 from pathlib import Path from types import SimpleNamespace import numpy as np import pytest from vllm.sampling_params import SamplingParams from tests.e2e.online_serving import personaplex_realtime_duplex as e2e_driver from vllm_omni.config.stage_config import ( load_deploy_config, merge_pipeline_deploy, ) from vllm_omni.experimental.fullduplex.engine.duplex_runtime import ( DuplexInputMode, ) from vllm_omni.experimental.fullduplex.engine.messages import DuplexFence from vllm_omni.experimental.fullduplex.personaplex.data_plane import ( PersonaPlexDataPlaneContext, PersonaPlexDataPlaneSession, ) from vllm_omni.experimental.fullduplex.personaplex.input import ( PersonaPlexPcmAppendBuffer, ) from vllm_omni.experimental.fullduplex.personaplex.runtime_extension import ( PersonaPlexDuplexRuntimeExtension, ) from vllm_omni.experimental.fullduplex.personaplex.serving_adapter import ( PersonaPlexServingRuntimeAdapter, ) from vllm_omni.model_executor.models.personaplex.pipeline import ( PERSONAPLEX_PIPELINE, ) pytestmark = [pytest.mark.core_model, pytest.mark.cpu] def _pcm_payload(samples: np.ndarray, *, sample_rate_hz: int = 24000) -> dict[str, object]: samples = np.ascontiguousarray(samples, dtype=" SimpleNamespace: frames = np.full((frame_count, e2e_driver.FRAME_SAMPLES), 7, dtype=" None: silent = np.zeros(e2e_driver.FRAME_SAMPLES, dtype=" None: partial_frame = np.zeros(e2e_driver.FRAME_SAMPLES - 1, dtype=" None: args = SimpleNamespace( max_frame_deficit=4, voiced_frame_rms_threshold=1e-3, min_voiced_frames=5, ) with pytest.raises(AssertionError, match="audible speech"): e2e_driver._session_result( _audio_client(frame_count), input_frames=frame_count, args=args, minimum_chunks=1, ) def test_e2e_driver_uses_absolute_audible_floor() -> None: args = SimpleNamespace( max_frame_deficit=4, voiced_frame_rms_threshold=1e-3, min_voiced_frames=5, ) e2e_driver._session_result( _audio_client(200, voiced_frames=5), input_frames=200, args=args, minimum_chunks=1, ) def test_e2e_driver_defaults_replacement_to_full_input() -> None: args = e2e_driver.parse_args( [ "--model", "/model", "--input-wav", "/input.wav", ] ) assert args.replacement_frames == 0 assert args.min_voiced_frames == 5 def test_e2e_driver_records_and_validates_input_sha256(tmp_path: Path) -> None: input_wav = tmp_path / "input.wav" input_wav.write_bytes(b"fixture-wav") identity = e2e_driver._input_identity( input_wav, expected_sha256="bade4b3c163edde390ff391207d34a887257d8a9cc3b621cc8c618b6e6761304", ) assert identity == { "path": str(input_wav.resolve()), "sha256": "bade4b3c163edde390ff391207d34a887257d8a9cc3b621cc8c618b6e6761304", } def test_e2e_driver_rejects_unexpected_input_sha256(tmp_path: Path) -> None: input_wav = tmp_path / "input.wav" input_wav.write_bytes(b"fixture-wav") with pytest.raises(ValueError, match="input WAV SHA-256 mismatch"): e2e_driver._input_identity(input_wav, expected_sha256="0" * 64) @pytest.mark.parametrize( ("attribute", "expected"), [ ( "duplex_runtime_extension", "vllm_omni.experimental.fullduplex.personaplex.runtime_extension.PersonaPlexDuplexRuntimeExtension", ), ( "duplex_serving_adapter", "vllm_omni.experimental.fullduplex.personaplex.serving_adapter.PersonaPlexServingRuntimeAdapter", ), ], ) def test_personaplex_pipeline_registers_unified_duplex_components( attribute: str, expected: str, ) -> None: assert PERSONAPLEX_PIPELINE.duplex_control_enabled is True assert getattr(PERSONAPLEX_PIPELINE, attribute) == expected @pytest.mark.parametrize( ("engine_arg", "expected"), [ ("skip_tokenizer_init", True), ("enable_prefix_caching", False), ], ) def test_personaplex_stage_engine_args( engine_arg: str, expected: bool, ) -> None: deploy_path = Path(__file__).parents[5] / "vllm_omni" / "deploy" / "personaplex.yaml" stages = merge_pipeline_deploy( PERSONAPLEX_PIPELINE, load_deploy_config(deploy_path), ) assert [stage.yaml_engine_args.get(engine_arg) for stage in stages] == [ expected, expected, ] def test_personaplex_duplex_capacity_is_propagated_to_all_model_stages() -> None: deploy_path = Path(__file__).parents[5] / "vllm_omni" / "deploy" / "personaplex.yaml" deploy = load_deploy_config(deploy_path) stages = merge_pipeline_deploy(PERSONAPLEX_PIPELINE, deploy) assert deploy.duplex_session.max_sessions == 2 assert [stage.yaml_engine_args.get("duplex_max_sessions") for stage in stages] == [2, 2] assert "personaplex_codec_max_sessions" not in deploy.connectors["connector_of_shared_memory"]["extra"] def test_personaplex_capabilities_are_honest() -> None: single = PersonaPlexServingRuntimeAdapter(lambda *_: None).capabilities(max_sessions=1) multi = PersonaPlexServingRuntimeAdapter(lambda *_: None).capabilities(max_sessions=2) assert multi.implementation_level == "model_native_duplex" assert multi.input_modes == ["append_audio_chunk"] assert multi.chunk_period_ms == 80 assert single.supports_multi_session is False assert multi.supports_multi_session is True assert multi.supports_multi_session_same_replica is True assert multi.supports_barge_in is False def test_pcm_buffer_emits_one_80ms_frame_transactionally() -> None: buffer = PersonaPlexPcmAppendBuffer() reservation = buffer.prepare_append( _pcm_payload(np.arange(1920, dtype=np.float32)), operation_id="op-1", chunk_period_ms=80, allow_emit=True, ) assert reservation is not None assert reservation.byte_count == 1920 * 4 assert buffer.pending_byte_count == 0 reservation.rollback() assert buffer.pending_byte_count == 1920 * 4 def test_pcm_buffer_rejects_changed_sample_rate() -> None: buffer = PersonaPlexPcmAppendBuffer() assert ( buffer.prepare_append( _pcm_payload(np.zeros(100, np.float32)), operation_id="op-1", chunk_period_ms=80, allow_emit=False, ) is None ) with pytest.raises(ValueError, match="sample_rate"): buffer.prepare_append( _pcm_payload(np.zeros(100, np.float32), sample_rate_hz=16000), operation_id="op-2", chunk_period_ms=80, allow_emit=False, ) def test_pcm_buffer_rejects_non_finite_samples() -> None: buffer = PersonaPlexPcmAppendBuffer() with pytest.raises(ValueError, match="finite"): buffer.prepare_append( _pcm_payload(np.array([0.0, np.nan], np.float32)), operation_id="op-1", chunk_period_ms=80, allow_emit=False, ) def test_runtime_extension_builds_one_frame_scheduler_append() -> None: extension = PersonaPlexDuplexRuntimeExtension() defaults = ( SamplingParams(temperature=0.8, top_k=10, max_tokens=10), SamplingParams(max_tokens=1024), ) configured = extension.configure_sampling_params(runtime_config={}, defaults=defaults) assert configured[0].temperature == 0.0 assert configured[0].top_k == 1 assert configured[0].max_tokens == 1 assert configured[1] is defaults[1] plan = extension.plan_append( request_id="req", fence=DuplexFence("session", incarnation=2, epoch=3), session_config={"instructions": "Be concise."}, runtime_config={"personaplex_prefill_slots": 4}, seq=1, turn_seq=1, mode=DuplexInputMode.APPEND_AUDIO_CHUNK, payload=_pcm_payload(np.zeros(1920, np.float32)), final=False, sampling_params=configured[0], ) assert len(plan.prompt["prompt_token_ids"]) == 5 duplex = plan.prompt["model_intermediate_buffer"]["duplex"] assert duplex["session_id"] == "session" assert duplex["incarnation"] == 2 assert duplex["epoch"] == 3 assert duplex["seq"] == 1 assert duplex["data_plane"] is True def test_projector_emits_only_cumulative_audio_and_text_suffixes() -> None: encoded_sizes: list[int] = [] def encode_audio(audio, _sample_rate, _response_format, _speed): size = int(np.asarray(audio, dtype=np.float32).size) encoded_sizes.append(size) return f"audio-{size}" projector = PersonaPlexDataPlaneSession(encode_audio) projector.begin_request("req") context = PersonaPlexDataPlaneContext(response_format="wav") first = SimpleNamespace( request_id="req", outputs=[SimpleNamespace(text="he", multimodal_output={})], multimodal_output={"model_outputs": np.arange(4, dtype=np.float32), "sr": 24000}, finished=False, ) second = SimpleNamespace( request_id="req", outputs=[SimpleNamespace(text="hello", multimodal_output={})], multimodal_output={"model_outputs": np.arange(6, dtype=np.float32), "sr": 24000}, finished=False, ) projected = list( projector.project( {"data_plane_outputs": [first, second]}, context=context, ) ) assert encoded_sizes == [4, 2] assert [item["text"] for item in projected] == ["he", "llo"] assert [item["audio_data"] for item in projected] == ["audio-4", "audio-2"] assert all(item["sample_rate_hz"] == 24000 for item in projected) assert all(item["end_of_turn"] is False for item in projected)