feat: integrate Qwen3-TTS support into podcast nodes (v1.11.0)

This commit is contained in:
Hawk Lee
2026-02-04 08:54:08 +08:00
parent 9d069fc0ea
commit 167edf8d20
5 changed files with 311 additions and 10 deletions
+42 -7
View File
@@ -971,11 +971,35 @@ hf download digital-avatar/ditto-talkinghead --local-dir ditto
| **克隆能力 (Cloning)** | **SOTA** (Zero-Shot)`<br>`只需 3-10秒,对**音色质感**还原极高。 | **SOTA** (稳定性)`<br>`对**说话韵律/口音**的捕捉最准。 | **良好** `<br>`适合克隆特定语气,而非纯粹音色。 |
| **多语言/方言** | **中/英** (双语优化) | **👑 霸主** (9种语言 + 18种方言) | **中/英** |
| **语音转换 (VC)** (Audio-to-Audio) | ❌**不支持** `<br>`仅支持 TTS (Text-to-Speech)。无法改变已有音频的音色。 | ✅**支持** `<br>`可以将任意音频转换为任意音色 (保留语调/停顿)。 | ❌**不支持** `<br>`纯 TTS 模型。仅支持 Text-to-Speech。 |
| **Qwen3-TTS** (1.7B/0.6B) | ✅**支持** `<br>`支持 CustomVoice (内置) 和 VoiceDesign (描述)。 | ✅**支持** `<br>`支持 3秒极速 Zero-shot 克隆。 | ✅**支持** `<br>`支持 10 种语言。 |
#### 3.13 Qwen3-TTS (New! 🔥)
- **用途**: 阿里巴巴 Qwen 团队推出的最新旗舰级 TTS 模型,支持 10 种主要语言及多种方言,具备极高的稳定性和表现力。
- **核心能力**:
- **CustomVoice**: 使用内置的高品质音色进行语音合成。提供 1.7B 和 0.6B 两种规格。
- **VoiceDesign**: 通过自然语言描述(如“活泼的少女音,带点羞涩”)从零设计音色。
- **VoiceClone**: 顶级的 3秒快速音色克隆,支持 X-Vector 模式提升稳定性。
- **环境要求**:
- **qwen-tts**: `pip install qwen-tts` (插件会自动尝试安装)。
- **Flash Attention 2**: 强烈推荐以获得最佳推理性能。
- **节点**:
- `🤖 Qwen3-TTS Loader`: 加载模型。支持 `Base` (克隆)、`CustomVoice` (内置音色) 和 `VoiceDesign` (音色设计) 模型。
- `🗣️ Qwen3-TTS Synthesis`: 执行合成。根据加载的模型类型自动切换功能。
- **模型下载**:
- `Qwen/Qwen3-TTS-12Hz-1.7B-Base` (或 0.6B-Base)
- `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice` (或 0.6B-CustomVoice)
- `Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign`
---
#### 💡 用户实测与选型指南 (Model Comparison & Selection)
**选型建议**:
- **追求“听起来最像真人” (音质+音色)**: 选 **VoxCPM 1.5**。它的 Tokenizer-free 架构带来了质的飞跃。
- **追求“方言/多语言/稳定性”**: 选 **CosyVoice 3.0**。目前依然是生产环境最稳的选择。
- **追求“多样化音色设计/最新 Qwen 生态/长语音流畅度”**: 选 **Qwen3-TTS**。其 VoiceDesign 功能能让你用描述语“捏”出从未听过的声音。
- **要做“长篇广播剧/播客”**: 选 **VibeVoice**。它的长窗口上下文优势依然不可替代。
### 4. 播客与对话生成 (Podcast & Dialogue Generation)
@@ -1005,6 +1029,7 @@ https://github.com/user-attachments/assets/9a5502c5-79e3-4fc8-8a2d-2cbdbdbbc860
- **TTS Engine**: 后端引擎选择。
- **CosyVoice**: 精准控制型。
- **VibeVoice**: 自然演绎型。
- **Qwen3-TTS**: 万能旗舰型。支持音色设计与内置高质量音色。
- **Speaker A/B/C**:
- **Ref Audio**: 参考音频 (用于 Zero-Shot 克隆)。
- **ID**: 内置音色 ID (如 CosyVoice 的 `Chinese Female`)。
@@ -1026,6 +1051,7 @@ https://github.com/user-attachments/assets/9a5502c5-79e3-4fc8-8a2d-2cbdbdbbc860
- **原理**:
- **CosyVoice**: 使用生成时的精确时长。
- **VibeVoice**: 使用**智能插值算法 (Smart Interpolation)**,根据字符长度自动计算长音频段内的单句时间轴。
- **Qwen3-TTS**: 基于生成的音频振幅精准断句,支持多角色时间轴导出。
#### 4.4 AIIA Subtitle to Segments (字幕转分段)
@@ -1069,13 +1095,13 @@ https://github.com/user-attachments/assets/9a5502c5-79e3-4fc8-8a2d-2cbdbdbbc860
#### 💡 引擎选型与最佳实践 (Best Practices)
| 特性 | **CosyVoice** | **VibeVoice** |
| :----------------- | :----------------------------------------- | :----------------------------------------------------------------------------------------------- |
| **核心优势** | **精准控制 (Instruction)** | **自然演绎 (Context-Aware)** |
| **情感控制** | ✅**支持** (使用 `[Happy]` 等标签) | ❌ 不支持显式标签 (依赖上下文) |
| **生成逻辑** | **逐句生成** (严格遵循每句话的指令) | **混合批处理** (Hybrid Batching) |
| **最佳场景** | 需要精确指定某句话语气、方言时 | 长篇对话、广播剧、闲聊 |
| **使用建议** | 可以在剧本中详细标注情感。 | **尽量减少 `(Pause)`**!`<br>`让多句对话连在一起,模型能更好地联系上下文产生自然语气。 |
| 特性 | **CosyVoice** | **VibeVoice** | **Qwen3-TTS** |
| :----------------- | :----------------------------------------- | :----------------------------------------------------------------------------------------------- | :------------------------------------------ |
| **核心优势** | **精准控制 (Instruction)** | **自然演绎 (Context-Aware)** | **万能旗舰 (Voice Design)** |
| **情感控制** | ✅**支持** (使用 `[Happy]` 等标签) | ❌ 不支持显式标签 (依赖上下文) | ✅**支持** (通过 `instruct` 或标签) |
| **生成逻辑** | **逐句生成** (严格遵循每句话的指令) | **混合批处理** (Hybrid Batching) | **动态引擎** (支持流式与批处理) |
| **最佳场景** | 需要精确指定某句话语气、方言时 | 长篇对话、广播剧、闲聊 | 音色定制、高质量配音、极速克隆 |
| **使用建议** | 可以在剧本中详细标注情感。 | **尽量减少 `(Pause)`**!`<br>`让多句对话连在一起,模型能更好地联系上下文产生自然语气。 | 尝试使用其 Voice Design 进行创意捏人。 |
#### 📝 综合测试剧本 (Example Script)
@@ -1175,6 +1201,15 @@ B: 太神奇了!那我们快去生成试试吧!
## Changelog
### [1.11.0] - 2026-02-04
- **Qwen3-TTS**: 新增阿里巴巴 **Qwen3-TTS** 全系列支持。
- **🤖 Qwen3-TTS Loader**: 支持加载 Base, CustomVoice, VoiceDesign 及其 1.7B/0.6B 版本。
- **🗣️ Qwen3-TTS Synthesis**: 实现全功能生成,包括 Zero-shot 克隆、音色设计和内置音色合成。
- **Podcast Integration**: **AIIA Dialogue TTS** 节点现在正式集成 Qwen3-TTS 引擎。
- 支持多角色混合场景下的 Qwen3 驱动,支持使用脚本标签触发 `instruct`。
- **Auto-Dependency**: 首次运行 Qwen3 节点会自动检测并安装 `qwen-tts` 库。
### [1.10.17] - 2026-02-03
- **Subtitle**: 引入“说话人 ID 为了映射 (Speaker Mapping)”机制。
+3
View File
@@ -136,6 +136,9 @@ else:
# 25. 处理 aiia_debug_nodes.py (新增调试节点)
_load_nodes_from_module(".aiia_debug_nodes", "aiia_debug_nodes")
# 26. 处理 aiia_qwen_nodes.py (新增 Qwen3-TTS)
_load_nodes_from_module(".aiia_qwen_nodes", "aiia_qwen_nodes")
# 告诉 ComfyUI 这个节点包有一个包含网页资源的 'js' 目录
WEB_DIRECTORY = "js"
+75 -2
View File
@@ -176,7 +176,7 @@ class AIIA_Dialogue_TTS:
return {
"required": {
"dialogue_json": ("STRING", {"forceInput": True}),
"tts_engine": (["CosyVoice", "VibeVoice"], {"default": "CosyVoice"}),
"tts_engine": (["CosyVoice", "VibeVoice", "Qwen3-TTS"], {"default": "CosyVoice"}),
"pause_duration": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 5.0, "step": 0.1}),
"speed_global": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 2.0}),
"batch_mode": (["Natural (Hybrid)", "Strict (Per-Speaker)", "Whole (Single Batch)"], {"default": "Natural (Hybrid)"}),
@@ -190,6 +190,7 @@ class AIIA_Dialogue_TTS:
"optional": {
"cosyvoice_model": ("COSYVOICE_MODEL",),
"vibevoice_model": ("VIBEVOICE_MODEL",),
"qwen_model": ("QWEN_MODEL",),
# Speaker A
"speaker_A_ref": ("AUDIO",),
@@ -248,7 +249,7 @@ class AIIA_Dialogue_TTS:
return None
def process_dialogue(self, dialogue_json, tts_engine, pause_duration, speed_global,
cosyvoice_model=None, vibevoice_model=None,
cosyvoice_model=None, vibevoice_model=None, qwen_model=None,
cfg_scale=1.5, temperature=0.8, top_k=20, top_p=0.95, **kwargs):
import json
import torch
@@ -260,6 +261,8 @@ class AIIA_Dialogue_TTS:
raise ValueError("选择 CosyVoice 引擎时,必须连接 'cosyvoice_model'!")
if tts_engine == "VibeVoice" and vibevoice_model is None:
raise ValueError("选择 VibeVoice 引擎时,必须连接 'vibevoice_model'!")
if tts_engine == "Qwen3-TTS" and qwen_model is None:
raise ValueError("选择 Qwen3-TTS 引擎时,必须连接 'qwen_model'!")
dialogue = json.loads(dialogue_json)
full_waveform = []
@@ -267,9 +270,11 @@ class AIIA_Dialogue_TTS:
from .aiia_cosyvoice_nodes import AIIA_CosyVoice_TTS
from .aiia_vibevoice_nodes import AIIA_VibeVoice_TTS
from .aiia_qwen_nodes import AIIA_Qwen_TTS
cosy_gen = AIIA_CosyVoice_TTS()
vibe_gen = AIIA_VibeVoice_TTS()
qwen_gen = AIIA_Qwen_TTS()
print(f"[AIIA Podcast] 开始处理对话,共 {len(dialogue)} 个片段。引擎: {tts_engine}")
@@ -402,6 +407,74 @@ class AIIA_Dialogue_TTS:
})
time_ptr[0] += 1.0
elif tts_engine == "Qwen3-TTS":
# Qwen3-TTS (Iterative)
for i, item in enumerate(batch_items):
spk_name = item["speaker"]
spk_key = get_speaker_key(spk_name)
text = item["text"]
emotion = item.get("emotion", "None")
# Mapping logic for Qwen
spk_id = kwargs.get(f"speaker_{spk_key}_id", "Vivian") # Default to Vivian if empty
if not spk_id.strip(): spk_id = "Vivian"
ref_audio = get_ref_audio(spk_key)
instruct = f"{emotion}." if emotion and emotion != "None" else ""
print(f" [Qwen Processing] {spk_name} (ID: {spk_id}): {text[:15]}...")
try:
# Call Qwen TTS
res = qwen_gen.generate(
qwen_model=qwen_model,
text=text,
language="Auto",
speaker=spk_id,
instruct=instruct,
reference_audio=ref_audio,
seed=42+i,
speed=speed_global
)
generated = res[0]
wav = generated["waveform"]
sr = generated["sample_rate"]
if sr_ptr[0] != sr:
if current_full_wav:
wav = torchaudio.transforms.Resample(sr, sr_ptr[0])(wav)
else:
sr_ptr[0] = sr
if wav.ndim == 3: wav = wav.squeeze(0)
if wav.ndim == 1: wav = wav.unsqueeze(0)
current_full_wav.append(wav)
# --- Timestamp Tracking ---
seg_duration = wav.shape[-1] / sr
seg_start = time_ptr[0]
seg_end = seg_start + seg_duration
segments_info.append({
"start": round(seg_start, 3),
"end": round(seg_end, 3),
"text": text,
"speaker": spk_name,
"visual": item.get("visual")
})
time_ptr[0] += seg_duration
# Add a small gap between segments
gap = 0.2
gap_samples = int(gap * sr_ptr[0])
current_full_wav.append(torch.zeros(1, gap_samples))
time_ptr[0] += gap
except Exception as e:
print(f"[Error] Qwen item generation failed: {e}")
current_full_wav.append(torch.zeros(1, 24000))
time_ptr[0] += 1.0
else:
# CosyVoice (Iterative)
for i, item in enumerate(batch_items):
+189
View File
@@ -0,0 +1,189 @@
import os
import sys
import torch
import torchaudio
import numpy as np
import folder_paths
import subprocess
# --- Robust Package Installation ---
def _install_qwen_tts_if_needed():
try:
from qwen_tts import Qwen3TTSModel
return
except ImportError:
print("[AIIA] qwen-tts missing. Attempting installation...")
try:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-U", "qwen-tts"])
print("[AIIA] qwen-tts installed successfully.")
except Exception as e:
print(f"[AIIA] Failed to install qwen-tts: {e}")
class AIIA_Qwen_Loader:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model_name": ([
"Qwen/Qwen3-TTS-12Hz-1.7B-Base",
"Qwen/Qwen3-TTS-12Hz-0.6B-Base",
"Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
"Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice",
"Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign"
], {"default": "Qwen/Qwen3-TTS-12Hz-1.7B-Base"}),
"device": (["cuda", "cpu", "auto", "mps"], {"default": "auto"}),
"dtype": (["bf16", "fp16", "fp32"], {"default": "bf16"}),
},
"optional": {
"local_path": ("STRING", {"default": ""}),
}
}
RETURN_TYPES = ("QWEN_MODEL",)
RETURN_NAMES = ("qwen_model",)
FUNCTION = "load_model"
CATEGORY = "AIIA/Loaders"
def load_model(self, model_name, device, dtype, local_path=""):
_install_qwen_tts_if_needed()
from qwen_tts import Qwen3TTSModel
# Resolve device
if device == "auto":
if torch.cuda.is_available(): device = "cuda"
elif torch.backends.mps.is_available(): device = "mps"
else: device = "cpu"
# Resolve dtype
torch_dtype = torch.bfloat16 if dtype == "bf16" else (torch.float16 if dtype == "fp16" else torch.float32)
# Resolve path
path = local_path if local_path and os.path.exists(local_path) else model_name
print(f"[AIIA] Loading Qwen3-TTS: {path} on {device} with {dtype}")
# Flash Attention check
attn_impl = "flash_attention_2" if (device == "cuda" and torch.cuda.get_device_capability()[0] >= 8) else "sdpa"
model = Qwen3TTSModel.from_pretrained(
path,
device_map=device,
torch_dtype=torch_dtype,
attn_implementation=attn_impl
)
model_type = "Base"
if "CustomVoice" in path: model_type = "CustomVoice"
elif "VoiceDesign" in path: model_type = "VoiceDesign"
return ({"model": model, "type": model_type, "name": path, "device": device},)
class AIIA_Qwen_TTS:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"qwen_model": ("QWEN_MODEL",),
"text": ("STRING", {"multiline": True, "default": "你好,这是 Qwen3-TTS 的测试。"}),
"language": (["Auto", "Chinese", "English", "Japanese", "Korean", "German", "French", "Russian", "Portuguese", "Spanish", "Italian"], {"default": "Chinese"}),
},
"optional": {
"speaker": ("STRING", {"default": "Vivian"}),
"instruct": ("STRING", {"multiline": True, "default": ""}),
"reference_audio": ("AUDIO",),
"reference_text": ("STRING", {"multiline": True, "default": ""}),
"x_vector_only": ("BOOLEAN", {"default": False}),
"seed": ("INT", {"default": 42, "min": -1, "max": 2147483647}),
"speed": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 2.0}),
}
}
RETURN_TYPES = ("AUDIO",)
RETURN_NAMES = ("audio",)
FUNCTION = "generate"
CATEGORY = "AIIA/Synthesis"
def generate(self, qwen_model, text, language, speaker="Vivian", instruct="", reference_audio=None, reference_text="", x_vector_only=False, seed=42, speed=1.0):
model = qwen_model["model"]
m_type = qwen_model["type"]
if seed >= 0:
torch.manual_seed(seed)
if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
lang_param = language if language != "Auto" else "Auto"
wavs = None
sr = 24000 # Default if unknown
try:
if m_type == "CustomVoice":
print(f"[AIIA] Qwen3-TTS CustomVoice: {speaker} | Instruct: {instruct}")
wavs, sr = model.generate_custom_voice(
text=text,
language=lang_param,
speaker=speaker,
instruct=instruct if instruct else None
)
elif m_type == "VoiceDesign":
print(f"[AIIA] Qwen3-TTS VoiceDesign: {instruct}")
wavs, sr = model.generate_voice_design(
text=text,
language=lang_param,
instruct=instruct
)
else: # Base / Clone
if reference_audio is not None:
# Convert ComfyUI Audio format to (numpy, sr) tuple
device = qwen_model["device"]
ref_wav = reference_audio["waveform"]
ref_sr = reference_audio["sample_rate"]
# Convert to mono if needed
if ref_wav.ndim == 3: ref_wav = ref_wav[0]
if ref_wav.shape[0] > 1: ref_wav = torch.mean(ref_wav, dim=0, keepdim=True)
ref_audio_data = (ref_wav.squeeze().cpu().numpy(), ref_sr)
print(f"[AIIA] Qwen3-TTS VoiceClone: Using provided reference.")
wavs, sr = model.generate_voice_clone(
text=text,
language=lang_param,
ref_audio=ref_audio_data,
ref_text=reference_text if reference_text else None,
x_vector_only_mode=x_vector_only
)
else:
# Fallback if no reference provided for Base model
# Typically Base model MUST have reference.
# We might want to provide a default one or error out.
raise ValueError("Qwen3-TTS Base model requires 'reference_audio' and 'reference_text' for cloning.")
# Process output
if wavs is not None and len(wavs) > 0:
audio_out = torch.from_numpy(wavs[0]).float()
if audio_out.ndim == 1: audio_out = audio_out.unsqueeze(0)
# Speed adj (Qwen3-TTS might not have native speed param in generate_* yet, so we use torchaudio if needed)
if speed != 1.0:
# Simple speed change via resampling (pitch change) - matches CosyVoice fallback
resampler = torchaudio.transforms.Resample(orig_freq=int(sr*speed), new_freq=sr)
audio_out = resampler(audio_out)
return ({"waveform": audio_out.unsqueeze(0), "sample_rate": sr},)
except Exception as e:
print(f"[AIIA] Qwen3-TTS Generation Error: {e}")
import traceback
traceback.print_exc()
raise e
NODE_CLASS_MAPPINGS = {
"AIIA_Qwen_Loader": AIIA_Qwen_Loader,
"AIIA_Qwen_TTS": AIIA_Qwen_TTS
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AIIA_Qwen_Loader": "🤖 Qwen3-TTS Loader",
"AIIA_Qwen_TTS": "🗣️ Qwen3-TTS Synthesis"
}
+2 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "aiia"
description = "The Ultimate AI Audio/Video toolkit for ComfyUI. Features an enhanced Ditto (with optimizations that outperform official demos and other SOTA talking head models in lip-sync accuracy and natural motion), EchoMimic V3 & FLOAT, VibeVoice & CosyVoice 3.0 (Zero-Shot Voice Cloning), Multi-Role Podcast Generation, and a powerful Media Browser."
version = "1.10.20"
version = "1.11.0"
license = {file = "LICENSE"}
readme = "README.md"
authors = [
@@ -17,6 +17,7 @@ dependencies = [
"huggingface_hub",
"opencv-python",
"ffmpeg-python",
"qwen-tts",
]
[project.urls]