Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions tests/entrypoints/openai_api/test_serving_speech.py
Original file line number Diff line number Diff line change
Expand Up @@ -3601,6 +3601,63 @@ def encode(self, text):
load_tokenizer.assert_called_once()


@pytest.fixture
def ming_flash_omni_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",
)
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="ming_tts"),
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 TestMingFlashOmniTTSServing:
def test_validate_ming_flash_omni_tts_rejects_ref_text(self, ming_flash_omni_tts_server):
request = OpenAICreateSpeechRequest(input="Hello", ref_text="Reference transcript")
error = ming_flash_omni_tts_server._validate_tts_request(request)
assert error is not None
assert "ref_text" in error

def test_validate_ming_flash_omni_tts_rejects_ref_audio(self, ming_flash_omni_tts_server):
request = OpenAICreateSpeechRequest(input="Hello", ref_audio="data:audio/wav;base64,abc")
error = ming_flash_omni_tts_server._validate_tts_request(request)
assert error is not None
assert "ref_audio" in error

def test_ming_flash_omni_tts_adapter_builds_prompt(self, ming_flash_omni_tts_server, mocker: MockerFixture):
ming_flash_omni_tts_server._build_ming_flash_omni_prompt = mocker.MagicMock(
return_value={
"prompt_token_ids": [1, 2, 3],
"additional_information": {"voice": ["test"]},
}
)
request = OpenAICreateSpeechRequest(input="Hello", voice="test")
asyncio.run(ming_flash_omni_tts_server._prepare_speech_generation(request))
ming_flash_omni_tts_server._build_ming_flash_omni_prompt.assert_called_once()


class TestTTSAsyncOffloading:
"""Tests for event-loop-safe offloading of blocking TTS operations."""

Expand Down
6 changes: 0 additions & 6 deletions tests/entrypoints/openai_api/test_tts_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,6 @@ def test_resolve_unknown_returns_none():
assert resolve_adapter(None) is None


def test_ming_flash_omni_not_migrated():
"""ming_flash_omni is intentionally excluded from the adapter migration in
this PR; it stays on the legacy inline dispatch in serving_speech.py."""
assert resolve_adapter("ming_flash_omni_tts") is None


