[Migrate] Move sampling parameter overrides from legacy dispatches to TTS adapters (#5272)
Signed-off-by: boatman <109857087+sphinxkkkbc@users.noreply.github.com>
This commit is contained in:
1 parent
4616823125
commit
65b3d41b3a
22 files changed
+565
-424
No files matched your search
@@ -66,7 +66,7 @@ repos:
|
||||
entry: python tools/pre_commit/check_tts_adapter.py
|
||||
language: python
|
||||
pass_filenames: false
|
||||
files: ^(vllm_omni/entrypoints/openai/(serving_speech\.py|tts_adapters/__init__\.py)|tools/pre_commit/check_tts_adapter\.py)$
|
||||
files: ^(vllm_omni/entrypoints/openai/serving_speech\.py|tools/pre_commit/check_tts_adapter\.py)$
|
||||
# The budget-is-now-slack reminder is printed on a PASSING run, and
|
||||
# pre-commit hides output from passing hooks unless the hook is verbose.
|
||||
verbose: true
|
||||
|
||||
@@ -26,8 +26,13 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def _serving(model_type: str = "audex") -> OmniOpenAIServingSpeech:
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import resolve_adapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import SpeechServingContext
|
||||
|
||||
serving = OmniOpenAIServingSpeech.__new__(OmniOpenAIServingSpeech)
|
||||
serving._tts_model_type = model_type
|
||||
adapter_cls = resolve_adapter(model_type)
|
||||
serving._adapter = adapter_cls(SpeechServingContext(server=serving)) if adapter_cls is not None else None
|
||||
serving._mark_ref_audio_artifact_ready_for_request = lambda request_id: None
|
||||
serving._discard_ref_audio_artifact_warmup = lambda request_id: None
|
||||
return serving
|
||||
@@ -154,7 +159,7 @@ class TestAudexCfgValidation:
|
||||
assert _adapter().validate(_speech_request(cfg_scale=None)) is None
|
||||
serving = TestAudexCfgInjection()._serving_with_fake_tokenizer()
|
||||
params = SimpleNamespace(extra_args={"cfg_scale": None})
|
||||
out = serving._inject_audex_cfg_pair_args("req-1", {"prompt": "p"}, params)
|
||||
out = serving._adapter._inject_audex_cfg_pair_args("req-1", {"prompt": "p"}, params)
|
||||
assert out.extra_args == {}
|
||||
|
||||
@pytest.mark.parametrize("key", ["cfg_role", "cfg_pair_id", "cfg_null_prompt"])
|
||||
@@ -177,21 +182,21 @@ class TestAudexCfgInjection:
|
||||
def test_no_extra_args_is_untouched(self):
|
||||
serving = self._serving_with_fake_tokenizer()
|
||||
params = SimpleNamespace(extra_args=None)
|
||||
out = serving._inject_audex_cfg_pair_args("req-1", {"prompt": self._cond_prompt()}, params)
|
||||
out = serving._adapter._inject_audex_cfg_pair_args("req-1", {"prompt": self._cond_prompt()}, params)
|
||||
assert out.extra_args is None
|
||||
assert params.extra_args is None
|
||||
|
||||
def test_scale_one_leaves_params_byte_identical(self):
|
||||
serving = self._serving_with_fake_tokenizer()
|
||||
params = SimpleNamespace(extra_args={"cfg_scale": 1.0})
|
||||
out = serving._inject_audex_cfg_pair_args("req-1", {"prompt": self._cond_prompt()}, params)
|
||||
out = serving._adapter._inject_audex_cfg_pair_args("req-1", {"prompt": self._cond_prompt()}, params)
|
||||
assert out.extra_args == {}
|
||||
|
||||
def test_guided_request_gains_pair_contract(self):
|
||||
serving = self._serving_with_fake_tokenizer()
|
||||
params = SimpleNamespace(extra_args={"cfg_scale": 1.5})
|
||||
cond = self._cond_prompt("Some transcription text here.")
|
||||
out = serving._inject_audex_cfg_pair_args("req-42", {"prompt": cond}, params)
|
||||
out = serving._adapter._inject_audex_cfg_pair_args("req-42", {"prompt": cond}, params)
|
||||
|
||||
extra = out.extra_args
|
||||
assert extra["cfg_scale"] == 1.5
|
||||
@@ -207,14 +212,14 @@ class TestAudexCfgInjection:
|
||||
serving = self._serving_with_fake_tokenizer()
|
||||
params = SimpleNamespace(extra_args={"cfg_scale": 1.5})
|
||||
with pytest.raises(ValueError, match="adapter-built text prompt"):
|
||||
serving._inject_audex_cfg_pair_args("req-1", {"prompt_token_ids": [1]}, params)
|
||||
serving._adapter._inject_audex_cfg_pair_args("req-1", {"prompt_token_ids": [1]}, params)
|
||||
|
||||
def test_injection_never_mutates_input_params(self):
|
||||
"""The input may be the engine's SHARED default params (review P2):
|
||||
pair state must land only on the returned clone."""
|
||||
serving = self._serving_with_fake_tokenizer()
|
||||
shared = SimpleNamespace(extra_args={"cfg_scale": 1.5}, temperature=0.1)
|
||||
out = serving._inject_audex_cfg_pair_args("req-7", {"prompt": self._cond_prompt()}, shared)
|
||||
out = serving._adapter._inject_audex_cfg_pair_args("req-7", {"prompt": self._cond_prompt()}, shared)
|
||||
assert out is not shared
|
||||
assert shared.extra_args == {"cfg_scale": 1.5}
|
||||
assert shared.temperature == 0.1
|
||||
@@ -252,11 +257,11 @@ def test_guided_injection_sets_guided_temperature():
|
||||
serving = TestAudexCfgInjection()._serving_with_fake_tokenizer()
|
||||
cond = TestAudexCfgInjection()._cond_prompt()
|
||||
params = SimpleNamespace(extra_args={"cfg_scale": 1.5}, temperature=0.1)
|
||||
out = serving._inject_audex_cfg_pair_args("req-1", {"prompt": cond}, params)
|
||||
out = serving._adapter._inject_audex_cfg_pair_args("req-1", {"prompt": cond}, params)
|
||||
assert out.temperature == 0.05
|
||||
|
||||
unguided = SimpleNamespace(extra_args={"cfg_scale": 1.0}, temperature=0.1)
|
||||
out = serving._inject_audex_cfg_pair_args("req-2", {"prompt": cond}, unguided)
|
||||
out = serving._adapter._inject_audex_cfg_pair_args("req-2", {"prompt": cond}, unguided)
|
||||
assert out.temperature == 0.1
|
||||
|
||||
|
||||
@@ -340,7 +345,7 @@ class TestAudexTTAInjection:
|
||||
def test_default_injection_applies_rvq_and_cfg3(self):
|
||||
serving = self._serving()
|
||||
params = SimpleNamespace(extra_args=None)
|
||||
out = serving._inject_audex_tta_args("req-9", self._prompt(), params)
|
||||
out = serving._adapter._inject_audex_tta_args("req-9", self._prompt(), params)
|
||||
|
||||
extra = out.extra_args
|
||||
rvq = extra["tta_rvq"]
|
||||
@@ -355,15 +360,15 @@ class TestAudexTTAInjection:
|
||||
def test_explicit_scale_one_disables_cfg_but_keeps_rvq(self):
|
||||
serving = self._serving()
|
||||
params = SimpleNamespace(extra_args={"cfg_scale": 1.0})
|
||||
out = serving._inject_audex_tta_args("req-1", self._prompt(), params)
|
||||
out = serving._adapter._inject_audex_tta_args("req-1", self._prompt(), params)
|
||||
assert "tta_rvq" in out.extra_args
|
||||
assert "cfg_scale" not in out.extra_args
|
||||
assert "cfg_role" not in out.extra_args
|
||||
|
||||
def test_rvq_contract_cached_per_process(self):
|
||||
serving = self._serving()
|
||||
out_a = serving._inject_audex_tta_args("a", self._prompt(), SimpleNamespace(extra_args=None))
|
||||
out_b = serving._inject_audex_tta_args("b", self._prompt(), SimpleNamespace(extra_args=None))
|
||||
out_a = serving._adapter._inject_audex_tta_args("a", self._prompt(), SimpleNamespace(extra_args=None))
|
||||
out_b = serving._adapter._inject_audex_tta_args("b", self._prompt(), SimpleNamespace(extra_args=None))
|
||||
assert out_a.extra_args["tta_rvq"] is out_b.extra_args["tta_rvq"]
|
||||
|
||||
def test_injection_never_mutates_input_params(self):
|
||||
@@ -372,8 +377,8 @@ class TestAudexTTAInjection:
|
||||
per-request pair state must land only on the returned clone."""
|
||||
serving = self._serving()
|
||||
shared = SimpleNamespace(extra_args=None)
|
||||
out_1 = serving._inject_audex_tta_args("req-1", self._prompt(), shared)
|
||||
out_2 = serving._inject_audex_tta_args("req-2", self._prompt(), shared)
|
||||
out_1 = serving._adapter._inject_audex_tta_args("req-1", self._prompt(), shared)
|
||||
out_2 = serving._adapter._inject_audex_tta_args("req-2", self._prompt(), shared)
|
||||
assert shared.extra_args is None
|
||||
assert out_1 is not shared and out_2 is not shared
|
||||
assert out_1.extra_args["cfg_pair_id"] == "req-1"
|
||||
|
||||
@@ -52,6 +52,7 @@ from vllm_omni.model_executor.models.ming_tts.constants import (
|
||||
KEY_SPEAKER_WAVEFORM,
|
||||
KEY_SPEAKER_WAVEFORM_LENGTHS,
|
||||
SPEAKER_EMBEDDING_DIM,
|
||||
TEXT_EOS_TOKEN_ID,
|
||||
)
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
|
||||
@@ -3618,7 +3619,21 @@ class TestCosyVoice3Serving:
|
||||
assert error is not None
|
||||
assert "max_new_tokens" in error
|
||||
|
||||
def test_prepare_speech_generation_cosyvoice3(self, cosyvoice3_server, mocker: MockerFixture):
|
||||
@pytest.mark.parametrize(
|
||||
("max_new_tokens", "expected_min_tokens", "expected_max_tokens"),
|
||||
[
|
||||
(None, 10, 2000),
|
||||
(5, 5, 5),
|
||||
],
|
||||
)
|
||||
def test_prepare_speech_generation_cosyvoice3(
|
||||
self,
|
||||
cosyvoice3_server,
|
||||
mocker: MockerFixture,
|
||||
max_new_tokens: int | None,
|
||||
expected_min_tokens: int,
|
||||
expected_max_tokens: int,
|
||||
):
|
||||
cosyvoice3_server._build_cosyvoice3_prompt = mocker.AsyncMock(
|
||||
return_value={
|
||||
"prompt": "Hello",
|
||||
@@ -3626,15 +3641,23 @@ class TestCosyVoice3Serving:
|
||||
"mm_processor_kwargs": {"prompt_text": "ref text", "sample_rate": 24000},
|
||||
}
|
||||
)
|
||||
cosyvoice3_server._apply_cosyvoice3_dynamic_tokens = mocker.MagicMock(side_effect=lambda spl, req: spl)
|
||||
cosyvoice3_server.model_config.hf_config = SimpleNamespace(
|
||||
min_token_text_ratio=1, max_token_text_ratio=200, allowed_special=True
|
||||
)
|
||||
cosyvoice3_server._cosyvoice3_tokenizer = mocker.MagicMock()
|
||||
mocker.patch("vllm_omni.model_executor.models.cosyvoice3.utils.extract_text_token", return_value=[None, 10])
|
||||
|
||||
request = OpenAICreateSpeechRequest(
|
||||
input="Hello",
|
||||
ref_audio="data:audio/wav;base64,abc",
|
||||
ref_text="Reference text",
|
||||
max_new_tokens=max_new_tokens,
|
||||
)
|
||||
request_id, generator, tts_params = asyncio.run(cosyvoice3_server._prepare_speech_generation(request))
|
||||
sampling_params = cosyvoice3_server.engine_client.generate.call_args.kwargs["sampling_params_list"][0]
|
||||
|
||||
assert sampling_params.min_tokens == expected_min_tokens
|
||||
assert sampling_params.max_tokens == expected_max_tokens
|
||||
assert request_id.startswith("speech-")
|
||||
assert generator == "generator"
|
||||
assert tts_params == {}
|
||||
@@ -3718,6 +3741,90 @@ class TestGLMTTSServing:
|
||||
assert text_token_len == 3
|
||||
load_tokenizer.assert_called_once()
|
||||
|
||||
def test_prepare_speech_generation_glm_tts(self, glm_tts_server, mocker: MockerFixture):
|
||||
glm_tts_server._build_glm_tts_prompt = mocker.AsyncMock(
|
||||
return_value={
|
||||
"prompt": "Hello",
|
||||
"multi_modal_data": {"audio": (np.zeros(24000), 24000)},
|
||||
"mm_processor_kwargs": {"prompt_text": "ref text", "sample_rate": 24000},
|
||||
}
|
||||
)
|
||||
request = OpenAICreateSpeechRequest(
|
||||
input="Hello",
|
||||
ref_audio="data:audio/wav;base64,abc",
|
||||
ref_text="Reference text",
|
||||
)
|
||||
glm_tts_server.model_config.hf_config = SimpleNamespace(min_token_text_ratio=1, max_token_text_ratio=200)
|
||||
glm_tts_server._estimate_glm_tts_text_token_len = mocker.MagicMock(return_value=10)
|
||||
request_id, generator, tts_params = asyncio.run(glm_tts_server._prepare_speech_generation(request))
|
||||
sampling_params = glm_tts_server.engine_client.generate.call_args.kwargs["sampling_params_list"][0]
|
||||
|
||||
assert sampling_params.min_tokens == 10
|
||||
assert sampling_params.max_tokens == 2000
|
||||
assert request_id.startswith("speech-")
|
||||
assert generator == "generator"
|
||||
assert tts_params == {}
|
||||
glm_tts_server._build_glm_tts_prompt.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ming_tts_server(mocker: MockerFixture):
|
||||
mocker.patch.object(OmniOpenAIServingSpeech, "_load_supported_speakers", return_value=set())
|
||||
mocker.patch.object(OmniOpenAIServingSpeech, "_load_codec_frame_rate", return_value=None)
|
||||
|
||||
mock_engine_client = mocker.MagicMock()
|
||||
mock_engine_client.errored = False
|
||||
mock_engine_client.model_config = mocker.MagicMock(
|
||||
model="inclusionAI/Ming-omni-tts-0.5B",
|
||||
hf_config=SimpleNamespace(min_token_text_ratio=2, max_token_text_ratio=20),
|
||||
)
|
||||
mock_engine_client.default_sampling_params_list = [
|
||||
SimpleNamespace(max_tokens=2048, min_tokens=None, extra_args=None)
|
||||
]
|
||||
mock_engine_client.tts_batch_max_items = 32
|
||||
mock_engine_client.generate = mocker.MagicMock(return_value="generator")
|
||||
mock_engine_client.stage_configs = [
|
||||
SimpleNamespace(
|
||||
engine_args=SimpleNamespace(
|
||||
model_stage="llm", model_arch="MingTTSForConditionalGeneration", worker_type="ar"
|
||||
),
|
||||
tts_args={},
|
||||
)
|
||||
]
|
||||
|
||||
mock_models = mocker.MagicMock()
|
||||
mock_models.is_base_model.return_value = True
|
||||
return OmniOpenAIServingSpeech(
|
||||
engine_client=mock_engine_client,
|
||||
models=mock_models,
|
||||
request_logger=mocker.MagicMock(),
|
||||
)
|
||||
|
||||
|
||||
class TestMingTTSServing:
|
||||
def test_prepare_speech_generation_ming(self, ming_tts_server, mocker: MockerFixture):
|
||||
ming_tts_server._adapter.build = mocker.AsyncMock(
|
||||
return_value=PreparedRequest(
|
||||
prompt={"prompt": "hello"},
|
||||
tts_params={},
|
||||
model_type="ming_tts",
|
||||
)
|
||||
)
|
||||
|
||||
request = OpenAICreateSpeechRequest(
|
||||
input="Hello",
|
||||
ref_audio="data:audio/wav;base64,abc",
|
||||
ref_text="Reference text",
|
||||
max_new_tokens=7,
|
||||
)
|
||||
asyncio.run(ming_tts_server._prepare_speech_generation(request))
|
||||
|
||||
assert ming_tts_server._tts_model_type == "ming_tts"
|
||||
ming_tts_server._adapter.build.assert_awaited_once()
|
||||
sampling_params = ming_tts_server.engine_client.generate.call_args.kwargs["sampling_params_list"][0]
|
||||
assert sampling_params.stop_token_ids == [TEXT_EOS_TOKEN_ID]
|
||||
assert sampling_params.max_tokens == 8
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ming_flash_omni_tts_server(mocker: MockerFixture):
|
||||
@@ -3901,6 +4008,9 @@ class TestTTSAsyncOffloading:
|
||||
model_type=adapter_model_type,
|
||||
)
|
||||
|
||||
def apply_sampling_overrides(self, sampling_params_list, request, prompt, request_id=None):
|
||||
return sampling_params_list
|
||||
|
||||
voxtral_server._tts_model_type = legacy_tts_model_type
|
||||
mocker.patch.object(voxtral_server, "_get_tts_adapter", return_value=FakeAdapter())
|
||||
log_info = mocker.patch("vllm_omni.entrypoints.openai.serving_speech.logger.info")
|
||||
|
||||
@@ -14,7 +14,6 @@ import itertools
|
||||
import pytest
|
||||
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import (
|
||||
LEGACY_TTS_DETECTORS,
|
||||
TTS_ADAPTER_REGISTRY,
|
||||
all_tts_stage_keys,
|
||||
detect_tts_model_type,
|
||||
@@ -282,27 +281,17 @@ def test_entry_stage_archs_is_ming_only():
|
||||
assert tts_entry_stage_archs() == frozenset({"MingTTSForConditionalGeneration"})
|
||||
|
||||
|
||||
def test_every_detected_type_is_adapter_backed_or_declared_legacy():
|
||||
"""Detection may only name a model that has an adapter or an explicit
|
||||
:data:`LEGACY_TTS_DETECTORS` entry — never an undeclared string."""
|
||||
legacy_names = {d.name for d in LEGACY_TTS_DETECTORS}
|
||||
def test_every_detected_type_is_adapter_backed():
|
||||
"""Detection may only name a model that has a registered adapter."""
|
||||
for stage, arch in itertools.product(_STAGES, _ARCHS): # full cross product on purpose
|
||||
detected = detect_tts_model_type(stage, arch)
|
||||
if detected is None:
|
||||
continue
|
||||
assert detected in TTS_ADAPTER_REGISTRY or detected in legacy_names, (
|
||||
assert detected in TTS_ADAPTER_REGISTRY, (
|
||||
f"detection produced undeclared model type {detected!r} for stage={stage!r} arch={arch!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_legacy_detectors_have_no_adapter():
|
||||
"""A legacy entry that gained an adapter must be deleted from the list."""
|
||||
for detector in LEGACY_TTS_DETECTORS:
|
||||
assert resolve_adapter(detector.name) is None, (
|
||||
f"{detector.name!r} now has an adapter; remove it from LEGACY_TTS_DETECTORS"
|
||||
)
|
||||
|
||||
|
||||
def test_no_ambiguous_detectors_at_equal_priority():
|
||||
"""Two detectors that can match the same deployment must be ordered.
|
||||
|
||||
|
||||
@@ -17,9 +17,7 @@ import pytest
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools" / "pre_commit"))
|
||||
|
||||
from check_tts_adapter import ( # noqa: E402
|
||||
MAX_LEGACY_DETECTORS,
|
||||
MAX_MODEL_TYPE_BRANCHES,
|
||||
_legacy_detectors,
|
||||
_model_type_branches,
|
||||
main,
|
||||
)
|
||||
@@ -60,12 +58,6 @@ def test_dict_dispatch_is_a_known_gap():
|
||||
assert _count('TABLE = {"foo": 1}\nvalue = TABLE[self._tts_model_type]') == 0
|
||||
|
||||
|
||||
def test_legacy_detector_counting():
|
||||
src = 'X = (LegacyDetector(name="a", stage_keys=frozenset({"s"})), LegacyDetector(name="b"))'
|
||||
found = _legacy_detectors(ast.parse(src))
|
||||
assert [name for _, name in found] == ["a", "b"]
|
||||
|
||||
|
||||
def test_gate_passes_on_current_tree():
|
||||
"""The recorded budgets must match reality, or the ratchet is decorative."""
|
||||
assert main([]) == 0
|
||||
@@ -74,6 +66,4 @@ def test_gate_passes_on_current_tree():
|
||||
def test_budgets_are_the_real_counts():
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
serving = root / "vllm_omni" / "entrypoints" / "openai" / "serving_speech.py"
|
||||
adapters = root / "vllm_omni" / "entrypoints" / "openai" / "tts_adapters" / "__init__.py"
|
||||
assert len(_model_type_branches(ast.parse(serving.read_text()))) == MAX_MODEL_TYPE_BRANCHES
|
||||
assert len(_legacy_detectors(ast.parse(adapters.read_text()))) == MAX_LEGACY_DETECTORS
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import copy
|
||||
from collections.abc import Sequence
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -200,6 +201,15 @@ def test_speech_extra_params_reach_model_sampler_as_sampling_metadata(monkeypatc
|
||||
async def build(self, request, sampling_params_list, has_inline_ref_audio):
|
||||
return PreparedRequest(prompt={"prompt": request.input}, tts_params={}, model_type="higgs_audio_v3")
|
||||
|
||||
def apply_sampling_overrides(
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: "OpenAICreateSpeechRequest",
|
||||
prompt: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> list:
|
||||
return sampling_params_list
|
||||
|
||||
engine = SimpleNamespace(
|
||||
errored=False,
|
||||
default_sampling_params_list=[SamplingParams(temperature=1.0, top_p=0.95, top_k=50)],
|
||||
|
||||
@@ -6,10 +6,8 @@ Per-model behaviour is being moved out of ``serving_speech.py`` and into
|
||||
nothing stopped a new model from adding one more ``self._tts_model_type == ...``
|
||||
branch faster than the migration removed them.
|
||||
|
||||
This gate makes the two debt counters monotonically non-increasing:
|
||||
|
||||
1. ``self._tts_model_type`` comparisons in ``serving_speech.py``
|
||||
2. entries in ``tts_adapters.LEGACY_TTS_DETECTORS`` (models with no adapter)
|
||||
This gate makes the number of ``self._tts_model_type`` comparisons in
|
||||
``serving_speech.py`` monotonically non-increasing.
|
||||
|
||||
Lowering a budget after removing branches is expected and encouraged — the check
|
||||
insists on it, so the recorded numbers cannot silently drift upward. Raising one
|
||||
@@ -28,11 +26,9 @@ from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SERVING_SPEECH = Path("vllm_omni/entrypoints/openai/serving_speech.py")
|
||||
ADAPTERS_INIT = Path("vllm_omni/entrypoints/openai/tts_adapters/__init__.py")
|
||||
|
||||
# Budgets. These may only go DOWN. See the module docstring.
|
||||
MAX_MODEL_TYPE_BRANCHES = 27
|
||||
MAX_LEGACY_DETECTORS = 0
|
||||
MAX_MODEL_TYPE_BRANCHES = 20
|
||||
|
||||
_GUIDANCE = """
|
||||
Move the per-model behaviour into its adapter under
|
||||
@@ -109,20 +105,6 @@ def _model_type_branches(tree: ast.AST) -> list[tuple[int, str]]:
|
||||
return found
|
||||
|
||||
|
||||
def _legacy_detectors(tree: ast.AST) -> list[tuple[int, str]]:
|
||||
"""Every ``LegacyDetector(...)`` construction."""
|
||||
found: list[tuple[int, str]] = []
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "LegacyDetector":
|
||||
name = "?"
|
||||
for kw in node.keywords:
|
||||
if kw.arg == "name" and isinstance(kw.value, ast.Constant):
|
||||
name = str(kw.value.value)
|
||||
found.append((node.lineno, name))
|
||||
found.sort()
|
||||
return found
|
||||
|
||||
|
||||
def _check(
|
||||
label: str,
|
||||
path: Path,
|
||||
@@ -151,21 +133,18 @@ def _check(
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
# pre-commit passes the staged filenames; we always audit the same two files,
|
||||
# pre-commit passes the staged filenames; we always audit the same file,
|
||||
# so they are accepted and ignored.
|
||||
parser.add_argument("filenames", nargs="*")
|
||||
parser.parse_args(argv)
|
||||
|
||||
serving_path = REPO_ROOT / SERVING_SPEECH
|
||||
adapters_path = REPO_ROOT / ADAPTERS_INIT
|
||||
for path in (serving_path, adapters_path):
|
||||
if not path.is_file():
|
||||
print(f"check_tts_adapter: expected file not found: {path}", file=sys.stderr)
|
||||
return 1
|
||||
if not serving_path.is_file():
|
||||
print(f"check_tts_adapter: expected file not found: {serving_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
notices: list[str] = []
|
||||
branches = _model_type_branches(ast.parse(serving_path.read_text()))
|
||||
detectors = _legacy_detectors(ast.parse(adapters_path.read_text()))
|
||||
|
||||
errors = _check(
|
||||
"`_tts_model_type` comparisons",
|
||||
@@ -176,15 +155,6 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"MAX_MODEL_TYPE_BRANCHES",
|
||||
notices,
|
||||
)
|
||||
errors += _check(
|
||||
"unmigrated model types in LEGACY_TTS_DETECTORS",
|
||||
ADAPTERS_INIT,
|
||||
len(detectors),
|
||||
MAX_LEGACY_DETECTORS,
|
||||
[f"line {lineno}: {name}" for lineno, name in detectors],
|
||||
"MAX_LEGACY_DETECTORS",
|
||||
notices,
|
||||
)
|
||||
|
||||
for notice in notices:
|
||||
print(f"check_tts_adapter: {notice}", file=sys.stderr)
|
||||
|
||||
@@ -92,19 +92,7 @@ logger = init_logger(__name__)
|
||||
# reference (issue #4644). Matches the offline example/test and upstream demo.
|
||||
_COSYVOICE3_PROMPT_DELIMITER = "<|endofprompt|>"
|
||||
_COSYVOICE3_PROMPT_PREFIX = f"You are a helpful assistant.{_COSYVOICE3_PROMPT_DELIMITER}"
|
||||
_SAMPLING_MAX_TOKENS_TTS_MODEL_TYPES = {
|
||||
"fish_tts",
|
||||
"qwen3_tts",
|
||||
"voxtral_tts",
|
||||
"cosyvoice3",
|
||||
"voxcpm2",
|
||||
"higgs_audio_v2",
|
||||
"higgs_audio_v3",
|
||||
"indextts2",
|
||||
"indextts2_5",
|
||||
"audex",
|
||||
"audex_tta",
|
||||
}
|
||||
|
||||
# Audex contract: zero-codec / invalid generations arrive as empty terminal
|
||||
# payloads and must fail the request, never serialize as a successful empty
|
||||
# WAV. Covers both the TTS ("audex") and TTA ("audex_tta") pipelines.
|
||||
@@ -444,7 +432,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
# Cached per process: the CosyVoice3 Qwen tokenizer + resolved model
|
||||
# path used for dynamic-token sizing. Without this, every request
|
||||
# re-ran snapshot_download + reloaded the tokenizer (~100 ms on the
|
||||
# TTFP critical path) in _apply_cosyvoice3_dynamic_tokens.
|
||||
# TTFP critical path) in the CosyVoice3 adapter's sampling override.
|
||||
self._cosyvoice3_tokenizer = None
|
||||
|
||||
# Determine TTS model type or None
|
||||
@@ -502,9 +490,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
|
||||
# Resolve the per-model serving adapter (RFC #4327), keyed on the
|
||||
# detected model-type. Every dedicated TTS model has an adapter; the
|
||||
# adapter owns request validation + prompt/param building. Sampling
|
||||
# overrides and the model-type label remain in the orchestrator tail
|
||||
# (keyed on ``_tts_model_type``) during this incremental migration.
|
||||
# adapter owns request validation, prompt/param building, and sampling
|
||||
# overrides; the model-type label remains available for compatibility.
|
||||
self._adapter = None
|
||||
if self._tts_stage is not None:
|
||||
adapter_cls = resolve_adapter(self._tts_model_type)
|
||||
@@ -2825,154 +2812,6 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
self._audex_tokenizer = AutoTokenizer.from_pretrained(os.path.join(root, folder))
|
||||
return self._audex_tokenizer
|
||||
|
||||
def _inject_audex_tta_args(self, request_id: str, prompt: Any, stage0_params: Any) -> Any:
|
||||
"""Complete the Audex TTA contract on a CLONE of the stage-0 params.
|
||||
|
||||
Always attaches the RVQ phase-mask contract (required for token
|
||||
validity) and the CFG pair contract at the official default scale
|
||||
3.0 unless the caller passed ``cfg_scale`` (1.0 disables guidance).
|
||||
|
||||
Returns the cloned params; the input is never mutated. On the
|
||||
no-extra_params online path the input is an element of the engine's
|
||||
SHARED ``default_sampling_params_list`` — writing per-request pair
|
||||
state (``cfg_pair_id``/``cfg_null_prompt``) into it would race
|
||||
concurrent requests and leak stale CFG metadata into later ones.
|
||||
"""
|
||||
import copy
|
||||
|
||||
from vllm_omni.model_executor.models.audex.prompt import build_tta_null_prompt
|
||||
from vllm_omni.model_executor.models.audex.tta import build_tta_phase_token_ids
|
||||
|
||||
cond_prompt = prompt.get("prompt") if isinstance(prompt, dict) else prompt
|
||||
if not isinstance(cond_prompt, str) or not cond_prompt:
|
||||
raise ValueError("Audex TTA requires the adapter-built caption prompt")
|
||||
|
||||
stage0_params = copy.deepcopy(stage0_params)
|
||||
tokenizer = self._get_audex_tokenizer()
|
||||
if self._audex_tta_rvq is None:
|
||||
phase_token_ids, start_tid, end_tid = build_tta_phase_token_ids(tokenizer)
|
||||
self._audex_tta_rvq = {
|
||||
"phase_token_ids": phase_token_ids,
|
||||
"start_tid": start_tid,
|
||||
"end_tid": end_tid,
|
||||
# Official generation cap (decode truncates at 500 frames).
|
||||
"codec_cap": 4000,
|
||||
# The TTA prompt ends with <audiogen_start>.
|
||||
"start_in_prompt": True,
|
||||
}
|
||||
|
||||
extra_args = getattr(stage0_params, "extra_args", None)
|
||||
if extra_args is None:
|
||||
extra_args = {}
|
||||
stage0_params.extra_args = extra_args
|
||||
extra_args["tta_rvq"] = self._audex_tta_rvq
|
||||
|
||||
cfg_scale = extra_args.get("cfg_scale")
|
||||
cfg_scale = 3.0 if cfg_scale is None else float(cfg_scale)
|
||||
if cfg_scale <= 1.0:
|
||||
extra_args.pop("cfg_scale", None)
|
||||
return stage0_params
|
||||
extra_args.update(
|
||||
{
|
||||
"cfg_scale": cfg_scale,
|
||||
"cfg_role": "cond",
|
||||
"cfg_pair_id": request_id,
|
||||
"cfg_null_prompt": build_tta_null_prompt(cond_prompt, tokenizer),
|
||||
}
|
||||
)
|
||||
return stage0_params
|
||||
|
||||
def _inject_audex_cfg_pair_args(self, request_id: str, prompt: Any, stage0_params: Any) -> Any:
|
||||
"""Complete the Audex CFG contract on a CLONE of the stage-0 params.
|
||||
|
||||
The adapter validated ``cfg_scale``; here (where the final request id
|
||||
exists) the cond role, pair id, and the length-matched null prompt are
|
||||
attached. ``cfg_scale`` absent/1.0 returns a clone value-identical to
|
||||
the non-CFG flow (the key is dropped rather than forwarded).
|
||||
|
||||
Returns the cloned params; the input is never mutated (it may be an
|
||||
element of the engine's shared ``default_sampling_params_list`` —
|
||||
see ``_inject_audex_tta_args``).
|
||||
"""
|
||||
import copy
|
||||
|
||||
stage0_params = copy.deepcopy(stage0_params)
|
||||
extra_args = getattr(stage0_params, "extra_args", None)
|
||||
if not extra_args or extra_args.get("cfg_scale") is None:
|
||||
if extra_args:
|
||||
extra_args.pop("cfg_scale", None)
|
||||
return stage0_params
|
||||
cfg_scale = float(extra_args["cfg_scale"])
|
||||
if cfg_scale <= 1.0:
|
||||
extra_args.pop("cfg_scale", None)
|
||||
return stage0_params
|
||||
|
||||
from vllm_omni.model_executor.models.audex.prompt import build_null_prompt
|
||||
|
||||
cond_prompt = prompt.get("prompt") if isinstance(prompt, dict) else prompt
|
||||
if not isinstance(cond_prompt, str) or not cond_prompt:
|
||||
raise ValueError("Audex CFG requires the adapter-built text prompt")
|
||||
null_prompt = build_null_prompt(cond_prompt, self._get_audex_tokenizer())
|
||||
extra_args.update(
|
||||
{
|
||||
"cfg_scale": cfg_scale,
|
||||
"cfg_role": "cond",
|
||||
"cfg_pair_id": request_id,
|
||||
"cfg_null_prompt": null_prompt,
|
||||
}
|
||||
)
|
||||
# Guided decoding sharpens the distribution; the unguided default
|
||||
# temperature (0.1) adds excess sampling noise under CFG. Measured
|
||||
# on the en-24 gate corpus at cfg 1.5: temp 0.1 -> CER 7.31%,
|
||||
# temp 0.05 -> CER 6.87% (unguided baseline 7.24%).
|
||||
stage0_params.temperature = 0.05
|
||||
return stage0_params
|
||||
|
||||
def _apply_cosyvoice3_dynamic_tokens(
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: OpenAICreateSpeechRequest,
|
||||
) -> list:
|
||||
"""Set min/max tokens from tokenized text length (ratios target tokens, not chars)."""
|
||||
import copy
|
||||
|
||||
from vllm_omni.model_executor.models.cosyvoice3.tokenizer import get_qwen_tokenizer
|
||||
from vllm_omni.model_executor.models.cosyvoice3.utils import extract_text_token
|
||||
|
||||
sampling_params_list = copy.deepcopy(sampling_params_list)
|
||||
hf_cfg = self.model_config.hf_config
|
||||
# Build the Qwen tokenizer once per process (resolving the model dir via
|
||||
# snapshot_download at most once) and reuse it across requests.
|
||||
tokenizer = self._cosyvoice3_tokenizer
|
||||
if tokenizer is None:
|
||||
model_path = self.engine_client.model_config.model
|
||||
if not os.path.isdir(model_path):
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
model_path = snapshot_download(model_path)
|
||||
tokenizer = get_qwen_tokenizer(
|
||||
token_path=os.path.join(model_path, hf_cfg.qwen_pretrain_path),
|
||||
skip_special_tokens=hf_cfg.skip_special_tokens,
|
||||
version=hf_cfg.version,
|
||||
)
|
||||
self._cosyvoice3_tokenizer = tokenizer
|
||||
_, text_token_len = extract_text_token(
|
||||
request.input,
|
||||
tokenizer,
|
||||
hf_cfg.allowed_special,
|
||||
)
|
||||
min_ratio = getattr(hf_cfg, "min_token_text_ratio", 2)
|
||||
max_ratio = getattr(hf_cfg, "max_token_text_ratio", 20)
|
||||
sampling_params_list[0].min_tokens = max(1, int(text_token_len * min_ratio))
|
||||
sampling_params_list[0].max_tokens = min(2048, int(text_token_len * max_ratio))
|
||||
logger.info(
|
||||
"CosyVoice3 dynamic tokens: text_tokens=%d, min_tokens=%d, max_tokens=%d",
|
||||
text_token_len,
|
||||
sampling_params_list[0].min_tokens,
|
||||
sampling_params_list[0].max_tokens,
|
||||
)
|
||||
return sampling_params_list
|
||||
|
||||
# ---- GLM-TTS helpers ----
|
||||
|
||||
async def _build_glm_tts_prompt(
|
||||
@@ -3172,9 +3011,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
|
||||
# Build prompt + tts_params via the per-model adapter (RFC #4327). Every
|
||||
# dedicated TTS model resolves to an adapter that owns its validation,
|
||||
# uploaded-speaker handling, and prompt/param building. Sampling
|
||||
# overrides and the model-type label remain in the orchestrator tail
|
||||
# below (keyed on ``_tts_model_type``) during this incremental migration.
|
||||
# uploaded-speaker handling, prompt/param building, and sampling
|
||||
# overrides. The model-type label remains available for compatibility.
|
||||
# Non-TTS deployments (no adapter) fall through to the rejection below.
|
||||
# Capture inline-ref-audio status BEFORE validate(): several adapters
|
||||
# apply uploaded speakers inside validate(), which sets request.ref_audio
|
||||
@@ -3225,63 +3063,6 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
model_type,
|
||||
)
|
||||
|
||||
# CosyVoice3: set dynamic min/max tokens based on text length.
|
||||
# The official model requires min_token_text_ratio to prevent early
|
||||
# EOS and max_token_text_ratio to cap generation length.
|
||||
if self._tts_model_type == "cosyvoice3" and sampling_params_list:
|
||||
sampling_params_list = self._apply_cosyvoice3_dynamic_tokens(sampling_params_list, request)
|
||||
|
||||
# GLM-TTS: set dynamic min/max tokens based on text length.
|
||||
if self._tts_model_type == "glm_tts" and sampling_params_list:
|
||||
import copy
|
||||
|
||||
sampling_params_list = copy.deepcopy(sampling_params_list)
|
||||
glm_metadata = prompt.get("additional_information") if isinstance(prompt, dict) else None
|
||||
text_len_value = None
|
||||
if isinstance(glm_metadata, dict):
|
||||
text_len_value = glm_metadata.get("glm_tts_text_token_len")
|
||||
if isinstance(text_len_value, list) and text_len_value:
|
||||
text_len_value = text_len_value[0]
|
||||
text_token_len = (
|
||||
int(text_len_value)
|
||||
if text_len_value is not None
|
||||
else self._estimate_glm_tts_text_token_len(request.input)
|
||||
)
|
||||
hf_cfg = self.model_config.hf_config
|
||||
min_ratio = getattr(hf_cfg, "min_token_text_ratio", 2)
|
||||
max_ratio = getattr(hf_cfg, "max_token_text_ratio", 20)
|
||||
stage_min_tokens = getattr(sampling_params_list[0], "min_tokens", None)
|
||||
stage_max_tokens = getattr(sampling_params_list[0], "max_tokens", None)
|
||||
cap_candidates = [int(cap) for cap in (stage_max_tokens, request.max_new_tokens) if cap is not None]
|
||||
hard_cap = min(cap_candidates) if cap_candidates else None
|
||||
|
||||
min_tokens = max(1, int(text_token_len * min_ratio))
|
||||
if stage_min_tokens is not None:
|
||||
min_tokens = max(min_tokens, int(stage_min_tokens))
|
||||
if hard_cap is not None:
|
||||
min_tokens = min(min_tokens, hard_cap)
|
||||
|
||||
max_tokens = max(min_tokens, int(text_token_len * max_ratio))
|
||||
if hard_cap is not None:
|
||||
max_tokens = min(max_tokens, hard_cap)
|
||||
sampling_params_list[0].min_tokens = min_tokens
|
||||
sampling_params_list[0].max_tokens = max_tokens
|
||||
seed = getattr(request, "seed", None)
|
||||
if seed is not None:
|
||||
sampling_params_list[0].seed = seed
|
||||
logger.info(
|
||||
"GLM-TTS dynamic tokens: text_tokens=%d, min_ratio=%s, max_ratio=%s, "
|
||||
"stage_min=%s, stage_max=%s, request_max=%s, min_tokens=%d, max_tokens=%d",
|
||||
text_token_len,
|
||||
min_ratio,
|
||||
max_ratio,
|
||||
stage_min_tokens,
|
||||
stage_max_tokens,
|
||||
request.max_new_tokens,
|
||||
min_tokens,
|
||||
max_tokens,
|
||||
)
|
||||
|
||||
# Apply model-specific extra parameters
|
||||
if request.extra_params is not None and sampling_params_list:
|
||||
if not isinstance(request.extra_params, dict):
|
||||
@@ -3300,52 +3081,10 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
sampling_params_list[0].extra_args.update(request.extra_params)
|
||||
logger.info("Applied extra_params: %s", request.extra_params)
|
||||
|
||||
# Audex CFG: turn an adapter-validated cfg_scale into the engine-side
|
||||
# pair contract. This must run here (not in the adapter) because the
|
||||
# pair id is the final request_id, which the adapter never sees.
|
||||
# The injectors return a CLONE: without extra_params above,
|
||||
# sampling_params_list still holds the engine's shared default params,
|
||||
# which must never carry per-request CFG state.
|
||||
if self._tts_model_type == "audex" and sampling_params_list:
|
||||
sampling_params_list[0] = self._inject_audex_cfg_pair_args(request_id, prompt, sampling_params_list[0])
|
||||
elif self._tts_model_type == "audex_tta" and sampling_params_list:
|
||||
sampling_params_list[0] = self._inject_audex_tta_args(request_id, prompt, sampling_params_list[0])
|
||||
|
||||
# Some TTS model defaults come from deploy YAML. Their AR
|
||||
# generation length is controlled by SamplingParams.max_tokens, so only
|
||||
# override it when the caller explicitly requests max_new_tokens.
|
||||
if (
|
||||
self._tts_model_type in _SAMPLING_MAX_TOKENS_TTS_MODEL_TYPES
|
||||
and request.max_new_tokens is not None
|
||||
and sampling_params_list
|
||||
):
|
||||
import copy
|
||||
|
||||
sampling_params_list = copy.deepcopy(sampling_params_list)
|
||||
sampling_params_list[0].max_tokens = request.max_new_tokens
|
||||
if self._tts_model_type == "cosyvoice3":
|
||||
sampling_params_list[0].min_tokens = min(
|
||||
getattr(sampling_params_list[0], "min_tokens", 0),
|
||||
request.max_new_tokens,
|
||||
)
|
||||
elif self._tts_model_type == "ming_tts" and sampling_params_list:
|
||||
import copy
|
||||
|
||||
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import (
|
||||
MOE_TEXT_EOS_TOKEN_ID,
|
||||
TEXT_EOS_TOKEN_ID,
|
||||
)
|
||||
|
||||
hf_config = self.engine_client.model_config.hf_config
|
||||
is_moe = getattr(hf_config, "model_type", "") == "bailingmm"
|
||||
stop_token_id = MOE_TEXT_EOS_TOKEN_ID if is_moe else TEXT_EOS_TOKEN_ID
|
||||
|
||||
sampling_params_list = copy.deepcopy(sampling_params_list)
|
||||
sampling_params_list[0].stop_token_ids = [int(stop_token_id)]
|
||||
if request.max_new_tokens is not None:
|
||||
# Ming emits TEXT_EOS after the latent decode budget is exhausted, so
|
||||
# Stage-0 needs one extra token beyond ming_max_decode_steps.
|
||||
sampling_params_list[0].max_tokens = int(request.max_new_tokens) + 1
|
||||
# Apply adapter-owned sampling overrides, including request-level token
|
||||
# limits and model-specific dynamic token or stop-token configuration.
|
||||
if sampling_params_list and (adapter := self._get_tts_adapter()) is not None:
|
||||
sampling_params_list = adapter.apply_sampling_overrides(sampling_params_list, request, prompt, request_id)
|
||||
|
||||
if request.seed is not None and sampling_params_list:
|
||||
import copy
|
||||
|
||||
@@ -9,8 +9,6 @@ key alone cannot distinguish.
|
||||
That metadata is the single source of truth. :func:`detect_tts_model_type` and
|
||||
:func:`all_tts_stage_keys` derive the whole stage -> model-type mapping from it,
|
||||
so a new TTS model is one adapter file and needs no edit to ``serving_speech.py``.
|
||||
Models still awaiting an adapter are listed explicitly in
|
||||
:data:`LEGACY_TTS_DETECTORS`.
|
||||
"""
|
||||
|
||||
from vllm.logger import init_logger
|
||||
@@ -18,7 +16,6 @@ from vllm.logger import init_logger
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import (
|
||||
ARTTSAdapter,
|
||||
DiffusionTTSAdapter,
|
||||
LegacyDetector,
|
||||
OutputPolicy,
|
||||
PreparedRequest,
|
||||
SpeechServingContext,
|
||||
@@ -29,10 +26,6 @@ logger = init_logger(__name__)
|
||||
|
||||
TTS_ADAPTER_REGISTRY: dict[str, type[TTSModelAdapter]] = {}
|
||||
|
||||
#: Model types the serving layer detects that have no adapter yet. See
|
||||
#: :class:`LegacyDetector`. This list must only ever shrink.
|
||||
LEGACY_TTS_DETECTORS: tuple[LegacyDetector, ...] = ()
|
||||
|
||||
|
||||
def register_tts_adapter(cls: type[TTSModelAdapter]) -> type[TTSModelAdapter]:
|
||||
"""Class decorator: index ``cls`` under its ``name`` (model-type)."""
|
||||
@@ -57,16 +50,13 @@ def resolve_adapter(model_type: str | None) -> type[TTSModelAdapter] | None:
|
||||
return TTS_ADAPTER_REGISTRY.get(model_type)
|
||||
|
||||
|
||||
def iter_tts_detectors() -> list[type[TTSModelAdapter] | LegacyDetector]:
|
||||
def iter_tts_detectors() -> list[type[TTSModelAdapter]]:
|
||||
"""Every detector, in resolution order.
|
||||
|
||||
Sorted by ``detect_priority`` then ``name``, so the order is total and does
|
||||
not depend on adapter import order.
|
||||
"""
|
||||
detectors: list[type[TTSModelAdapter] | LegacyDetector] = [
|
||||
*TTS_ADAPTER_REGISTRY.values(),
|
||||
*LEGACY_TTS_DETECTORS,
|
||||
]
|
||||
detectors = list(TTS_ADAPTER_REGISTRY.values())
|
||||
detectors.sort(key=lambda d: (d.detect_priority, d.name))
|
||||
return detectors
|
||||
|
||||
@@ -132,12 +122,10 @@ from vllm_omni.entrypoints.openai.tts_adapters import ( # noqa: E402,F401
|
||||
__all__ = [
|
||||
"ARTTSAdapter",
|
||||
"DiffusionTTSAdapter",
|
||||
"LegacyDetector",
|
||||
"OutputPolicy",
|
||||
"PreparedRequest",
|
||||
"SpeechServingContext",
|
||||
"TTSModelAdapter",
|
||||
"LEGACY_TTS_DETECTORS",
|
||||
"TTS_ADAPTER_REGISTRY",
|
||||
"all_tts_model_types",
|
||||
"all_tts_stage_keys",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Audex (Nemotron-Labs-Audex-2B) TTS serving adapter."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import register_tts_adapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest, apply_max_new_tokens
|
||||
from vllm_omni.model_executor.models.audex.prompt import build_cond_prompt
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -17,8 +17,8 @@ if TYPE_CHECKING:
|
||||
AUDEX_CFG_SCALE_MIN = 1.0
|
||||
AUDEX_CFG_SCALE_MAX = 10.0
|
||||
|
||||
# Internal CFG pair-plumbing keys; injected by the serving layer once the
|
||||
# request id exists, never accepted from callers.
|
||||
# Internal CFG pair-plumbing keys; injected by this adapter once it receives
|
||||
# the final request id, never accepted from callers.
|
||||
_AUDEX_INTERNAL_CFG_KEYS = ("cfg_role", "cfg_pair_id", "cfg_null_prompt")
|
||||
|
||||
|
||||
@@ -74,3 +74,65 @@ class AudexAdapter(ARTTSAdapter):
|
||||
) -> PreparedRequest:
|
||||
prompt = {"prompt": build_cond_prompt(request.input)}
|
||||
return PreparedRequest(prompt=prompt, tts_params={}, model_type="audex")
|
||||
|
||||
def apply_sampling_overrides(
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: "OpenAICreateSpeechRequest",
|
||||
prompt: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> list:
|
||||
if request_id is None:
|
||||
raise ValueError("Audex CFG requires a request id")
|
||||
stage0_params = sampling_params_list[0]
|
||||
sampling_params_list[0] = self._inject_audex_cfg_pair_args(
|
||||
request_id=request_id, prompt=prompt, stage0_params=stage0_params
|
||||
)
|
||||
return apply_max_new_tokens(sampling_params_list, request)
|
||||
|
||||
def _inject_audex_cfg_pair_args(self, request_id: str, prompt: Any, stage0_params: Any) -> Any:
|
||||
"""Complete the Audex CFG contract on a CLONE of the stage-0 params.
|
||||
|
||||
The adapter validated ``cfg_scale``; here (where the final request id
|
||||
exists) the cond role, pair id, and the length-matched null prompt are
|
||||
attached. ``cfg_scale`` absent/1.0 returns a clone value-identical to
|
||||
the non-CFG flow (the key is dropped rather than forwarded).
|
||||
|
||||
Returns the cloned params; the input is never mutated (it may be an
|
||||
element of the engine's shared ``default_sampling_params_list`` —
|
||||
see ``_inject_audex_tta_args``).
|
||||
"""
|
||||
server = self.ctx.server
|
||||
import copy
|
||||
|
||||
stage0_params = copy.deepcopy(stage0_params)
|
||||
extra_args = getattr(stage0_params, "extra_args", None)
|
||||
if not extra_args or extra_args.get("cfg_scale") is None:
|
||||
if extra_args:
|
||||
extra_args.pop("cfg_scale", None)
|
||||
return stage0_params
|
||||
cfg_scale = float(extra_args["cfg_scale"])
|
||||
if cfg_scale <= 1.0:
|
||||
extra_args.pop("cfg_scale", None)
|
||||
return stage0_params
|
||||
|
||||
from vllm_omni.model_executor.models.audex.prompt import build_null_prompt
|
||||
|
||||
cond_prompt = prompt.get("prompt") if isinstance(prompt, dict) else prompt
|
||||
if not isinstance(cond_prompt, str) or not cond_prompt:
|
||||
raise ValueError("Audex CFG requires the adapter-built text prompt")
|
||||
null_prompt = build_null_prompt(cond_prompt, server._get_audex_tokenizer())
|
||||
extra_args.update(
|
||||
{
|
||||
"cfg_scale": cfg_scale,
|
||||
"cfg_role": "cond",
|
||||
"cfg_pair_id": request_id,
|
||||
"cfg_null_prompt": null_prompt,
|
||||
}
|
||||
)
|
||||
# Guided decoding sharpens the distribution; the unguided default
|
||||
# temperature (0.1) adds excess sampling noise under CFG. Measured
|
||||
# on the en-24 gate corpus at cfg 1.5: temp 0.1 -> CER 7.31%,
|
||||
# temp 0.05 -> CER 6.87% (unguided baseline 7.24%).
|
||||
stage0_params.temperature = 0.05
|
||||
return stage0_params
|
||||
@@ -2,16 +2,16 @@
|
||||
"""Audex (Nemotron-Labs-Audex-2B) text-to-audio serving adapter.
|
||||
|
||||
Caption → general audio through the ``audex_tta`` pipeline.
|
||||
The adapter builds the TTA prompt; the serving layer completes the request
|
||||
after the final request id exists: the RVQ phase-mask contract
|
||||
The adapter builds the TTA prompt and, after receiving the final request id,
|
||||
completes the sampling parameters with the RVQ phase-mask contract
|
||||
(``extra_args["tta_rvq"]``) and the CFG pair contract (official default
|
||||
scale 3.0 — guidance is effectively mandatory for TTA quality).
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import register_tts_adapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest, apply_max_new_tokens
|
||||
from vllm_omni.model_executor.models.audex.prompt import build_tta_cond_prompt
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -21,8 +21,7 @@ AUDEX_TTA_DEFAULT_CFG_SCALE = 3.0
|
||||
AUDEX_TTA_CFG_SCALE_MIN = 1.0
|
||||
AUDEX_TTA_CFG_SCALE_MAX = 10.0
|
||||
|
||||
# Internal plumbing keys; injected by the serving layer, never accepted
|
||||
# from callers.
|
||||
# Internal plumbing keys; injected by this adapter, never accepted from callers.
|
||||
_INTERNAL_KEYS = ("cfg_role", "cfg_pair_id", "cfg_null_prompt", "tta_rvq")
|
||||
|
||||
|
||||
@@ -67,3 +66,76 @@ class AudexTTAAdapter(ARTTSAdapter):
|
||||
) -> PreparedRequest:
|
||||
prompt = {"prompt": build_tta_cond_prompt(request.input)}
|
||||
return PreparedRequest(prompt=prompt, tts_params={}, model_type="audex_tta")
|
||||
|
||||
def apply_sampling_overrides(
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: "OpenAICreateSpeechRequest",
|
||||
prompt: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> list:
|
||||
if request_id is None:
|
||||
raise ValueError("Audex TTA requires a request id")
|
||||
stage0_params = sampling_params_list[0]
|
||||
sampling_params_list[0] = self._inject_audex_tta_args(
|
||||
request_id=request_id, prompt=prompt, stage0_params=stage0_params
|
||||
)
|
||||
return apply_max_new_tokens(sampling_params_list, request)
|
||||
|
||||
def _inject_audex_tta_args(self, request_id: str, prompt: Any, stage0_params: Any) -> Any:
|
||||
"""Complete the Audex TTA contract on a CLONE of the stage-0 params.
|
||||
|
||||
Always attaches the RVQ phase-mask contract (required for token
|
||||
validity) and the CFG pair contract at the official default scale
|
||||
3.0 unless the caller passed ``cfg_scale`` (1.0 disables guidance).
|
||||
|
||||
Returns the cloned params; the input is never mutated. On the
|
||||
no-extra_params online path the input is an element of the engine's
|
||||
SHARED ``default_sampling_params_list`` — writing per-request pair
|
||||
state (``cfg_pair_id``/``cfg_null_prompt``) into it would race
|
||||
concurrent requests and leak stale CFG metadata into later ones.
|
||||
"""
|
||||
server = self.ctx.server
|
||||
import copy
|
||||
|
||||
from vllm_omni.model_executor.models.audex.prompt import build_tta_null_prompt
|
||||
from vllm_omni.model_executor.models.audex.tta import build_tta_phase_token_ids
|
||||
|
||||
cond_prompt = prompt.get("prompt") if isinstance(prompt, dict) else prompt
|
||||
if not isinstance(cond_prompt, str) or not cond_prompt:
|
||||
raise ValueError("Audex TTA requires the adapter-built caption prompt")
|
||||
|
||||
stage0_params = copy.deepcopy(stage0_params)
|
||||
tokenizer = server._get_audex_tokenizer()
|
||||
if server._audex_tta_rvq is None:
|
||||
phase_token_ids, start_tid, end_tid = build_tta_phase_token_ids(tokenizer)
|
||||
server._audex_tta_rvq = {
|
||||
"phase_token_ids": phase_token_ids,
|
||||
"start_tid": start_tid,
|
||||
"end_tid": end_tid,
|
||||
# Official generation cap (decode truncates at 500 frames).
|
||||
"codec_cap": 4000,
|
||||
# The TTA prompt ends with <audiogen_start>.
|
||||
"start_in_prompt": True,
|
||||
}
|
||||
|
||||
extra_args = getattr(stage0_params, "extra_args", None)
|
||||
if extra_args is None:
|
||||
extra_args = {}
|
||||
stage0_params.extra_args = extra_args
|
||||
extra_args["tta_rvq"] = server._audex_tta_rvq
|
||||
|
||||
cfg_scale = extra_args.get("cfg_scale")
|
||||
cfg_scale = 3.0 if cfg_scale is None else float(cfg_scale)
|
||||
if cfg_scale <= 1.0:
|
||||
extra_args.pop("cfg_scale", None)
|
||||
return stage0_params
|
||||
extra_args.update(
|
||||
{
|
||||
"cfg_scale": cfg_scale,
|
||||
"cfg_role": "cond",
|
||||
"cfg_pair_id": request_id,
|
||||
"cfg_null_prompt": build_tta_null_prompt(cond_prompt, tokenizer),
|
||||
}
|
||||
)
|
||||
return stage0_params
|
||||
@@ -7,9 +7,7 @@ model's request normalization, validation, prompt/param building, sampling
|
||||
overrides, and output policy, so adding a model means writing one adapter file
|
||||
instead of editing the shared serving module in ~10 scattered places.
|
||||
|
||||
See the RFC for the full design (issue #4327). This is the foundation landed in
|
||||
the first migration PR; Qwen3-TTS is the first model routed through it while the
|
||||
remaining models stay on the legacy path until individually migrated.
|
||||
See the RFC for the full design (issue #4327).
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
@@ -39,6 +37,21 @@ def conditioning_cache_salt(request: "OpenAICreateSpeechRequest", tts_params: di
|
||||
return _conditioning_cache_salt_fn(request, tts_params)
|
||||
|
||||
|
||||
def apply_max_new_tokens(
|
||||
sampling_params_list: list,
|
||||
request: "OpenAICreateSpeechRequest",
|
||||
) -> list:
|
||||
"""Apply a request-level ``max_new_tokens`` limit."""
|
||||
if request.max_new_tokens is None:
|
||||
return sampling_params_list
|
||||
|
||||
import copy
|
||||
|
||||
sampling_params_list = copy.deepcopy(sampling_params_list)
|
||||
sampling_params_list[0].max_tokens = request.max_new_tokens
|
||||
return sampling_params_list
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutputPolicy:
|
||||
"""How the orchestrator aggregates engine output for a model.
|
||||
@@ -184,6 +197,8 @@ class TTSModelAdapter(ABC):
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: "OpenAICreateSpeechRequest",
|
||||
prompt: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> list:
|
||||
"""Apply model-specific sampling mutations.
|
||||
|
||||
@@ -228,37 +243,10 @@ class DiffusionTTSAdapter(TTSModelAdapter):
|
||||
return frozenset(params) if params is not None else frozenset()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LegacyDetector:
|
||||
"""A model type the serving layer detects but has no adapter for yet.
|
||||
|
||||
Detection has to keep naming these models so ``serving_speech.py`` can route
|
||||
them down its legacy path, but they own none of the adapter contract. Each
|
||||
entry is a migration debt: delete it when the model gets an adapter. The
|
||||
pre-commit gate (``tools/pre_commit/check_tts_adapter.py``) fails if the list
|
||||
grows.
|
||||
|
||||
Exposes the subset of the :class:`TTSModelAdapter` class surface that
|
||||
detection reads, so both kinds live in one sorted sequence.
|
||||
"""
|
||||
|
||||
name: str
|
||||
stage_keys: frozenset[str] = frozenset()
|
||||
model_archs: frozenset[str] = frozenset()
|
||||
arch_identifies_entry_stage: bool = False
|
||||
detect_priority: int = 100
|
||||
|
||||
def matches(self, model_stage: str | None, model_arch: str | None) -> bool:
|
||||
if model_arch is not None and model_arch in self.model_archs:
|
||||
return True
|
||||
return model_stage is not None and model_stage in self.stage_keys
|
||||
|
||||
|
||||
# Re-exported here to avoid import cycles at call sites.
|
||||
__all__ = [
|
||||
"ARTTSAdapter",
|
||||
"DiffusionTTSAdapter",
|
||||
"LegacyDetector",
|
||||
"OutputPolicy",
|
||||
"PreparedRequest",
|
||||
"SpeechServingContext",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""CosyVoice3 TTS serving adapter."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import register_tts_adapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest
|
||||
@@ -9,6 +12,8 @@ from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, Prepare
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@register_tts_adapter
|
||||
class CosyVoice3Adapter(ARTTSAdapter):
|
||||
@@ -47,6 +52,63 @@ class CosyVoice3Adapter(ARTTSAdapter):
|
||||
self, request: "OpenAICreateSpeechRequest", sampling_params_list: list, has_inline_ref_audio: bool
|
||||
) -> PreparedRequest:
|
||||
prompt = await self.ctx.server._build_cosyvoice3_prompt(request, has_inline_ref_audio=has_inline_ref_audio)
|
||||
# NOTE: CosyVoice3 dynamic-token sampling stays in the orchestrator tail
|
||||
# (keyed on _tts_model_type) during this incremental migration.
|
||||
return PreparedRequest(prompt=prompt, tts_params={}, model_type="cosyvoice3")
|
||||
|
||||
def apply_sampling_overrides(
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: "OpenAICreateSpeechRequest",
|
||||
prompt: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> list:
|
||||
"""Set dynamic min/max tokens based on tokenized text length.
|
||||
|
||||
The official model uses ``min_token_text_ratio`` to prevent early EOS
|
||||
and ``max_token_text_ratio`` to cap the generated token count.
|
||||
"""
|
||||
import copy
|
||||
|
||||
from vllm_omni.model_executor.models.cosyvoice3.tokenizer import get_qwen_tokenizer
|
||||
from vllm_omni.model_executor.models.cosyvoice3.utils import extract_text_token
|
||||
|
||||
server = self.ctx.server
|
||||
sampling_params_list = copy.deepcopy(sampling_params_list)
|
||||
hf_cfg = server.model_config.hf_config
|
||||
# Build the Qwen tokenizer once per process (resolving the model dir via
|
||||
# snapshot_download at most once) and reuse it across requests.
|
||||
tokenizer = server._cosyvoice3_tokenizer
|
||||
if tokenizer is None:
|
||||
model_path = server.engine_client.model_config.model
|
||||
if not os.path.isdir(model_path):
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
model_path = snapshot_download(model_path)
|
||||
tokenizer = get_qwen_tokenizer(
|
||||
token_path=os.path.join(model_path, hf_cfg.qwen_pretrain_path),
|
||||
skip_special_tokens=hf_cfg.skip_special_tokens,
|
||||
version=hf_cfg.version,
|
||||
)
|
||||
server._cosyvoice3_tokenizer = tokenizer
|
||||
_, text_token_len = extract_text_token(
|
||||
request.input,
|
||||
tokenizer,
|
||||
hf_cfg.allowed_special,
|
||||
)
|
||||
min_ratio = getattr(hf_cfg, "min_token_text_ratio", 2)
|
||||
max_ratio = getattr(hf_cfg, "max_token_text_ratio", 20)
|
||||
sampling_params_list[0].min_tokens = max(1, int(text_token_len * min_ratio))
|
||||
sampling_params_list[0].max_tokens = min(2048, int(text_token_len * max_ratio))
|
||||
|
||||
if request.max_new_tokens is not None:
|
||||
sampling_params_list[0].max_tokens = request.max_new_tokens
|
||||
sampling_params_list[0].min_tokens = min(
|
||||
getattr(sampling_params_list[0], "min_tokens", 0),
|
||||
request.max_new_tokens,
|
||||
)
|
||||
logger.info(
|
||||
"CosyVoice3 dynamic tokens: text_tokens=%d, min_tokens=%d, max_tokens=%d",
|
||||
text_token_len,
|
||||
sampling_params_list[0].min_tokens,
|
||||
sampling_params_list[0].max_tokens,
|
||||
)
|
||||
return sampling_params_list
|
||||
@@ -1,10 +1,15 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Fish Speech serving adapter (retires the legacy ``_is_fish_speech`` flag)."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import register_tts_adapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest, conditioning_cache_salt
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import (
|
||||
ARTTSAdapter,
|
||||
PreparedRequest,
|
||||
apply_max_new_tokens,
|
||||
conditioning_cache_salt,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
|
||||
@@ -53,3 +58,12 @@ class FishSpeechAdapter(ARTTSAdapter):
|
||||
tts_params = {}
|
||||
prompt["cache_salt"] = conditioning_cache_salt(request, tts_params)
|
||||
return PreparedRequest(prompt=prompt, tts_params=tts_params, model_type="fish_speech")
|
||||
|
||||
def apply_sampling_overrides(
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: "OpenAICreateSpeechRequest",
|
||||
prompt: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> list:
|
||||
return apply_max_new_tokens(sampling_params_list, request)
|
||||
@@ -1,7 +1,9 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""GLM-TTS serving adapter."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import register_tts_adapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest
|
||||
@@ -9,6 +11,8 @@ from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, Prepare
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@register_tts_adapter
|
||||
class GlmTTSAdapter(ARTTSAdapter):
|
||||
@@ -43,6 +47,63 @@ class GlmTTSAdapter(ARTTSAdapter):
|
||||
self, request: "OpenAICreateSpeechRequest", sampling_params_list: list, has_inline_ref_audio: bool
|
||||
) -> PreparedRequest:
|
||||
prompt = await self.ctx.server._build_glm_tts_prompt(request, has_inline_ref_audio=has_inline_ref_audio)
|
||||
# GLM-TTS dynamic-token sampling stays in the orchestrator tail
|
||||
# (keyed on _tts_model_type) during this incremental migration.
|
||||
return PreparedRequest(prompt=prompt, tts_params={}, model_type="glm_tts")
|
||||
|
||||
def apply_sampling_overrides(
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: "OpenAICreateSpeechRequest",
|
||||
prompt: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> list:
|
||||
# GLM-TTS: set dynamic min/max tokens based on text length.
|
||||
import copy
|
||||
|
||||
server = self.ctx.server
|
||||
sampling_params_list = copy.deepcopy(sampling_params_list)
|
||||
glm_metadata = prompt.get("additional_information") if isinstance(prompt, dict) else None
|
||||
text_len_value = None
|
||||
if isinstance(glm_metadata, dict):
|
||||
text_len_value = glm_metadata.get("glm_tts_text_token_len")
|
||||
if isinstance(text_len_value, list) and text_len_value:
|
||||
text_len_value = text_len_value[0]
|
||||
text_token_len = (
|
||||
int(text_len_value)
|
||||
if text_len_value is not None
|
||||
else server._estimate_glm_tts_text_token_len(request.input)
|
||||
)
|
||||
hf_cfg = server.model_config.hf_config
|
||||
min_ratio = getattr(hf_cfg, "min_token_text_ratio", 2)
|
||||
max_ratio = getattr(hf_cfg, "max_token_text_ratio", 20)
|
||||
stage_min_tokens = getattr(sampling_params_list[0], "min_tokens", None)
|
||||
stage_max_tokens = getattr(sampling_params_list[0], "max_tokens", None)
|
||||
cap_candidates = [int(cap) for cap in (stage_max_tokens, request.max_new_tokens) if cap is not None]
|
||||
hard_cap = min(cap_candidates) if cap_candidates else None
|
||||
|
||||
min_tokens = max(1, int(text_token_len * min_ratio))
|
||||
if stage_min_tokens is not None:
|
||||
min_tokens = max(min_tokens, int(stage_min_tokens))
|
||||
if hard_cap is not None:
|
||||
min_tokens = min(min_tokens, hard_cap)
|
||||
|
||||
max_tokens = max(min_tokens, int(text_token_len * max_ratio))
|
||||
if hard_cap is not None:
|
||||
max_tokens = min(max_tokens, hard_cap)
|
||||
sampling_params_list[0].min_tokens = min_tokens
|
||||
sampling_params_list[0].max_tokens = max_tokens
|
||||
seed = getattr(request, "seed", None)
|
||||
if seed is not None:
|
||||
sampling_params_list[0].seed = seed
|
||||
logger.info(
|
||||
"GLM-TTS dynamic tokens: text_tokens=%d, min_ratio=%s, max_ratio=%s, "
|
||||
"stage_min=%s, stage_max=%s, request_max=%s, min_tokens=%d, max_tokens=%d",
|
||||
text_token_len,
|
||||
min_ratio,
|
||||
max_ratio,
|
||||
stage_min_tokens,
|
||||
stage_max_tokens,
|
||||
request.max_new_tokens,
|
||||
min_tokens,
|
||||
max_tokens,
|
||||
)
|
||||
return sampling_params_list
|
||||
@@ -1,10 +1,10 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Higgs-Audio v2 serving adapter."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import register_tts_adapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest, apply_max_new_tokens
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
|
||||
@@ -99,3 +99,12 @@ class HiggsAudioV2Adapter(ARTTSAdapter):
|
||||
additional["voice_name"] = voice_lower
|
||||
additional["voice_created_at"] = server._voice_created_at(voice_lower)
|
||||
return PreparedRequest(prompt=prompt, tts_params={}, model_type="higgs_audio_v2")
|
||||
|
||||
def apply_sampling_overrides(
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: "OpenAICreateSpeechRequest",
|
||||
prompt: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> list:
|
||||
return apply_max_new_tokens(sampling_params_list, request)
|
||||
@@ -1,10 +1,10 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Higgs-Audio v3 serving adapter."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import register_tts_adapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest, apply_max_new_tokens
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
|
||||
@@ -36,3 +36,12 @@ class HiggsAudioV3Adapter(ARTTSAdapter):
|
||||
) -> PreparedRequest:
|
||||
prompt = await self.ctx.server._build_higgs_audio_v3_params(request)
|
||||
return PreparedRequest(prompt=prompt, tts_params={}, model_type="higgs_audio_v3")
|
||||
|
||||
def apply_sampling_overrides(
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: "OpenAICreateSpeechRequest",
|
||||
prompt: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> list:
|
||||
return apply_max_new_tokens(sampling_params_list, request)
|
||||
@@ -14,7 +14,7 @@ from vllm.inputs import tokens_input
|
||||
from vllm.utils import random_uuid
|
||||
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import register_tts_adapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest, apply_max_new_tokens
|
||||
from vllm_omni.model_executor.models.indextts2.configuration_indextts2 import (
|
||||
INDEXTTS25_MAX_DURATION_FACTOR,
|
||||
INDEXTTS25_MIN_DURATION_FACTOR,
|
||||
@@ -241,6 +241,15 @@ class IndexTTS2Adapter(ARTTSAdapter):
|
||||
prompt["cache_salt"] = indextts2_conditioning_cache_salt(request, tts_params)
|
||||
return PreparedRequest(prompt=prompt, tts_params=tts_params, model_type=self.name)
|
||||
|
||||
def apply_sampling_overrides(
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: OpenAICreateSpeechRequest,
|
||||
prompt: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> list:
|
||||
return apply_max_new_tokens(sampling_params_list, request)
|
||||
|
||||
async def _build_params(self, request: OpenAICreateSpeechRequest) -> dict[str, Any]:
|
||||
server = self.ctx.server
|
||||
params: dict[str, Any] = {"text": [request.input]}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Ming-TTS (dense) serving adapter."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from vllm.logger import init_logger
|
||||
|
||||
@@ -73,9 +73,36 @@ class MingTTSAdapter(ARTTSAdapter):
|
||||
voice_created_at=uploaded_audio_created_at,
|
||||
)
|
||||
tts_params = prompt.get("additional_information", {})
|
||||
# Ming stop-token / max_tokens sampling stays in the orchestrator tail.
|
||||
return PreparedRequest(prompt=prompt, tts_params=tts_params, model_type="ming_tts")
|
||||
|
||||
def apply_sampling_overrides(
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: "OpenAICreateSpeechRequest",
|
||||
prompt: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> list:
|
||||
import copy
|
||||
|
||||
server = self.ctx.server
|
||||
|
||||
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import (
|
||||
MOE_TEXT_EOS_TOKEN_ID,
|
||||
TEXT_EOS_TOKEN_ID,
|
||||
)
|
||||
|
||||
hf_config = server.engine_client.model_config.hf_config
|
||||
is_moe = getattr(hf_config, "model_type", "") == "bailingmm"
|
||||
stop_token_id = MOE_TEXT_EOS_TOKEN_ID if is_moe else TEXT_EOS_TOKEN_ID
|
||||
|
||||
sampling_params_list = copy.deepcopy(sampling_params_list)
|
||||
sampling_params_list[0].stop_token_ids = [int(stop_token_id)]
|
||||
if request.max_new_tokens is not None:
|
||||
# Ming emits TEXT_EOS after the latent decode budget is exhausted, so
|
||||
# Stage-0 needs one extra token beyond ming_max_decode_steps.
|
||||
sampling_params_list[0].max_tokens = int(request.max_new_tokens) + 1
|
||||
return sampling_params_list
|
||||
|
||||
def _validate_ming_tts_single_speaker_request(self, request: "OpenAICreateSpeechRequest") -> str | None:
|
||||
server = self.ctx.server
|
||||
if request.ref_audio is not None:
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
"""Qwen3-TTS serving adapter."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import register_tts_adapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest, apply_max_new_tokens
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
|
||||
@@ -163,3 +163,12 @@ class Qwen3TTSAdapter(ARTTSAdapter):
|
||||
model_type=tts_params.get("task_type", ["unknown"])[0],
|
||||
warmup_artifact_key=warmup_key,
|
||||
)
|
||||
|
||||
def apply_sampling_overrides(
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: "OpenAICreateSpeechRequest",
|
||||
prompt: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> list:
|
||||
return apply_max_new_tokens(sampling_params_list, request)
|
||||
@@ -1,10 +1,10 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""VoxCPM2 serving adapter (AR base-LM + diffusion side-computation)."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import register_tts_adapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest, apply_max_new_tokens
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
|
||||
@@ -70,3 +70,12 @@ class VoxCPM2Adapter(ARTTSAdapter):
|
||||
additional["voice_name"] = voice_lower
|
||||
additional["voice_created_at"] = server._voice_created_at(voice_lower)
|
||||
return PreparedRequest(prompt=prompt, tts_params=tts_params, model_type="voxcpm2")
|
||||
|
||||
def apply_sampling_overrides(
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: "OpenAICreateSpeechRequest",
|
||||
prompt: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> list:
|
||||
return apply_max_new_tokens(sampling_params_list, request)
|
||||
@@ -1,10 +1,10 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Voxtral TTS serving adapter."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import register_tts_adapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest, apply_max_new_tokens
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
|
||||
@@ -48,3 +48,12 @@ class VoxtralTTSAdapter(ARTTSAdapter):
|
||||
) -> PreparedRequest:
|
||||
prompt = await self.ctx.server._build_voxtral_prompt_async(request)
|
||||
return PreparedRequest(prompt=prompt, tts_params={}, model_type="voxtral_tts")
|
||||
|
||||
def apply_sampling_overrides(
|
||||
self,
|
||||
sampling_params_list: list,
|
||||
request: "OpenAICreateSpeechRequest",
|
||||
prompt: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> list:
|
||||
return apply_max_new_tokens(sampling_params_list, request)
|
||||
Reference in new issue
Block a user