Prompt48 commited on
Commit
2743fb5
·
verified ·
1 Parent(s): d80bf02

Upload edit\Qwen3-TTS-test\qwen_tts\inference\qwen3_tts_model.py with huggingface_hub

Browse files
edit//Qwen3-TTS-test//qwen_tts//inference//qwen3_tts_model.py ADDED
@@ -0,0 +1,877 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2026 The Alibaba Qwen team.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ import base64
17
+ import io
18
+ import urllib.request
19
+ from dataclasses import dataclass
20
+ from typing import Any, Dict, List, Optional, Tuple, Union
21
+ from urllib.parse import urlparse
22
+
23
+ import librosa
24
+ import numpy as np
25
+ import soundfile as sf
26
+ import torch
27
+ from transformers import AutoConfig, AutoModel, AutoProcessor
28
+
29
+ from ..core.models import Qwen3TTSConfig, Qwen3TTSForConditionalGeneration, Qwen3TTSProcessor
30
+
31
+ AudioLike = Union[
32
+ str, # wav path, URL, base64
33
+ np.ndarray, # waveform (requires sr)
34
+ Tuple[np.ndarray, int], # (waveform, sr)
35
+ ]
36
+
37
+ MaybeList = Union[Any, List[Any]]
38
+
39
+
40
+ @dataclass
41
+ class VoiceClonePromptItem:
42
+ """
43
+ Container for one sample's voice-clone prompt information that can be fed to the model.
44
+
45
+ Fields are aligned with `Qwen3TTSForConditionalGeneration.generate(..., voice_clone_prompt=...)`.
46
+ """
47
+ ref_code: Optional[torch.Tensor] # (T, Q) or (T,) depending on tokenizer 25Hz/12Hz
48
+ ref_spk_embedding: torch.Tensor # (D,)
49
+ x_vector_only_mode: bool
50
+ icl_mode: bool
51
+ ref_text: Optional[str] = None
52
+
53
+
54
+ class Qwen3TTSModel:
55
+ """
56
+ A HuggingFace-style wrapper for Qwen3 TTS models (CustomVoice/VoiceDesign/Base) that provides:
57
+ - from_pretrained() initialization via AutoModel/AutoProcessor
58
+ - generation APIs for:
59
+ * CustomVoice: generate_custom_voice()
60
+ * VoiceDesign: generate_voice_design()
61
+ * Base: generate_voice_clone() + create_voice_clone_prompt()
62
+ - consistent output: (wavs: List[np.ndarray], sample_rate: int)
63
+
64
+ Notes:
65
+ - This wrapper expects the underlying model class to be `Qwen3TTSForConditionalGeneration`
66
+ - Language / speaker validation is done via model methods:
67
+ model.get_supported_languages(), model.get_supported_speakers()
68
+ """
69
+
70
+ def __init__(self, model: Qwen3TTSForConditionalGeneration, processor, generate_defaults: Optional[Dict[str, Any]] = None):
71
+ self.model = model
72
+ self.processor = processor
73
+ self.generate_defaults = generate_defaults or {}
74
+
75
+ self.device = getattr(model, "device", None)
76
+ if self.device is None:
77
+ try:
78
+ self.device = next(model.parameters()).device
79
+ except StopIteration:
80
+ self.device = torch.device("cpu")
81
+
82
+ @classmethod
83
+ def from_pretrained(
84
+ cls,
85
+ pretrained_model_name_or_path: str,
86
+ **kwargs,
87
+ ) -> "Qwen3TTSModel":
88
+ """
89
+ Load a Qwen3 TTS model and its processor in HuggingFace `from_pretrained` style.
90
+
91
+ This method:
92
+ 1) Loads config via AutoConfig (so your side can register model_type -> config/model).
93
+ 2) Loads the model via AutoModel.from_pretrained(...), forwarding `kwargs` unchanged.
94
+ 3) Loads the processor via AutoProcessor.from_pretrained(model_path).
95
+ 4) Loads optional `generate_config.json` from the model directory/repo snapshot if present.
96
+
97
+ Args:
98
+ pretrained_model_name_or_path (str):
99
+ HuggingFace repo id or local directory of the model.
100
+ **kwargs:
101
+ Forwarded as-is into `AutoModel.from_pretrained(...)`.
102
+ Typical examples: device_map="cuda:0", dtype=torch.bfloat16, attn_implementation="flash_attention_2".
103
+
104
+ Returns:
105
+ Qwen3TTSModel:
106
+ Wrapper instance containing `model`, `processor`, and generation defaults.
107
+ """
108
+ AutoConfig.register("qwen3_tts", Qwen3TTSConfig)
109
+ AutoModel.register(Qwen3TTSConfig, Qwen3TTSForConditionalGeneration)
110
+ AutoProcessor.register(Qwen3TTSConfig, Qwen3TTSProcessor)
111
+
112
+ model = AutoModel.from_pretrained(pretrained_model_name_or_path, **kwargs)
113
+ if not isinstance(model, Qwen3TTSForConditionalGeneration):
114
+ raise TypeError(
115
+ f"AutoModel returned {type(model)}, expected Qwen3TTSForConditionalGeneration. "
116
+ )
117
+
118
+ processor = AutoProcessor.from_pretrained(pretrained_model_name_or_path, fix_mistral_regex=True,)
119
+
120
+ generate_defaults = model.generate_config
121
+ return cls(model=model, processor=processor, generate_defaults=generate_defaults)
122
+
123
+ def _supported_languages_set(self) -> Optional[set]:
124
+ langs = getattr(self.model, "get_supported_languages", None)
125
+ if callable(langs):
126
+ v = langs()
127
+ if v is None:
128
+ return None
129
+ return set([str(x).lower() for x in v])
130
+ return None
131
+
132
+ def _supported_speakers_set(self) -> Optional[set]:
133
+ spks = getattr(self.model, "get_supported_speakers", None)
134
+ if callable(spks):
135
+ v = spks()
136
+ if v is None:
137
+ return None
138
+ return set([str(x).lower() for x in v])
139
+ return None
140
+
141
+ def _validate_languages(self, languages: List[str]) -> None:
142
+ """
143
+ Validate that requested languages are supported by the model.
144
+
145
+ Args:
146
+ languages (List[str]): Language names for each sample.
147
+
148
+ Raises:
149
+ ValueError: If any language is not supported.
150
+ """
151
+ supported = self._supported_languages_set()
152
+ if supported is None:
153
+ return
154
+
155
+ bad = []
156
+ for lang in languages:
157
+ if lang is None:
158
+ bad.append(lang)
159
+ continue
160
+ if str(lang).lower() not in supported:
161
+ bad.append(lang)
162
+ if bad:
163
+ raise ValueError(f"Unsupported languages: {bad}. Supported: {sorted(supported)}")
164
+
165
+ def _validate_speakers(self, speakers: List[Optional[str]]) -> None:
166
+ """
167
+ Validate that requested speakers are supported by the Instruct model.
168
+
169
+ Args:
170
+ speakers (List[Optional[str]]): Speaker names for each sample.
171
+
172
+ Raises:
173
+ ValueError: If any speaker is not supported.
174
+ """
175
+ supported = self._supported_speakers_set()
176
+ if supported is None:
177
+ return
178
+
179
+ bad = []
180
+ for spk in speakers:
181
+ if spk is None or spk == "":
182
+ continue
183
+ if str(spk).lower() not in supported:
184
+ bad.append(spk)
185
+ if bad:
186
+ raise ValueError(f"Unsupported speakers: {bad}. Supported: {sorted(supported)}")
187
+
188
+ def _is_probably_base64(self, s: str) -> bool:
189
+ if s.startswith("data:audio"):
190
+ return True
191
+ if ("/" not in s and "\\" not in s) and len(s) > 256:
192
+ return True
193
+ return False
194
+
195
+ def _is_url(self, s: str) -> bool:
196
+ try:
197
+ u = urlparse(s)
198
+ return u.scheme in ("http", "https") and bool(u.netloc)
199
+ except Exception:
200
+ return False
201
+
202
+ def _decode_base64_to_wav_bytes(self, b64: str) -> bytes:
203
+ if "," in b64 and b64.strip().startswith("data:"):
204
+ b64 = b64.split(",", 1)[1]
205
+ return base64.b64decode(b64)
206
+
207
+ def _load_audio_to_np(self, x: str) -> Tuple[np.ndarray, int]:
208
+ if self._is_url(x):
209
+ with urllib.request.urlopen(x) as resp:
210
+ audio_bytes = resp.read()
211
+ with io.BytesIO(audio_bytes) as f:
212
+ audio, sr = sf.read(f, dtype="float32", always_2d=False)
213
+ elif self._is_probably_base64(x):
214
+ wav_bytes = self._decode_base64_to_wav_bytes(x)
215
+ with io.BytesIO(wav_bytes) as f:
216
+ audio, sr = sf.read(f, dtype="float32", always_2d=False)
217
+ else:
218
+ audio, sr = librosa.load(x, sr=None, mono=True)
219
+
220
+ if audio.ndim > 1:
221
+ audio = np.mean(audio, axis=-1)
222
+
223
+ return audio.astype(np.float32), int(sr)
224
+
225
+ def _normalize_audio_inputs(self, audios: Union[AudioLike, List[AudioLike]]) -> List[Tuple[np.ndarray, int]]:
226
+ """
227
+ Normalize audio inputs into a list of (waveform, sr).
228
+
229
+ Supported forms:
230
+ - str: wav path / URL / base64 audio string
231
+ - (np.ndarray, sr): waveform + sampling rate
232
+ - list of the above
233
+
234
+ Args:
235
+ audios:
236
+ Audio input(s).
237
+
238
+ Returns:
239
+ List[Tuple[np.ndarray, int]]:
240
+ List of (float32 waveform, original sr).
241
+
242
+ Raises:
243
+ ValueError: If a numpy waveform is provided without sr.
244
+ """
245
+ if isinstance(audios, list):
246
+ items = audios
247
+ else:
248
+ items = [audios]
249
+
250
+ out: List[Tuple[np.ndarray, int]] = []
251
+ for a in items:
252
+ if isinstance(a, str):
253
+ out.append(self._load_audio_to_np(a))
254
+ elif isinstance(a, tuple) and len(a) == 2 and isinstance(a[0], np.ndarray):
255
+ out.append((a[0].astype(np.float32), int(a[1])))
256
+ elif isinstance(a, np.ndarray):
257
+ raise ValueError("For numpy waveform input, pass a tuple (audio, sr).")
258
+ else:
259
+ raise TypeError(f"Unsupported audio input type: {type(a)}")
260
+ for i, a in enumerate(out):
261
+ if a[0].ndim > 1:
262
+ a[0] = np.mean(a[0], axis=-1).astype(np.float32)
263
+ out[i] = (a[0], a[1])
264
+ return out
265
+
266
+ def _ensure_list(self, x: MaybeList) -> List[Any]:
267
+ return x if isinstance(x, list) else [x]
268
+
269
+ def _build_assistant_text(self, text: str) -> str:
270
+ return f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
271
+
272
+ def _build_ref_text(self, text: str) -> str:
273
+ return f"<|im_start|>assistant\n{text}<|im_end|>\n"
274
+
275
+ def _build_instruct_text(self, instruct: str) -> str:
276
+ return f"<|im_start|>user\n{instruct}<|im_end|>\n"
277
+
278
+ def _tokenize_texts(self, texts: List[str]) -> List[torch.Tensor]:
279
+ input_ids = []
280
+ for text in texts:
281
+ input = self.processor(text=text, return_tensors="pt", padding=True)
282
+ input_id = input["input_ids"].to(self.device)
283
+ input_id = input_id.unsqueeze(0) if input_id.dim() == 1 else input_id
284
+ input_ids.append(input_id)
285
+ return input_ids
286
+
287
+ def _merge_generate_kwargs(
288
+ self,
289
+ do_sample: Optional[bool] = None,
290
+ top_k: Optional[int] = None,
291
+ top_p: Optional[float] = None,
292
+ temperature: Optional[float] = None,
293
+ repetition_penalty: Optional[float] = None,
294
+ subtalker_dosample: Optional[bool] = None,
295
+ subtalker_top_k: Optional[int] = None,
296
+ subtalker_top_p: Optional[float] = None,
297
+ subtalker_temperature: Optional[float] = None,
298
+ max_new_tokens: Optional[int] = None,
299
+ **kwargs,
300
+ ) -> Dict[str, Any]:
301
+ """
302
+ Merge user-provided generation arguments with defaults from `generate_config.json`.
303
+
304
+ Rule:
305
+ - If the user explicitly passes a value (not None), use it.
306
+ - Otherwise, use the value from generate_config.json if present.
307
+ - Otherwise, fall back to the hard defaults.
308
+
309
+ Args:
310
+ do_sample, top_k, top_p, temperature, repetition_penalty,
311
+ subtalker_dosample, subtalker_top_k, subtalker_top_p, subtalker_temperature, max_new_tokens:
312
+ Common generation parameters.
313
+ **kwargs:
314
+ Other arguments forwarded to model.generate().
315
+
316
+ Returns:
317
+ Dict[str, Any]: Final kwargs to pass into model.generate().
318
+ """
319
+ hard_defaults = dict(
320
+ do_sample=True,
321
+ top_k=50,
322
+ top_p=1.0,
323
+ temperature=0.9,
324
+ repetition_penalty=1.05,
325
+ subtalker_dosample=True,
326
+ subtalker_top_k=50,
327
+ subtalker_top_p=1.0,
328
+ subtalker_temperature=0.9,
329
+ max_new_tokens=2048,
330
+ )
331
+
332
+ def pick(name: str, user_val: Any) -> Any:
333
+ if user_val is not None:
334
+ return user_val
335
+ if name in self.generate_defaults:
336
+ return self.generate_defaults[name]
337
+ return hard_defaults[name]
338
+
339
+ merged = dict(kwargs)
340
+ merged.update(
341
+ do_sample=pick("do_sample", do_sample),
342
+ top_k=pick("top_k", top_k),
343
+ top_p=pick("top_p", top_p),
344
+ temperature=pick("temperature", temperature),
345
+ repetition_penalty=pick("repetition_penalty", repetition_penalty),
346
+ subtalker_dosample=pick("subtalker_dosample", subtalker_dosample),
347
+ subtalker_top_k=pick("subtalker_top_k", subtalker_top_k),
348
+ subtalker_top_p=pick("subtalker_top_p", subtalker_top_p),
349
+ subtalker_temperature=pick("subtalker_temperature", subtalker_temperature),
350
+ max_new_tokens=pick("max_new_tokens", max_new_tokens),
351
+ )
352
+ return merged
353
+
354
+ # voice clone model
355
+ @torch.inference_mode()
356
+ def create_voice_clone_prompt(
357
+ self,
358
+ ref_audio: Union[AudioLike, List[AudioLike]],
359
+ ref_text: Optional[Union[str, List[Optional[str]]]] = None,
360
+ x_vector_only_mode: Union[bool, List[bool]] = False,
361
+ ) -> List[VoiceClonePromptItem]:
362
+ """
363
+ Build voice-clone prompt items from reference audio (and optionally reference text) using Base model.
364
+
365
+ Modes:
366
+ - x_vector_only_mode=True:
367
+ Only speaker embedding is used to clone voice; ref_text/ref_code are ignored.
368
+ This is mutually exclusive with ICL.
369
+ - x_vector_only_mode=False:
370
+ ICL mode is enabled automatically (icl_mode=True). In this case ref_text is required,
371
+ because the model continues/conditions on the reference text + reference speech codes.
372
+
373
+ Batch behavior:
374
+ - ref_audio can be a single item or a list.
375
+ - ref_text and x_vector_only_mode can be scalars or lists.
376
+ - If any of them are lists with length > 1, lengths must match.
377
+
378
+ Audio input:
379
+ - str: local wav path / URL / base64
380
+ - (np.ndarray, sr): waveform + sampling rate
381
+
382
+ Args:
383
+ ref_audio:
384
+ Reference audio(s) used to extract:
385
+ - ref_code via `model.speech_tokenizer.encode(...)`
386
+ - ref_spk_embedding via `model.extract_speaker_embedding(...)` (resampled to 24k)
387
+ ref_text:
388
+ Reference transcript(s). Required when x_vector_only_mode=False (ICL mode).
389
+ x_vector_only_mode:
390
+ Whether to use speaker embedding only. If False, ICL mode will be used.
391
+
392
+ Returns:
393
+ List[VoiceClonePromptItem]:
394
+ List of prompt items that can be converted into `voice_clone_prompt` dict.
395
+
396
+ Raises:
397
+ ValueError:
398
+ - If x_vector_only_mode=False but ref_text is missing.
399
+ - If batch lengths mismatch.
400
+ """
401
+ if self.model.tts_model_type != "base":
402
+ raise ValueError(
403
+ f"model with \ntokenizer_type: {self.model.tokenizer_type}\n"
404
+ f"tts_model_size: {self.model.tts_model_size}\n"
405
+ f"tts_model_type: {self.model.tts_model_type}\n"
406
+ "does not support create_voice_clone_prompt, Please check Model Card or Readme for more details."
407
+ )
408
+
409
+ ref_audio_list = self._ensure_list(ref_audio)
410
+ ref_text_list = self._ensure_list(ref_text) if isinstance(ref_text, list) else ([ref_text] * len(ref_audio_list))
411
+ xvec_list = self._ensure_list(x_vector_only_mode) if isinstance(x_vector_only_mode, list) else ([x_vector_only_mode] * len(ref_audio_list))
412
+
413
+ if len(ref_text_list) != len(ref_audio_list) or len(xvec_list) != len(ref_audio_list):
414
+ raise ValueError(
415
+ f"Batch size mismatch: ref_audio={len(ref_audio_list)}, ref_text={len(ref_text_list)}, x_vector_only_mode={len(xvec_list)}"
416
+ )
417
+
418
+ normalized = self._normalize_audio_inputs(ref_audio_list)
419
+
420
+ ref_wavs_for_code: List[np.ndarray] = []
421
+ ref_sr_for_code: List[int] = []
422
+ for wav, sr in normalized:
423
+ ref_wavs_for_code.append(wav)
424
+ ref_sr_for_code.append(sr)
425
+
426
+ if len(set(ref_sr_for_code)) == 1:
427
+ enc = self.model.speech_tokenizer.encode(ref_wavs_for_code, sr=ref_sr_for_code[0])
428
+ ref_codes = enc.audio_codes
429
+ else:
430
+ ref_codes = []
431
+ for wav, sr in normalized:
432
+ ref_codes.append(self.model.speech_tokenizer.encode(wav, sr=sr).audio_codes[0])
433
+
434
+ items: List[VoiceClonePromptItem] = []
435
+ for i, ((wav, sr), code, rtext, xvec_only) in enumerate(zip(normalized, ref_codes, ref_text_list, xvec_list)):
436
+ if not xvec_only:
437
+ if rtext is None or rtext == "":
438
+ raise ValueError(f"ref_text is required when x_vector_only_mode=False (ICL mode). Bad index={i}")
439
+
440
+ wav_resample = wav
441
+ if sr != self.model.speaker_encoder_sample_rate:
442
+ wav_resample = librosa.resample(y=wav_resample.astype(np.float32),
443
+ orig_sr=int(sr),
444
+ target_sr=self.model.speaker_encoder_sample_rate)
445
+
446
+ spk_emb = self.model.extract_speaker_embedding(audio=wav_resample,
447
+ sr=self.model.speaker_encoder_sample_rate)
448
+
449
+ items.append(
450
+ VoiceClonePromptItem(
451
+ ref_code=None if xvec_only else code,
452
+ ref_spk_embedding=spk_emb,
453
+ x_vector_only_mode=bool(xvec_only),
454
+ icl_mode=bool(not xvec_only),
455
+ ref_text=rtext,
456
+ )
457
+ )
458
+ return items
459
+
460
+ def _prompt_items_to_voice_clone_prompt(self, items: List[VoiceClonePromptItem]) -> Dict[str, Any]:
461
+ return dict(
462
+ ref_code=[it.ref_code for it in items],
463
+ ref_spk_embedding=[it.ref_spk_embedding for it in items],
464
+ x_vector_only_mode=[it.x_vector_only_mode for it in items],
465
+ icl_mode=[it.icl_mode for it in items],
466
+ )
467
+
468
+ # voice clone model
469
+ @torch.no_grad()
470
+ def generate_voice_clone(
471
+ self,
472
+ text: Union[str, List[str]],
473
+ language: Union[str, List[str]] = None,
474
+ ref_audio: Optional[Union[AudioLike, List[AudioLike]]] = None,
475
+ ref_text: Optional[Union[str, List[Optional[str]]]] = None,
476
+ x_vector_only_mode: Union[bool, List[bool]] = False,
477
+ voice_clone_prompt: Optional[Union[Dict[str, Any], List[VoiceClonePromptItem]]] = None,
478
+ non_streaming_mode: bool = False,
479
+ **kwargs,
480
+ ) -> Tuple[List[np.ndarray], int]:
481
+ """
482
+ Voice clone speech using the Base model.
483
+
484
+ You can provide either:
485
+ - (ref_audio, ref_text, x_vector_only_mode) and let this method build the prompt, OR
486
+ - `VoiceClonePromptItem` returned by `create_voice_clone_prompt`, OR
487
+ - a list of `VoiceClonePromptItem` returned by `create_voice_clone_prompt`.
488
+
489
+ `ref_audio` Supported forms:
490
+ - str: wav path / URL / base64 audio string
491
+ - (np.ndarray, sr): waveform + sampling rate
492
+ - list of the above
493
+
494
+ Input flexibility:
495
+ - text/language can be scalar or list.
496
+ - prompt can be single or batch.
497
+ - If batch mode (len(text)>1), lengths must match.
498
+
499
+ Args:
500
+ text:
501
+ Text(s) to synthesize.
502
+ language:
503
+ Language(s) for each sample.
504
+ ref_audio:
505
+ Reference audio(s) for prompt building. Required if voice_clone_prompt is not provided.
506
+ ref_text:
507
+ Reference text(s) used for ICL mode (required when x_vector_only_mode=False).
508
+ x_vector_only_mode:
509
+ If True, only speaker embedding is used (ignores ref_text/ref_code).
510
+ If False, ICL mode is used automatically.
511
+ voice_clone_prompt:
512
+ list[VoiceClonePromptItem] from `create_voice_clone_prompt`.
513
+ non_streaming_mode:
514
+ Using non-streaming text input, this option currently only simulates streaming text input when set to `false`,
515
+ rather than enabling true streaming input or streaming generation.
516
+ do_sample:
517
+ Whether to use sampling, recommended to be set to `true` for most use cases.
518
+ top_k:
519
+ Top-k sampling parameter.
520
+ top_p:
521
+ Top-p sampling parameter.
522
+ temperature:
523
+ Sampling temperature; higher => more random.
524
+ repetition_penalty:
525
+ Penalty to reduce repeated tokens/codes.
526
+ subtalker_dosample:
527
+ Sampling switch for the sub-talker (only valid for qwen3-tts-tokenizer-v2) if applicable.
528
+ subtalker_top_k:
529
+ Top-k for sub-talker sampling (only valid for qwen3-tts-tokenizer-v2).
530
+ subtalker_top_p:
531
+ Top-p for sub-talker sampling (only valid for qwen3-tts-tokenizer-v2).
532
+ subtalker_temperature:
533
+ Temperature for sub-talker sampling (only valid for qwen3-tts-tokenizer-v2).
534
+ max_new_tokens:
535
+ Maximum number of new codec tokens to generate.
536
+ **kwargs:
537
+ Any other keyword arguments supported by HuggingFace Transformers `generate()` can be passed.
538
+ They will be forwarded to the underlying `Qwen3TTSForConditionalGeneration.generate(...)`.
539
+
540
+ Returns:
541
+ Tuple[List[np.ndarray], int]:
542
+ (wavs, sample_rate)
543
+
544
+ Raises:
545
+ ValueError:
546
+ If batch sizes mismatch or required prompt inputs are missing.
547
+ """
548
+ if self.model.tts_model_type != "base":
549
+ raise ValueError(
550
+ f"model with \ntokenizer_type: {self.model.tokenizer_type}\n"
551
+ f"tts_model_size: {self.model.tts_model_size}\n"
552
+ f"tts_model_type: {self.model.tts_model_type}\n"
553
+ "does not support generate_voice_clone, Please check Model Card or Readme for more details."
554
+ )
555
+
556
+ texts = self._ensure_list(text)
557
+ languages = self._ensure_list(language) if isinstance(language, list) else ([language] * len(texts) if language is not None else ["Auto"] * len(texts))
558
+ if len(languages) == 1 and len(texts) > 1:
559
+ languages = languages * len(texts)
560
+ if len(texts) != len(languages):
561
+ raise ValueError(f"Batch size mismatch: text={len(texts)}, language={len(languages)}")
562
+
563
+ self._validate_languages(languages)
564
+
565
+ if voice_clone_prompt is None:
566
+ if ref_audio is None:
567
+ raise ValueError("Either `voice_clone_prompt` or `ref_audio` must be provided.")
568
+ prompt_items = self.create_voice_clone_prompt(ref_audio=ref_audio, ref_text=ref_text, x_vector_only_mode=x_vector_only_mode)
569
+ if len(prompt_items) == 1 and len(texts) > 1:
570
+ prompt_items = prompt_items * len(texts)
571
+ if len(prompt_items) != len(texts):
572
+ raise ValueError(f"Batch size mismatch: prompt={len(prompt_items)}, text={len(texts)}")
573
+ voice_clone_prompt_dict = self._prompt_items_to_voice_clone_prompt(prompt_items)
574
+ ref_texts_for_ids = [it.ref_text for it in prompt_items]
575
+ else:
576
+ if isinstance(voice_clone_prompt, list):
577
+ prompt_items = voice_clone_prompt
578
+ if len(prompt_items) == 1 and len(texts) > 1:
579
+ prompt_items = prompt_items * len(texts)
580
+ if len(prompt_items) != len(texts):
581
+ raise ValueError(f"Batch size mismatch: prompt={len(prompt_items)}, text={len(texts)}")
582
+ voice_clone_prompt_dict = self._prompt_items_to_voice_clone_prompt(prompt_items)
583
+ ref_texts_for_ids = [it.ref_text for it in prompt_items]
584
+ else:
585
+ voice_clone_prompt_dict = voice_clone_prompt
586
+ ref_texts_for_ids = None
587
+
588
+ input_texts = [self._build_assistant_text(t) for t in texts]
589
+ input_ids = self._tokenize_texts(input_texts)
590
+
591
+ ref_ids = None
592
+ if ref_texts_for_ids is not None:
593
+ ref_ids = []
594
+ for i, rt in enumerate(ref_texts_for_ids):
595
+ if rt is None or rt == "":
596
+ ref_ids.append(None)
597
+ else:
598
+ ref_tok = self._tokenize_texts([self._build_ref_text(rt)])[0]
599
+ ref_ids.append(ref_tok)
600
+
601
+ gen_kwargs = self._merge_generate_kwargs(**kwargs)
602
+
603
+ talker_codes_list, _ = self.model.generate(
604
+ input_ids=input_ids,
605
+ ref_ids=ref_ids,
606
+ voice_clone_prompt=voice_clone_prompt_dict,
607
+ languages=languages,
608
+ non_streaming_mode=non_streaming_mode,
609
+ **gen_kwargs,
610
+ )
611
+
612
+ codes_for_decode = []
613
+ for i, codes in enumerate(talker_codes_list):
614
+ ref_code_list = voice_clone_prompt_dict.get("ref_code", None)
615
+ if ref_code_list is not None and ref_code_list[i] is not None:
616
+ codes_for_decode.append(torch.cat([ref_code_list[i].to(codes.device), codes], dim=0))
617
+ else:
618
+ codes_for_decode.append(codes)
619
+
620
+ wavs_all, fs = self.model.speech_tokenizer.decode([{"audio_codes": c} for c in codes_for_decode])
621
+
622
+ wavs_out: List[np.ndarray] = []
623
+ for i, wav in enumerate(wavs_all):
624
+ ref_code_list = voice_clone_prompt_dict.get("ref_code", None)
625
+ if ref_code_list is not None and ref_code_list[i] is not None:
626
+ ref_len = int(ref_code_list[i].shape[0])
627
+ total_len = int(codes_for_decode[i].shape[0])
628
+ cut = int(ref_len / max(total_len, 1) * wav.shape[0])
629
+ wavs_out.append(wav[cut:])
630
+ else:
631
+ wavs_out.append(wav)
632
+
633
+ return wavs_out, fs
634
+
635
+ # voice design model
636
+ @torch.no_grad()
637
+ def generate_voice_design(
638
+ self,
639
+ text: Union[str, List[str]],
640
+ instruct: Union[str, List[str]],
641
+ language: Union[str, List[str]] = None,
642
+ non_streaming_mode: bool = True,
643
+ **kwargs,
644
+ ) -> Tuple[List[np.ndarray], int]:
645
+ """
646
+ Generate speech with the VoiceDesign model using natural-language style instructions.
647
+
648
+ Args:
649
+ text:
650
+ Text(s) to synthesize.
651
+ language:
652
+ Language(s) for each sample.
653
+ instruct:
654
+ Instruction(s) describing desired voice/style. Empty string is allowed (treated as no instruction).
655
+ non_streaming_mode:
656
+ Using non-streaming text input, this option currently only simulates streaming text input when set to `false`,
657
+ rather than enabling true streaming input or streaming generation.
658
+ do_sample:
659
+ Whether to use sampling, recommended to be set to `true` for most use cases.
660
+ top_k:
661
+ Top-k sampling parameter.
662
+ top_p:
663
+ Top-p sampling parameter.
664
+ temperature:
665
+ Sampling temperature; higher => more random.
666
+ repetition_penalty:
667
+ Penalty to reduce repeated tokens/codes.
668
+ subtalker_dosample:
669
+ Sampling switch for the sub-talker (only valid for qwen3-tts-tokenizer-v2) if applicable.
670
+ subtalker_top_k:
671
+ Top-k for sub-talker sampling (only valid for qwen3-tts-tokenizer-v2).
672
+ subtalker_top_p:
673
+ Top-p for sub-talker sampling (only valid for qwen3-tts-tokenizer-v2).
674
+ subtalker_temperature:
675
+ Temperature for sub-talker sampling (only valid for qwen3-tts-tokenizer-v2).
676
+ max_new_tokens:
677
+ Maximum number of new codec tokens to generate.
678
+ **kwargs:
679
+ Any other keyword arguments supported by HuggingFace Transformers `generate()` can be passed.
680
+ They will be forwarded to the underlying `Qwen3TTSForConditionalGeneration.generate(...)`.
681
+
682
+ Returns:
683
+ Tuple[List[np.ndarray], int]:
684
+ (wavs, sample_rate)
685
+ """
686
+ if self.model.tts_model_type != "voice_design":
687
+ raise ValueError(
688
+ f"model with \ntokenizer_type: {self.model.tokenizer_type}\n"
689
+ f"tts_model_size: {self.model.tts_model_size}\n"
690
+ f"tts_model_type: {self.model.tts_model_type}\n"
691
+ "does not support generate_voice_design, Please check Model Card or Readme for more details."
692
+ )
693
+
694
+ texts = self._ensure_list(text)
695
+ languages = self._ensure_list(language) if isinstance(language, list) else ([language] * len(texts) if language is not None else ["Auto"] * len(texts))
696
+ instructs = self._ensure_list(instruct)
697
+
698
+ if len(languages) == 1 and len(texts) > 1:
699
+ languages = languages * len(texts)
700
+ if len(instructs) == 1 and len(texts) > 1:
701
+ instructs = instructs * len(texts)
702
+
703
+ if not (len(texts) == len(languages) == len(instructs)):
704
+ raise ValueError(f"Batch size mismatch: text={len(texts)}, language={len(languages)}, instruct={len(instructs)}")
705
+
706
+ self._validate_languages(languages)
707
+
708
+ input_ids = self._tokenize_texts([self._build_assistant_text(t) for t in texts])
709
+
710
+ instruct_ids: List[Optional[torch.Tensor]] = []
711
+ for ins in instructs:
712
+ if ins is None or ins == "":
713
+ instruct_ids.append(None)
714
+ else:
715
+ instruct_ids.append(self._tokenize_texts([self._build_instruct_text(ins)])[0])
716
+
717
+ gen_kwargs = self._merge_generate_kwargs(**kwargs)
718
+
719
+ talker_codes_list, _ = self.model.generate(
720
+ input_ids=input_ids,
721
+ instruct_ids=instruct_ids,
722
+ languages=languages,
723
+ non_streaming_mode=non_streaming_mode,
724
+ **gen_kwargs,
725
+ )
726
+
727
+ wavs, fs = self.model.speech_tokenizer.decode([{"audio_codes": c} for c in talker_codes_list])
728
+ return wavs, fs
729
+
730
+ # custom voice model
731
+ @torch.no_grad()
732
+ def generate_custom_voice(
733
+ self,
734
+ text: Union[str, List[str]],
735
+ speaker: Union[str, List[str]],
736
+ language: Union[str, List[str]] = None,
737
+ instruct: Optional[Union[str, List[str]]] = None,
738
+ non_streaming_mode: bool = True,
739
+ **kwargs,
740
+ ) -> Tuple[List[np.ndarray], int]:
741
+ """
742
+ Generate speech with the CustomVoice model using a predefined speaker id, optionally controlled by instruction text.
743
+
744
+ Args:
745
+ text:
746
+ Text(s) to synthesize.
747
+ language:
748
+ Language(s) for each sample.
749
+ speaker:
750
+ Speaker name(s). Will be validated against `model.get_supported_speakers()` (case-insensitive).
751
+ instruct:
752
+ Optional instruction(s). If None, treated as empty (no instruction).
753
+ non_streaming_mode:
754
+ Using non-streaming text input, this option currently only simulates streaming text input when set to `false`,
755
+ rather than enabling true streaming input or streaming generation.
756
+ do_sample:
757
+ Whether to use sampling, recommended to be set to `true` for most use cases.
758
+ top_k:
759
+ Top-k sampling parameter.
760
+ top_p:
761
+ Top-p sampling parameter.
762
+ temperature:
763
+ Sampling temperature; higher => more random.
764
+ repetition_penalty:
765
+ Penalty to reduce repeated tokens/codes.
766
+ subtalker_dosample:
767
+ Sampling switch for the sub-talker (only valid for qwen3-tts-tokenizer-v2) if applicable.
768
+ subtalker_top_k:
769
+ Top-k for sub-talker sampling (only valid for qwen3-tts-tokenizer-v2).
770
+ subtalker_top_p:
771
+ Top-p for sub-talker sampling (only valid for qwen3-tts-tokenizer-v2).
772
+ subtalker_temperature:
773
+ Temperature for sub-talker sampling (only valid for qwen3-tts-tokenizer-v2).
774
+ max_new_tokens:
775
+ Maximum number of new codec tokens to generate.
776
+ **kwargs:
777
+ Any other keyword arguments supported by HuggingFace Transformers `generate()` can be passed.
778
+ They will be forwarded to the underlying `Qwen3TTSForConditionalGeneration.generate(...)`.
779
+
780
+ Returns:
781
+ Tuple[List[np.ndarray], int]:
782
+ (wavs, sample_rate)
783
+
784
+ Raises:
785
+ ValueError:
786
+ If any speaker/language is unsupported or batch sizes mismatch.
787
+ """
788
+ if self.model.tts_model_type != "custom_voice":
789
+ raise ValueError(
790
+ f"model with \ntokenizer_type: {self.model.tokenizer_type}\n"
791
+ f"tts_model_size: {self.model.tts_model_size}\n"
792
+ f"tts_model_type: {self.model.tts_model_type}\n"
793
+ "does not support generate_custom_voice, Please check Model Card or Readme for more details."
794
+ )
795
+
796
+ texts = self._ensure_list(text)
797
+ languages = self._ensure_list(language) if isinstance(language, list) else ([language] * len(texts) if language is not None else ["Auto"] * len(texts))
798
+ speakers = self._ensure_list(speaker)
799
+ if self.model.tts_model_size in "0b6": # for 0b6 model, instruct is not supported
800
+ instruct = None
801
+ instructs = self._ensure_list(instruct) if isinstance(instruct, list) else ([instruct] * len(texts) if instruct is not None else [""] * len(texts))
802
+
803
+ if len(languages) == 1 and len(texts) > 1:
804
+ languages = languages * len(texts)
805
+ if len(speakers) == 1 and len(texts) > 1:
806
+ speakers = speakers * len(texts)
807
+ if len(instructs) == 1 and len(texts) > 1:
808
+ instructs = instructs * len(texts)
809
+
810
+ if not (len(texts) == len(languages) == len(speakers) == len(instructs)):
811
+ raise ValueError(
812
+ f"Batch size mismatch: text={len(texts)}, language={len(languages)}, speaker={len(speakers)}, instruct={len(instructs)}"
813
+ )
814
+
815
+ self._validate_languages(languages)
816
+ self._validate_speakers(speakers)
817
+
818
+ input_ids = self._tokenize_texts([self._build_assistant_text(t) for t in texts])
819
+
820
+ instruct_ids: List[Optional[torch.Tensor]] = []
821
+ for ins in instructs:
822
+ if ins is None or ins == "":
823
+ instruct_ids.append(None)
824
+ else:
825
+ instruct_ids.append(self._tokenize_texts([self._build_instruct_text(ins)])[0])
826
+
827
+ gen_kwargs = self._merge_generate_kwargs(**kwargs)
828
+
829
+ talker_codes_list, _ = self.model.generate(
830
+ input_ids=input_ids,
831
+ instruct_ids=instruct_ids,
832
+ languages=languages,
833
+ speakers=speakers,
834
+ non_streaming_mode=non_streaming_mode,
835
+ **gen_kwargs,
836
+ )
837
+
838
+ wavs, fs = self.model.speech_tokenizer.decode([{"audio_codes": c} for c in talker_codes_list])
839
+ return wavs, fs
840
+
841
+
842
+ def get_supported_speakers(self) -> Optional[List[str]]:
843
+ """
844
+ List supported speaker names for the current model.
845
+
846
+ This is a convenience wrapper around `model.get_supported_speakers()`.
847
+ If the underlying model does not expose speaker constraints (returns None),
848
+ this method also returns None.
849
+
850
+ Returns:
851
+ Optional[List[str]]:
852
+ - A sorted list of supported speaker names (lowercased), if available.
853
+ - None if the model does not provide supported speakers.
854
+ """
855
+ supported = self._supported_speakers_set()
856
+ if supported is None:
857
+ return None
858
+ return sorted(supported)
859
+
860
+
861
+ def get_supported_languages(self) -> Optional[List[str]]:
862
+ """
863
+ List supported language names for the current model.
864
+
865
+ This is a convenience wrapper around `model.get_supported_languages()`.
866
+ If the underlying model does not expose language constraints (returns None),
867
+ this method also returns None.
868
+
869
+ Returns:
870
+ Optional[List[str]]:
871
+ - A sorted list of supported language names (lowercased), if available.
872
+ - None if the model does not provide supported languages.
873
+ """
874
+ supported = self._supported_languages_set()
875
+ if supported is None:
876
+ return None
877
+ return sorted(supported)