[Bugfix] Add TTS configuration options (#1177)

Signed-off-by: Yanick Schraner <yanick.schraner@bs.ch>
This commit is contained in:
Yanick Schraner authored and GitHub committed 2026-02-26 00:14:44 +08:00
1 parent 1eebb374f1
commit 97f4c5b1e7
9 files changed
+220 -11

No files matched your search

+12
View File
@@ -273,3 +273,15 @@ Default: `True`
Penalty applied to tokens that have already appeared in the generated sequence. Values greater than 1.0 discourage repetition, while values less than 1.0 encourage it. A value of 1.0 applies no penalty.
Default: `1.1`
### `tts_args` (TTS stages only)
Configuration for Text-to-Speech specific parameters. This section is only applicable to TTS model stages (e.g., `qwen3_tts`).
#### `tts_args.max_instructions_length`
Maximum character length for voice style/emotion instructions. Instructions exceeding this limit will be rejected with a validation error.
Default: `500`
This value can be overridden at runtime using the `--tts-max-instructions-length` CLI parameter when starting the server.
@@ -90,6 +90,25 @@ curl -X POST http://localhost:8091/v1/audio/speech \
curl http://localhost:8091/v1/audio/voices
```
## Server Configuration
### TTS-Specific CLI Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `--tts-max-instructions-length` | int | 500 | Maximum character length for voice style instructions |
**Example with custom instructions length:**
```bash
vllm serve Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
--omni \
--port 8000 \
--tts-max-instructions-length 1000
```
This is useful when you need longer voice style descriptions, especially for the VoiceDesign task type.
## API Reference
### Endpoint
@@ -291,6 +291,7 @@ class TestTTSMethods:
mock_engine_client = MagicMock()
mock_engine_client.errored = False
mock_engine_client.stage_list = None
mock_engine_client.tts_max_instructions_length = None
mock_models = MagicMock()
mock_models.is_base_model.return_value = True
return OmniOpenAIServingSpeech(
@@ -299,16 +300,35 @@ class TestTTSMethods:
request_logger=MagicMock(),
)
def test_is_tts_model(self, speech_server):
"""Test TTS model detection."""
# No stage_list -> False
assert speech_server._is_tts_model() is False
def test_is_tts_detection_no_stage(self, speech_server):
"""Test TTS model detection when no TTS stage exists."""
# Fixture creates server with stage_list = None -> _is_tts should be False
assert speech_server._is_tts is False
assert speech_server._tts_stage is None
# With qwen3_tts stage -> True
def test_is_tts_detection_with_tts_stage(self):
"""Test TTS model detection when TTS stage exists."""
mock_engine_client = MagicMock()
mock_engine_client.errored = False
mock_engine_client.tts_max_instructions_length = None
# Create a TTS stage
mock_stage = MagicMock()
mock_stage.model_stage = "qwen3_tts"
speech_server.engine_client.stage_list = [mock_stage]
assert speech_server._is_tts_model() is True
mock_stage.tts_args = {}
mock_engine_client.stage_list = [mock_stage]
mock_models = MagicMock()
mock_models.is_base_model.return_value = True
server = OmniOpenAIServingSpeech(
engine_client=mock_engine_client,
models=mock_models,
request_logger=MagicMock(),
)
assert server._is_tts is True
assert server._tts_stage is mock_stage
def test_estimate_prompt_len_fallback(self, speech_server):
"""Test prompt length estimation falls back to 2048 when model is unavailable."""
@@ -381,3 +401,108 @@ class TestTTSMethods:
# Verify speakers are normalized to lowercase
assert server.supported_speakers == {"ryan", "vivian", "aiden"}
def test_max_instructions_length_default(self, speech_server):
"""Test default max instructions length (500) when no config provided."""
# Fixture creates server with no CLI override and no TTS stage
assert speech_server._max_instructions_length == 500
def test_max_instructions_length_cli_override(self):
"""Test CLI override (stored in engine_client) takes highest priority."""
mock_engine_client = MagicMock()
mock_engine_client.errored = False
mock_engine_client.stage_list = None
# CLI override is stored in engine_client
mock_engine_client.tts_max_instructions_length = 1000
mock_models = MagicMock()
mock_models.is_base_model.return_value = True
server = OmniOpenAIServingSpeech(
engine_client=mock_engine_client,
models=mock_models,
request_logger=MagicMock(),
)
# Value is cached during __init__
assert server._max_instructions_length == 1000
def test_max_instructions_length_stage_config(self):
"""Test stage config value is used when no CLI override."""
mock_engine_client = MagicMock()
mock_engine_client.errored = False
mock_engine_client.tts_max_instructions_length = None # No CLI override
mock_models = MagicMock()
mock_models.is_base_model.return_value = True
# Mock stage with tts_args
mock_stage = MagicMock()
mock_stage.model_stage = "qwen3_tts"
mock_stage.tts_args = {"max_instructions_length": 750}
mock_engine_client.stage_list = [mock_stage]
server = OmniOpenAIServingSpeech(
engine_client=mock_engine_client,
models=mock_models,
request_logger=MagicMock(),
)
# Value is cached during __init__
assert server._max_instructions_length == 750
def test_max_instructions_length_cli_overrides_stage_config(self):
"""Test CLI override (in engine_client) takes precedence over stage config."""
mock_engine_client = MagicMock()
mock_engine_client.errored = False
# CLI override stored in engine_client
mock_engine_client.tts_max_instructions_length = 2000
mock_models = MagicMock()
mock_models.is_base_model.return_value = True
# Mock stage with tts_args
mock_stage = MagicMock()
mock_stage.model_stage = "qwen3_tts"
mock_stage.tts_args = {"max_instructions_length": 750}
mock_engine_client.stage_list = [mock_stage]
server = OmniOpenAIServingSpeech(
engine_client=mock_engine_client,
models=mock_models,
request_logger=MagicMock(),
)
# CLI value (2000) should override stage config (750)
assert server._max_instructions_length == 2000
def test_validate_instructions_length_uses_cached_value(self):
"""Test instructions length validation uses cached _max_instructions_length."""
mock_engine_client = MagicMock()
mock_engine_client.errored = False
mock_engine_client.stage_list = None
# CLI override with max length of 10 characters
mock_engine_client.tts_max_instructions_length = 10
mock_models = MagicMock()
mock_models.is_base_model.return_value = True
server = OmniOpenAIServingSpeech(
engine_client=mock_engine_client,
models=mock_models,
request_logger=MagicMock(),
)
# Verify cached value
assert server._max_instructions_length == 10
# Instructions within limit should pass
req = OpenAICreateSpeechRequest(
input="Hello",
task_type="VoiceDesign",
instructions="short",
)
assert server._validate_tts_request(req) is None
# Instructions exceeding limit should fail
req = OpenAICreateSpeechRequest(
input="Hello",
task_type="VoiceDesign",
instructions="this is too long",
)
error = server._validate_tts_request(req)
assert error is not None
assert "max 10 characters" in error
+8
View File
@@ -290,6 +290,14 @@ class OmniServeCommand(CLISubcommand):
type=int,
help="The max size of generate image (height * width).",
)
# TTS-specific parameters
omni_config_group.add_argument(
"--tts-max-instructions-length",
type=int,
default=None,
help="Maximum length for TTS voice style instructions (overrides stage config, default: 500).",
)
return serve_parser
+3
View File
@@ -257,6 +257,9 @@ class OmniBase:
stage_configs_path = kwargs.get("stage_configs_path", None)
# TTS-specific CLI overrides
self.tts_max_instructions_length: int | None = kwargs.get("tts_max_instructions_length", None)
# Load stage configurations from YAML
config_path, stage_configs = load_and_resolve_stage_configs(
model,
+1
View File
@@ -285,6 +285,7 @@ class OmniStage:
self.final_output = getattr(stage_config, "final_output", False)
self.final_output_type = getattr(stage_config, "final_output_type", None)
self.tts_args = _to_dict(getattr(stage_config, "tts_args", {}))
default_sampling_params = getattr(stage_config, "default_sampling_params", {})
# For LLM stage, this can directly be a SamplingParams-compatible dict;
# For diffusion stage, this only serves as default values for diffusion kwargs.
+41 -4
View File
@@ -61,11 +61,48 @@ _TTS_MAX_NEW_TOKENS_MAX = 4096
class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Find and cache the TTS stage (if any) during initialization
self._tts_stage = self._find_tts_stage()
self._is_tts = self._tts_stage is not None
# Cache TTS configuration values (computed once, reused per request)
self._max_instructions_length = self._compute_max_instructions_length()
# Load supported speakers
self.supported_speakers = self._load_supported_speakers()
logger.info(f"Loaded {len(self.supported_speakers)} supported speakers: {sorted(self.supported_speakers)}")
self._tts_tokenizer = None
def _find_tts_stage(self):
"""Find and return the TTS stage from the stage list, or None if not found."""
stage_list = getattr(self.engine_client, "stage_list", None)
if stage_list is None:
return None
for stage in stage_list:
if getattr(stage, "model_stage", None) in _TTS_MODEL_STAGES:
return stage
return None
def _compute_max_instructions_length(self) -> int:
"""Compute max instructions length with precedence: CLI > stage config > default.
Called once during initialization; result is cached in self._max_instructions_length.
"""
# 1. CLI override takes highest priority (stored in engine_client)
cli_override = getattr(self.engine_client, "tts_max_instructions_length", None)
if cli_override is not None:
return cli_override
# 2. Try to get from TTS stage config
if self._tts_stage is not None:
tts_args = getattr(self._tts_stage, "tts_args", {})
if "max_instructions_length" in tts_args:
return tts_args["max_instructions_length"]
# 3. Default fallback
return _TTS_MAX_INSTRUCTIONS_LENGTH
def _load_supported_speakers(self) -> set[str]:
"""Load supported speakers (case-insensitive) from the model configuration."""
try:
@@ -164,9 +201,9 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
if task_type == "VoiceDesign" and not request.instructions:
return "VoiceDesign task requires 'instructions' to describe the voice"
# Validate instructions length
if request.instructions and len(request.instructions) > _TTS_MAX_INSTRUCTIONS_LENGTH:
return f"Instructions too long (max {_TTS_MAX_INSTRUCTIONS_LENGTH} characters)"
# Validate instructions length (using cached value from initialization)
if request.instructions and len(request.instructions) > self._max_instructions_length:
return f"Instructions too long (max {self._max_instructions_length} characters)"
# Validate max_new_tokens range
if request.max_new_tokens is not None:
@@ -300,7 +337,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
request_id = f"speech-{random_uuid()}"
try:
if self._is_tts_model():
if self._is_tts:
# Validate TTS parameters
validation_error = self._validate_tts_request(request)
if validation_error:
@@ -65,6 +65,8 @@ stage_args:
# Distributed connector configuration
input_connectors:
from_stage_0: connector_of_shared_memory
tts_args:
max_instructions_length: 500
default_sampling_params:
temperature: 0.0
top_p: 1.0
@@ -59,6 +59,8 @@ stage_args:
final_output_type: audio
input_connectors:
from_stage_0: connector_of_shared_memory
tts_args:
max_instructions_length: 500
default_sampling_params:
temperature: 0.0
top_p: 1.0