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 )
@@ -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 = {},
0 commit comments