refactor: simplify Qwen3-TTS UI with Model Router and bundle support
This commit is contained in:
@@ -985,8 +985,9 @@ hf download digital-avatar/ditto-talkinghead --local-dir ditto
|
||||
- **Flash Attention 2**: 强烈推荐以获得最佳推理性能。
|
||||
- **节点**:
|
||||
- `🤖 Qwen3-TTS Loader`: 加载模型。支持 `Base (Clone)`、`CustomVoice (Presets)` 和 `VoiceDesign` 模型。
|
||||
- `🗣️ Qwen3-TTS Synthesis`: 执行合成。根据加载的模型类型自动切换功能。
|
||||
- `🎙️ Qwen3-TTS Dialogue (Specialist)`: **[旗舰级]** 专为 Qwen3 设计的对话节点。支持为每个角色独立设置克隆、预设或设计模式,真正发挥全系列模型优势。
|
||||
- `🗣️ Qwen3-TTS Synthesis`: 执行合成。支持单模型连接或通过 Router 连接的 Bundle。
|
||||
- `🔌 Qwen3-Model Router (Bundle)`: **[新]** 路由节点。将多个分立的 Qwen 模型捆绑成一个,供对话节点自动调用。
|
||||
- `🎙️ Qwen3-TTS Dialogue (Specialist)`: **[旗帜级]** 专为 Qwen3 设计的对话节点。单输入设计,支持通过 Router 实现混合克隆/捏人。
|
||||
- **模型列表**:
|
||||
- `Qwen/Qwen3-TTS-12Hz-1.7B-Base` (或 0.6B-Base)
|
||||
- `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice` (或 0.6B-CustomVoice)
|
||||
@@ -994,13 +995,19 @@ hf download digital-avatar/ditto-talkinghead --local-dir ditto
|
||||
|
||||
#### 📊 模型功能映射表 (Model Capability Mapping)
|
||||
|
||||
| 模型版本 | **音色克隆 (Clone)** | **预设音色 (Presets)** | **文字捏人 (Design)** | **方言支持 (Dialect)** |
|
||||
| 模型版本 | **音色克隆 (Clone)** | **情感控制 (Emotion)** | **文字捏人 (Design)** | **方言支持 (Dialect)** |
|
||||
| :--- | :---: | :---: | :---: | :---: |
|
||||
| **Base** (1.7B/0.6B) | **👑 最强** | ❌ 不支持 | ❌ 不支持 | ⚠️ 仅限录音自带 |
|
||||
| **CustomVoice** (1.7B) | ⚠️ 效果极差 | **👑 最佳** | ⚠️ 指令干扰严重 | ⚠️ 效果一般 |
|
||||
| **VoiceDesign** (1.7B) | ❌ 不支持 | ⚠️ 部分支持 | **👑 专家** | **👑 完美支持** |
|
||||
| **Base** (1.7B/0.6B) | **👑 最强** | ❌ 仅限录音自带 | ❌ 不支持 | ⚠️ 仅限录音自带 |
|
||||
| **CustomVoice** (1.7B) | ⚠️ 效果极差 | ✅ 支持 | ⚠️ 指令干扰严重 | ⚠️ 效果一般 |
|
||||
| **VoiceDesign** (1.7B) | ❌ 不支持 | **👑 专家** | **👑 专家** | **👑 完美支持** |
|
||||
| **CustomVoice** (0.6B) | ⚠️ 效果极差 | ✅ 支持 | ✅ 表现优异 | ✅ 表现优异 |
|
||||
|
||||
> [!TIP]
|
||||
> **关于 UI 简化**:
|
||||
> 现在的对话节点只有一个 `qwen_model` 输入槽。
|
||||
> - 如果你只需要一种模型,直接连上即可。
|
||||
> - 如果你想实现“Speaker A 克隆,Speaker B 捏人”的混合效果,请使用 `🔌 Qwen3-Model Router` 节点进行打包连接。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **结论**:
|
||||
> 1. 做 **3秒音色克隆**:必须连 `Base` 模型。
|
||||
|
||||
+9
-13
@@ -195,21 +195,16 @@ class AIIA_Dialogue_TTS:
|
||||
"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)"}),
|
||||
"qwen_model": ("QWEN_MODEL",), # Primary Qwen model
|
||||
|
||||
# VibeVoice Specific Params
|
||||
"max_batch_char": ("INT", {"default": 1000, "min": 100, "max": 32768}),
|
||||
"cfg_scale": ("FLOAT", {"default": 1.5, "min": 1.0, "max": 10.0, "step": 0.1}),
|
||||
"temperature": ("FLOAT", {"default": 0.8, "min": 0.1, "max": 2.0}),
|
||||
"top_k": ("INT", {"default": 20, "min": 0, "max": 100}),
|
||||
"top_p": ("FLOAT", {"default": 0.95, "min": 0.0, "max": 1.0, "step": 0.05}),
|
||||
"max_batch_char": ("INT", {"default": 1000, "min": 100, "max": 32768}),
|
||||
},
|
||||
"optional": {
|
||||
"cosyvoice_model": ("COSYVOICE_MODEL",),
|
||||
"vibevoice_model": ("VIBEVOICE_MODEL",),
|
||||
"qwen_base_model": ("QWEN_MODEL",), # Optional specialized Base
|
||||
"qwen_custom_model": ("QWEN_MODEL",), # Optional specialized CustomVoice
|
||||
"qwen_design_model": ("QWEN_MODEL",), # Optional specialized VoiceDesign
|
||||
"qwen_model": ("QWEN_MODEL",), # Primary Qwen model (can be a Bundle)
|
||||
|
||||
# Speaker A
|
||||
"speaker_A_ref": ("AUDIO",),
|
||||
@@ -348,9 +343,8 @@ class AIIA_Dialogue_TTS:
|
||||
|
||||
|
||||
def process_dialogue(self, dialogue_json, tts_engine, pause_duration, speed_global, batch_mode,
|
||||
qwen_model=None, cosyvoice_model=None, vibevoice_model=None,
|
||||
qwen_base_model=None, qwen_custom_model=None, qwen_design_model=None,
|
||||
cfg_scale=1.5, temperature=0.8, top_k=20, top_p=0.95, max_batch_char=1000, **kwargs):
|
||||
max_batch_char=1000, cfg_scale=1.5, temperature=0.8, top_k=20, top_p=0.95,
|
||||
qwen_model=None, cosyvoice_model=None, vibevoice_model=None, **kwargs):
|
||||
# Robustness: ensure max_batch_char is correctly picked up even if shifted or provided as kwarg
|
||||
max_batch_char = kwargs.get("max_batch_char", max_batch_char)
|
||||
import json
|
||||
@@ -533,10 +527,12 @@ class AIIA_Dialogue_TTS:
|
||||
me = f"{me},{el}" if me else el
|
||||
ins = f"{me}。" if me else ""
|
||||
|
||||
# Routing: Use bundle if available, else use the single connected model
|
||||
tm = qwen_model
|
||||
if ref is not None and qwen_base_model is not None: tm = qwen_base_model
|
||||
elif ins and qwen_design_model is not None and ref is None: tm = qwen_design_model
|
||||
elif qwen_custom_model is not None: tm = qwen_custom_model
|
||||
if qwen_model.get("is_bundle"):
|
||||
if ref is not None: tm = qwen_model.get("base") or qwen_model.get("default")
|
||||
elif ins: tm = qwen_model.get("design") or qwen_model.get("default")
|
||||
else: tm = qwen_model.get("custom") or qwen_model.get("default")
|
||||
|
||||
# Dialect is part of compatibility
|
||||
return {
|
||||
|
||||
+94
-29
@@ -186,9 +186,23 @@ class AIIA_Qwen_TTS:
|
||||
CATEGORY = "AIIA/Synthesis"
|
||||
|
||||
def generate(self, qwen_model, text, language, speaker="Vivian", instruct="", reference_audio=None, reference_text="", zero_shot_mode=False, emotion="None", dialect="None", seed=42, speed=1.0, cfg_scale=1.5, temperature=0.8, top_k=20, top_p=0.95):
|
||||
model = qwen_model["model"]
|
||||
m_type = qwen_model["type"]
|
||||
model_path = qwen_model.get("path", "").lower()
|
||||
# 0. Handle Bundle Routing
|
||||
active_qwen = qwen_model
|
||||
if qwen_model.get("is_bundle"):
|
||||
# Auto-route based on generation intent
|
||||
if reference_audio is not None or zero_shot_mode:
|
||||
active_qwen = qwen_model.get("base") or qwen_model.get("default")
|
||||
elif instruct.strip() or dialect != "None" or emotion != "None":
|
||||
active_qwen = qwen_model.get("design") or qwen_model.get("custom") or qwen_model.get("default")
|
||||
else:
|
||||
active_qwen = qwen_model.get("custom") or qwen_model.get("default")
|
||||
|
||||
if not active_qwen:
|
||||
raise ValueError("[AIIA Qwen] No active model found in bundle or input!")
|
||||
|
||||
model = active_qwen["model"]
|
||||
m_type = active_qwen["type"]
|
||||
model_path = active_qwen.get("path", "").lower()
|
||||
|
||||
# Warning for Base model with instruct
|
||||
if "base" in model_path and (instruct.strip() or dialect != "None" or emotion != "None"):
|
||||
@@ -319,19 +333,12 @@ class AIIA_Qwen_Dialogue_TTS:
|
||||
"cfg_scale": ("FLOAT", {"default": 1.5, "min": 1.0, "max": 10.0, "step": 0.1}),
|
||||
"temperature": ("FLOAT", {"default": 0.8, "min": 0.1, "max": 2.0, "step": 0.1}),
|
||||
"top_k": ("INT", {"default": 20, "min": 0, "max": 100}),
|
||||
"top_p": ("FLOAT", {"default": 0.95, "min": 0.0, "max": 1.0, "step": 0.05}),
|
||||
"zero_shot_mode": ("BOOLEAN", {"default": False}),
|
||||
"max_batch_char": ("INT", {"default": 1000, "min": 100, "max": 32768}),
|
||||
"dialect_note": ("STRING", {"default": "💡 提示:方言建议配合 Design 模式使用。", "is_label": True}),
|
||||
"base_note": ("STRING", {"default": "⚠️ 注意:Clone 模式下的 Base 模型不支持文字指令控制。", "is_label": True}),
|
||||
},
|
||||
"optional": {
|
||||
"qwen_base_model": ("QWEN_MODEL",),
|
||||
"qwen_custom_model": ("QWEN_MODEL",),
|
||||
"qwen_design_model": ("QWEN_MODEL",),
|
||||
|
||||
"qwen_model": ("QWEN_MODEL",),
|
||||
"preset_note": ("STRING", {"default": QWEN_PRESET_NOTE, "is_label": True}),
|
||||
|
||||
# Speaker A
|
||||
"speaker_A_mode": (["Clone", "Preset", "Design"], {"default": "Clone"}),
|
||||
"speaker_A_id": (QWEN_SPEAKER_LIST, {"default": "Vivian"}),
|
||||
@@ -341,7 +348,6 @@ class AIIA_Qwen_Dialogue_TTS:
|
||||
"speaker_A_design": ("STRING", {"multiline": True, "default": ""}),
|
||||
"speaker_A_ref": ("AUDIO",),
|
||||
"speaker_A_ref_text": ("STRING", {"multiline": True, "default": ""}),
|
||||
|
||||
# Speaker B
|
||||
"speaker_B_mode": (["Clone", "Preset", "Design"], {"default": "Clone"}),
|
||||
"speaker_B_id": (QWEN_SPEAKER_LIST, {"default": "Vivian"}),
|
||||
@@ -351,7 +357,6 @@ class AIIA_Qwen_Dialogue_TTS:
|
||||
"speaker_B_design": ("STRING", {"multiline": True, "default": ""}),
|
||||
"speaker_B_ref": ("AUDIO",),
|
||||
"speaker_B_ref_text": ("STRING", {"multiline": True, "default": ""}),
|
||||
|
||||
# Speaker C
|
||||
"speaker_C_mode": (["Clone", "Preset", "Design"], {"default": "Design"}),
|
||||
"speaker_C_id": (QWEN_SPEAKER_LIST, {"default": "Vivian"}),
|
||||
@@ -469,31 +474,50 @@ class AIIA_Qwen_Dialogue_TTS:
|
||||
spk_expression_preset = kwargs.get(f"speaker_{spk_key}_expression", "None")
|
||||
spk_dialect_preset = kwargs.get(f"speaker_{spk_key}_dialect", "None")
|
||||
design = kwargs.get(f"speaker_{spk_key}_design", "")
|
||||
qwen_model = kwargs.get("qwen_model")
|
||||
if qwen_model is None:
|
||||
raise ValueError("AIIA_Qwen_Dialogue_TTS: qwen_model is required. Connect a single Qwen model or a Qwen3 Model Router (Bundle)!")
|
||||
|
||||
# 0. Specialized Qwen Routing from Bundle
|
||||
def get_model_from_bundle(mode, ref=None):
|
||||
if not qwen_model.get("is_bundle"): return qwen_model
|
||||
if mode == "Clone" or ref is not None:
|
||||
return qwen_model.get("base") or qwen_model.get("default")
|
||||
elif mode == "Design":
|
||||
return qwen_model.get("design") or qwen_model.get("default")
|
||||
else: # Preset
|
||||
return qwen_model.get("custom") or qwen_model.get("default")
|
||||
|
||||
# Extract Speaker Params
|
||||
def get_speaker_params(prefix):
|
||||
mode = kwargs.get(f"{prefix}_mode", "Preset")
|
||||
ref = kwargs.get(f"{prefix}_ref")
|
||||
tm = get_model_from_bundle(mode, ref)
|
||||
return {
|
||||
"tm": tm,
|
||||
"id": kwargs.get(f"{prefix}_id", "Vivian"),
|
||||
"ref": ref,
|
||||
"exp": kwargs.get(f"{prefix}_expression", ""),
|
||||
"dialect": kwargs.get(f"{prefix}_dialect", "None")
|
||||
}
|
||||
|
||||
ref_audio = get_ref_audio_with_fallback(spk_key) if mode == "Clone" else None
|
||||
ref_text = kwargs.get(f"speaker_{spk_key}_ref_text", "")
|
||||
|
||||
qwen_model = None
|
||||
if mode == "Clone":
|
||||
qwen_model = kwargs.get("qwen_base_model") or kwargs.get("qwen_custom_model")
|
||||
elif mode == "Preset":
|
||||
qwen_model = kwargs.get("qwen_custom_model")
|
||||
elif mode == "Design":
|
||||
qwen_model = kwargs.get("qwen_design_model")
|
||||
|
||||
if qwen_model is None:
|
||||
qwen_model = kwargs.get("qwen_base_model") or kwargs.get("qwen_custom_model") or kwargs.get("qwen_design_model")
|
||||
# Determine the actual Qwen model to use for this segment
|
||||
segment_qwen_model = get_model_from_bundle(mode, ref_audio)
|
||||
|
||||
# Unique key for "homogeneity"
|
||||
# For Preset, we can merge DIFFERENT speakers by using [Speaker] tags
|
||||
# So they only need to share the same qwen_model and mode="Preset"
|
||||
if mode == "Preset":
|
||||
param_hash = (f"Preset_{id(qwen_model)}", spk_dialect_preset)
|
||||
param_hash = (f"Preset_{id(segment_qwen_model)}", spk_dialect_preset)
|
||||
elif mode == "Clone":
|
||||
# Must share same ref_audio and ref_text and dialect
|
||||
param_hash = (f"Clone_{id(qwen_model)}", id(ref_audio), ref_text, spk_dialect_preset)
|
||||
param_hash = (f"Clone_{id(segment_qwen_model)}", id(ref_audio), ref_text, spk_dialect_preset)
|
||||
else: # Design
|
||||
# Must share the same design text and dialect
|
||||
param_hash = (f"Design_{id(qwen_model)}", design, spk_dialect_preset)
|
||||
param_hash = (f"Design_{id(segment_qwen_model)}", design, spk_dialect_preset)
|
||||
|
||||
return {
|
||||
"spk_name": spk_name,
|
||||
@@ -505,7 +529,7 @@ class AIIA_Qwen_Dialogue_TTS:
|
||||
"spk_expression_preset": spk_expression_preset,
|
||||
"spk_dialect_preset": spk_dialect_preset,
|
||||
"mode": mode,
|
||||
"qwen_model": qwen_model,
|
||||
"qwen_model": segment_qwen_model, # Use the resolved model
|
||||
"ref_audio": ref_audio,
|
||||
"ref_text": ref_text,
|
||||
"design": design,
|
||||
@@ -681,14 +705,55 @@ class AIIA_Qwen_Dialogue_TTS:
|
||||
final_wav = torch.cat(full_waveform, dim=1)
|
||||
return ({"waveform": final_wav.unsqueeze(0), "sample_rate": sample_rate}, json.dumps(segments_info, ensure_ascii=False))
|
||||
|
||||
class AIIA_Qwen_Model_Router:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"qwen_default": ("QWEN_MODEL",),
|
||||
"qwen_base": ("QWEN_MODEL",),
|
||||
"qwen_custom": ("QWEN_MODEL",),
|
||||
"qwen_design": ("QWEN_MODEL",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("QWEN_MODEL",)
|
||||
RETURN_NAMES = ("qwen_bundle",)
|
||||
FUNCTION = "bundle"
|
||||
CATEGORY = "AIIA/Loaders"
|
||||
|
||||
def bundle(self, **kwargs):
|
||||
qwen_default = kwargs.get("qwen_default")
|
||||
qwen_base = kwargs.get("qwen_base")
|
||||
qwen_custom = kwargs.get("qwen_custom")
|
||||
qwen_design = kwargs.get("qwen_design")
|
||||
|
||||
if all(m is None for m in [qwen_default, qwen_base, qwen_custom, qwen_design]):
|
||||
raise ValueError("[AIIA Qwen Router] At least one Qwen model must be connected!")
|
||||
|
||||
bundle = {
|
||||
"is_bundle": True,
|
||||
"default": qwen_default or qwen_base or qwen_custom or qwen_design,
|
||||
"base": qwen_base,
|
||||
"custom": qwen_custom,
|
||||
"design": qwen_design,
|
||||
"path": (qwen_default or qwen_base or qwen_custom or qwen_design).get("path", "")
|
||||
}
|
||||
return (bundle,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"AIIA_Qwen_Loader": AIIA_Qwen_Loader,
|
||||
"AIIA_Qwen_TTS": AIIA_Qwen_TTS,
|
||||
"AIIA_Qwen_Dialogue_TTS": AIIA_Qwen_Dialogue_TTS
|
||||
"AIIA_Qwen_Dialogue_TTS": AIIA_Qwen_Dialogue_TTS,
|
||||
"AIIA_Qwen_Model_Router": AIIA_Qwen_Model_Router
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"AIIA_Qwen_Loader": "🤖 Qwen3-TTS Loader",
|
||||
"AIIA_Qwen_TTS": "🗣️ Qwen3-TTS Synthesis",
|
||||
"AIIA_Qwen_Dialogue_TTS": "🎙️ Qwen3-TTS Dialogue (Specialist)"
|
||||
"AIIA_Qwen_Dialogue_TTS": "🎙️ Qwen3-TTS Dialogue (Specialist)",
|
||||
"AIIA_Qwen_Model_Router": "🔌 Qwen3-Model Router (Bundle)"
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user