Feat: non_streaming_mode for Qwen3-TTS Base Models During Online Inference (#4198)

Signed-off-by: nagisa-kun <1434936049@qq.com>
Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
Co-authored-by: Yueqian Lin <linyueqian@outlook.com>
Co-authored-by: Sy03 <1370724210@qq.com>
This commit is contained in:
authored and GitHub committed 2026-06-11 22:15:05 +00:00
1 parent 03cc888485
commit 152343d5fb
5 files changed
+149 -4

No files matched your search

+5 -1
View File
@@ -114,6 +114,7 @@ Content-Type: application/json
| `instructions` | string | "" | Voice style/emotion instructions |
| `max_new_tokens` | integer | 2048 | Maximum tokens to generate |
| `initial_codec_chunk_frames` | integer | null | Per-request initial chunk size override for TTFA tuning. When null, IC is computed dynamically based on server load. |
| `non_streaming_mode` | bool | null | Qwen3-TTS prompt construction mode override. Does not affect HTTP response streaming or async-chunk pipelining. When null, Qwen3-TTS uses model defaults: Base=false, CustomVoice/VoiceDesign=true. |
| `stream` | bool | false | Stream raw PCM chunks as they are decoded (requires `response_format="pcm"`) |
**Supported languages:** Only applicable to Qwen3-TTS. Derived from the model configuration (`talker_config.codec_language_id` in the checkpoint's `config.json`), plus `Auto`, which is always accepted. Official Qwen3-TTS checkpoints support: Auto, Chinese, English, Japanese, Korean, German, French, Russian, Portuguese, Spanish, Italian.
@@ -332,7 +333,8 @@ curl -X POST http://localhost:8091/v1/audio/speech \
"input": "Hello, this is a cloned voice",
"task_type": "Base",
"ref_audio": "https://example.com/reference.wav",
"ref_text": "Original transcript of the reference audio"
"ref_text": "Original transcript of the reference audio",
"non_streaming_mode": true
}' --output cloned.wav
```
@@ -454,6 +456,7 @@ Content-Type: application/json
| `ref_audio` | string | null | Default reference audio (Base task) |
| `ref_text` | string | null | Default reference transcript (Base task) |
| `max_new_tokens` | integer | null | Default max tokens |
| `non_streaming_mode` | bool | null | Default Qwen3-TTS prompt construction mode override. Does not affect HTTP response streaming or async-chunk pipelining. When null, Qwen3-TTS uses model defaults: Base=false, CustomVoice/VoiceDesign=true. |
Each item in the `items` array requires only `input` (the text). All other fields are optional and override the batch-level defaults when set:
@@ -469,6 +472,7 @@ Each item in the `items` array requires only `input` (the text). All other field
| `ref_audio` | string | Override reference audio |
| `ref_text` | string | Override reference transcript |
| `max_new_tokens` | integer | Override max tokens |
| `non_streaming_mode` | bool | Override Qwen3-TTS prompt construction mode. Does not affect HTTP response streaming or async-chunk pipelining. When null, inherits the batch-level value (then the model default). |
### Response Format
@@ -31,6 +31,7 @@ from vllm_omni.entrypoints.openai.protocol.audio import (
CreateAudio,
OpenAICreateSpeechRequest,
SpeechBatchItem,
StreamingSpeechSessionConfig,
)
from vllm_omni.entrypoints.openai.serving_speech import (
_TTS_LANGUAGES,
@@ -1235,6 +1236,49 @@ class TestTTSMethods:
assert params["language"] == ["English"]
assert params["task_type"] == ["CustomVoice"]
def test_build_tts_params_base_non_streaming_mode_true(self, speech_server):
"""Base task should pass through an explicit non_streaming_mode override."""
req = OpenAICreateSpeechRequest(
input="Hello",
task_type="Base",
ref_audio="data:audio/wav;base64,abc",
ref_text="reference",
non_streaming_mode=True,
)
params = speech_server._build_tts_params(req)
assert params["task_type"] == ["Base"]
assert params["non_streaming_mode"] == [True]
def test_build_tts_params_base_omits_non_streaming_mode_by_default(self, speech_server):
"""Base task should keep using the model default when no override is sent."""
req = OpenAICreateSpeechRequest(
input="Hello",
task_type="Base",
ref_audio="data:audio/wav;base64,abc",
ref_text="reference",
)
params = speech_server._build_tts_params(req)
assert params["task_type"] == ["Base"]
assert "non_streaming_mode" not in params
def test_build_tts_params_explicit_non_streaming_mode_overrides_voicedesign_default(self, speech_server):
"""Explicit false should not be replaced by the VoiceDesign fallback."""
req = OpenAICreateSpeechRequest(
input="Hello",
task_type="VoiceDesign",
instructions="warm and calm",
non_streaming_mode=False,
)
params = speech_server._build_tts_params(req)
assert params["task_type"] == ["VoiceDesign"]
assert params["non_streaming_mode"] == [False]
def test_load_supported_speakers(self, mocker: MockerFixture):
"""Test _load_supported_speakers."""
mock_engine_client = mocker.MagicMock()
@@ -2023,6 +2067,34 @@ class TestMergeBatchItem:
assert merged.task_type == "CustomVoice"
assert merged.max_new_tokens == 512
def test_non_streaming_mode_batch_default_used(self):
"""Batch-level non_streaming_mode should be used when item doesn't specify one."""
batch = BatchSpeechRequest(
items=[SpeechBatchItem(input="hi")],
non_streaming_mode=True,
)
merged = OmniOpenAIServingSpeech._merge_batch_item(batch, batch.items[0])
assert merged.non_streaming_mode is True
def test_non_streaming_mode_item_override_wins(self):
"""Per-item false should override a true batch-level default."""
batch = BatchSpeechRequest(
items=[SpeechBatchItem(input="hi", non_streaming_mode=False)],
non_streaming_mode=True,
)
merged = OmniOpenAIServingSpeech._merge_batch_item(batch, batch.items[0])
assert merged.non_streaming_mode is False
def test_streaming_speech_session_config_accepts_non_streaming_mode():
config = StreamingSpeechSessionConfig(non_streaming_mode=True)
assert config.non_streaming_mode is True
class TestAsyncOmniSupportedTasks:
"""Test that AsyncOmni reports correct supported tasks based on output modalities."""
@@ -2811,6 +2883,52 @@ class TestTTSAsyncOffloading:
qwen3_tts_server._build_tts_params.assert_called_once()
qwen3_tts_server._estimate_prompt_len_async.assert_awaited_once()
def test_prepare_speech_generation_qwen3_voicedesign_non_streaming_mode_false(
self, qwen3_tts_server, mocker: MockerFixture
):
"""VoiceDesign explicit false should reach the model prompt additional_information."""
qwen3_tts_server._validate_tts_request = mocker.MagicMock(return_value=None)
qwen3_tts_server._estimate_prompt_len_async = mocker.AsyncMock(return_value=512)
request = OpenAICreateSpeechRequest(
input="hello",
task_type="VoiceDesign",
instructions="warm and calm",
non_streaming_mode=False,
)
_request_id, _generator, tts_params = asyncio.run(qwen3_tts_server._prepare_speech_generation(request))
assert tts_params["task_type"] == ["VoiceDesign"]
assert tts_params["non_streaming_mode"] == [False]
prompt = qwen3_tts_server.engine_client.generate.call_args.kwargs["prompt"]
assert prompt["additional_information"] is tts_params
assert prompt["additional_information"]["non_streaming_mode"] == [False]
def test_prepare_speech_generation_qwen3_base_non_streaming_mode_true(
self, qwen3_tts_server, mocker: MockerFixture
):
"""Base explicit true should reach the model prompt additional_information."""
qwen3_tts_server._validate_tts_request = mocker.MagicMock(return_value=None)
qwen3_tts_server._resolve_ref_audio = mocker.AsyncMock(return_value=([0.0] * 48000, 24000))
qwen3_tts_server._get_resolved_ref_audio_artifact_key = mocker.MagicMock(return_value=None)
qwen3_tts_server._estimate_prompt_len_async = mocker.AsyncMock(return_value=512)
request = OpenAICreateSpeechRequest(
input="hello",
task_type="Base",
ref_audio="data:audio/wav;base64,abc",
ref_text="reference transcript",
non_streaming_mode=True,
)
_request_id, _generator, tts_params = asyncio.run(qwen3_tts_server._prepare_speech_generation(request))
assert tts_params["task_type"] == ["Base"]
assert tts_params["ref_text"] == ["reference transcript"]
assert tts_params["non_streaming_mode"] == [True]
prompt = qwen3_tts_server.engine_client.generate.call_args.kwargs["prompt"]
assert prompt["additional_information"] is tts_params
assert prompt["additional_information"]["non_streaming_mode"] == [True]
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."""
wav_list = [0.0] * 48000
@@ -133,6 +133,14 @@ class OpenAICreateSpeechRequest(BaseModel):
ge=0,
description="Per-request initial chunk size override. If null, computed dynamically based on server load.",
)
non_streaming_mode: bool | None = Field(
default=None,
description=(
"Qwen3-TTS prompt construction mode override. This does not "
"control HTTP response streaming or async-chunk pipelining. "
"When null, use model defaults: Base=False, CustomVoice/VoiceDesign=True."
),
)
extra_params: dict[str, Any] | None = Field(
default=None,
description=("Optional model-specific parameters passed directly to the model's extra_args."),
@@ -370,6 +378,7 @@ class SpeechBatchItem(BaseModel):
x_vector_only_mode: bool | None = None
max_new_tokens: int | None = None
initial_codec_chunk_frames: int | None = Field(default=None, ge=0)
non_streaming_mode: bool | None = None
class BatchSpeechRequest(BaseModel):
@@ -389,6 +398,7 @@ class BatchSpeechRequest(BaseModel):
x_vector_only_mode: bool | None = None
max_new_tokens: int | None = None
initial_codec_chunk_frames: int | None = Field(default=None, ge=0)
non_streaming_mode: bool | None = None
class SpeechBatchItemResult(BaseModel):
@@ -423,6 +433,14 @@ class StreamingSpeechSessionConfig(BaseModel):
ge=0,
description="Initial chunk size for reduced TTFA. Overrides stage config for this session.",
)
non_streaming_mode: bool | None = Field(
default=None,
description=(
"Qwen3-TTS prompt construction mode override. This does not "
"control WebSocket audio streaming or async-chunk pipelining. "
"When null, use model defaults: Base=False, CustomVoice/VoiceDesign=True."
),
)
ref_audio: str | None = None
ref_text: str | None = None
x_vector_only_mode: bool | None = None
@@ -2815,9 +2815,12 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
if request.initial_codec_chunk_frames is not None:
params["initial_codec_chunk_frames"] = [request.initial_codec_chunk_frames]
# VoiceDesign requires non_streaming_mode (match offline script behaviour).
# CustomVoice and Base rely on the model default (True and False respectively).
if params["task_type"][0] == "VoiceDesign":
if request.non_streaming_mode is not None:
params["non_streaming_mode"] = [request.non_streaming_mode]
# Preserve the legacy VoiceDesign fallback when the request omits an
# explicit override. CustomVoice and Base rely on model defaults
# (True and False respectively).
elif params["task_type"][0] == "VoiceDesign":
params["non_streaming_mode"] = [True]
return params
@@ -3974,6 +3977,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
x_vector_only_mode=_pick("x_vector_only_mode"),
max_new_tokens=_pick("max_new_tokens"),
initial_codec_chunk_frames=_pick("initial_codec_chunk_frames"),
non_streaming_mode=_pick("non_streaming_mode"),
)
async def create_speech_batch(
@@ -221,6 +221,7 @@ class OmniStreamingSpeechHandler:
speed=config.speed,
max_new_tokens=config.max_new_tokens,
initial_codec_chunk_frames=config.initial_codec_chunk_frames,
non_streaming_mode=config.non_streaming_mode,
ref_audio=config.ref_audio,
ref_text=config.ref_text,
x_vector_only_mode=config.x_vector_only_mode,