Code 【VITS】训练中文galgame语音模型 - Step 1 部署 开发 4次访问 09-02 17:36 最近的智能灯带DIY项目马上要结束了,想着嵌入式后续可以做一个早上叫醒服务,如果有一个萌妹声线准时准,并且每天不重样的喊你起床,顺带播报播报当日天气、拥堵情况、穿衣建议啥的岂不是美滋滋~~ 脑海中随即想到了几年前B站风靡一时的宁宁AI,初步调研后,发现当时该Up主使用的是最早的google模型`tacotron`,但是音质杂音干扰较多,后续切换到韩国人团队的开发的`Vits`端到端语音模型,效果优异并且后续衍生出很多相关版本,发现有让二次元角色唱歌、快速拷贝声线等,但是`Vits`仍是现在好用、质量高的模型。 ...... [========] ## 提取语料数据 模型训练离不开的是基础的语料数据,我把目标选到了我Steam上唯一的中文Galgame 三色绘恋中。虽然没玩过你,但是你滴作用还是大大滴! 据观察,该游戏女主之一 墨小菊 是语音数量较多的角色。大约6000+条高质量语音,这不提取了狠狠炼丹! ### 提取台本、音频 我们可以通过软件`GARbro`随意解包 基于`KiriKiri2`开发的游戏文件。该软件一方面是资源浏览器,一方面可以导出文件。  如图所示,根据经验和命名,我们可以在data.xp3中找到台本、voice.xp3等三个文件中找到角色的语音。   ### 格式转化 我们提取的台本格式.ks、语音 .ogg 都可以通过python进行格式转化。 .ks 可以直接以utf-16格式打开,存储为utf8即可。 .ogg 要么通过格式工厂,要么可以通过python脚本简单实现转化,最终转化为.wav格式。 这里贴一个转化的脚本(不过贴不贴无所谓,问一下通用文本的AI就有了) ```python import os from pydub import AudioSegment def convert_audio_files(input_dir, output_dir, target_sample_rate=22050): """ 批量转换音频文件格式为WAV,并设置目标采样率 参数: input_dir: 输入音频文件所在目录 output_dir: 输出转换后文件的目录 target_sample_rate: 目标采样率,默认22050 """ # 创建输出目录(如果不存在) os.makedirs(output_dir, exist_ok=True) # 获取输入目录下的所有文件 for filename in os.listdir(input_dir): input_path = os.path.join(input_dir, filename) # 跳过目录,只处理文件 if os.path.isdir(input_path): continue try: # 提取文件名(不含扩展名) file_name_without_ext = os.path.splitext(filename)[0] # 输出文件路径(统一为WAV格式) output_path = os.path.join(output_dir, f"{file_name_without_ext}.wav") # 读取音频文件(pydub会自动识别格式) audio = AudioSegment.from_file(input_path) # 设置目标采样率 audio = audio.set_frame_rate(target_sample_rate) # 导出为WAV格式 audio.export(output_path, format="wav") print(f"转换成功: {filename} -> {os.path.basename(output_path)}") except Exception as e: print(f"转换失败: {filename},错误信息: {str(e)}") if __name__ == "__main__": # 输入目录:input/audio input_directory = os.path.join("input", "audio") # 输出目录:out/audio output_directory = os.path.join("out", "audio") # 检查输入目录是否存在 if not os.path.exists(input_directory): print(f"错误:输入目录不存在 - {input_directory}") print("请确保已创建input/audio目录并放入音频文件") else: print(f"开始转换 {input_directory} 目录下的音频文件...") convert_audio_files(input_directory, output_directory) print("转换完成!结果保存在 out/audio 目录下") ``` > 要注意的是,语音转化是需要将采样率控制在22050kHz,频道默认单声道。  ### 台本关联语音 我们目前已经得到了txt文本和wav语音文件,但是台本目前包含多个人物对话、场景描述、甚至是控制代码。并且两者还没有对应上关系。 **然而**,台本并不好提取,如你所见,台本是txt的没有固定结构,仅只能通过正则表达式套取解析,这里贴一个提取墨小菊的正则规则 ```python import re mxj_pattern = re.compile( r'(?:\[墨小菊\s+voice=(\d+)\]\s*\[墨小菊[^\]]*\]\s*)?' # 可选 voice 行 r'[^;](【[^】]*?墨小菊[^】]*?】)' # 捕获完整角色方括号 r'『(.*?)』', # 捕获台词 re.DOTALL ) ``` **即使**,可以提取了所有台本,但是如何才能和语音对上呢?我脑海里产生了几个思路————1.结合CE Hook游戏的台本函数,手动扫完文本和对应的语音;2.观察规则,逻辑匹配。这里我偷懒选择了2,但是实际上还是很坑。 先说说规则:按照序章-终章章节顺序,顺序排列人物语音,如mxj_60532.ogg 表示第六章墨小菊的第532句话。 可是实际上制作组的台本并不准确,会出现有语音无台本、有台本无语音、台本和语音顺序错误、文件命名错误等各种问题。没辙,只能遇到问题时,一个一个匹配再手动调整编号。 **总之**,耗费了90%的精力总算是**才**把墨小菊的语音匹配完成了。效果如下: ```json [ { "path": "00a.ks", "character": "【墨小菊/??】", "index": "00001", "text": "邱、邱诚……", }, { "path": "00a.ks", "character": "【墨小菊/??】", "index": "00002", "text": "我们……一起回去吧……", }, { "path": "00a.ks", "character": "【墨小菊/??】", "index": "00003", "text": "再这样站下去……会感冒的……", } ... ] ``` ### 提取filelist.txt filelist.txt是`VITS`要求的训练语料文件,该文件要求记录语音位置、文本、说话人、语言等。对于单说话人,提供语音路径和文本即可。 我们现在有了json文件和对应关系可以很简单的生成他,同时,为了提升语料质量,我们可以对台本台词做以下处理: - 文本分词 - 统一省略号、破折号长度 - 统一全角半角字符 - 阿拉伯字母转中文大写 - 删除无语音意义符号(比如书名号) - 跳跃无台词文本(galgame很常见的`......`,有些虽然有嗯嗯啊啊的语气,但是太多了肯定影响效果) 这里仍然贴一个AI给我的脚本: ```pyton import jieba import re import json # 初始化jieba jieba.initialize() # 可选,显式初始化 # 添加自定义词汇 custom_words = ['墨小菊', '青岚', '星辉', '邱成', '纳克萨玛斯', '40人团', 'VITS'] for word in custom_words: jieba.add_word(word) def convert_numbers_to_chinese(text): """将文本中的数字转换为中文读法""" # 简单实现:处理基本数字 num_map = { '0': '零', '1': '一', '2': '二', '3': '三', '4': '四', '5': '五', '6': '六', '7': '七', '8': '八', '9': '九' } def num_to_chinese(match): num_str = match.group() # 如果是纯数字 if num_str.isdigit(): # 对于短数字直接转换 if len(num_str) <= 4: return ''.join([num_map[char] for char in num_str]) # 对于长数字可以添加更多规则(如万、亿单位) else: return num_str # 暂时返回原数字,避免复杂处理 return num_str # 匹配数字(整数和小数) text = re.sub(r'\d+\.?\d*', num_to_chinese, text) return text def preprocess_text_for_vits(text): """VITS文本预处理函数""" # 1. 移除角色标记 text = re.sub(r'【[^】]*】', '', text) # 2. 标准化省略号(所有长省略号统一为 ...) text = re.sub(r'[……]{2,}', '...', text) # 3. 处理破折号(根据上下文决定保留或转换) text = re.sub(r'——+', '--', text) # 转换为双短横线 # 4. 保留波浪号(表示特殊语调) text = text.replace('~', '~') # 5. 标准化其他标点 text = re.sub(r'[,;:]', ',', text) text = re.sub(r'[。]', '.', text) text = re.sub(r'[!]', '!', text) text = re.sub(r'[?]', '?', text) text = re.sub(r'[「」『』""()()]', '', text) # 6. 处理数字和英文(您的数据中有"40人团"、"纳克萨玛斯"等) text = convert_numbers_to_chinese(text) # 分词 seg_list = jieba.cut(text, cut_all=False) # 过滤和连接 words = [] for word in seg_list: if re.match(r'^[^\w\s]+$', word) and word not in [',', '.', '...']: continue if word.strip(): words.append(word) return ' '.join(words) def process_json_to_filelist(json_file_path, output_file_path): """ 从JSON文件读取数据并生成VITS训练文件列表 参数: json_file_path: JSON文件路径 output_file_path: 输出文件路径 """ # 读取JSON文件 with open(json_file_path, 'r', encoding='utf-8') as f: data = json.load(f) # 处理每条数据 vits_lines = [] for item in data: # 处理文本 processed_text = preprocess_text_for_vits(item['text']) # 如果处理后的文本不为空,添加到输出列表 if processed_text.strip(): audio_path = f"out/audio/mxj_{item['index']}.wav" vits_lines.append(f"{audio_path}|{processed_text}") # 写入输出文件 with open(output_file_path, 'w', encoding='utf-8') as f: f.write('\n'.join(vits_lines)) print(f"处理完成!共生成 {len(vits_lines)} 条数据") print(f"输出文件: {output_file_path}") # 使用示例 if __name__ == "__main__": # 替换为您的JSON文件路径 json_file_path = "out/processed_001/001.json" # 替换为您想要的输出文件路径 output_file_path = "filelist.txt" # 处理数据 process_json_to_filelist(json_file_path, output_file_path) ``` 最终处理完毕的filelist.txt格式应该如下: ```txt out/audio/mxj_00001.wav|邱 邱诚 ... out/audio/mxj_00002.wav|我们 ... 一起 回去 吧 ... out/audio/mxj_00003.wav|再 这样 站 下去 ... 会 感冒 的 ... ``` ## 总结 总而言之,我们本节提取了必要的语料资源,并根据Vits官方示例的标准进行了格式化,最终得到了filelist.txt文件。虽然路途艰险,但是后面就是简单的调用和花时间花设备训练了。 < 【父与子】浪漫与科学的时代 【Docker Desktop】拉库不配镜像源 > 让浏览器记住我!