195 lines
5.8 KiB
Python
195 lines
5.8 KiB
Python
import os
|
|
import torch
|
|
from typing import Any, Dict
|
|
|
|
import folder_paths
|
|
|
|
|
|
from .tools.seeder import TorchSeedContext
|
|
from .tools.logger import get_logger
|
|
|
|
from .ChatTTS.core import Chat
|
|
|
|
logger = get_logger("ChatTTS")
|
|
|
|
|
|
class ChatTTSLoader:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
chattts_dir = os.path.join(folder_paths.models_dir, "chattts")
|
|
if not os.path.exists(chattts_dir):
|
|
os.makedirs(chattts_dir, exist_ok=True)
|
|
files = [
|
|
f
|
|
for f in os.listdir(chattts_dir)
|
|
if f.endswith(('.pth', '.pt', '.ckpt', '.safetensors'))
|
|
]
|
|
return {
|
|
"required": {
|
|
"model_path": (["(auto)"] + sorted(files), {"default": "(auto)"}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("MODEL",)
|
|
FUNCTION = "load"
|
|
CATEGORY = "chattts"
|
|
|
|
def load(self, model_path):
|
|
# 获取模型路径
|
|
chattts_dir = os.path.join(folder_paths.models_dir, "chattts")
|
|
local_model_path = None
|
|
|
|
if model_path == "(auto)":
|
|
# 检查本地模型
|
|
files = [
|
|
f
|
|
for f in os.listdir(chattts_dir)
|
|
if f.endswith(('.pth', '.pt', '.ckpt', '.safetensors'))
|
|
]
|
|
|
|
if files:
|
|
# 使用本地模型
|
|
local_model_path = os.path.join(chattts_dir, files[0])
|
|
logger.info(f"Using local model: {local_model_path}")
|
|
else:
|
|
# 本地没有模型,尝试下载
|
|
logger.info("No local model found, attempting to download...")
|
|
else:
|
|
local_model_path = os.path.join(chattts_dir, model_path)
|
|
if not os.path.exists(local_model_path):
|
|
logger.error(f"Model file not found: {local_model_path}")
|
|
raise FileNotFoundError(f"Model file not found: {local_model_path}")
|
|
|
|
# 初始化ChatTTS并加载模型
|
|
chat = Chat(logger)
|
|
|
|
if local_model_path:
|
|
# 使用本地模型文件
|
|
loaded = chat.load("custom", custom_path=local_model_path)
|
|
else:
|
|
# 从huggingface下载模型
|
|
logger.info("Downloading model from huggingface...")
|
|
loaded = chat.load("huggingface", force_redownload=False)
|
|
|
|
if not loaded:
|
|
raise RuntimeError("Failed to load ChatTTS model")
|
|
|
|
logger.info(f"ChatTTS model loaded successfully")
|
|
return (chat,)
|
|
|
|
|
|
class ChatTTS_SpeakerSample:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"model": ("MODEL",),
|
|
"seed": ("INT", {"default": 2, "min": 1, "max": 4294967295}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("DICT",)
|
|
FUNCTION = "sample"
|
|
CATEGORY = "chattts"
|
|
|
|
def sample(self, model, seed):
|
|
# 严格按照 WebUI 实现,使用随机种子采样
|
|
with TorchSeedContext(seed):
|
|
spk_emb = model.sample_random_speaker()
|
|
logger.info(f"Sampled speaker with seed: {seed}")
|
|
return ({"spk_emb": spk_emb},)
|
|
|
|
|
|
class ChatTTS_Sampler:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"model": ("MODEL",),
|
|
"text": ("STRING", {"multiline": True, "dynamicPrompts": False}),
|
|
},
|
|
"optional": {
|
|
"speaker_params": ("DICT", {}),
|
|
"seed": ("INT", {"default": 2, "min": 1, "max": 4294967295}),
|
|
"temperature": (
|
|
"FLOAT",
|
|
{"default": 0.3, "min": 0.0, "max": 2.0, "step": 0.01},
|
|
),
|
|
"top_P": (
|
|
"FLOAT",
|
|
{"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01},
|
|
),
|
|
"top_K": ("INT", {"default": 20, "min": 1, "max": 100}),
|
|
"stream": ("BOOLEAN", {"default": False}),
|
|
"split_batch": ("INT", {"default": 0, "min": 0, "max": 32}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("AUDIO", "DICT")
|
|
FUNCTION = "synthesize"
|
|
CATEGORY = "chattts"
|
|
|
|
def synthesize(
|
|
self,
|
|
model,
|
|
text,
|
|
speaker_params=None,
|
|
seed=2,
|
|
temperature=0.3,
|
|
top_P=0.7,
|
|
top_K=20,
|
|
stream=False,
|
|
split_batch=0,
|
|
):
|
|
# 严格按照 WebUI funcs.py 实现
|
|
spk_emb = speaker_params.get("spk_emb", "") if speaker_params else ""
|
|
|
|
# 使用WebUI相同的参数构造
|
|
params_infer_code = ChatTTS.Chat.InferCodeParams(
|
|
spk_emb=spk_emb,
|
|
temperature=temperature,
|
|
top_P=top_P,
|
|
top_K=top_K,
|
|
manual_seed=seed,
|
|
)
|
|
|
|
with TorchSeedContext(seed):
|
|
# 按照 WebUI 的方式调用 infer 方法
|
|
wav = model.infer(
|
|
text,
|
|
skip_refine_text=True,
|
|
params_infer_code=params_infer_code,
|
|
stream=stream,
|
|
split_text=split_batch > 0,
|
|
max_split_batch=split_batch,
|
|
)
|
|
|
|
# 处理返回值
|
|
if stream:
|
|
audio = None
|
|
for sr, arr in wav:
|
|
audio = {"waveform": torch.tensor(arr).unsqueeze(0), "sample_rate": sr}
|
|
break
|
|
else:
|
|
audio = {
|
|
"waveform": torch.tensor(wav[0]).unsqueeze(0),
|
|
"sample_rate": 24000,
|
|
}
|
|
|
|
meta = {"seed": seed, "spk_emb": spk_emb}
|
|
logger.info(f"Synthesized audio: text={text[:20]}..., seed={seed}")
|
|
return (audio, meta)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"ChatTTSLoader": ChatTTSLoader,
|
|
"ChatTTS_SpeakerSample": ChatTTS_SpeakerSample,
|
|
"ChatTTS_Sampler": ChatTTS_Sampler,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"ChatTTSLoader": "ChatTTS 模型加载",
|
|
"ChatTTS_SpeakerSample": "ChatTTS 说话人采样",
|
|
"ChatTTS_Sampler": "ChatTTS 文本转语音",
|
|
}
|