两个月的暑假已经结束了,假期里自学了一点深度学习的内容,很多地方还是一知半解的,这里稍微记录一下。之前刷到一个很有意思的语音合成视频,抱着试试看的心态想自己做一个模型,于是给自己挖了一个大坑……涉及到深度学习的知识我还需要慢慢学,因此本篇博客重点还是记录下自己的踩坑操作原理部分以后搞明白了再更新
总的来说,我通过拆包游戏客户端获得5.6万条语音文件,通过github上的一个声纹识别项目分离其中一个角色的语音文件。接着用百度的语音识别API将语音识别为文本后,人工校正一遍文本,然后转换为拼音+音标,以此制作语音数据训练集和测试集。基于开源项目Tacotron2训练角色语音模型,经历400 epoch后初步训练成型,最后基于HiFiGAN合成语音。整个后半段流程是在google colab上完成的,为了完成模型训练我申请了4个谷歌账号…不得不说白嫖的GPU真香~
可以说整个项目大部分时间花费在整理数据集上,根据我自己的经验,数据集的语音长度在2秒-10秒之间效果最好,数量大约在2000条左右(为了涵盖尽可能多的汉字发音)。需要注意一点,不管拆包的原语音采样率如何,都要统一重采样到22050 hz ,这是Tacotron2训练模型的要求。
首先是这款国内游戏的拆包,所有角色的语音文件都在目录D:\Genshin Impact\Genshin Impact Game\YuanShen_Data\StreamingAssets\Audio\GeneratedSoundBanks\Windows\Chinese下,我们使用软件Extractor2.5进行音频文件拆包。
Extractor2.5是个非常好用的游戏解包工具,我们将所有pck源文件所在目录输进去(可以批量选中文件),确定输出目录,点击开始即可。
运行结束之后可以看到这个游戏拆包有56958条语音文件…点击左下角反选,全部解压到自己的文件夹中。
但是你会发现解压出来的wav文件无法打开,需要使用vgmstream进行解密和转码(项目地址戳这里) 。
可以看到vgmstream-win文件夹只有一个可执行程序test.exe,其他都是dll库文件。
这个test.exe是不能直接运行的,需要把程序拖到刚才拆包的语音文件上,但是几万条语音我们不可能一个个拖过去,因此我们在语音的文件夹下, 写一个如下的批处理文件(命名为批处理.bat),运行批处理就可以了。
1 2 3 4 5 @echo off for /r %%i in (*.wav) do ( "D:\zhuomian\vgmstream-win\test.exe" "%%~nxi" ) pause
运行后生成的wav.wav文件就可以正常播放了,所有音频采样率均为48000Hz(采样率很重要,贯穿整个项目)。
1.2 基于Tensorflow的声纹识别 这部分内容来源于github(项目地址戳这里 ),作者基于tensorflow做了个声纹识别模型,通过把语音数据转换短时傅里叶变换的幅度谱,使用librosa计算音频的特征,以此来训练、评估模型。因为我只用到了对比部分,因此我下载了作者预训练的模型,以及对声纹对比文件infer_contrast.py 做了修改。
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 import argparseimport functoolsimport numpy as npimport tensorflow as tffrom utils.reader import load_audiofrom utils.utility import add_arguments, print_argumentsimport os,shutilimport gcos.environ['TF_CPP_MIN_LOG_LEVEL' ]='2' parser = argparse.ArgumentParser(description=__doc__) add_arg = functools.partial(add_arguments, argparser=parser) add_arg('audio_path1' , str , 'audio_db/Paimon.wav' , '标准的派蒙音频' ) add_arg('audio_path2' , str , 'audio_db/Klee.wav' , '标准的可莉音频' ) add_arg('audio_path3' , str , 'audio_db/Kokomi.wav' , '标准的心海音频' ) add_arg('input_shape' , str , '(257, 257, 1)' , '数据输入的形状' ) add_arg('threshold' , float , 0.8 , '判断是否为同一个人的阈值' ) add_arg('model_path' , str , 'models1/infer_model.h5' , '预测模型的路径' ) args = parser.parse_args() model = tf.keras.models.load_model(args.model_path,compile =False ) model = tf.keras.models.Model(inputs=model.input , outputs=model.get_layer('batch_normalization' ).output) input_shape = eval (args.input_shape) def infer (audio_path ): data = load_audio(audio_path, mode='test' , spec_len=input_shape[1 ]) data = data[np.newaxis, :] feature = model.predict(data) return feature if __name__ == '__main__' : feature1 = infer(args.audio_path1)[0 ] feature2 = infer(args.audio_path2)[0 ] feature3 = infer(args.audio_path3)[0 ] datapath = "./test2" dirs = os.listdir(datapath) for audio in dirs: personx = 'test2/%s' % (audio) featurex = infer(personx)[0 ] dist1 = np.dot(feature1, featurex) / (np.linalg.norm(feature1) * np.linalg.norm(featurex)) if dist1 > args.threshold: print ("%s 符合派蒙模型,相似度为:%f" % (personx, dist1)) shutil.move("./test2/%s" % (audio),"./dataset/Paimon" ) else : dist2 = np.dot(feature2, featurex) / (np.linalg.norm(feature2) * np.linalg.norm(featurex)) if dist2 > args.threshold: print ("%s 符合可莉模型,相似度为:%f" % (personx, dist2)) shutil.move("./test2/%s" % (audio),"./dataset/Klee" ) else : dist3 = np.dot(feature3, featurex) / (np.linalg.norm(feature3) * np.linalg.norm(featurex)) if dist3 > args.threshold: print ("%s 符合心海模型,相似度为:%f" % (personx, dist3)) shutil.move("./test2/%s" % (audio),"./dataset/Kokomi" ) gc.collect()
需要注意一点,为了提高识别的准确性,这个项目要求的语音长度不能低于1.7s,因此我用ffmpeg将所有长度低于2s的短音频全部过滤了(这里不赘述实现过程)。
之后将三个角色的标准语音分别放在audio_db文件夹下,识别的原理是通过预测函数提取三个角色的音频特征值,对5.6万条音频分别比对三个角色的标准音频特征,求对角余弦值,在多次试验后选择了对角余弦值0.8,作为判断两条语音是否为同一个人的阈值。
直接在集群上运行infer_contrast.py,相似度高于0.8的音频则会被挑选到对应的dataset文件夹中。
实际上这个声纹识别的结果仅能作为参考,不能保证百分百正确,原因有很多:
因此识别的结果需要进行人工校正,也就是需要自己听一遍到底是不是这个角色的语音 = =(最好同下一步一起进行,省时间)
这里我验证并分离出2293条长度2秒以上的派蒙语音,以其中的1820条作为训练集,473条作为测试集。后续训练模型用到的时候会说。
1.3 基于百度语音识别API的语音转文本 光有语音还不行,我们要训练模型就要有对应的文本 。很多单机游戏(比如柚子社的游戏)有解包脚本,可以完整解出所有资源,其中就包括语音文件和对应的文本。但是解包有客户端的游戏不同,比如这款游戏发布不同版本的客户端,文件结构就会发生很大的改变,导致以前做的文件定位统统失效,而且包括文本在内的很多文件也是加密的,无法解出(也可能是我个人问题)。
因此,我们还是需要借助语音识别的软件将语音转成文本。这里涉及到另一个问题,不管多么强大的语音转文字技术,都是在已有的数据集基础上不断训练模型而产生的,游戏中有相当多新造的词(比如中二台词,游戏人名,地点等等),这在转化文本过程中是肯定无法百分百准确的,甚至会“空耳”产生歧义。
因此转文本这一步结束后需要人工校准,至少保证读音正确。
我是在百度AI开放平台 申请了语音识别API,每个账号有200万次免费调用额度,但是限制并发数2(没办法,既然是白嫖就忍忍)。
查看官方放在github上的demo,改一改就可以调用API了(每当问我不会使用的时候都是看demo然后魔改2333)。
我这里以官网提供的asr_raw.py 为例,直接下载,并修改成如下:
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 import sysimport jsonimport timeimport gcimport osimport timeIS_PY3 = sys.version_info.major == 3 if IS_PY3: from urllib.request import urlopen from urllib.request import Request from urllib.error import URLError from urllib.parse import urlencode timer = time.perf_counter else : import urllib2 from urllib2 import urlopen from urllib2 import Request from urllib2 import URLError from urllib import urlencode if sys.platform == "win32" : timer = time.clock else : timer = time.time API_KEY = 'XXXXXXXX' SECRET_KEY = 'XXXXXXXX' FORMAT = "wav" ; CUID = '123456PYTHON' ; RATE = 16000 ; DEV_PID = 1537 ; ASR_URL = 'http://vop.baidu.com/server_api' SCOPE = 'audio_voice_assistant_get' class DemoError (Exception ): pass """ TOKEN start """ TOKEN_URL = 'http://aip.baidubce.com/oauth/2.0/token' def fetch_token (): params = {'grant_type' : 'client_credentials' , 'client_id' : API_KEY, 'client_secret' : SECRET_KEY} post_data = urlencode(params) if (IS_PY3): post_data = post_data.encode('utf-8' ) req = Request(TOKEN_URL, post_data) try : f = urlopen(req) result_str = f.read() except URLError as err: print ('token http response http code : ' + str (err.code)) result_str = err.read() if (IS_PY3): result_str = result_str.decode() result = json.loads(result_str) if ('access_token' in result.keys() and 'scope' in result.keys()): if SCOPE and (not SCOPE in result['scope' ].split(' ' )): raise DemoError('scope is not correct' ) return result['access_token' ] else : raise DemoError('MAYBE API_KEY or SECRET_KEY not correct: access_token or scope not found in token response' ) """ TOKEN end """ if __name__ == '__main__' : token = fetch_token() """ httpHandler = urllib2.HTTPHandler(debuglevel=1) opener = urllib2.build_opener(httpHandler) urllib2.install_opener(opener) """ for audio in range (1 ,1825 ): AUDIO_FILE = str ('/public/home/wlxie/test4voice/baiduyun/training_16K/train' + str (audio) + '.wav' ) speech_data = [] with open (AUDIO_FILE, 'rb' ) as speech_file: speech_data = speech_file.read() length = len (speech_data) if length == 0 : raise DemoError('file %s length read 0 bytes' % AUDIO_FILE) params = {'cuid' : CUID, 'token' : token, 'dev_pid' : DEV_PID} params_query = urlencode(params); headers = { 'Content-Type' : 'audio/' + FORMAT + '; rate=' + str (RATE), 'Content-Length' : length } url = ASR_URL + "?" + params_query req = Request(ASR_URL + "?" + params_query, speech_data, headers) try : begin = timer() f = urlopen(req) result_str = f.read() except URLError as err: print ('asr http response http code : ' + str (err.code)) result_str = err.read() result_str = result_str.decode() result = json.loads(result_str) res = result['result' ][0 ] print ('train' +str (audio) + '.wav' + '识别结果:' + res) with open ("training_1800_result.txt" , "a" ) as of: of.write('train' + str (audio) + '.wav' + "|" + res + '\n' ) gc.collect()
这里也有一个大坑 ,这个语音转文本API要求音源采样率必须是16000Hz,前面说到我们解包得到的音频是48000Hz,而且后面训练模型要求采样率为22050Hz!也就是说如果我们现在把所有音频转成16000Hz的话,势必会对训练模型产生影响(高频可以转低频,但是低频转高频语音质量不会有一丁点儿的提升),因此我这边用拆包音频做了两个备份,一个是转成16000Hz,放在training_16K文件下,专门用于语音转文本;一个是转成22050Hz,放在training_22K文件下,专门用于后续训练模型。重采样仍然用我们的老朋友ffmpeg ,因为就一行命令的事这里也不赘述了。
前面也说到这个API并发数限制为2,经常是用着用着就断开了(也是我比较笨比,不会写限制并发数发送请求的代码),所以我将训练集的1825个语音写了个小脚本,重命名为train1.wav-train1825.wav,所以才用了for循环一句一句调用API转文本,到哪个地方断了也可以迅速找出来并继续。
总之效果如下,训练集1825条语音和测试集473条语音全部转换为文本,且能清晰地看到一一对应关系:
一眼看效果还不错,为了保证准确率,将txt文件传回本地,人工校正吧 (语气词部分本来是要去除的,但是工作量会比较大放弃了,起码要保证发音没问题)。
这个数据集因为不是标准的普通话数据集(标准数据集可以找标贝,就有那种纯合成的标准普通话),声优也有特殊的口癖和发音,额,这是无法避免的。
1.4 基于pypinyin的汉字转拼音 因为后面训练模型的Tacotron2是基于英文模型开发出来的,我们无法直接用中文文本训练。一个行之有效的方法是将中文转换成拼音+数字声调的方式,这样数据就可以顺利地被载入。
这里推荐一下pypinyin模块,该模块安装比较方便(直接用pip),也是个非常实用和高质量的汉字拼音转换工具!
我将人工校准后的txt文件传回集群,去掉前面的“|”之前的内容,再写个小脚本将所有标点符号删除 ,接着汉字转拼音,这里就记录下pypinyin的用法吧。
1 2 3 4 5 6 7 8 9 from pypinyin import lazy_pinyin, Styleimport linecacheoutput_file = open ("/public/home/wlxie/test4voice/baiduyun/training_pinyin.txt" ,"w" ) readlist = list (range (1 ,1821 )) for i in readlist: text = linecache.getline("/public/home/wlxie/test4voice/baiduyun/cheat_training.txt" ,i) text = " " .join(lazy_pinyin(text, style=Style.TONE3)) output_file.write(text)
然后将拼音前按照Tacotron2训练的要求,加上了音频文件对应的colab路径(为什么用这个路径我下一篇博客再说明),以及每句话末尾加个英文的句号,最后输出结果如下:
同样的方法对测试集也转拼音,这样前期的数据集文件就制作完成啦!接下来就是重点——训练模型。下篇博客接着说完。