From b5e38f9fa3f9210a6d5bc01a81d39077ce0c9a18 Mon Sep 17 00:00:00 2001 From: zhaoyafei Date: Tue, 14 Jul 2026 09:43:32 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(inference):=20=E5=A2=9E=E5=8A=A0=20MLX?= =?UTF-8?q?/Apple=20Silicon=20=E5=90=8E=E7=AB=AF=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 backend_utils 统一解析设备/dtype/attn:mlx 与 auto 自动映射到 本机可用后端(Apple Silicon 落到 mps,无则 cpu) - mps 上 bfloat16 自动降级为 float16,非 CUDA 下自动关闭 flash_attention_2 - from_pretrained(模型/tokenizer) 与 demo CLI 走统一解析,默认设备自动探测 - examples 去除写死的 cuda:0 与 torch.cuda.synchronize,改用跨后端同步 - gitignore 忽略本地模型目录产出的音频 验收:examples/test_model_12hz_base.py 在 mps 上 12 个 case 全部跑通 --- .gitignore | 9 ++- examples/test_model_12hz_base.py | 12 +-- examples/test_model_12hz_custom_voice.py | 14 ++-- examples/test_model_12hz_voice_design.py | 14 ++-- examples/test_tokenizer_12hz.py | 3 +- qwen_tts/cli/demo.py | 11 +-- qwen_tts/inference/backend_utils.py | 89 +++++++++++++++++++++++ qwen_tts/inference/qwen3_tts_model.py | 4 +- qwen_tts/inference/qwen3_tts_tokenizer.py | 4 +- 9 files changed, 134 insertions(+), 26 deletions(-) create mode 100644 qwen_tts/inference/backend_utils.py diff --git a/.gitignore b/.gitignore index 1f6b14cc..c02ad958 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,11 @@ wheels/ .idea/ .vscode/ venv/ -env/ \ No newline at end of file +env/ + +# example/*.py里默认的模型checkpoints路径 +Qwen/ + +# example 脚本生成的音频输出目录 +qwen3_tts_test_voice_clone_output_wav/ +*.wav \ No newline at end of file diff --git a/examples/test_model_12hz_base.py b/examples/test_model_12hz_base.py index ec7ea4b5..ab5bf9af 100644 --- a/examples/test_model_12hz_base.py +++ b/examples/test_model_12hz_base.py @@ -19,6 +19,7 @@ import soundfile as sf from qwen_tts import Qwen3TTSModel +from qwen_tts.inference.backend_utils import default_device, normalize_attn_implementation, normalize_dtype_for_device, synchronize_device def ensure_dir(d: str): @@ -26,12 +27,12 @@ def ensure_dir(d: str): def run_case(tts: Qwen3TTSModel, out_dir: str, case_name: str, call_fn): - torch.cuda.synchronize() + synchronize_device(str(tts.device)) t0 = time.time() wavs, sr = call_fn() - torch.cuda.synchronize() + synchronize_device(str(tts.device)) t1 = time.time() print(f"[{case_name}] time: {t1 - t0:.3f}s, n_wavs={len(wavs)}, sr={sr}") @@ -40,7 +41,8 @@ def run_case(tts: Qwen3TTSModel, out_dir: str, case_name: str, call_fn): def main(): - device = "cuda:0" + device = default_device() + dtype = normalize_dtype_for_device(torch.bfloat16, device) MODEL_PATH = "Qwen/Qwen3-TTS-12Hz-1.7B-Base/" OUT_DIR = "qwen3_tts_test_voice_clone_output_wav" ensure_dir(OUT_DIR) @@ -48,8 +50,8 @@ def main(): tts = Qwen3TTSModel.from_pretrained( MODEL_PATH, device_map=device, - dtype=torch.bfloat16, - attn_implementation="flash_attention_2", + dtype=dtype, + attn_implementation=normalize_attn_implementation("flash_attention_2", device), ) # Reference audio(s) diff --git a/examples/test_model_12hz_custom_voice.py b/examples/test_model_12hz_custom_voice.py index 560ecd13..37f17594 100644 --- a/examples/test_model_12hz_custom_voice.py +++ b/examples/test_model_12hz_custom_voice.py @@ -18,21 +18,23 @@ import soundfile as sf from qwen_tts import Qwen3TTSModel +from qwen_tts.inference.backend_utils import default_device, normalize_attn_implementation, normalize_dtype_for_device, synchronize_device def main(): - device = "cuda:0" + device = default_device() + dtype = normalize_dtype_for_device(torch.bfloat16, device) MODEL_PATH = "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice/" tts = Qwen3TTSModel.from_pretrained( MODEL_PATH, device_map=device, - dtype=torch.bfloat16, - attn_implementation="flash_attention_2", + dtype=dtype, + attn_implementation=normalize_attn_implementation("flash_attention_2", device), ) # -------- Single (with instruct) -------- - torch.cuda.synchronize() + synchronize_device(device) t0 = time.time() wavs, sr = tts.generate_custom_voice( @@ -42,7 +44,7 @@ def main(): instruct="用特别愤怒的语气说", ) - torch.cuda.synchronize() + synchronize_device(device) t1 = time.time() print(f"[CustomVoice Single] time: {t1 - t0:.3f}s") @@ -65,7 +67,7 @@ def main(): max_new_tokens=2048, ) - torch.cuda.synchronize() + synchronize_device(device) t1 = time.time() print(f"[CustomVoice Batch] time: {t1 - t0:.3f}s") diff --git a/examples/test_model_12hz_voice_design.py b/examples/test_model_12hz_voice_design.py index 7e755391..825a5c21 100644 --- a/examples/test_model_12hz_voice_design.py +++ b/examples/test_model_12hz_voice_design.py @@ -18,21 +18,23 @@ import soundfile as sf from qwen_tts import Qwen3TTSModel +from qwen_tts.inference.backend_utils import default_device, normalize_attn_implementation, normalize_dtype_for_device, synchronize_device def main(): - device = "cuda:0" + device = default_device() + dtype = normalize_dtype_for_device(torch.bfloat16, device) MODEL_PATH = "Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign/" tts = Qwen3TTSModel.from_pretrained( MODEL_PATH, device_map=device, - dtype=torch.bfloat16, - attn_implementation="flash_attention_2", + dtype=dtype, + attn_implementation=normalize_attn_implementation("flash_attention_2", device), ) # -------- Single -------- - torch.cuda.synchronize() + synchronize_device(device) t0 = time.time() wavs, sr = tts.generate_voice_design( @@ -41,7 +43,7 @@ def main(): instruct="体现撒娇稚嫩的萝莉女声,音调偏高且起伏明显,营造出黏人、做作又刻意卖萌的听觉效果。", ) - torch.cuda.synchronize() + synchronize_device(device) t1 = time.time() print(f"[VoiceDesign Single] time: {t1 - t0:.3f}s") @@ -68,7 +70,7 @@ def main(): max_new_tokens=2048, ) - torch.cuda.synchronize() + synchronize_device(device) t1 = time.time() print(f"[VoiceDesign Batch] time: {t1 - t0:.3f}s") diff --git a/examples/test_tokenizer_12hz.py b/examples/test_tokenizer_12hz.py index 595809e3..95cef999 100644 --- a/examples/test_tokenizer_12hz.py +++ b/examples/test_tokenizer_12hz.py @@ -18,6 +18,7 @@ import soundfile as sf from qwen_tts import Qwen3TTSTokenizer +from qwen_tts.inference.backend_utils import default_device audio_1 = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/tokenizer_demo_1.wav" audio_2 = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/tokenizer_demo_2.wav" @@ -25,7 +26,7 @@ # -------- Single input: wav path -------- tokenizer_12hz = Qwen3TTSTokenizer.from_pretrained( "Qwen/Qwen3-TTS-Tokenizer-12Hz", - device_map="cuda:0", + device_map=default_device(), ) enc1 = tokenizer_12hz.encode(audio_1) diff --git a/qwen_tts/cli/demo.py b/qwen_tts/cli/demo.py index e267861d..47d37e0f 100644 --- a/qwen_tts/cli/demo.py +++ b/qwen_tts/cli/demo.py @@ -28,6 +28,7 @@ import torch from .. import Qwen3TTSModel, VoiceClonePromptItem +from ..inference.backend_utils import default_device, normalize_attn_implementation, normalize_device_name, normalize_dtype_for_device def _title_case_display(s: str) -> str: @@ -91,8 +92,8 @@ def build_parser() -> argparse.ArgumentParser: # Model loading / from_pretrained args parser.add_argument( "--device", - default="cuda:0", - help="Device for device_map, e.g. cpu, cuda, cuda:0 (default: cuda:0).", + default=default_device(), + help="Device for device_map, e.g. cpu, mps, mlx, cuda, cuda:0 (default: auto-detect).", ) parser.add_argument( "--dtype", @@ -602,12 +603,12 @@ def main(argv=None) -> int: ckpt = _resolve_checkpoint(args) - dtype = _dtype_from_str(args.dtype) - attn_impl = "flash_attention_2" if args.flash_attn else None + dtype = normalize_dtype_for_device(_dtype_from_str(args.dtype), normalize_device_name(args.device)) + attn_impl = normalize_attn_implementation("flash_attention_2" if args.flash_attn else None, args.device) tts = Qwen3TTSModel.from_pretrained( ckpt, - device_map=args.device, + device_map=normalize_device_name(args.device), dtype=dtype, attn_implementation=attn_impl, ) diff --git a/qwen_tts/inference/backend_utils.py b/qwen_tts/inference/backend_utils.py new file mode 100644 index 00000000..dfca4edf --- /dev/null +++ b/qwen_tts/inference/backend_utils.py @@ -0,0 +1,89 @@ +# coding=utf-8 +# Copyright 2026 The Alibaba Qwen team. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Any, Dict, Optional + +import torch + + +_MLX_ALIASES = {"mlx", "mlx:0", "apple-mlx"} +_MPS_ALIASES = {"mps", "mps:0"} +_CUDA_PREFIXES = ("cuda",) + + +def _has_mps() -> bool: + return bool(torch.backends.mps.is_built() and torch.backends.mps.is_available()) + + +def default_device() -> str: + if torch.cuda.is_available(): + return "cuda:0" + if _has_mps(): + return "mps" + return "cpu" + + +def device_supports_bfloat16(device: str) -> bool: + device = normalize_device_name(device) + if device.startswith("cuda"): + return True + if device == "cpu": + return True + return False + + +def normalize_device_name(device: Optional[str]) -> str: + raw = (device or "").strip().lower() + if not raw or raw == "auto": + return default_device() + if raw in _MLX_ALIASES: + return "mps" if _has_mps() else "cpu" + if raw in _MPS_ALIASES: + return "mps" if _has_mps() else "cpu" + if raw.startswith(_CUDA_PREFIXES): + return raw if torch.cuda.is_available() else ("mps" if _has_mps() else "cpu") + if raw == "cpu": + return "cpu" + return raw + + +def normalize_dtype_for_device(dtype: Optional[torch.dtype], device: str) -> Optional[torch.dtype]: + if dtype is None: + return None + if dtype == torch.bfloat16 and not device_supports_bfloat16(device): + return torch.float16 if device != "cpu" else torch.float32 + return dtype + + +def normalize_attn_implementation(attn_implementation: Optional[str], device: str) -> Optional[str]: + device = normalize_device_name(device) + if attn_implementation == "flash_attention_2" and not device.startswith("cuda"): + return None + return attn_implementation + + +def resolve_model_load_kwargs(kwargs: Dict[str, Any]) -> Dict[str, Any]: + resolved = dict(kwargs) + device = normalize_device_name(resolved.get("device_map")) + resolved["device_map"] = device + + dtype_key = "dtype" if "dtype" in resolved else "torch_dtype" if "torch_dtype" in resolved else None + if dtype_key is not None: + resolved[dtype_key] = normalize_dtype_for_device(resolved.get(dtype_key), device) + + if "attn_implementation" in resolved: + resolved["attn_implementation"] = normalize_attn_implementation(resolved.get("attn_implementation"), device) + + return resolved + + +def synchronize_device(device: Optional[str]) -> None: + name = normalize_device_name(device) + if name.startswith("cuda") and torch.cuda.is_available(): + torch.cuda.synchronize() + return + if name == "mps" and hasattr(torch, "mps") and torch.backends.mps.is_available(): + torch.mps.synchronize() diff --git a/qwen_tts/inference/qwen3_tts_model.py b/qwen_tts/inference/qwen3_tts_model.py index f4d33bf3..5ce00675 100644 --- a/qwen_tts/inference/qwen3_tts_model.py +++ b/qwen_tts/inference/qwen3_tts_model.py @@ -26,6 +26,7 @@ import torch from transformers import AutoConfig, AutoModel, AutoProcessor +from .backend_utils import resolve_model_load_kwargs from ..core.models import Qwen3TTSConfig, Qwen3TTSForConditionalGeneration, Qwen3TTSProcessor AudioLike = Union[ @@ -109,7 +110,8 @@ def from_pretrained( AutoModel.register(Qwen3TTSConfig, Qwen3TTSForConditionalGeneration) AutoProcessor.register(Qwen3TTSConfig, Qwen3TTSProcessor) - model = AutoModel.from_pretrained(pretrained_model_name_or_path, **kwargs) + model_load_kwargs = resolve_model_load_kwargs(kwargs) + model = AutoModel.from_pretrained(pretrained_model_name_or_path, **model_load_kwargs) if not isinstance(model, Qwen3TTSForConditionalGeneration): raise TypeError( f"AutoModel returned {type(model)}, expected Qwen3TTSForConditionalGeneration. " diff --git a/qwen_tts/inference/qwen3_tts_tokenizer.py b/qwen_tts/inference/qwen3_tts_tokenizer.py index 24f8768e..384e1529 100644 --- a/qwen_tts/inference/qwen3_tts_tokenizer.py +++ b/qwen_tts/inference/qwen3_tts_tokenizer.py @@ -26,6 +26,7 @@ from torch.nn.utils.rnn import pad_sequence from transformers import AutoConfig, AutoFeatureExtractor, AutoModel +from .backend_utils import resolve_model_load_kwargs from ..core import ( Qwen3TTSTokenizerV1Config, Qwen3TTSTokenizerV1Model, @@ -85,7 +86,8 @@ def from_pretrained(cls, pretrained_model_name_or_path: str, **kwargs) -> "Qwen3 AutoModel.register(Qwen3TTSTokenizerV2Config, Qwen3TTSTokenizerV2Model) inst.feature_extractor = AutoFeatureExtractor.from_pretrained(pretrained_model_name_or_path) - inst.model = AutoModel.from_pretrained(pretrained_model_name_or_path, **kwargs) + model_load_kwargs = resolve_model_load_kwargs(kwargs) + inst.model = AutoModel.from_pretrained(pretrained_model_name_or_path, **model_load_kwargs) inst.config = inst.model.config inst.device = getattr(inst.model, "device", None) From e44f1e0f06f1f5d4efad9ef81585fb6900d24799 Mon Sep 17 00:00:00 2001 From: zhaoyafei Date: Tue, 14 Jul 2026 09:53:27 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(examples):=20=E8=A1=A5=E4=BF=AE=20Custo?= =?UTF-8?q?mVoice/VoiceDesign=20batch=20=E6=AE=B5=E6=AE=8B=E7=95=99?= =?UTF-8?q?=E7=9A=84=20cuda.synchronize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上次批量替换漏掉了 batch 段(后跟空行而非 t1)的 torch.cuda.synchronize, 在无 CUDA 的 mps 环境会抛 AssertionError。改为跨后端的 synchronize_device。 验收:四个 example 脚本在 mps 上均已实测通过(rc=0) --- examples/test_model_12hz_custom_voice.py | 2 +- examples/test_model_12hz_voice_design.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/test_model_12hz_custom_voice.py b/examples/test_model_12hz_custom_voice.py index 37f17594..631c89df 100644 --- a/examples/test_model_12hz_custom_voice.py +++ b/examples/test_model_12hz_custom_voice.py @@ -56,7 +56,7 @@ def main(): speakers = ["Vivian", "Ryan"] instructs = ["", "Very happy."] - torch.cuda.synchronize() + synchronize_device(device) t0 = time.time() wavs, sr = tts.generate_custom_voice( diff --git a/examples/test_model_12hz_voice_design.py b/examples/test_model_12hz_voice_design.py index 825a5c21..00f2cd20 100644 --- a/examples/test_model_12hz_voice_design.py +++ b/examples/test_model_12hz_voice_design.py @@ -60,7 +60,7 @@ def main(): "Speak in an incredulous tone, but with a hint of panic beginning to creep into your voice." ] - torch.cuda.synchronize() + synchronize_device(device) t0 = time.time() wavs, sr = tts.generate_voice_design( From 08230875a418c1eada8deb2c3a649fa514245288 Mon Sep 17 00:00:00 2001 From: zhaoyafei Date: Tue, 14 Jul 2026 10:14:14 +0800 Subject: [PATCH 3/4] =?UTF-8?q?refactor(examples):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E5=88=B0=20outputs/<=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E5=90=8D>/=20=E5=AD=90=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 四个 example 脚本的音频输出统一到 outputs/<脚本名>/,目录名由 __file__ 自动派生,脚本与输出目录一一对应 - 之前 base 用独立长目录名、其余三个直接写仓库根目录,不统一 - gitignore 旧输出目录规则替换为 outputs/ 验收:四个脚本在 mps 上均 rc=0,产物落到各自 outputs 子目录 (base 20 / custom 3 / design 3 / tokenizer 9) --- .gitignore | 2 +- examples/test_model_12hz_base.py | 2 +- examples/test_model_12hz_custom_voice.py | 7 +++++-- examples/test_model_12hz_voice_design.py | 7 +++++-- examples/test_tokenizer_12hz.py | 16 ++++++++++------ 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index c02ad958..d1ddb33f 100644 --- a/.gitignore +++ b/.gitignore @@ -27,5 +27,5 @@ env/ Qwen/ # example 脚本生成的音频输出目录 -qwen3_tts_test_voice_clone_output_wav/ +outputs/ *.wav \ No newline at end of file diff --git a/examples/test_model_12hz_base.py b/examples/test_model_12hz_base.py index ab5bf9af..0ecd2e80 100644 --- a/examples/test_model_12hz_base.py +++ b/examples/test_model_12hz_base.py @@ -44,7 +44,7 @@ def main(): device = default_device() dtype = normalize_dtype_for_device(torch.bfloat16, device) MODEL_PATH = "Qwen/Qwen3-TTS-12Hz-1.7B-Base/" - OUT_DIR = "qwen3_tts_test_voice_clone_output_wav" + OUT_DIR = os.path.join("outputs", os.path.splitext(os.path.basename(__file__))[0]) ensure_dir(OUT_DIR) tts = Qwen3TTSModel.from_pretrained( diff --git a/examples/test_model_12hz_custom_voice.py b/examples/test_model_12hz_custom_voice.py index 631c89df..93488fef 100644 --- a/examples/test_model_12hz_custom_voice.py +++ b/examples/test_model_12hz_custom_voice.py @@ -13,6 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import os import time import torch import soundfile as sf @@ -25,6 +26,8 @@ def main(): device = default_device() dtype = normalize_dtype_for_device(torch.bfloat16, device) MODEL_PATH = "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice/" + OUT_DIR = os.path.join("outputs", os.path.splitext(os.path.basename(__file__))[0]) + os.makedirs(OUT_DIR, exist_ok=True) tts = Qwen3TTSModel.from_pretrained( MODEL_PATH, @@ -48,7 +51,7 @@ def main(): t1 = time.time() print(f"[CustomVoice Single] time: {t1 - t0:.3f}s") - sf.write("qwen3_tts_test_custom_single.wav", wavs[0], sr) + sf.write(os.path.join(OUT_DIR, "single.wav"), wavs[0], sr) # -------- Batch (some empty instruct) -------- texts = ["其实我真的有发现,我是一个特别善于观察别人情绪的人。", "She said she would be here by noon."] @@ -72,7 +75,7 @@ def main(): print(f"[CustomVoice Batch] time: {t1 - t0:.3f}s") for i, w in enumerate(wavs): - sf.write(f"qwen3_tts_test_custom_batch_{i}.wav", w, sr) + sf.write(os.path.join(OUT_DIR, f"batch_{i}.wav"), w, sr) if __name__ == "__main__": diff --git a/examples/test_model_12hz_voice_design.py b/examples/test_model_12hz_voice_design.py index 00f2cd20..934015a3 100644 --- a/examples/test_model_12hz_voice_design.py +++ b/examples/test_model_12hz_voice_design.py @@ -13,6 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import os import time import torch import soundfile as sf @@ -25,6 +26,8 @@ def main(): device = default_device() dtype = normalize_dtype_for_device(torch.bfloat16, device) MODEL_PATH = "Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign/" + OUT_DIR = os.path.join("outputs", os.path.splitext(os.path.basename(__file__))[0]) + os.makedirs(OUT_DIR, exist_ok=True) tts = Qwen3TTSModel.from_pretrained( MODEL_PATH, @@ -47,7 +50,7 @@ def main(): t1 = time.time() print(f"[VoiceDesign Single] time: {t1 - t0:.3f}s") - sf.write("qwen3_tts_test_voice_design_single.wav", wavs[0], sr) + sf.write(os.path.join(OUT_DIR, "single.wav"), wavs[0], sr) # -------- Batch -------- texts = [ @@ -75,7 +78,7 @@ def main(): print(f"[VoiceDesign Batch] time: {t1 - t0:.3f}s") for i, w in enumerate(wavs): - sf.write(f"qwen3_tts_test_voice_design_batch_{i}.wav", w, sr) + sf.write(os.path.join(OUT_DIR, f"batch_{i}.wav"), w, sr) if __name__ == "__main__": diff --git a/examples/test_tokenizer_12hz.py b/examples/test_tokenizer_12hz.py index 95cef999..b9beabcc 100644 --- a/examples/test_tokenizer_12hz.py +++ b/examples/test_tokenizer_12hz.py @@ -14,6 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import io +import os import requests import soundfile as sf @@ -23,6 +24,9 @@ audio_1 = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/tokenizer_demo_1.wav" audio_2 = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/tokenizer_demo_2.wav" +OUT_DIR = os.path.join("outputs", os.path.splitext(os.path.basename(__file__))[0]) +os.makedirs(OUT_DIR, exist_ok=True) + # -------- Single input: wav path -------- tokenizer_12hz = Qwen3TTSTokenizer.from_pretrained( "Qwen/Qwen3-TTS-Tokenizer-12Hz", @@ -31,36 +35,36 @@ enc1 = tokenizer_12hz.encode(audio_1) wavs1, out_sr1 = tokenizer_12hz.decode(enc1) -sf.write("decoded_single_12hz.wav", wavs1[0], out_sr1) +sf.write(os.path.join(OUT_DIR, "decoded_single_12hz.wav"), wavs1[0], out_sr1) # -------- Batch input: wav path list -------- enc2 = tokenizer_12hz.encode([audio_1, audio_2]) wavs2, out_sr2 = tokenizer_12hz.decode(enc2) for i, w in enumerate(wavs2): - sf.write(f"decoded_batch_12hz_{i}.wav", w, out_sr2) + sf.write(os.path.join(OUT_DIR, f"decoded_batch_12hz_{i}.wav"), w, out_sr2) # -------- Decode input as dict (12hz) -------- # Take the first sample codes and pass as a dict. dict_input_12hz = {"audio_codes": enc2.audio_codes[0]} # torch.Tensor wavs_d1, out_sr_d1 = tokenizer_12hz.decode(dict_input_12hz) -sf.write("decoded_dict_12hz.wav", wavs_d1[0], out_sr_d1) +sf.write(os.path.join(OUT_DIR, "decoded_dict_12hz.wav"), wavs_d1[0], out_sr_d1) # -------- Decode input as list[dict] (12hz) -------- list_dict_input_12hz = [{"audio_codes": c} for c in enc2.audio_codes] # list of torch.Tensor wavs_d2, out_sr_d2 = tokenizer_12hz.decode(list_dict_input_12hz) for i, w in enumerate(wavs_d2): - sf.write(f"decoded_listdict_12hz_{i}.wav", w, out_sr_d2) + sf.write(os.path.join(OUT_DIR, f"decoded_listdict_12hz_{i}.wav"), w, out_sr_d2) # -------- Decode input as list[dict] with numpy (12hz) -------- # Convert codes to numpy to simulate "serialized" payload. list_dict_numpy_12hz = [{"audio_codes": c.cpu().numpy()} for c in enc2.audio_codes] wavs_d3, out_sr_d3 = tokenizer_12hz.decode(list_dict_numpy_12hz) for i, w in enumerate(wavs_d3): - sf.write(f"decoded_listdict_numpy_12hz_{i}.wav", w, out_sr_d3) + sf.write(os.path.join(OUT_DIR, f"decoded_listdict_numpy_12hz_{i}.wav"), w, out_sr_d3) # -------- Numpy input (must pass sr) -------- data = requests.get(audio_2, timeout=30).content y, sr = sf.read(io.BytesIO(data)) enc3 = tokenizer_12hz.encode(y, sr=sr) wavs3, out_sr3 = tokenizer_12hz.decode(enc3) -sf.write("decoded_numpy_12hz.wav", wavs3[0], out_sr3) \ No newline at end of file +sf.write(os.path.join(OUT_DIR, "decoded_numpy_12hz.wav"), wavs3[0], out_sr3) \ No newline at end of file From 26a5dacbc1644772df13f34966838e601a59c03c Mon Sep 17 00:00:00 2001 From: zhaoyafei Date: Tue, 14 Jul 2026 11:11:53 +0800 Subject: [PATCH 4/4] =?UTF-8?q?docs(readme):=20=E6=89=8B=E5=8A=A8=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=91=BD=E4=BB=A4=20--local=5Fdir=20=E5=8A=A0=20Qwen/?= =?UTF-8?q?=20=E5=89=8D=E7=BC=80=E5=AF=B9=E9=BD=90=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README 手动下载命令原本存到裸 ./<模型名>/,与 examples/finetuning 脚本里的 Qwen/<模型名> 加载路径对不上,照文档下载后需手动改路径 - 12 条 modelscope/huggingface-cli 命令统一改为 ./Qwen/<模型名>/ - 补充说明:从仓库根目录运行时脚本可直接读到本地权重,无需改路径 - 脚本保持 Qwen/<模型名> repo id 双关写法,不影响缺失时自动下载 --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8287bac4..a3d0a615 100644 --- a/README.md +++ b/README.md @@ -83,23 +83,25 @@ During model loading in the qwen-tts package or vLLM, model weights will be auto ```bash # Download through ModelScope (recommended for users in Mainland China) pip install -U modelscope -modelscope download --model Qwen/Qwen3-TTS-Tokenizer-12Hz --local_dir ./Qwen3-TTS-Tokenizer-12Hz -modelscope download --model Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice --local_dir ./Qwen3-TTS-12Hz-1.7B-CustomVoice -modelscope download --model Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign --local_dir ./Qwen3-TTS-12Hz-1.7B-VoiceDesign -modelscope download --model Qwen/Qwen3-TTS-12Hz-1.7B-Base --local_dir ./Qwen3-TTS-12Hz-1.7B-Base -modelscope download --model Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice --local_dir ./Qwen3-TTS-12Hz-0.6B-CustomVoice -modelscope download --model Qwen/Qwen3-TTS-12Hz-0.6B-Base --local_dir ./Qwen3-TTS-12Hz-0.6B-Base +modelscope download --model Qwen/Qwen3-TTS-Tokenizer-12Hz --local_dir ./Qwen/Qwen3-TTS-Tokenizer-12Hz +modelscope download --model Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice --local_dir ./Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice +modelscope download --model Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign --local_dir ./Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign +modelscope download --model Qwen/Qwen3-TTS-12Hz-1.7B-Base --local_dir ./Qwen/Qwen3-TTS-12Hz-1.7B-Base +modelscope download --model Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice --local_dir ./Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice +modelscope download --model Qwen/Qwen3-TTS-12Hz-0.6B-Base --local_dir ./Qwen/Qwen3-TTS-12Hz-0.6B-Base # Download through Hugging Face pip install -U "huggingface_hub[cli]" -huggingface-cli download Qwen/Qwen3-TTS-Tokenizer-12Hz --local-dir ./Qwen3-TTS-Tokenizer-12Hz -huggingface-cli download Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice --local-dir ./Qwen3-TTS-12Hz-1.7B-CustomVoice -huggingface-cli download Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign --local-dir ./Qwen3-TTS-12Hz-1.7B-VoiceDesign -huggingface-cli download Qwen/Qwen3-TTS-12Hz-1.7B-Base --local-dir ./Qwen3-TTS-12Hz-1.7B-Base -huggingface-cli download Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice --local-dir ./Qwen3-TTS-12Hz-0.6B-CustomVoice -huggingface-cli download Qwen/Qwen3-TTS-12Hz-0.6B-Base --local-dir ./Qwen3-TTS-12Hz-0.6B-Base +huggingface-cli download Qwen/Qwen3-TTS-Tokenizer-12Hz --local-dir ./Qwen/Qwen3-TTS-Tokenizer-12Hz +huggingface-cli download Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice --local-dir ./Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice +huggingface-cli download Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign --local-dir ./Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign +huggingface-cli download Qwen/Qwen3-TTS-12Hz-1.7B-Base --local-dir ./Qwen/Qwen3-TTS-12Hz-1.7B-Base +huggingface-cli download Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice --local-dir ./Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice +huggingface-cli download Qwen/Qwen3-TTS-12Hz-0.6B-Base --local-dir ./Qwen/Qwen3-TTS-12Hz-0.6B-Base ``` +The `--local_dir` above keeps the `Qwen/` prefix so the downloaded layout (e.g. `./Qwen/Qwen3-TTS-12Hz-1.7B-Base`) matches the default `Qwen/` paths used in the code snippets below and in `examples/*.py` / `finetuning/*.py`. When you run from the repo root, the scripts pick up the local weights directly with no path changes. + ## Quickstart