封面 《9-nine - 春色春恋春熙风》
省流版本
感觉 hugging face 的 cpu 跑的效果和我本地跑的效果完全不一样
demo: demo
测试文本: 誕生日おめでとうございます
效果语音:
测试视频:
前言
临近生日想整个活,就做一个喜欢角色的 TTS 模型吧
调研
上次搞 TTS 还是去年(2022)的时候想用 vits 来做一个爱莉希雅的 TTS 模型,根据网上的拆包工具拆出来崩坏 3 的语音包,但是没找出文本,不想自己标注和用语音识别标注就搁置了,如今过了一年多技术上应该有进步所以来看看新的方法,而且相对于崩坏 3,galgame 更加好拆且相当于一个自带标注的数据集,用来训练再合适不过了。
最简单的是去看 tts 的综述 ,但是这篇是 21 年的,不含这几年的进展,后面在知乎上找到一个实时更新文章 。
所以先试试 HierSpeech++,首先是最新,其次是 zero-shot 模型,这样的话就不需要怎么训练微调。其项目和演示如下地址
demo
尝试输入音频作为 prompt, 文本 prompt 用 “失敗した”
输入音频 prompt
调了调输出音频结果如下,效果有点难崩,可见在分词和预测时长上还是有点问题需要微调,不如换个方法。
输出结果
推特吐槽了一下,被一作找上门了,原因是没有日语训练,底下用 phoneme 库转音标所以可以发送。和作者说了一下发了份 MoeGoe 的 cleaners 代码
相关链接 推特
拆包
9nine 的拆包可以直接用现成的 GARbro 进行拆包,我觉得比 escu:de 那些好拆多了,那种拆包封包找不到啥现成程序,还得自己写,有空的话可以写一下怎么拆 escu:de 的游戏。春春春拆包如下,voice.xp3
里面是语音包,data.xp3
里面包含了文本包,在 scn
里面。
关于 scn 我搜怎么解包的时候刚好看到有人也拆包春春春做 tts,那么直接参考他的方案用 freemote
观察 freemote
解码出来的信息,可以看到里面有主要有两类 json,我们要的主要在.json
里面,经过观察文本和对音的语音文件主要在 texts
和 voices
里面
提取
我只想要四位女主其他人是什么 的声音文件,因此直接在别人的提取脚本上小改一下,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 from tqdm import tqdmimport osimport jsoncharacter = ["都" , "天" , "春風" , "希亜" ] def load_json_files (base_path ): file_lists = [] files = os.listdir(base_path) for file in tqdm(files): if file.endswith(".txt.json" ): file_lists.append(file) return file_lists def load_voice_and_text_from_json (json_file: str , collapse_LF: bool = True ): train_list = [] with open (json_file, encoding="utf-8" ) as f: json_text = json.load(f) scenes = json_text["scenes" ] for scene in scenes: if "texts" in scene.keys(): texts = scene["texts" ] for text_list in texts: if isinstance (text_list, list ): if text_list[0 ] in character: if text_list[3 ] is not None : txt = text_list[2 ][1 :-1 ] voice = text_list[3 ][0 ]["voice" ] if collapse_LF: txt = txt.replace("\\n" , "" ) train_list.append(f"{text_list[0 ] } |{txt} |{voice} .ogg\n" ) return train_list if __name__ == "__main__" : base_path = "./data/scn" file_lists = load_json_files(base_path) voices = [] for file in tqdm(file_lists): voice = load_voice_and_text_from_json(os.path.join(base_path, file)) voices.extend(voice) with open ("./train.txt" , "w" , encoding="utf-8" ) as f: f.writelines(voices)
其实这样提取还是有一点点小问题,比如语音文本有一下一些问题,比如单纯的?,还有一些特殊符号,这些主要是游戏中一些特殊文本显示。对于前者采用 tacotron2-japanese 进行清理,不用 bert-vits2 的 cleaner 是因为有插入停止词不好处理,后者直接训练集里面去掉
1 2 春風|?|hk0154.ogg 天|あぁ……うん、っぽい、けど、%D$vl1;え? ラブレター?|sr0679_ep2.ogg
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 + import torch + from text import text_to_sequence - def load_voice_and_text_from_json(json_file: str, collapse_LF: bool = True): train_list = [] with open(json_file, encoding="utf-8") as f: json_text = json.load(f) scenes = json_text["scenes"] for scene in scenes: if "texts" in scene.keys(): texts = scene["texts"] for text_list in texts: # 先确保是list if isinstance(text_list, list): if text_list[0] in character: - if text_list[3] is not None: - txt = text_list[2][1:-1] - voice = text_list[3][0]["voice"] + txt:str = text_list[2][1:-1] + if txt.find('%') != -1: + continue + if ( + len( + torch.IntTensor( + text_to_sequence(txt, ["japanese_cleaners"]) + ) + ) + > 1 + ): + if text_list[3] is not None: + voice = text_list[3][0]["voice"] if collapse_LF: txt = txt.replace("\\n", "") train_list.append( f"{text_list[0] }|{txt}|{voice}.ogg\n" ) return train_list
二次清理数据
训练过程中发现存在一些单纯的…
还有 hs 时候的呻吟 ,这些对训练结果影响较大,因此需要去除一下
简单 eda
首先看一下数据分布
希亜: 370 条
天: 576 条
春風: 2031 条(不愧是学姐主场)
都: 190 条(不愧是女路人)
语音总时长: 3.92h
训练
我这里采用的是 Bert-Vits2
模型,这个项目将 bert 和 vits 结合起来,主题来说就是使用 Bert 预先对文本进行处理,后面再送入,此外他们还加了个 emotion 情绪编码器,可以使得语音带上情绪。Bert-VITS2
项目的文档较少,主要都需要自己来读代码进行理解。另外该项目使用.wav
文件作为输入,所以我们得事先转换一下
1 2 3 4 5 def ogg2wav (ogg,out_base ): filename,ext = os.path.splitext(ogg) filename = os.path.basename(filename) y,sr = librosa.load(ogg) sf.write(os.path.join(out_base,filename+".wav" ),y,sr)
另外我们需要准备一份 file.list
文件,这个文件是用来指定训练集和验证集的,格式如下
1 2 3 4 # wave_path 要是绝对路径 {wav_path}|{speaker_name}|{language}|{text} 例如 hk1447.wav|春風|JP|その……く、九條さんが、作ったことあるって、 聞いて……
修改 config.yml
,默认读取 config.yml,所以不用改代码,所以我们得做一下简单修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 dataset_path: "Data/" mirror: "" openi_token: "" resample: sampling_rate: 44100 in_dir: "audios/raw" out_dir: "audios/wavs" preprocess_text: transcription_path: "filelists/haruka.list" cleaned_path: "" train_path: "filelists/train.list" val_path: "filelists/val.list" config_path: "haruka.json" val_per_spk: 40 max_val_total: 200 clean: true bert_gen: config_path: "haruka.json" num_processes: 2 device: "cuda" use_multi_device: false emo_gen: config_path: "haruka.json" num_processes: 2 device: "cuda" train_ms: env: MASTER_ADDR: "localhost" MASTER_PORT: 10086 WORLD_SIZE: 1 LOCAL_RANK: 0 RANK: 0 base: use_base_model: false repo_id: "Stardust_minus/Bert-VITS2" model_image: "Bert-VITS2_2.1-Emo底模" model: "models" config_path: "haruka.json" num_workers: 16 spec_cache: True keep_ckpts: 8 webui: device: "cuda" model: "models/G_18000.pth" config_path: "configs/haruka.json" port: 7860 share: false debug: false language_identification_library: "langid" server: port: 5000 device: "cuda" models: - model: "" config: "" device: "cuda" language: "ZH" speakers: - speaker: "科比" sdp_ratio: 0.2 noise_scale: 0.6 noise_scale_w: 0.8 length_scale: 1 - speaker: "五条悟" sdp_ratio: 0.3 noise_scale: 0.7 noise_scale_w: 0.8 length_scale: 0.5 - speaker: "安倍晋三" sdp_ratio: 0.2 noise_scale: 0.6 noise_scale_w: 0.8 length_scale: 1.2 - model: "" config: "" device: "cpu" language: "JP" speakers: [] translate: "app_key": "" "secret_key": ""
文本预处理
需要注意一下我们要先去 hugging face 上把缺少的权重先下载下来
1 python preprocess_text.py
这里主要执行 text2sequence,且检查能否找到音频和是否有重复音频
重采样
Bert 预处理
Emo 预处理
这里主要获取情感信息
训练
需要注意我们修改一下 config.json
里面的内容
效果
下面为第 23000iter 的效果,天的声音依然有一些电音
后续继续训练一下
输入文本 “誕生日おめでとうございます”,输出
放一个 hugging face 的 demo 链接,可以去体验一下
demo
最终效果
后记
vits 系列模型都需要大规模数据,虽然我已经加入了经过原神训练的预训练模型,但是还是可以看到天和喵都的数据量不太多容易电音,春风学姐的就好一点
虽然我喜欢春风学姐,但我其实是喵都厨
参考文献
A Survey on Neural Speech Synthesis
举世无双语音合成系统 VITS 发展历程(2023.11.22 HierSpeech++)
HierSpeech++
HierSpeech++ demo
基于 tacotron2 的 galgame 角色语音合成 + galgame 解包 保姆级教程(千恋万花版)
freemote
tacotron2-japanese
Bert-VITS2