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)
|