[Bugfix] Add TTS configuration options (#1177)
Signed-off-by: Yanick Schraner <yanick.schraner@bs.ch>
This commit is contained in:
1 parent
1eebb374f1
commit
97f4c5b1e7
9 files changed
+220
-11
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user