MeloTTS中文发音人训练

📅 2026/7/24 20:51:50
MeloTTS中文发音人训练
​MeloTTS https://github.com/myshell-ai/MeloTTSMeloTTS是一个可以把文字转换成声音的工具它支持英语、西班牙语、法语、中文、日语和韩语等多种语言。它可以让你听到不同的语言和口音比如美式英语、英式英语、印度英语、澳大利亚英语等。它还可以调节语速让声音快速或慢速地说出来。它的声音非常自然和流畅就像真人在说话一样。接下来我们开始训练自己的音频。1: 下载准备环境要求有GPU环境。git clone https://github.com/myshell-ai/MeloTTS.git cd MeloTTS pip install -e . python -m unidic download2: 原始发音人数据准备我们用xmj2002/genshin_ch_10npc首先下载声音模型git clone https://hf-mirror.com/datasets/xmj2002/genshin_ch_10npc将声音文件及中文文本提取出来import numpy as np from scipy.io import wavfile from datasets import load_dataset import os #https://blog.lukeewin.top/archives/melotts-zh-model-train os.environ[HF_ENDPOINT] https://hf-mirror.com # 指定目录路径 data_dir g:/xxxx/xunlian/genshin_ch_10npc/train # 获取所有 .parquet 文件路径 parquet_files [os.path.join(data_dir, f) for f in os.listdir(data_dir) if f.endswith(.parquet)] mls load_dataset(parquet,splittrain, data_filesparquet_files) ddir xunlian/spk_pai mpInpcName { 派蒙: 0, } for sample in mls: npcName sample[npcName] # 假设数据集中有 npcName 字段 save_dir os.path.join(ddir, npcName) os.makedirs(save_dir, exist_okTrue) print(f文件夹已创建{save_dir}) mpInpcName[npcName] 0 for sample in mls: npcName sample[npcName] # 假设数据集中有 npcName 字段 lg sample[language] if len(sample[text]) 0:#if sample[language] CHS and len(sample[text]) 0: print(sample) # 获取音频数据 audio_array sample[audio][array] # 将音频数据缩放到 -32768 到 32767 之间 audio_array np.int16(audio_array / np.max(np.abs(audio_array)) * 32767) _dir os.path.join(ddir, npcName) # 保存音频数据 audio_path os.path.join(_dir, f{mpInpcName[npcName]}_{lg}.wav) wavfile.write(audio_path, sample[audio][sampling_rate], audio_array) # 保存文本内容 text_path os.path.join(_dir, f{mpInpcName[npcName]}_{lg}.txt) with open(text_path, w, encodingutf-8) as f: f.write(sample[text]) mpInpcName[npcName] 13:原始音频转换成音频转码 44.1k 1 pcm_s16le我们使用ffmpeg 命令行转换ffmpeg 下载只需要到github下载最新版本就行 https://ffmpeg.org/download.htmlffmpeg -i ./zjdx/zjdx.wav -ar 44100 -ac 1 -acodec pcm_s16le ./output/zjdx.wavimport os import subprocess import shutil import re def process_convert_audio(input_path: str, output_path: str): 音频转码 44.1k 1 pcm_s16le param input_path: 要转码的音频所在路径 param output_path: 转码后的音频保存路径 return: os.makedirs(output_path, exist_okTrue) if os.path.exists(input_path) and os.path.isdir(input_path): for root, dirs, files in os.walk(input_path): for audio in files: output_file_name os.path.basename(audio) input_file os.path.join(root, audio) output_file os.path.join(output_path, output_file_name) #判断是否是文本文档如果是文本文档直接处理特殊字符并且复制到输出目录text re.sub(r.*?|「.*?」|\{.*?\}|#|\$UNRELEASED, , txt) if audio.endswith(.txt): try: # 读取文本内容 with open(input_file, r, encodingutf-8) as f: txt f.read() # 移除特殊字符 processed_text re.sub(r.*?|「.*?」|\{.*?\}|#|\$UNRELEASED, , txt) # 保存处理后的文本到输出目录 with open(output_file, w, encodingutf-8) as f: f.write(processed_text) print(f文本文件已处理: {output_file}) except Exception as e: print(f处理文本文件失败: {input_file}, 错误: {e}) else: try: command [ ffmpeg, -y, # 添加 -y 参数强制覆盖输出文件 -i, input_file, -ar, 44100, # 设置采样率为 44.1kHz -ac, 1, # 设置单声道 -acodec, pcm_s16le, # 设置音频编码为 PCM 16-bit little-endian output_file ] subprocess.run(command, checkTrue) print(f转换成功: {output_file}) except subprocess.CalledProcessError as e: print(f转换失败: {input_file}, 错误: {e}) if __name__ __main__: odir xunlian/spk_pai_44.1k ddir xunlian/spk_pai # 查找ddir里面所有的文件夹名字命名为npcName npcNames [f.name for f in os.scandir(ddir) if f.is_dir()] # 循环调用process_convert_audio for npcName in npcNames: process_convert_audio(os.path.join(ddir, npcName), os.path.join(odir, npcName))4音频降噪此步可省略可用resemble-enhance降噪pip install resemble-enhance --upgrade5:生成metadata文件from pathlib import Path import re import logging from concurrent.futures import ThreadPoolExecutor, as_completed logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(metadata_gen.log, encodingutf-8), logging.StreamHandler() ] ) def gen_metadata(audio_path: str, label_path: str, metadata_file: str): 生成 metadata.list 文件 param audio_path: 音频所在路径 param label_path: 标签所在的路径 param metadata_file: 保存生成后的 metadata.list 文件 return: logging.info(f开始生成metadata文件) logging.info(f音频路径: {audio_path}) logging.info(f标签路径: {label_path}) logging.info(f输出文件: {metadata_file}) label_dict {} # 遍历标签文件 label_path_obj Path(label_path) if label_path_obj.exists() and label_path_obj.is_dir(): label_files list(label_path_obj.glob(*.txt)) logging.info(f发现 {len(label_files)} 个标签文件) total len(label_files) skip_count 0 def read_label(label_file): 读取单个标签文件返回 (stem, text) 或 None try: if label_file.stat().st_size 1024 * 1024: return (skip, label_file.name) with label_file.open(r, encodingutf-8) as f: txt f.read(4096).strip().replace(\n, ).replace(\r, ) return (label_file.stem, txt) except Exception as e: return (error, f{label_file.name}: {e}) # 多线程并发读取 with ThreadPoolExecutor(max_workers16) as executor: futures {executor.submit(read_label, lf): lf for lf in label_files} done_count 0 for future in as_completed(futures): result future.result() done_count 1 if result[0] skip: skip_count 1 elif result[0] error: logging.error(f读取标签文件失败 {result[1]}) else: label_dict[result[0]] result[1] if done_count % 500 0 or done_count total: logging.info(f加载标签进度: {done_count}/{total}) else: logging.warning(f标签路径不存在或不是目录: {label_path}) logging.info(f成功加载 {len(label_dict)} 个标签, 跳过大文件 {skip_count} 个) if label_dict: sample_labels list(label_dict.keys())[:5] logging.info(f标签文件样例: {sample_labels}) # 删除metadata_file metadata_path Path(metadata_file) if metadata_path.exists(): metadata_path.unlink() logging.info(f已删除旧的metadata文件: {metadata_file}) # 遍历音频文件 audio_path_obj Path(audio_path) if audio_path_obj.exists() and audio_path_obj.is_dir(): audio_files list(audio_path_obj.glob(*.wav)) logging.info(f发现 {len(audio_files)} 个音频文件) if audio_files: sample_audios [f.stem for f in audio_files[:5]] logging.info(f音频文件样例: {sample_audios}) success_count 0 fail_count 0 no_label_count 0 empty_label_count 0 no_label_samples [] empty_label_samples [] total_audio len(audio_files) lines [] for idx, audio_file in enumerate(audio_files, 1): audio_name audio_file.stem if audio_name in label_dict: label_txt label_dict[audio_name] if len(label_txt) ! 0: lines.append(f{audio_file}|ZH-yunyang|ZH_MIX_EN|{label_txt}\n) success_count 1 else: if len(empty_label_samples) 5: empty_label_samples.append(audio_file.name) empty_label_count 1 else: if len(no_label_samples) 5: no_label_samples.append(audio_file.name) no_label_count 1 if idx % 500 0 or idx total_audio: logging.info(f处理音频进度: {idx}/{total_audio}) if no_label_samples: logging.warning(f未找到对应标签({no_label_count}个)样例: {no_label_samples}) if empty_label_samples: logging.warning(f标签内容为空({empty_label_count}个)样例: {empty_label_samples}) # 一次性写入所有记录 try: with open(metadata_file, w, encodingutf-8) as f: f.writelines(lines) logging.info(f已写入 {len(lines)} 条记录到 {metadata_file}) except Exception as e: logging.error(f写入metadata文件失败: {str(e)}) logging.info(f生成完成 - 成功: {success_count}, 无标签: {no_label_count}, 空标签: {empty_label_count}) else: logging.error(f音频路径不存在或不是目录: {audio_path}) if __name__ __main__: odir xunlian/yunyang logging.info(f扫描目录: {odir}) if Path(odir).exists() and Path(odir).is_dir(): gen_metadata(odir, odir, Path(odir) / metadata.list) else: logging.error(f目录不存在: {odir})这里面代码|ZH-yunyang|ZH| 中ZH-zhida代表你的发音人名字可自定义。ZH是原始音频文字如果是中英文混合可以改成ZH_MIX_EN6: 修改中文底膜中文特化模型下载https://openi.pcl.ac.cn/Stardust_minus/Bert-VITS2/modelmanage/show_model修改melo/download_utils.py对应的值PRETRAINED_MODELS { G.pth: ./model/basespeakers/pretrained/G.pth, D.pth: ./model/basespeakers/pretrained/D.pth, DUR.pth: ./model/basespeakers/pretrained/DUR.pth, }对应的你的下载G_0,D_0,DUR_0.可以用中英日底膜https://openi.pcl.ac.cn/Stardust_minus/Bert-VITS2/modelmanage/model_readme_tmpl?nameBert-VITS2%E4%B8%AD%E6%97%A5%E8%8B%B1%E5%BA%95%E6%A8%A1-fix7:训练生成config.json使用如下命令PYTHONPATH$PWD python /data-1T/apps/melotts/melo/preprocess_text.py --config_path /data-1T/apps/melotts/melo/configs/config.json --metadata /data-1T/apps/melotts/xunlian/output/metadata.list把/data-1T/apps/melotts/xunlian/output/metadata.list改为自己的metadata.list文件路径。运行上面的命令之后会在metadata.list同级目录中生成几个文件其中一个是config.json文件我们需要打开这个文件修改一些训练参数。train: { log_interval: 200, eval_interval: 1000,// 多少轮保存一个 seed: 52, epochs: 10000, // 总共训练轮次 batch_size: 6, ...... }根据自己的数据集大小和显卡情况修改。这里我训练的数据集为10066条数据显卡为单张RTX4090D显卡所以我这里设置epochs500batch_size16。8创建目录zh-zjdx将 config.json 放入该目录下这样做的目的是在训练启动的时候会自动在源码中的melo/logs下面自动创建一个这样的目录用于存放训练过程中的数据。9:开始训练。PYTHONPATH$PWD bash ./melo/train.sh /data-1T/apps/melotts/zh-zjdx/config.json 1上面这条命令中需要输入两个参数一个是配置文件一个是GPU数量我这里只有一张显卡所以最后一个参数是1。10:推理使用下面命令进行推理。PYTHONPATH$PWD python ./melo/infer.py --text 这个是如何改音标的如果要训练方言的模型 -m ./logs/zh-zjdx/G_1000.pth -o ./out -l ZH注意上面命令中并没有显示指定配置文件其实代码内部会根据G_n.pth所在的目录中查找配置文件。如果想要在网页端使用那么需要修改app.py代码。models { EN: TTS(languageEN, devicedevice), ZH: TTS(languageZH, devicedevice), ZH-zjdx: TTS(languageZH, devicedevice,config_path./logs/zh-zjdx/config.json,ckpt_path./logs/zh-zjdx/G_39000.pth), } speaker_ids models[EN].hps.data.spk2id default_text_dict { ZH-zjdx: text-to-speech 领域近年来发展迅速, EN: The field of text-to-speech has seen rapid development recently., ZH: text-to-speech 领域近年来发展迅速, } ..... language gr.Radio([EN,ZH,ZH-zjdx], labelLanguage, valueEN) ......然后启动这个app.py就可以在网页端进行语音合成了。补充一下cpu推理内存只涨不跌问题1当用cpu推理的时候发现内存只涨不跌最后发现是python 垃圾回收问题导致的(windows没问题Linux出现的问题最终给出判断)再Linux使用jemalloc可以解决问题# Ubuntu/Debian sudo apt-get install libjemalloc-dev # CentOS/RHEL sudo yum install jemalloc-devel # macOS (使用 Homebrew) brew install jemalloc然后使用如下命令启动PYTHONPATH$PWD LD_PRELOAD/usr/lib/x86_64-linux-gnu/libjemalloc.so python main.py最后注意事项如果你训练出来的音频是胡乱说话。根本听不出来说的什么那么可能有两个问题导致的1底膜没对请参考6选项设置底膜2可能训练轮次不够或者原始音频量不够。