def test_voxcpm2_resolves():
"""VoxCPM2 (the served ``latent_generator`` model) resolves cleanly.

Expand Down
51 changes: 1 addition & 50 deletions vllm_omni/entrypoints/openai/serving_speech.py
Original file line number Diff line number Diff line change
Expand Up @@ -1520,9 +1520,6 @@ def _is_tts_model(self) -> bool:

def _validate_tts_request(self, request: OpenAICreateSpeechRequest) -> str | None:
"""Validate TTS request parameters. Returns error message or None."""
if self._tts_model_type == "ming_flash_omni_tts":
return self._validate_ming_flash_omni_tts_request(request)

adapter = self._get_tts_adapter()
if adapter is not None:
return adapter.validate(request)
Expand Down Expand Up @@ -1556,41 +1553,6 @@ def _voxcpm2_encode(self, text: str) -> list[int]:
ids = self._voxcpm2_tokenizer.encode(text, add_special_tokens=True)
return split_multichar_chinese(ids, self._voxcpm2_split_map)

def _validate_ming_flash_omni_tts_request(self, request: OpenAICreateSpeechRequest) -> str | None:
"""Validate Ming-flash-omni standalone-talker request parameters."""
if not request.input or not request.input.strip():
return "Input text cannot be empty"
if request.instructions is not None:
if not isinstance(request.instructions, str):
return "instructions must be a string"
if len(request.instructions) > self._max_instructions_length:
return f"instructions exceeds max length {self._max_instructions_length}"

if request.task_type is not None:
return "'task_type' is not supported for Ming-flash-omni TTS"
if request.language is not None:
return "'language' is not supported for Ming-flash-omni TTS (language is inferred from input text)"
if request.x_vector_only_mode is not None:
return "'x_vector_only_mode' is not supported for Ming-flash-omni TTS"
if request.initial_codec_chunk_frames is not None:
return "'initial_codec_chunk_frames' is not supported for Ming-flash-omni TTS"

# Per-request voice cloning from raw audio is not yet wired up: Ming
# extracts spk_emb / prompt_wav_lat / prompt_wav_emb model-side via
# register_prompt_wav() at engine init. For ad-hoc cloning, callers
# should pre-compute speaker_embedding and pass it directly.
if request.ref_audio is not None:
return (
"'ref_audio' is not yet supported for Ming-flash-omni TTS; "
"use a preset 'voice' or 'speaker_embedding' instead"
)
if request.ref_text is not None:
return "'ref_text' is not yet supported for Ming-flash-omni TTS"

if request.max_new_tokens is not None and request.max_new_tokens <= 0:
return "'max_new_tokens' must be a positive integer"
return None

def _validate_ref_audio_format(self, ref_audio: str) -> str | None:
"""Validate ref_audio is a supported URI format. Returns error or None."""
if not isinstance(ref_audio, str):
Expand Down Expand Up @@ -3246,18 +3208,7 @@ async def _prepare_speech_generation(
# inline vs. via an uploaded voice.
model_type: str | None = None
has_inline_ref_audio = (request.ref_audio is not None) if has_inline_ref_audio is None else has_inline_ref_audio
if self._tts_model_type == "ming_flash_omni_tts":
# ming_flash_omni is intentionally NOT migrated onto the adapter
# framework in this PR (it has no registered adapter); keep it on the
# legacy inline dispatch so serving still works.
model_type = "ming_flash_omni_tts"
validation_error = self._validate_ming_flash_omni_tts_request(request)
if validation_error:
raise ValueError(validation_error)
prompt = self._build_ming_flash_omni_prompt(request)
tts_params = {}
qwen3_ref_audio_warmup_artifact_key = None
elif (adapter := self._get_tts_adapter()) is not None:
if (adapter := self._get_tts_adapter()) is not None:
validation_error = adapter.validate(request)
if validation_error:
raise ValueError(validation_error)
Expand Down
1 change: 1 addition & 0 deletions vllm_omni/entrypoints/openai/tts_adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def resolve_adapter(model_type: str | None) -> type[TTSModelAdapter] | None:
higgs_audio_v2,
higgs_audio_v3,
indextts2,
ming_flash_omni_tts,
ming_tts,
moss_tts,
omnivoice,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# SPDX-License-Identifier: Apache-2.0
"""MingFlashOmniTTS serving adapter."""

from typing import TYPE_CHECKING

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

if TYPE_CHECKING:
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest

logger = init_logger(__name__)


@register_tts_adapter
class MingFlashOmniTTSAdapter(ARTTSAdapter):
stage_keys = frozenset({"ming_tts"})
name = "ming_flash_omni_tts"

def validate(self, request: "OpenAICreateSpeechRequest") -> str | None:
"""Validate Ming-flash-omni standalone-talker request parameters."""
server = self.ctx.server
if not request.input or not request.input.strip():
return "Input text cannot be empty"
if request.instructions is not None:
if not isinstance(request.instructions, str):
return "instructions must be a string"
if len(request.instructions) > server._max_instructions_length:
return f"instructions exceeds max length {server._max_instructions_length}"

if request.task_type is not None:
return "'task_type' is not supported for Ming-flash-omni TTS"
if request.language is not None:
return "'language' is not supported for Ming-flash-omni TTS (language is inferred from input text)"
if request.x_vector_only_mode is not None:
return "'x_vector_only_mode' is not supported for Ming-flash-omni TTS"
if request.initial_codec_chunk_frames is not None:
return "'initial_codec_chunk_frames' is not supported for Ming-flash-omni TTS"

# Per-request voice cloning from raw audio is not yet wired up: Ming
# extracts spk_emb / prompt_wav_lat / prompt_wav_emb model-side via
# register_prompt_wav() at engine init. For ad-hoc cloning, callers
# should pre-compute speaker_embedding and pass it directly.
if request.ref_audio is not None:
return (
"'ref_audio' is not yet supported for Ming-flash-omni TTS; "
"use a preset 'voice' or 'speaker_embedding' instead"
)
if request.ref_text is not None:
return "'ref_text' is not yet supported for Ming-flash-omni TTS"

if request.max_new_tokens is not None and request.max_new_tokens <= 0:
return "'max_new_tokens' must be a positive integer"
return None

async def build(
self, request: "OpenAICreateSpeechRequest", sampling_params_list: list, has_inline_ref_audio: bool
) -> PreparedRequest:
server = self.ctx.server
prompt = server._build_ming_flash_omni_prompt(request)
return PreparedRequest(prompt=prompt, tts_params={}, model_type="ming_flash_omni_tts")
Loading