抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

两个月的暑假已经结束了,假期里自学了一点深度学习的内容,很多地方还是一知半解的,这里稍微记录一下。之前刷到一个很有意思的语音合成视频,抱着试试看的心态想自己做一个模型,于是给自己挖了一个大坑……涉及到深度学习的知识我还需要慢慢学,因此本篇博客重点还是记录下自己的踩坑操作原理部分以后搞明白了再更新

总的来说,我通过拆包游戏客户端获得5.6万条语音文件,通过github上的一个声纹识别项目分离其中一个角色的语音文件。接着用百度的语音识别API将语音识别为文本后,人工校正一遍文本,然后转换为拼音+音标,以此制作语音数据训练集和测试集。基于开源项目Tacotron2训练角色语音模型,经历400 epoch后初步训练成型,最后基于HiFiGAN合成语音。整个后半段流程是在google colab上完成的,为了完成模型训练我申请了4个谷歌账号…不得不说白嫖的GPU真香~

1. 制作数据集

可以说整个项目大部分时间花费在整理数据集上,根据我自己的经验,数据集的语音长度在2秒-10秒之间效果最好,数量大约在2000条左右(为了涵盖尽可能多的汉字发音)。需要注意一点,不管拆包的原语音采样率如何,都要统一重采样到22050 hz,这是Tacotron2训练模型的要求。

1.1 Extractor2.5 + vgmstream-win拆包

首先是这款国内游戏的拆包,所有角色的语音文件都在目录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 argparse
import functools
import numpy as np
import tensorflow as tf
from utils.reader import load_audio
from utils.utility import add_arguments, print_arguments
import os,shutil
import gc

os.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文件夹中。

实际上这个声纹识别的结果仅能作为参考,不能保证百分百正确,原因有很多:

  • 1.声优都是怪物,一个人用好多相似的声线配了不同角色,导致无法分辨出不同角色的语音(假阳性)。

  • 2.一句话的语调不同会表现出音频特征值不同,而这个算法下会导致对角余弦值偏小,从而判断成发声的是不同的人(假阴性)。

因此识别的结果需要进行人工校正,也就是需要自己听一遍到底是不是这个角色的语音= =(最好同下一步一起进行,省时间)

这里我验证并分离出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
# coding=utf-8
import sys
import json
import time
import gc
import os
import time

IS_PY3 = sys.version_info.major == 3 # 判断你用的是python3.x还是2.x版本,推荐还是用3.x

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:
# On most other platforms the best timer is time.time()
timer = time.time

API_KEY = 'XXXXXXXX' # 改成你自己的,下面一条一样
SECRET_KEY = 'XXXXXXXX'
FORMAT = "wav"; # 文件后缀只支持 pcm/wav/amr 格式
CUID = '123456PYTHON';
RATE = 16000; # 固定值,这里一定一定要注意采样率
DEV_PID = 1537; # 1537 表示识别普通话,使用输入法模型。根据文档填写PID,选择语言及识别模型
ASR_URL = 'http://vop.baidu.com/server_api'
SCOPE = 'audio_voice_assistant_get' # 有此scope表示有asr能力,没有请在网页里勾选,非常旧的应用可能没有
class DemoError(Exception):
pass

""" TOKEN start """

TOKEN_URL = 'http://aip.baidubce.com/oauth/2.0/token'
# 核对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(' ')): # SCOPE = False 忽略检查
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, Style
import linecache

output_file = open("/public/home/wlxie/test4voice/baiduyun/training_pinyin.txt","w")
readlist = list(range(1,1821)) # 人工校准的时候去掉了4条不是该角色的音频
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路径(为什么用这个路径我下一篇博客再说明),以及每句话末尾加个英文的句号,最后输出结果如下:

同样的方法对测试集也转拼音,这样前期的数据集文件就制作完成啦!接下来就是重点——训练模型。下篇博客接着说完。

欢迎小伙伴们留言评论~