From d2aa07159c82f816b3c22c78e636aa3ef90994f5 Mon Sep 17 00:00:00 2001 From: Sicheng Hua <164001602+sc-hua@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:19:37 +0800 Subject: [PATCH] [Bugfix][Qwen3-TTS] Ignore voice labels for inline Base cloning (#6974) Signed-off-by: sc-hua --- .../test_qwen3_tts_base_expansion.py | 20 +++++++++++++++++++ .../openai_api/test_serving_speech.py | 13 ++++++++++-- .../openai/tts_adapters/qwen3_tts.py | 8 ++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/e2e/online_serving/test_qwen3_tts_base_expansion.py b/tests/e2e/online_serving/test_qwen3_tts_base_expansion.py index abf80fb85..5fa7672a0 100644 --- a/tests/e2e/online_serving/test_qwen3_tts_base_expansion.py +++ b/tests/e2e/online_serving/test_qwen3_tts_base_expansion.py @@ -152,3 +152,23 @@ def test_xvector_then_icl_same_ref_audio_keeps_engine_alive(omni_server, online_ online_client.send_audio_speech_request({**base_request, "x_vector_only_mode": True}) online_client.send_audio_speech_request({**base_request, "x_vector_only_mode": False, "ref_text": REF_TEXT}) online_client.send_audio_speech_request({**base_request, "x_vector_only_mode": True}) + + +@hardware_test(res={"cuda": "L4"}, num_cards=1) +@pytest.mark.parametrize("omni_server", tts_async_chunk_server_params, indirect=True) +def test_inline_ref_audio_cache_ignores_openai_voice_label(omni_server, online_client) -> None: + """An OpenAI voice label must not identify an inline Base voice clone.""" + base_request = { + "model": omni_server.model, + "input": get_prompt(), + "stream": False, + "timeout": DEFAULT_AUDIO_SPEECH_TIMEOUT_S, + "response_format": "wav", + "task_type": "Base", + "ref_audio": REF_AUDIO_URL, + "ref_text": REF_TEXT, + "min_audio_bytes": 1, + } + + online_client.send_audio_speech_request({**base_request, "voice": "voice-a"}) + online_client.send_audio_speech_request({**base_request, "voice": "voice-b"}) diff --git a/tests/entrypoints/openai_api/test_serving_speech.py b/tests/entrypoints/openai_api/test_serving_speech.py index 6508bea6b..7295418fb 100644 --- a/tests/entrypoints/openai_api/test_serving_speech.py +++ b/tests/entrypoints/openai_api/test_serving_speech.py @@ -5507,8 +5507,9 @@ class TestTTSAsyncOffloading: assert prompt["additional_information"]["non_streaming_mode"] == [True] assert "full_utterance_decode" not in prompt["additional_information"] - def test_qwen3_repeated_ref_audio_hot_path_sends_cache_key_without_waveform(self, qwen3_tts_server): - """After a ref artifact is marked ready, repeated requests avoid ref_audio payload IPC.""" + def test_qwen3_inline_ref_audio_hot_path_does_not_use_named_speaker_cache(self, qwen3_tts_server, mocker): + """An OpenAI-compatible voice does not identify an inline voice clone.""" + ignored_voice_log = mocker.patch("vllm_omni.entrypoints.openai.tts_adapters.qwen3_tts.logger.info") wav_list = [0.0] * 48000 artifact_key = "a" * 40 ref_audio = "data:audio/wav;base64,same" @@ -5531,6 +5532,7 @@ class TestTTSAsyncOffloading: request = OpenAICreateSpeechRequest( input="hello", + voice="voice-a", task_type="Base", ref_audio=ref_audio, ref_text="reference", @@ -5540,7 +5542,14 @@ class TestTTSAsyncOffloading: ) assert request_id == "req-hot" + assert request.voice == "voice-a" + ignored_voice_log.assert_called_once_with( + "Ignoring voice=%r for Qwen3-TTS Base request because inline ref_audio takes precedence", + "voice-a", + ) assert "ref_audio" not in tts_params + assert "speaker" not in tts_params + assert "voice_created_at" not in tts_params assert tts_params["_qwen3_tts_ref_audio_cache_key"] == [artifact_key] assert tts_params["ref_code_length"] == [50] prompt = qwen3_tts_server.engine_client.generate.call_args.kwargs["prompt"] diff --git a/vllm_omni/entrypoints/openai/tts_adapters/qwen3_tts.py b/vllm_omni/entrypoints/openai/tts_adapters/qwen3_tts.py index 07ee715ea..3a9a0a25d 100644 --- a/vllm_omni/entrypoints/openai/tts_adapters/qwen3_tts.py +++ b/vllm_omni/entrypoints/openai/tts_adapters/qwen3_tts.py @@ -431,6 +431,14 @@ class Qwen3TTSAdapter(ARTTSAdapter): Qwen3-TTS ref-audio artifact tracked after ``generate()``. """ server = self.ctx.server + # Inline Base cloning derives its voice from ref_audio, not the + # OpenAI-compatible voice label. + if has_inline_ref_audio and request.task_type == "Base" and request.voice is not None: + logger.info( + "Ignoring voice=%r for Qwen3-TTS Base request because inline ref_audio takes precedence", + request.voice, + ) + request = request.model_copy(update={"voice": None}) qwen3_ref_audio_warmup_artifact_key: str | None = None tts_params = self._build_tts_params(request) # Resolve ref_audio (explicit or auto-set for uploaded voices)