88import os
99import struct
1010import wave
11+ from dataclasses import FrozenInstanceError , replace
1112from inspect import Signature , signature
1213from pathlib import Path
1314from 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 )
@@ -3590,12 +3627,11 @@ def cosyvoice3_server(mocker: MockerFixture):
35903627 mock_models = mocker .MagicMock ()
35913628 mock_models .is_base_model .return_value = True
35923629
3593- server = OmniOpenAIServingSpeech (
3630+ return OmniOpenAIServingSpeech (
35943631 engine_client = mock_engine_client ,
35953632 models = mock_models ,
35963633 request_logger = mocker .MagicMock (),
35973634 )
3598- return server
35993635
36003636
36013637class TestCosyVoice3Serving :
@@ -3715,14 +3751,12 @@ def glm_tts_server(mocker: MockerFixture):
37153751
37163752 mock_models = mocker .MagicMock ()
37173753 mock_models .is_base_model .return_value = True
3718- server = OmniOpenAIServingSpeech (
3754+ return OmniOpenAIServingSpeech (
37193755 engine_client = mock_engine_client ,
37203756 models = mock_models ,
37213757 request_logger = mocker .MagicMock (),
37223758 )
37233759
3724- return server
3725-
37263760
37273761class TestGLMTTSServing :
37283762 def test_validate_glm_tts_requires_ref_audio (self , glm_tts_server ):
@@ -3797,14 +3831,12 @@ def ming_flash_omni_tts_server(mocker: MockerFixture):
37973831 mock_models = mocker .MagicMock ()
37983832 mock_models .is_base_model .return_value = True
37993833
3800- server = OmniOpenAIServingSpeech (
3834+ return OmniOpenAIServingSpeech (
38013835 engine_client = mock_engine_client ,
38023836 models = mock_models ,
38033837 request_logger = mocker .MagicMock (),
38043838 )
38053839
3806- return server
3807-
38083840
38093841class TestMingFlashOmniTTSServing :
38103842 def test_validate_ming_flash_omni_tts_rejects_ref_text (self , ming_flash_omni_tts_server ):
@@ -4133,7 +4165,10 @@ def test_qwen3_repeated_ref_audio_hot_path_sends_cache_key_without_waveform(self
41334165 artifact_key ,
41344166 )
41354167 qwen3_tts_server ._ref_audio_model_artifact_ready .add ((artifact_key , False ))
4136- qwen3_tts_server ._adapter .capabilities .codec_frame_rate = 25.0
4168+ qwen3_tts_server ._adapter .capabilities = replace (
4169+ qwen3_tts_server ._adapter .capabilities ,
4170+ codec_frame_rate = 25.0 ,
4171+ )
41374172 qwen3_tts_server ._tts_tokenizer = lambda _text , padding = False : {"input_ids" : list (range (10 ))}
41384173 qwen3_tts_server .engine_client .model_config .hf_config .talker_config = SimpleNamespace (
41394174 codec_language_id = {},
0 commit comments