Skip to content

Commit 4859687

Browse files
committed
frozen built-in adapter capabilities
Signed-off-by: boatman <109857087+sphinxkkkbc@users.noreply.github.com>
1 parent 5b09489 commit 4859687

2 files changed

Lines changed: 84 additions & 42 deletions

File tree

tests/entrypoints/openai_api/test_serving_speech.py

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import os
99
import struct
1010
import wave
11+
from dataclasses import FrozenInstanceError, replace
1112
from inspect import Signature, signature
1213
from pathlib import Path
1314
from types import SimpleNamespace
@@ -1090,7 +1091,10 @@ def test_upload_ming_audio_voice_defers_speaker_extraction(self, speech_server,
10901091
speech_server._adapter = speech_server._get_tts_adapter()
10911092
speech_server.uploaded_speakers_dir = tmp_path
10921093
speech_server.uploaded_speakers = {}
1093-
speech_server._adapter.capabilities.supported_speakers = set()
1094+
speech_server._adapter.capabilities = replace(
1095+
speech_server._adapter.capabilities,
1096+
supported_speakers=frozenset(),
1097+
)
10941098
speech_server._speaker_cache.clear()
10951099

10961100
left = np.full(32000, 0.5, dtype=np.float32)
@@ -1186,16 +1190,19 @@ def test_precomputed_qwen3_voice_infers_base_without_ref_audio(self, speech_serv
11861190
"""Precomputed Qwen3 voices are reusable by name without per-request ref_audio."""
11871191
speech_server._tts_model_type = "qwen3_tts"
11881192
speech_server._adapter = speech_server._get_tts_adapter()
1189-
speech_server._adapter.capabilities.precomputed_speakers = {
1190-
"alice": {
1191-
"name": "Alice",
1192-
"model_type": "qwen3_tts",
1193-
"mode": "icl",
1194-
"ref_text": "reference transcript",
1195-
"ref_code_length": 3,
1196-
}
1197-
}
1198-
speech_server._adapter.capabilities.supported_speakers = {"alice"}
1193+
speech_server._adapter.capabilities = replace(
1194+
speech_server._adapter.capabilities,
1195+
precomputed_speakers={
1196+
"alice": {
1197+
"name": "Alice",
1198+
"model_type": "qwen3_tts",
1199+
"mode": "icl",
1200+
"ref_text": "reference transcript",
1201+
"ref_code_length": 3,
1202+
}
1203+
},
1204+
supported_speakers=frozenset({"alice"}),
1205+
)
11991206

12001207
req = OpenAICreateSpeechRequest(input="Hello", voice="Alice")
12011208
assert speech_server._validate_tts_request(req) is None
@@ -1226,15 +1233,18 @@ def test_uploaded_qwen3_voice_wins_over_same_named_precomputed_voice(self, speec
12261233
"embedding_source": "direct",
12271234
}
12281235
}
1229-
speech_server._adapter.capabilities.precomputed_speakers = {
1230-
"alice": {
1231-
"name": "Alice",
1232-
"model_type": "qwen3_tts",
1233-
"mode": "icl",
1234-
"ref_text": "precomputed transcript",
1235-
"ref_code_length": 3,
1236-
}
1237-
}
1236+
speech_server._adapter.capabilities = replace(
1237+
speech_server._adapter.capabilities,
1238+
precomputed_speakers={
1239+
"alice": {
1240+
"name": "Alice",
1241+
"model_type": "qwen3_tts",
1242+
"mode": "icl",
1243+
"ref_text": "precomputed transcript",
1244+
"ref_code_length": 3,
1245+
}
1246+
},
1247+
)
12381248

12391249
req = OpenAICreateSpeechRequest(input="Hello", voice="Alice")
12401250
assert speech_server._validate_tts_request(req) is None
@@ -1266,7 +1276,10 @@ def test_precomputed_qwen3_missing_safetensors_is_not_registered(self, speech_se
12661276
profiles = speech_server._adapter._load_precomputed_speakers()
12671277
assert profiles == {}
12681278

1269-
speech_server._adapter.capabilities.precomputed_speakers = profiles
1279+
speech_server._adapter.capabilities = replace(
1280+
speech_server._adapter.capabilities,
1281+
precomputed_speakers=profiles,
1282+
)
12701283
assert "alice" not in speech_server._adapter.capabilities.precomputed_speakers
12711284
req = OpenAICreateSpeechRequest(input="Hello", voice="Alice")
12721285
assert speech_server._validate_tts_request(req) is not None
@@ -1300,7 +1313,10 @@ def test_precomputed_qwen3_icl_without_ref_code_is_not_registered(self, speech_s
13001313
profiles = speech_server._adapter._load_precomputed_speakers()
13011314
assert profiles == {}
13021315

1303-
speech_server._adapter.capabilities.precomputed_speakers = profiles
1316+
speech_server._adapter.capabilities = replace(
1317+
speech_server._adapter.capabilities,
1318+
precomputed_speakers=profiles,
1319+
)
13041320
assert "alice" not in speech_server._adapter.capabilities.precomputed_speakers
13051321
req = OpenAICreateSpeechRequest(input="Hello", voice="Alice")
13061322
assert speech_server._validate_tts_request(req) is not None
@@ -1321,7 +1337,10 @@ def test_precomputed_voxcpm2_missing_safetensors_is_not_registered(self, speech_
13211337
profiles = speech_server._adapter._load_precomputed_speakers()
13221338
assert profiles == {}
13231339

1324-
speech_server._adapter.capabilities.precomputed_speakers = profiles
1340+
speech_server._adapter.capabilities = replace(
1341+
speech_server._adapter.capabilities,
1342+
precomputed_speakers=profiles,
1343+
)
13251344
assert "bob" not in speech_server._adapter.capabilities.precomputed_speakers
13261345
req = OpenAICreateSpeechRequest(input="Hello", voice="Bob")
13271346
assert speech_server._validate_tts_request(req) is not None
@@ -1332,7 +1351,10 @@ def test_prepare_voxcpm2_rejects_supported_speaker_without_voice_profile(self, s
13321351
speech_server._tts_model_type = "voxcpm2"
13331352
speech_server._adapter = speech_server._get_tts_adapter()
13341353
speech_server.uploaded_speakers = {}
1335-
speech_server._adapter.capabilities.precomputed_speakers = {}
1354+
speech_server._adapter.capabilities = replace(
1355+
speech_server._adapter.capabilities,
1356+
precomputed_speakers={},
1357+
)
13361358
speech_server.engine_client.default_sampling_params_list = [SimpleNamespace(max_tokens=2048)]
13371359
speech_server.engine_client.generate = mocker.MagicMock(return_value="generator")
13381360
speech_server._build_voxcpm2_prompt = mocker.AsyncMock(
@@ -1350,7 +1372,10 @@ def test_prepare_voxcpm2_accepts_default_voice(self, speech_server, mocker):
13501372
speech_server._tts_model_type = "voxcpm2"
13511373
speech_server._adapter = speech_server._get_tts_adapter()
13521374
speech_server.uploaded_speakers = {}
1353-
speech_server._adapter.capabilities.precomputed_speakers = {}
1375+
speech_server._adapter.capabilities = replace(
1376+
speech_server._adapter.capabilities,
1377+
precomputed_speakers={},
1378+
)
13541379
speech_server.engine_client.default_sampling_params_list = [SimpleNamespace(max_tokens=2048)]
13551380
speech_server.engine_client.generate = mocker.MagicMock(return_value=iter(()))
13561381
speech_server._build_voxcpm2_prompt = mocker.AsyncMock(
@@ -1367,14 +1392,17 @@ def test_prepare_voxcpm2_precomputed_voice_sets_model_cache_key(self, speech_ser
13671392
speech_server._tts_model_type = "voxcpm2"
13681393
speech_server._adapter = speech_server._get_tts_adapter()
13691394
speech_server.uploaded_speakers = {}
1370-
speech_server._adapter.capabilities.precomputed_speakers = {
1371-
"alice": {
1372-
"name": "Alice",
1373-
"model_type": "voxcpm2",
1374-
"mode": "reference",
1375-
"ref_audio_feat_len": 2,
1376-
}
1377-
}
1395+
speech_server._adapter.capabilities = replace(
1396+
speech_server._adapter.capabilities,
1397+
precomputed_speakers={
1398+
"alice": {
1399+
"name": "Alice",
1400+
"model_type": "voxcpm2",
1401+
"mode": "reference",
1402+
"ref_audio_feat_len": 2,
1403+
}
1404+
},
1405+
)
13781406
speech_server.engine_client.default_sampling_params_list = [SimpleNamespace(max_tokens=2048)]
13791407
speech_server.engine_client.generate = mocker.MagicMock(return_value=iter(()))
13801408
speech_server._build_voxcpm2_prompt = mocker.AsyncMock(
@@ -1472,6 +1500,9 @@ def test_load_supported_speakers(self, mocker: MockerFixture):
14721500

14731501
# Verify speakers are normalized to lowercase
14741502
assert server._adapter.capabilities.supported_speakers == {"ryan", "vivian", "aiden"}
1503+
assert isinstance(server._adapter.capabilities.supported_speakers, frozenset)
1504+
with pytest.raises(FrozenInstanceError):
1505+
setattr(server._adapter.capabilities, "codec_frame_rate", 25.0)
14751506

14761507
def test_load_supported_speakers_skips_non_tts_omni_model(
14771508
self,
@@ -1518,7 +1549,10 @@ def test_validate_language_custom_dialect_accepted(self, speech_server):
15181549
"""A language present in the model config passes validation, case-insensitively."""
15191550
speech_server._tts_model_type = "qwen3_tts"
15201551
speech_server._adapter = speech_server._get_tts_adapter()
1521-
speech_server._adapter.capabilities.supported_languages = {"Chinese", "English", "Beijing_Dialect", "Auto"}
1552+
speech_server._adapter.capabilities = replace(
1553+
speech_server._adapter.capabilities,
1554+
supported_languages=frozenset({"Chinese", "English", "Beijing_Dialect", "Auto"}),
1555+
)
15221556
for language in ("Beijing_Dialect", "beijing_dialect", "English", "english", "Auto", "AUTO"):
15231557
req = OpenAICreateSpeechRequest(input="Hello", language=language)
15241558
result = speech_server._validate_tts_request(req)
@@ -1530,7 +1564,10 @@ def test_validate_language_unknown_rejected(self, speech_server):
15301564
"""A language not in the configured set is rejected."""
15311565
speech_server._tts_model_type = "qwen3_tts"
15321566
speech_server._adapter = speech_server._get_tts_adapter()
1533-
speech_server._adapter.capabilities.supported_languages = {"Chinese", "English", "Auto"}
1567+
speech_server._adapter.capabilities = replace(
1568+
speech_server._adapter.capabilities,
1569+
supported_languages=frozenset({"Chinese", "English", "Auto"}),
1570+
)
15341571
for language in ("Klingon", "klingon"):
15351572
req = OpenAICreateSpeechRequest(input="Hello", language=language)
15361573
assert "Invalid language" in speech_server._validate_tts_request(req)
@@ -4133,7 +4170,10 @@ def test_qwen3_repeated_ref_audio_hot_path_sends_cache_key_without_waveform(self
41334170
artifact_key,
41344171
)
41354172
qwen3_tts_server._ref_audio_model_artifact_ready.add((artifact_key, False))
4136-
qwen3_tts_server._adapter.capabilities.codec_frame_rate = 25.0
4173+
qwen3_tts_server._adapter.capabilities = replace(
4174+
qwen3_tts_server._adapter.capabilities,
4175+
codec_frame_rate=25.0,
4176+
)
41374177
qwen3_tts_server._tts_tokenizer = lambda _text, padding=False: {"input_ids": list(range(10))}
41384178
qwen3_tts_server.engine_client.model_config.hf_config.talker_config = SimpleNamespace(
41394179
codec_language_id={},

vllm_omni/entrypoints/openai/tts_adapters/base.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,10 @@ class SpeechServingContext:
105105
diffusion_engine: Any | None = None
106106

107107

108-
@dataclass
108+
@dataclass(frozen=True)
109109
class TTSCapabilities:
110110
precomputed_speakers: dict[str, dict[str, Any]] = field(default_factory=dict)
111-
supported_speakers: set[str] = field(default_factory=set)
111+
supported_speakers: frozenset[str] = frozenset()
112112
supported_languages: frozenset[str] = DEFAULT_TTS_LANGUAGES
113113
codec_frame_rate: float | None = None
114114

@@ -225,10 +225,12 @@ def validate_tts_embedding_dim(self, emb_dim: int) -> str | None:
225225
return None
226226

227227
def load_capabilities(self) -> TTSCapabilities:
228-
self.capabilities.precomputed_speakers = self._load_precomputed_speakers()
229-
self.capabilities.supported_speakers = self._load_supported_speakers()
230-
self.capabilities.supported_languages = self._load_supported_languages()
231-
self.capabilities.codec_frame_rate = self._load_codec_frame_rate()
228+
self.capabilities = TTSCapabilities(
229+
precomputed_speakers=self._load_precomputed_speakers(),
230+
supported_speakers=frozenset(self._load_supported_speakers()),
231+
supported_languages=self._load_supported_languages(),
232+
codec_frame_rate=self._load_codec_frame_rate(),
233+
)
232234
return self.capabilities
233235

234236
def _load_precomputed_speakers(self) -> dict[str, dict[str, Any]]:

0 commit comments

Comments
 (0)