Files
chenpipi0807-ComfyUI-Index-TTS/tts_models.py
T
chenpipi0807 dd7c17b8d1 Update tts_models.py
增加了对 Apple Silicon MPS 设备的检测
2025-05-11 17:49:52 +08:00

611 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import sys
import yaml
import time
import torch
import numpy as np
import tempfile
import soundfile as sf
import traceback
import torchaudio
from typing import List, Tuple, Optional, Dict, Any, Union
from pathlib import Path
# 确保当前目录在导入路径中
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.append(current_dir)
# 导入官方IndexTTS实现
from indextts.infer import IndexTTS
class IndexTTSModel:
"""Wrapper for official IndexTTS implementation"""
def __init__(self, model_dir="models/Index-TTS"):
"""
Initialize IndexTTS model
Args:
model_dir: Model directory path
"""
print(f"[IndexTTS-Model] ==== 初始化IndexTTS模型器 =====")
print(f"[IndexTTS-Model] 输入模型目录: {model_dir}")
# 解析路径
self.model_dir = model_dir
self.abs_model_dir = os.path.abspath(model_dir)
print(f"[IndexTTS-Model] 模型绝对路径: {self.abs_model_dir}")
# 检查设备:支持 CUDA 和 Apple Silicon MPS
if torch.cuda.is_available():
self.device = "cuda:0"
print(f"[IndexTTS-Model] 使用设备: {self.device}")
print(f"[IndexTTS-Model] CUDA可用,显存: {torch.cuda.get_device_properties(0).total_memory / (1024**3):.2f}GB")
print(f"[IndexTTS-Model] 当前GPU: {torch.cuda.get_device_name(0)}")
elif hasattr(torch, 'mps') and torch.mps.is_available():
self.device = "mps"
print(f"[IndexTTS-Model] 使用设备: {self.device} (Apple Silicon GPU)")
else:
self.device = "cpu"
print(f"[IndexTTS-Model] 使用设备: {self.device} (未检测到支持的GPU)")
self.model = None
self.cfg_path = os.path.join(self.model_dir, "config.yaml")
print(f"[IndexTTS-Model] 配置路径: {self.cfg_path}")
try:
# 创建目录
if not os.path.exists(self.model_dir):
print(f"[IndexTTS-Model] 模型目录不存在,创建路径: {self.model_dir}")
os.makedirs(self.model_dir, exist_ok=True)
# 检查配置文件
if os.path.exists(self.cfg_path):
print(f"[IndexTTS-Model] 配置文件存在: {self.cfg_path}")
file_size = os.path.getsize(self.cfg_path) / 1024 # KB
print(f"[IndexTTS-Model] 配置文件大小: {file_size:.2f}KB")
else:
print(f"[IndexTTS-Model] 错误: 配置文件不存在: {self.cfg_path}")
raise FileNotFoundError(f"配置文件不存在: {self.cfg_path}")
# 检查必要模型文件
required_files = [
"bigvgan_generator.pth",
"bpe.model",
"gpt.pth",
"config.yaml"
]
missing_files = []
for file in required_files:
file_path = os.path.join(self.model_dir, file)
if os.path.exists(file_path):
file_size = os.path.getsize(file_path) / (1024*1024) # MB
print(f"[IndexTTS-Model] 必要模型文件存在: {file} ({file_size:.2f}MB)")
else:
missing_files.append(file)
print(f"[IndexTTS-Model] 警告: 模型文件 {file} 不存在,可能会影响模型正常运行")
if missing_files:
print(f"[IndexTTS-Model] 缺失文件列表: {missing_files}")
raise FileNotFoundError(f"缺失必要模型文件: {missing_files}")
# 列出模型目录中的所有文件
print(f"[IndexTTS-Model] 模型目录内容: {os.listdir(self.model_dir)}")
print(f"[IndexTTS-Model] 模型检查完成!")
# 在初始化时就加载模型,而不是延迟加载
print(f"[IndexTTS-Model] 开始加载模型实例...")
self._lazy_load_model()
except Exception as e:
import traceback
print(f"[IndexTTS-Model] 检查模型文件失败: {e}")
print(f"[IndexTTS-Model] 错误详情:")
traceback.print_exc()
raise
def _lazy_load_model(self):
"""
Lazy load model, only when needed
Returns:
bool: Success or not
"""
if self.model is not None:
return True
print(f"[IndexTTS-Model] 开始加载模型...")
start_time = time.time()
try:
# 确保可以访问到indextts模块
indextts_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'indextts')
if indextts_path not in sys.path and os.path.exists(indextts_path):
print(f"[IndexTTS-Model] 将{indextts_path}添加到sys.path")
sys.path.append(indextts_path)
# 确保可以访问到父目录
parent_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if parent_path not in sys.path:
print(f"[IndexTTS-Model] 将{parent_path}添加到sys.path")
sys.path.append(parent_path)
# 使用设备
device = self.device
is_fp16 = False if device == "cpu" else True
use_cuda_kernel = device.startswith("cuda")
# Apple MPS 设备特殊处理
if device == "mps":
print(f"[IndexTTS-Model] 检测到 Apple Silicon GPU,调整参数配置")
# 在 MPS 设备上保持 FP16 开启但禁用 CUDA 内核
is_fp16 = True
use_cuda_kernel = False
print(f"[IndexTTS-Model] 创建模型实例, 使用设备: {device}, FP16: {is_fp16}, CUDA Kernel: {use_cuda_kernel}")
# 创建简单的模型调用包装器
# 定义前处理函数,在调用IndexTTS实例之前修复路径问题
def patch_model_paths(module_name):
orig_import = __import__
def custom_import(name, *args, **kwargs):
module = orig_import(name, *args, **kwargs)
if name == module_name:
# 修改IndexTTS.__init__方法以加载正确路径的BPE模型
orig_init = module.IndexTTS.__init__
def patched_init(self, *args, **kwargs):
# 调用原始函数
result = orig_init(self, *args, **kwargs)
# 修复BPE模型路径
# 检查是否包含"checkpoints/"
if hasattr(self, 'bpe_path') and 'checkpoints/' in self.bpe_path:
orig_path = self.bpe_path
# 移除checkpoints/路径
corrected_path = os.path.join(self.model_dir, os.path.basename(self.bpe_path))
if os.path.exists(corrected_path):
print(f"[IndexTTS-Model] 修复bpe_path路径: {orig_path} -> {corrected_path}")
self.bpe_path = corrected_path
# 重新加载分词器
self.tokenizer = spm.SentencePieceProcessor(model_file=self.bpe_path)
print(f"[IndexTTS-Model] 成功重新加载分词器: {self.bpe_path}")
return result
# 替换原始的__init__方法
module.IndexTTS.__init__ = patched_init
return module
return custom_import
# 保存原始导入函数
original_import = __import__
# 替换导入函数为包装版本
__builtins__['__import__'] = patch_model_paths('indextts.infer')
try:
# 现在加载模型
model = IndexTTS(
cfg_path=self.cfg_path,
model_dir=self.model_dir,
is_fp16=is_fp16,
device=device,
use_cuda_kernel=use_cuda_kernel
)
finally:
# 恢复原始导入函数
__builtins__['__import__'] = original_import
# 检查并直接修复bpe_path
if hasattr(model, 'bpe_path') and not os.path.exists(model.bpe_path):
# 尝试直接在模型目录根目录下查找
base_name = os.path.basename(model.bpe_path)
corrected_path = os.path.join(self.model_dir, base_name)
if os.path.exists(corrected_path):
print(f"[IndexTTS-Model] 手动修复bpe_path路径: {model.bpe_path} -> {corrected_path}")
model.bpe_path = corrected_path
# 重新对分词器初始化
import sentencepiece as spm
model.tokenizer = spm.SentencePieceProcessor(model_file=model.bpe_path)
print(f"[IndexTTS-Model] 成功重新加载分词器: {model.bpe_path}")
# 测试模型是否有infer方法
if not hasattr(model, 'infer'):
print(f"[IndexTTS-Model] 错误: 模型没有infer方法")
raise AttributeError("Model does not have 'infer' method")
self.model = model
load_time = time.time() - start_time
print(f"[IndexTTS-Model] 模型成功加载,耗时: {load_time:.2f}秒")
return True
except Exception as e:
self.model = None # 确保在失败时设置为None
print(f"[IndexTTS-Model] 模型加载失败: {e}")
print(f"[IndexTTS-Model] 错误详情:")
traceback.print_exc()
raise ValueError(f"无法加载 IndexTTS 模型: {e}")
return False
def infer_fast(self, reference_audio, text, output_path=None, language="auto", speed=1.0, verbose=False, temperature=1.0, top_p=0.8, top_k=30, repetition_penalty=10.0, length_penalty=0.0, num_beams=3, max_mel_tokens=600, bucket_enable=True):
"""
Fast inference method (fallback to regular infer as this is a wrapper)
Args:
reference_audio: Reference audio path or audio data tuple (audio_data, sample_rate)
text: Text to synthesize
output_path: Output file path (optional)
language: Text language (auto, zh, en)
speed: Speech speed factor
verbose: Whether to print detailed logs
temperature: Model temperature
top_p: Nucleus sampling probability
top_k: Top-k sampling
repetition_penalty: Penalty for repeating tokens
length_penalty: Penalty for length
num_beams: Number of beams for beam search
max_mel_tokens: Maximum number of mel tokens
bucket_enable: Whether to enable sentence bucketing
Returns:
If output_path is provided: output_path
Otherwise: (sample_rate, audio_data)
"""
# 由于我们是wrapper,这里实际上直接调用普通推理方法,不实现快速推理
print(f"[IndexTTS-Model] 调用快速推理模式 (回退到普通推理)")
return self.infer(
reference_audio=reference_audio,
text=text,
output_path=output_path,
language=language,
speed=speed,
verbose=verbose,
temperature=temperature,
top_p=top_p,
top_k=top_k,
repetition_penalty=repetition_penalty,
length_penalty=length_penalty,
num_beams=num_beams,
max_mel_tokens=max_mel_tokens
)
def infer(self, reference_audio, text, output_path=None, language="auto", speed=1.0, verbose=False, temperature=1.0, top_p=0.8, top_k=30, repetition_penalty=10.0, length_penalty=0.0, num_beams=3, max_mel_tokens=600):
"""
Generate speech using reference audio
Args:
reference_audio: Reference audio path or audio data tuple (audio_data, sample_rate)
text: Text to synthesize
output_path: Output file path (optional)
language: Text language (auto, zh, en)
speed: Speech speed factor
verbose: Whether to print detailed logs
temperature: Model temperature
top_p: Nucleus sampling probability
top_k: Top-k sampling
repetition_penalty: Penalty for repeating tokens
length_penalty: Penalty for length
num_beams: Number of beams for beam search
max_mel_tokens: Maximum number of mel tokens
Returns:
If output_path is provided: output_path
Otherwise: (sample_rate, audio_data)
"""
if self.model is None:
print("[IndexTTS-Model] 模型未初始化,尝试加载...")
self._lazy_load_model()
infer_start_time = time.time()
print(f"[IndexTTS-Model] 开始生成语音... 文本长度: {len(text)}")
# 判断是否需要执行中文数字转换的标志
skip_chinese_number_conversion = False
# 根据language参数处理文本
if language == "en":
# 英文模式:跳过中文数字转换
skip_chinese_number_conversion = True
print(f"[IndexTTS-Model] 使用英文模式,将跳过中文数字转换")
elif language == "auto":
# 自动模式:检查文本中是否主要为英文
# 计算英文字符比例
english_chars = sum(1 for c in text if ord('a') <= ord(c.lower()) <= ord('z'))
chinese_chars = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
total_chars = len(text.strip())
if total_chars > 0:
english_ratio = english_chars / total_chars
chinese_ratio = chinese_chars / total_chars
# 如果英文字符占比明显高于中文字符,则使用英文模式
if english_ratio > 0.5 and english_ratio > chinese_ratio * 2:
skip_chinese_number_conversion = True
print(f"[IndexTTS-Model] 自动检测到英文文本占比高({english_ratio:.2f}),将跳过中文数字转换")
# 在执行预处理前,临时修改模型的预处理逻辑
original_preprocess_func = None
if skip_chinese_number_conversion and hasattr(self.model, 'preprocess_text'):
# 保存原始预处理函数
original_preprocess_func = self.model.preprocess_text
# 替换为处理英文数字的预处理函数
def modified_preprocess(text_input):
# 导入必要的模块
import re
# 英文数字转换函数
def num_to_english(num_str):
"""将数字字符串转换为英文表示"""
# 单位数字到英文的映射
units = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine',
'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen',
'seventeen', 'eighteen', 'nineteen']
# 十位数到英文的映射
tens = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety']
# 处理0
if num_str == '0':
return 'zero'
# 将字符串转换为整数
num = int(num_str)
if num == 0:
return 'zero'
# 处理负数
if num < 0:
return 'negative ' + num_to_english(str(abs(num)))
# 处理千位及以上
words = []
if num >= 1000000000: # 十亿
words.append(num_to_english(str(num // 1000000000)) + ' billion')
num %= 1000000000
if num >= 1000000: # 百万
words.append(num_to_english(str(num // 1000000)) + ' million')
num %= 1000000
if num >= 1000: # 千位
words.append(num_to_english(str(num // 1000)) + ' thousand')
num %= 1000
if num >= 100: # 百位
words.append(units[num // 100] + ' hundred')
num %= 100
if num > 0:
# 添加"and",如果前面已有词
if words and (num < 100):
words.append('and')
if num < 20: # 20以下
words.append(units[num])
else: # 20-99
word = tens[num // 10]
if num % 10 > 0: # 如果有个位
word += '-' + units[num % 10]
words.append(word)
return ' '.join(words)
# 处理空格分隔的数字(如"4 0 9 0")- 每个数字单独转换
def process_spaced_digits(text):
# 匹配由空格分隔的数字序列
pattern = re.compile(r'\b(\d(\s+\d)+)\b')
for match in pattern.finditer(text):
spaced_digits = match.group(0)
# 单独处理每个数字
converted = ' '.join(['zero' if d == '0' else num_to_english(d) if d.isdigit() else d for d in spaced_digits.split()])
text = text.replace(spaced_digits, converted)
return text
# 处理连续的数字(如"4090")- 作为一个整体转换
def process_consecutive_digits(text):
# 匹配连续的数字
pattern = re.compile(r'\b\d{2,}\b')
for match in pattern.finditer(text):
digits = match.group(0)
# 将整体数字转换为英文
converted = num_to_english(digits)
text = text.replace(digits, converted)
return text
# 处理单个数字
def process_single_digit(text):
# 匹配单个数字
pattern = re.compile(r'\b\d\b')
for match in pattern.finditer(text):
digit = match.group(0)
# 转换单个数字
converted = 'zero' if digit == '0' else num_to_english(digit)
text = text.replace(digit, converted)
return text
# 在英文模式下应用数字处理
processed_text = text_input
# 首先处理有空格的数字序列
processed_text = process_spaced_digits(processed_text)
# 然后处理连续的数字
processed_text = process_consecutive_digits(processed_text)
# 最后处理单个数字
processed_text = process_single_digit(processed_text)
print(f"[IndexTTS-Model] 英文数字处理: '{text_input}' -> '{processed_text}'")
# 应用标准文本处理(不包括中文数字转换)
if hasattr(self.model, 'normalizer'):
# 保存原始的use_chinese判断函数
original_use_chinese = self.model.normalizer.use_chinese
# 强制返回False的函数,确保不执行中文数字转换
def force_english(s):
return False
# 替换为强制英文模式
self.model.normalizer.use_chinese = force_english
# 处理文本
normalized_text = self.model.normalizer.infer(processed_text)
# 恢复原始函数
self.model.normalizer.use_chinese = original_use_chinese
return normalized_text
else:
# 如果没有normalizer,返回处理过的文本
return processed_text
# 替换预处理函数
self.model.preprocess_text = modified_preprocess
print(f"[IndexTTS-Model] 已应用修改后的文本预处理逻辑,不执行中文数字转换")
try:
# 处理参考音频
print(f"[IndexTTS-Model] 处理参考音频")
final_ref_audio = None
if isinstance(reference_audio, str):
if not os.path.exists(reference_audio):
print(f"[IndexTTS-Model] 错误: 参考音频文件不存在: {reference_audio}")
raise FileNotFoundError(f"参考音频文件不存在: {reference_audio}")
print(f"[IndexTTS-Model] 使用文件路径作为参考音频: {reference_audio}")
final_ref_audio = reference_audio
else:
# 假设输入是(audio_data, sample_rate)元组
try:
audio_data, sample_rate = reference_audio
print(f"[IndexTTS-Model] 使用音频数据作为参考,采样率: {sample_rate}Hz, 形状: {audio_data.shape if hasattr(audio_data, 'shape') else 'unknown'}")
# 使用与原始实现相同的方式准备参考音频
# 首先确保数据是torch tensor
if isinstance(audio_data, np.ndarray):
print(f"[IndexTTS-Model] 将NumPy数组转换为Torch张量")
audio_data = torch.from_numpy(audio_data)
print(f"[IndexTTS-Model] 原始张量形状: {audio_data.shape}, 维度: {audio_data.dim()}D")
# 处理音频数据维度 - 直接将其转换为2D
if audio_data.dim() == 3 and audio_data.shape[0] == 1: # [1, channels, samples]
print(f"[IndexTTS-Model] 将 [1, channels, samples] 转换为 [channels, samples]")
audio_data = audio_data.squeeze(0) # 去除批次维度,变成 [channels, samples]
# 如果还是3D或更高维度,尝试其他方法
if audio_data.dim() > 2:
print(f"[IndexTTS-Model] 张量仍然是{audio_data.dim()}D,尝试将其平均为2D")
# 如果是 [batch, channels, samples] 格式,选择第一个批次和从多声道取均值
if audio_data.dim() == 3:
audio_data = audio_data[0] # 选择第一个批次 -> [channels, samples]
if audio_data.shape[0] > 1: # 多声道
audio_data = torch.mean(audio_data, dim=0, keepdim=True) # 声道平均值 -> [1, samples]
# 再次检查维度,确保是2D
if audio_data.dim() > 2:
print(f"[IndexTTS-Model] 警告: 重试降维后仍然是 {audio_data.dim()}D 张量,尝试直接重塑形状")
# 最后手段: 强制将所有数据打平成 [1, -1] 形状
audio_data = audio_data.reshape(1, -1)
print(f"[IndexTTS-Model] 处理后的张量形状: {audio_data.shape}, 维度: {audio_data.dim()}D")
# 确保采样率正确
if sample_rate != 24000:
print(f"[IndexTTS-Model] 将采样率从 {sample_rate}Hz 转换为 24000Hz")
audio_data = torchaudio.transforms.Resample(sample_rate, 24000)(audio_data)
sample_rate = 24000
# 创建临时文件保存音频数据
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_file:
temp_path = temp_file.name
print(f"[IndexTTS-Model] 将处理后的音频数据保存为临时文件: {temp_path}")
torchaudio.save(temp_path, audio_data, sample_rate)
final_ref_audio = temp_path
print(f"[IndexTTS-Model] 成功保存临时文件: {temp_path}")
except Exception as e:
print(f"[IndexTTS-Model] 错误: 处理音频数据时出错: {e}")
raise ValueError(f"无法处理参考音频数据: {e}")
# 构造输出路径
final_output_path = output_path
if final_output_path is None:
# 创建临时文件作为输出路径
temp_output = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
final_output_path = temp_output.name
temp_output.close() # 关闭但不删除
print(f"[IndexTTS-Model] 未提供输出路径,将使用临时文件: {final_output_path}")
# 这里调用实际的模型推理
print(f"[IndexTTS-Model] 开始生成语音,文本长度: {len(text)}")
start_time = time.time()
# 再次检查模型是否加载成功
if self.model is None:
print(f"[IndexTTS-Model] 错误: 模型未成功加载,无法执行推理")
raise ValueError("模型未成功加载,请检查模型文件和错误日志")
# 使用原始IndexTTS模型进行推理
# 注意:原始IndexTTS模型的infer方法没有verbose、temperature等参数
# 我们只传递它支持的基本参数,避免错误
if len(text) > 100:
print(f"[IndexTTS-Model] 文本较长,使用批量推理模式")
try:
# 直接使用基础参数,避免传递额外参数
result = self.model.infer(final_ref_audio, text, final_output_path)
except Exception as e:
print(f"[IndexTTS-Model] 批量推理失败: {e},回退到普通推理模式")
result = self.model.infer(final_ref_audio, text, final_output_path)
else:
print(f"[IndexTTS-Model] 文本较短,使用普通推理模式")
result = self.model.infer(final_ref_audio, text, final_output_path)
end_time = time.time()
print(f"[IndexTTS-Model] 语音生成完成,耗时: {end_time - start_time:.2f}秒")
# 根据输出类型返回结果
if output_path:
print(f"[IndexTTS-Model] 返回保存的文件路径: {output_path}")
return output_path
else:
if isinstance(result, tuple) and len(result) == 2:
# 如果result已经是(sample_rate, audio_data)格式
sample_rate, audio_data = result
print(f"[IndexTTS-Model] 返回内存中的音频数据,采样率: {sample_rate}Hz, 形状: {audio_data.shape if hasattr(audio_data, 'shape') else 'unknown'}")
return result
elif os.path.exists(final_output_path):
# 读取临时文件并返回数据
audio_data, sample_rate = sf.read(final_output_path)
print(f"[IndexTTS-Model] 返回内存中的音频数据,采样率: {sample_rate}Hz, 形状: {audio_data.shape if hasattr(audio_data, 'shape') else 'unknown'}")
# 删除临时文件
try:
os.unlink(final_output_path)
except Exception as e:
print(f"[IndexTTS-Model] 警告: 无法删除临时文件: {e}")
return (sample_rate, audio_data)
else:
print(f"[IndexTTS-Model] 错误: 输出文件不存在: {final_output_path}")
raise FileNotFoundError(f"输出文件不存在: {final_output_path}")
except Exception as e:
print(f"[IndexTTS-Model] 语音生成失败: {e}")
print(f"[IndexTTS-Model] 错误详情:")
traceback.print_exc()
raise
finally:
# 恢复原始预处理函数(如果有修改)
if original_preprocess_func is not None and hasattr(self.model, 'preprocess_text'):
self.model.preprocess_text = original_preprocess_func
print(f"[IndexTTS-Model] 已恢复原始文本预处理逻辑")
infer_end_time = time.time()
print(f"[IndexTTS-Model] 总耗时: {infer_end_time - infer_start_time:.2f}秒")