diff --git a/tests/model_executor/models/qwen3_omni/test_qwen3_omni_default_speaker.py b/tests/model_executor/models/qwen3_omni/test_qwen3_omni_default_speaker.py new file mode 100644 index 000000000..4d8b2993d --- /dev/null +++ b/tests/model_executor/models/qwen3_omni/test_qwen3_omni_default_speaker.py @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project +"""Regression tests for the Qwen3-Omni default talker voice. + +The released checkpoint lists ``talker_config.speaker_id`` as +``{"chelsie": 2301, "ethan": 2302, "aiden": 2303}``. The default voice used to +be the first key, so a request that named no speaker was spoken by Chelsie, +while the reference ``generate(speaker="Ethan")`` (and other servers) use Ethan. +Benchmarks that omit ``speaker`` then compared two different voices. +""" + +import pytest + +from vllm_omni.model_executor.models.qwen3_omni.qwen3_omni import ( + DEFAULT_SPEAKER, + Qwen3OmniMoeForConditionalGeneration, + resolve_default_speaker, +) + +pytestmark = [pytest.mark.core_model, pytest.mark.cpu] + +# Order as shipped in Qwen/Qwen3-Omni-30B-A3B-Instruct config.json. +CHECKPOINT_SPEAKER_IDS = {"chelsie": 2301, "ethan": 2302, "aiden": 2303} + + +def test_default_speaker_is_ethan_even_when_listed_second() -> None: + assert DEFAULT_SPEAKER == "ethan" + assert resolve_default_speaker(CHECKPOINT_SPEAKER_IDS) == "ethan" + + +def test_default_speaker_falls_back_to_first_key_without_ethan() -> None: + assert resolve_default_speaker({"chelsie": 1, "aiden": 2}) == "chelsie" + + +def test_unknown_or_missing_speaker_uses_ethan_token() -> None: + model = object.__new__(Qwen3OmniMoeForConditionalGeneration) + model.tts_text_spk_token_ids = dict(CHECKPOINT_SPEAKER_IDS) + model.default_tts_text_spk_type = resolve_default_speaker(model.tts_text_spk_token_ids) + + assert model._get_text_spk_token_id("not-a-speaker") == CHECKPOINT_SPEAKER_IDS["ethan"] + assert model._get_text_spk_token_id(model.default_tts_text_spk_type) == CHECKPOINT_SPEAKER_IDS["ethan"] + assert model._get_text_spk_token_id("chelsie") == CHECKPOINT_SPEAKER_IDS["chelsie"] diff --git a/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py b/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py index 7b8c6844b..92b1d8c6d 100644 --- a/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py +++ b/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py @@ -67,6 +67,20 @@ from vllm_omni.model_executor.stage_input_processors.chunk_size_utils import par from vllm_omni.platforms import current_omni_platform from vllm_omni.utils.device_copy import to_device_nonblocking +# Voice used when a request names no speaker. Matches the reference +# ``Qwen3OmniMoeForConditionalGeneration.generate(speaker="Ethan")``; the +# checkpoint's ``talker_config.speaker_id`` lists "chelsie" first, so taking the +# first key would silently switch the default to a different (female) voice. +DEFAULT_SPEAKER = "ethan" + + +def resolve_default_speaker(speaker_token_ids: dict[str, int]) -> str: + """Return the speaker key used for requests that do not name one.""" + if DEFAULT_SPEAKER in speaker_token_ids: + return DEFAULT_SPEAKER + return next(iter(speaker_token_ids)) + + # Special token IDs for Qwen3 Omni MoE # Reference: https://huggingface.co/Qwen/Qwen3-Omni-30B-A3B-Instruct/blob/main/tokenizer_config.json @@ -552,7 +566,7 @@ class Qwen3OmniMoeForConditionalGeneration( intermediate_tensors: IntermediateTensors | None = None, inputs_embeds: torch.Tensor | None = None, generate_audio: bool = True, - voice_type: str = "ethan", + voice_type: str = DEFAULT_SPEAKER, codec: torch.Tensor | None = None, sampling_metadata: SamplingMetadata | None = None, logits_index: int | None = None, @@ -963,7 +977,7 @@ class Qwen3OmniMoeForConditionalGeneration( "prefix_caching": talker_hf_config.audio_start_token_id, } - self.default_tts_text_spk_type = list(self.tts_text_spk_token_ids.keys())[0] + self.default_tts_text_spk_type = resolve_default_speaker(self.tts_text_spk_token_ids) return set(["thinker_embedding.weight", "talker_embedding.weight"])