# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """ E2E online tests for Qwen3-TTS WebSocket streaming speech. """ import asyncio import json import os import pytest import websockets from tests.helpers.mark import hardware_test from tests.helpers.runtime import OmniServerParams from tests.helpers.stage_config import get_deploy_config_path os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" MODEL = "Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice" tts_ws_server_params = [ pytest.param( OmniServerParams( model=MODEL, stage_config_path=get_deploy_config_path("qwen3_tts.yaml"), server_args=["--trust-remote-code", "--enforce-eager", "--disable-log-stats"], env_dict={"VLLM_DISABLE_COMPILE_CACHE": "1"}, ), id="qwen3-tts-ws-customvoice", ) ] async def _run_ws_session(host: str, port: int, model: str) -> dict: uri = f"ws://{host}:{port}/v1/audio/speech/stream" starts: list[dict] = [] dones: list[dict] = [] chunk_lengths: dict[int, list[int]] = {} session_done: dict | None = None async with websockets.connect(uri, max_size=None) as ws: await ws.send( json.dumps( { "type": "session.config", "model": model, "voice": "vivian", "language": "English", "response_format": "pcm", "stream_audio": True, } ) ) await ws.send( json.dumps( { "type": "input.text", "text": ( "Hello, this is a websocket streaming test for Qwen three TTS, " "and this sentence is intentionally long enough to produce audio chunks. " "This is the second sentence." ), } ) ) await ws.send(json.dumps({"type": "input.done"})) while True: message = await asyncio.wait_for(ws.recv(), timeout=180) if isinstance(message, bytes): if not starts: raise AssertionError("Received audio bytes before audio.start") sentence_index = starts[-1]["sentence_index"] chunk_lengths.setdefault(sentence_index, []).append(len(message)) continue payload = json.loads(message) msg_type = payload.get("type") if msg_type == "audio.start": starts.append(payload) chunk_lengths.setdefault(payload["sentence_index"], []) elif msg_type == "audio.done": dones.append(payload) elif msg_type == "session.done": session_done = payload break elif msg_type == "error": raise AssertionError(f"WebSocket error: {payload['message']}") else: raise AssertionError(f"Unexpected WebSocket message: {payload}") return { "starts": starts, "dones": dones, "chunk_lengths": chunk_lengths, "session_done": session_done, } class TestQwen3TTSWebSocket: @pytest.mark.advanced_model @pytest.mark.tts @hardware_test(res={"cuda": "L4"}, num_cards=4) @pytest.mark.parametrize("omni_server", tts_ws_server_params, indirect=True) def test_streaming_pcm_output(self, omni_server) -> None: result = asyncio.run(_run_ws_session(omni_server.host, omni_server.port, omni_server.model)) starts = result["starts"] dones = result["dones"] chunk_lengths = result["chunk_lengths"] session_done = result["session_done"] assert session_done is not None assert session_done["total_sentences"] == 2 assert len(starts) == 2 assert len(dones) == 2 for idx, start in enumerate(starts): assert start["type"] == "audio.start" assert start["sentence_index"] == idx assert start["format"] == "pcm" assert start["sample_rate"] == 24000 assert start["sentence_text"] for done in dones: sentence_index = done["sentence_index"] total_bytes = done["total_bytes"] assert done["error"] is False assert total_bytes > 0 assert chunk_lengths[sentence_index], f"Expected binary PCM frames for sentence {sentence_index}" assert sum(chunk_lengths[sentence_index]) == total_bytes