import re
import torch
import torchaudio
from typing import List, Union
from multiprocessing import Pool, cpu_count
import os
import ChatTTS
def process_single_text(args):
"""处理单个文本的辅助函数
Args:
args: 包含以下参数的元组:
- text: 要处理的文本
- embedding_path: 音色文件路径
- output_path: 输出文件路径
- temperature: 温度参数
- index: 当前文本的索引
Returns:
str: 生成的音频文件路径或错误信息
"""
text, embedding_path, output_path, temperature, index = args
try:
# 初始化模型
chat = ChatTTS.Chat()
# 设置torch相关配置
torch._dynamo.config.cache_size_limit = 64
torch._dynamo.config.suppress_errors = True
torch.set_float32_matmul_precision('high')
# 获取本地模型路径
home_dir = os.path.expanduser('~')
local_model_path = os.path.join(
home_dir,
'.cache/huggingface/hub/models--2Noise--ChatTTS/snapshots/1a3c04a8b0651689bd9242fbb55b1f4b5a9aef84'
)
# 加载模型和音色嵌入
chat.load(source='custom', custom_path=local_model_path, compile=False)
spk_emb = torch.load(embedding_path, map_location=torch.device('cpu'))
# 设置推理参数
params_infer_code = ChatTTS.Chat.InferCodeParams(
prompt="[speed_1]",
temperature=temperature,
repetition_penalty=1.05,
max_new_token=2048,
stream_batch=24,
stream_speed=12000,
spk_emb=spk_emb
)
# 设置文本细化参数
params_refine_text = ChatTTS.Chat.RefineTextParams(
prompt='[oral_2][laugh_0][break_4]'
)
# 文本细化
refined_texts = chat.infer(
[text],
refine_text_only=True,
params_refine_text=params_refine_text
)
print("文本细化后的文本:", refined_texts)
# 生成语音
wavs = chat.infer(
refined_texts,
params_infer_code=params_infer_code,
skip_refine_text=True,
do_text_normalization=False
)
wav = wavs[0]
# 生成输出文件路径
file_name, file_ext = output_path.rsplit('.', 1)
current_output = f"{file_name}_{index+1}.{file_ext}"
# 保存音频文件
torchaudio.save(current_output, torch.from_numpy(wav).unsqueeze(0), 24000)
return current_output
except Exception as e:
return f"错误: {str(e)}"
def generate_speech_parallel(
texts: Union[str, List[str]],
embedding_path: str = 'seed_11_restored_emb.pt',
output_path: str = 'output.wav',
temperature: float = 0.03,
max_workers: int = None
) -> List[str]:
"""并行处理多个文本生成语音
Args:
texts: 单个文本字符串或文本列表
embedding_path: 音色文件路径
output_path: 输出文件路径
temperature: 控制生成随机性的温度参数
max_workers: 最大进程数,默认使用CPU核心数
Returns:
List[str]: 成功生成的音频文件路径列表
"""
# 确保文本是列表格式
if isinstance(texts, str):
texts = [texts]
# 设置最大进程数
if max_workers is None:
max_workers = cpu_count()
# 准备参数
args = [(text, embedding_path, output_path, temperature, i)
for i, text in enumerate(texts)]
# 使用进程池并行处理
with Pool(max_workers) as pool:
results = pool.map(process_single_text, args)
# 过滤出成功的结果
successful_files = [result for result in results if not result.startswith("错误")]
# 打印错误信息
for result in results:
if result.startswith("错误"):
print(result)
return successful_files
def split_text(text: str, max_length: int = 150) -> List[str]:
"""将长文本智能分割成短文本列表
Args:
text: 输入的长文本
max_length: 每段文本的最大长度
处理流程:
1. 首先按句号、感叹号、问号分割
2. 如果单句超过最大长度,则按逗号、分号、冒号进一步分割
3. 确保每段文本不超过最大长度限制
Returns:
List[str]: 分割后的文本段落列表
"""
# 清理文本
text = re.sub(r'\s+', ' ', text.strip())
# 使用正则表达式按句子结束符分割文本,并保留分隔符
sentences = re.split('([。!?])', text)
segments = []
current_segment = ""
# 遍历分割后的句子列表
for i in range(0, len(sentences) - 1, 2):
sentence = sentences[i] + sentences[i + 1]
# 如果单个句子就超过最大长度,可以考虑按逗号分割
if len(sentence) > max_length:
sub_sentences = re.split('([,;:])', sentence)
for j in range(0, len(sub_sentences) - 1, 2):
sub_sentence = sub_sentences[j] + sub_sentences[j + 1]
segments.append(sub_sentence)
continue
# 判断当前段落加上新句子后的长度是否超过最大限制
if len(current_segment) + len(sentence) <= max_length:
current_segment += sentence
else:
if current_segment:
segments.append(current_segment)
current_segment = sentence
# 处理最后一个段落
if current_segment:
segments.append(current_segment)
return segments
def merge_audio_files(file_paths: List[str], output_path: str) -> str:
"""按顺序合并多个音频文件
Args:
file_paths: 要合并的音频文件路径列表
output_path: 合并后的输出文件路径
处理流程:
1. 按文件名顺序读取所有音频文件
2. 在时间维度上拼接音频
3. 保存合并后的文件
4. 删除临时文件
Returns:
str: 合并后的音频文件路径,失败返回None
"""
try:
# 读取所有音频文件
waves = []
for file_path in sorted(file_paths): # 确保按文件名顺序排序
wave, sr = torchaudio.load(file_path)
waves.append(wave)
# 在时间维度上拼接所有音频
merged_wave = torch.cat(waves, dim=1)
# 保存合并后的音频
final_output = output_path.rsplit('_', 1)[0] + '_merged.wav' # 移除序号,添加merged标记
torchaudio.save(final_output, merged_wave, sr)
# 删除临时文件
for file_path in file_paths:
try:
os.remove(file_path)
except Exception as e:
print(f"警告:无法删除临时文件 {file_path}: {str(e)}")
return final_output
except Exception as e:
print(f"合并音频文件时出错: {str(e)}")
return None
def preprocess_numbers(text):
"""将文本中的阿拉伯数字转换为中文数字
Args:
text: 包含数字的原始文本
功能特点:
1. 支持到亿级别的数字转换
2. 处理特殊情况如"10"转"十"
3. 正确处理零的显示规则
4. 支持万、亿等单位的正确添加
Returns:
str: 数字已转换为中文的文本
"""
def convert_digit_section(digit_str):
"""转换四位以内的数字段"""
number_map = {
'0': '零', '1': '一', '2': '二', '3': '三', '4': '四',
'5': '五', '6': '六', '7': '七', '8': '八', '9': '九'
}
unit_map = ['', '十', '百', '千']
result = ''
digit_str = digit_str.zfill(4) # 补零到4位
has_previous_digit = False # 用于处理零的显示
for i, d in enumerate(digit_str):
if d == '0':
if has_previous_digit: # 只有在前面有非零数字时才显示零
if result[-1] != '零': # 避免重复的零
result += '零'
has_previous_digit = False
else:
result += number_map[d] + unit_map[3-i]
has_previous_digit = True
# 清理结果
result = result.rstrip('零') # 移除末尾的零
# 处理特殊情况
if result.startswith('一十'):
result = result[1:]
return result or '零'
def convert_number(match):
"""转换匹配到的完整数字"""
num = match.group(0)
# 处理小于100的数字的特殊情况
if len(num) <= 2:
if num == '10':
return '十'
elif num.startswith('0'):
return convert_digit_section(num.lstrip('0'))
elif len(num) == 2 and num.startswith('1'):
return '十' + (convert_digit_section(num[1]) if num[1] != '0' else '')
# 对于大数字,按4位分段处理
units = ['', '万', '亿']
segments = []
while num:
seg = num[-4:] if len(num) >= 4 else num
converted = convert_digit_section(seg)
if converted != '零': # 只添加非零的段
segments.append(converted)
num = num[:-4]
# 添加单位并反转
result = ''
for i, seg in enumerate(segments):
if seg != '零': # 只给非零段加单位
result = seg + units[i] + result
return result or '零'
# 使用正则表达式查找数字并替换
import re
return re.sub(r'\d+', convert_number, text)
def generate_long_speech(
text: str,
embedding_path: str = 'seed_11_restored_emb.pt',
output_path: str = 'output.wav',
temperature: float = 0.03,
max_length: int = 150,
max_workers: int = None
) -> str:
"""长文本转语音的主函数
Args:
text: 要转换的长文本
embedding_path: 音色文件路径
output_path: 输出文件路径
temperature: 生成温度参数
max_length: 文本分段最大长度
max_workers: 最大并行进程数
处理流程:
1. 验证输入参数
2. 预处理文本中的数字
3. 将长文本分割成短文本
4. 并行生成语音
5. 合并生成的音频文件
Returns:
str: 最终合并后的音频文件路径
"""
# 参数验证
if not text or not text.strip():
raise ValueError("文本不能为空")
if not os.path.exists(embedding_path):
raise FileNotFoundError(f"找不到音色文件: {embedding_path}")
# 预处理文本中的数字
text = preprocess_numbers(text)
print("数字转换后的文本:", text)
# 分割文本
segments = split_text(text, max_length)
print(f"文本已分割为 {len(segments)} 段")
# 并行生成语音
files = generate_speech_parallel(
segments,
embedding_path=embedding_path,
output_path=output_path,
temperature=temperature,
max_workers=max_workers
)
if not files:
print("没有成功生成的音频文件")
return None
# 合并音频文件
print("正在合并音频文件...")
merged_file = merge_audio_files(files, output_path)
if merged_file:
print(f"音频文件已合并: {merged_file}")
else:
print("音频合并失败")
return merged_file
# 使用示例
if __name__ == "__main__":
# 测试长文本
long_text = """
贝亚特丽丝·维特波临终前苦楚万分,感伤和恐惧都不能使痛苦缓解片刻,
终于在2月份一个炎热的早晨去世,那天我发现宪法广场高耸的广告铁架换了一个不知什么牌子的香烟广告;
那件事让我伤心,因为我明白不停顿的广大的世界已经同她远离,广告牌的变化是一系列无穷无尽的变化中的第一个。
世界会变,但是我始终如一,我带着悲哀的自负想道;我知道我对她不合情理的爱慕有时使她难以容忍;
如今她死了,我可以专心致志地怀念她,不抱希望,但也没有屈辱感。
"""
print("开始处理长文本...")
final_file = generate_long_speech(
long_text,
embedding_path='seed_11_restored_emb.pt',
output_path='long_text_output.wav',
temperature=0.03,
max_length=150
)
print(f"最终生成的文件: {final_file}")
chattts生成长文本语音的方法。并行。使用最新api。解决数字读不出。
于 2024-11-04 10:56:09 首次发布