shartoo +

merlin语音合成中文前端处理2-实践

本文总阅读量
欢迎star我的博客

0 数据示例

以 THSCH-30数据集为例子。THSCH-30数据集分为两部分音频文本。音频文件列表如下:

mtts_mandarin_text

文本内容,全部文本存放在一个文件内。内容如下:

mtts_mandarin_text

1 数据预处理

我们先处理文本内容,第一步是数据预处理。主要完成以下操作:

  1. 去除所有标点符号。
  2. 去掉所有数字和字母
  3. 替换所有句子结束的标点符号为 #4,即 re.sub('[,.,。]', '#4', txt)。其中的逗号,点号,句号,都替换为符号#4,此处的#4代表了不同的韵律标注层次。具体的不同层次,参考下面的说明

以 下面这句为示例

'A11_0 绿是阳春烟景大块文章的底色四月的林峦更是绿得鲜活秀媚诗意盎然'

经过第一步的处理,其中A11_0是句子编号,不会被处理。以空格分割之后会处理后面的中文文本,经过处理,后面的文本没有变化。没有数字和字母,也没有标点符号。

2 添加拼音

第二步是给每个中文添加拼音标注。使用的是pypinyin。示例代码如下:

 numstr, txt = line.split(' ')
        txt = re.sub('#\d', '', txt)
        pinyin_list = pinyin(txt, style=Style.TONE3)
        new_pinyin_list = []
        for item in pinyin_list:
            if not item:
                logger.warning(
                    '{file_num} do not generate right pinyin'.format(numstr))
            if not item[0][-1].isdigit(): # 对于没有 声调的拼音,统一添加声调5
                phone = item[0] + '5'
            else:
                phone = item[0]
            new_pinyin_list.append(phone)
        lab_file = os.path.join(wav_dir_path, numstr + '.lab')
        with open(lab_file, 'w') as oid:
            oid.write(' '.join(new_pinyin_list))

这样,上面的一句中文对应的拼音内容为:

lv4 shi4 yang2 chun1 yan1 jing3 da4 kuai4 wen2 zhang1 de5 di3 se4 si4 yue4 de5 lin2 luan2 geng4 shi4 lv4 de2 xian1 huo2 xiu4 mei4 shi1 yi4 ang4 ran2

可以看到,每个字都有对应的拼音。其中,有一步处理是给没有音调的字统一添加为音调5。有些词比如是没有音调的,统一被添加音调5。这一步会将拼音内容写入一个标注文本A11_0.lab

3 强制对齐

这一步使用了语音识别模型,对语音进行强制对齐。语音识别模型的作用是识别语音中每个字的发音起止时间,并存储为TextGrid格式,这个格式是语音标注软件Praat的标注格式。 此步骤依赖以下内容:

3.1 拼音词典

此步骤依赖的拼音词典是 mandarin_mtts.lexicon,其中的内容如下:

a1 a1
a2 a2
a3 a3
a4 a4
a5 a5
ai1 ai1
ai2 ai2
ai3 ai3
ai4 ai4
ai5 ai5
an1 an1
an2 an2
an3 an3
an4 an4
an5 an5
ang1 ang1
ang2 ang2
ang3 ang3
ang4 ang4
ang5 ang5
...

3.2 强制对齐工具

MTTS所使用的强制对齐工具为 Montreal-Forced-Aligner

3.3 强制对齐模型

预备知识

汉字按照长度可以划分为:句子短语汉字(音节)音素。而音素由声母韵母元音静音组成。

对齐模型使用的是由THSCH-30数据集所训练的中文语音识别模型,下载地址为 THSCH-30语音识别模型

3.4 输出结果

这一步主要是对语音音频文件处理,并得到识别结果。识别的结果是直接到音素,存储为TextGrid格式,示例文本对应的音频文件 A11_0.wav所得到的TextGrid标注文件内容如下:

File type = "ooTextFile"
Object class = "TextGrid"

xmin = 0.0
xmax = 7.8
tiers? <exists>
size = 2
item []:
    item [1]:
        class = "IntervalTier"
        name = "words"
        xmin = 0.0
        xmax = 7.8
        intervals: size = 33
            intervals [1]:
                xmin = 0.0
                xmax = 1.100
                text = ""
            intervals [2]:
                xmin = 1.100
                xmax = 1.350
                text = "lv4"
            intervals [3]:
                xmin = 1.350
                xmax = 1.460
                text = "shi4"
         ....
            intervals [59]:
                xmin = 6.980
                xmax = 7.150
                text = "ang4"
            intervals [60]:
                xmin = 7.150
                xmax = 7.280
                text = "r"
            intervals [61]:
                xmin = 7.280
                xmax = 7.430
                text = "an2"
            intervals [62]:
                xmin = 7.430
                xmax = 7.780
                text = "sp"
            intervals [63]:
                xmin = 7.780
                xmax = 7.8
                text = ""

可以看到,一共识别到了64个字。其中解析如下:

4 TextGrid标注格式转换为SFS格式

SFS即为声韵母标注,主要将每个字的音素标注为以下三类:

类型 说明 示例
s 时长超过100ms的静音 sil,sp
d 时长短于100ms的静音 -
a 辅音 包含```b’, ‘p’, ‘m’, ‘f’, ‘d’, ‘t’, ‘n’, ‘l’, ‘g’, ‘k’, ‘h’, ‘j’, ‘q’, ‘x’, ‘zh’, ‘ch’, ‘sh’, ‘r’, ‘z’, ‘c’, ‘s’, ‘y’, ‘w’
b 元音 -

同时将发音起止时间的单位从秒更改为 纳秒,即乘以10的6次方。上一步得到的TextGrid格式的标注转换为sfs格式之后,内容如下:

11000000 s
12400000 a
13500000 b
....
67500000 b
68500000 a
69800000 b
71500000 b
72800000 a
74300000 b
77800000 s

5 sfs到真实标注文件

此步骤依赖于sfs标注文件原始中文文本。上面的步骤示例中的A11_0文本和sfs标注即可。得到的标注结果如下:

0 11000000 xx^xx-sil+l=v4@xx@/A:xx-xx^xx@/B:xx+xx@xx^xx^xx+xx#xx-xx-/C:xx_xx^xx#xx+xx+xx&/D:xx=xx!xx@xx-xx&/E:xx|xx-xx@xx#xx&xx!xx-xx#/F:xx^xx=xx_xx-xx!
11000000 12400000 xx^sil-l+v4=sh@v@/A:xx-4^4@/B:0+29@1^1^1+30#1-30-/C:xx_a^v#xx+1+1&/D:xx=30!xx@1-1&/E:xx|30-xx@xx#1&xx!1-1#/F:xx^30=17_1-1!
12400000 13500000 sil^l-v4+sh=ih4@v@/A:xx-4^4@/B:0+29@1^1^1+30#1-30-/C:xx_a^v#xx+1+1&/D:xx=30!xx@1-1&/E:xx|30-xx@xx#1&xx!1-1#/F:xx^30=17_1-1!
13500000 14300000 l^v4-sh+ih4=y@ih@/A:4-4^2@/B:1+28@1^1^2+29#2-29-/C:a_v^n#1+1+2&/D:xx=30!xx@1-1&/E:xx|30-xx@xx#1&xx!1-1#/F:xx^30=17_1-1!
14300000 14600000 v4^sh-ih4+y=iang2@ih@/A:4-4^2@/B:1+28@1^1^2+29#2-29-/C:a_v^n#1+1+2&/D:xx=30!xx@1-1&/E:xx|30-xx@xx#1&xx!1-1#/F:xx^30=17_1-1!
...
66900000 67500000 ei4^sh-ih1+y=i4@ih@/A:4-1^4@/B:26+3@1^2^27+4#27-4-/C:a_n^z#2+2+2&/D:xx=30!xx@1-1&/E:xx|30-xx@xx#1&xx!1-1#/F:xx^30=17_1-1!
67500000 68500000 sh^ih1-y+i4=ang4@i@/A:1-4^4@/B:27+2@2^1^28+3#28-3-/C:a_n^z#2+2+2&/D:xx=30!xx@1-1&/E:xx|30-xx@xx#1&xx!1-1#/F:xx^30=17_1-1!
68500000 69800000 ih1^y-i4+ang4=r@i@/A:1-4^4@/B:27+2@2^1^28+3#28-3-/C:a_n^z#2+2+2&/D:xx=30!xx@1-1&/E:xx|30-xx@xx#1&xx!1-1#/F:xx^30=17_1-1!
69800000 71500000 y^i4-ang4+r=an2@ang@/A:4-4^2@/B:28+1@1^2^29+2#29-2-/C:n_z^xx#2+2+xx&/D:xx=30!xx@1-1&/E:xx|30-xx@xx#1&xx!1-1#/F:xx^30=17_1-1!
71500000 72800000 i4^ang4-r+an2=sil@an@/A:4-2^xx@/B:29+0@2^1^30+1#30-1-/C:n_z^xx#2+2+xx&/D:xx=30!xx@1-1&/E:xx|30-xx@xx#1&xx!1-1#/F:xx^30=17_1-1!
72800000 74300000 ang4^r-an2+sil=xx@an@/A:4-2^xx@/B:29+0@2^1^30+1#30-1-/C:n_z^xx#2+2+xx&/D:xx=30!xx@1-1&/E:xx|30-xx@xx#1&xx!1-1#/F:xx^30=17_1-1!
74300000 77800000 r^an2-sil+xx=xx@xx@/A:xx-xx^xx@/B:xx+xx@xx^xx^xx+xx#xx-xx-/C:xx_xx^xx#xx+xx+xx&/D:xx=xx!xx@xx-xx&/E:xx|xx-xx@xx#xx&xx!xx-xx#/F:xx^xx=xx_xx-xx!

5.1 中文分词,词性标注,韵律标注

如果原中文内容里没有进行韵律标注,韵律标注以#分割。就会默认所有的分词结果里的每个词都是#0,但是最后一个是#4,即最后一个代表当前语句结束。参照第数据预处理部分的韵律不同值代表的不同意义。 第一步是对输入的原始中文进行分词,还是以上面步骤的示例文本为例,使用jieba分词的posseg分词和词性标注得到的结果如下:

['绿', '是', '阳春', '烟景', '大块文章', '的', '底色', '四月', '的', '林峦', '更是', '绿', '得', '鲜活', '秀媚', '诗意', '盎然']
[('l', 'v4'), ('sh', 'ih4'), ('y', 'iang2'), ('ch', 'un1'), ('y', 'ian1'), ('j', 'ing3'), ('d', 'a4'), ('k', 'uai4'), ('w', 'uen2'), ('zh', 'ang1'), ('d', 'e5'), ('d', 'i3'), ('s', 'e4'), ('s', 'ic4'), ('y', 've4'), ('d', 'e5'), ('l', 'in2'), ('l', 'uan2'), ('g', 'eng4'), ('sh', 'ih4'), ('l', 'v4'), ('d', 'e2'), ('x', 'ian1'), ('h', 'uo2'), ('x', 'iu4'), ('m', 'ei4'), ('sh', 'ih1'), ('y', 'i4'), ('ang4',), ('r', 'an2')]

5.2 获取音素类型和时间

分为两种情况,有sfs标注文件的和没有的。

使用sfs标注文件 sfs标注文件中每一行都是如下内容:

11000000 s
12400000 a
13500000 b
..

每一行以空格分割,分别代表了当前第i个音素的开始时间音素类型。音素类型参考第4节的表。分别读取并保存

音素类型列表:phs_type =   ['s', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 's', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'b', 'a', 'b', 's']
音素起止时间列表: times = ['0', 11000000, 12400000, 13500000, 14300000, 14600000, 15100000, 16500000, 17600000, 18300000, 19000000, 20400000, 21400000, 23100000, 24100000, 25200000, 26200000, 26900000, 27799999, 28700000, 29600000, 30299999, 31000000, 31500000, 32200000, 33200000, 34800000, 37400000, 37700000, 39100000, 39900000, 40700000, 41500000, 42200000, 42699999, 43500000, 44300000, 45500000, 47300000, 48300000, 49100000, 50900000, 52000000, 53000000, 53700000, 54300000, 54600000, 56000000, 57100000, 58700000, 59900000, 61500000, 62699999, 63900000, 65199999, 66900000, 67500000, 68500000, 69800000, 71500000, 72800000, 74300000, 77800000]

音素起止时间列表(63)比音素类型列表(62)多一个,开始时间0。

没有sfs标注文件

如果没有sfs标注的时间,程序可以自动生成,方法是计算得到语句的所有因素长度,并将默认起止时间都设置为0.,音素类型都默认设置为a

参考韵律列表。韵律列表其实就是当前语句被分词之后,每个词的停顿间隙。以#0,#1,#2,#3,#4标识,分别代表了不同层次的韵律。当前示例语句被分词为如下

mtts_mandarin_text

原始语句如下,没有带任何标点符号:

绿是阳春烟景大块文章的底色四月的林峦更是绿得鲜活秀媚诗意盎然

分词结果如下:

语句分词结果:  ['绿', '是', '阳春', '烟景', '大块文章', '的', '底色', '四月', '的', '林峦', '更是', '绿', '得', '鲜活', '秀媚', '诗意', '盎然']

对应的韵律列表如下:

 ['#0', '#0', '#0', '#0', '#0', '#0', '#0', '#0', '#0', '#0', '#0', '#0', '#0', '#0', '#0', '#0', '#4']

比如对分词结果 阳春得到的音素为列表

[('y', 'iang2'), ('ch', 'un1')]

长度为4,向音素类型列表中添加对应长度的默认值a

phs_type =  ['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'] (后面四个是当前词`阳春`的音素对应的音素类型)

最后得到的音素起止时间列表和音素类型列表分别为:

time=  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
phs_type=['s', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 's']

可以看到,与有sfs对比,长度一致。都是对应音素数目的长度,只不过没有sfs文件的都是默认值。

6 音素状态决策树

上面已经得到了音素对应的起止时间和音素类型,下一步是构建音素状态决策树。以英文为示意图,如下:

mtts_mandarin_text

  1. 首先,一整句话是有前后承接关系的,当绿是阳春这几个词出现时,绿字是前缀,字是字的后缀。这只是字面上的上下文关系,当前需要构建音素级别的上下文承接关系。所以需要进一步细化。 2.韵律标注层次有 ,由小到大(以MTTS前端为例,其他标注格式不一定,标贝数据只有,#1,#2,#3,#4)。
    • 音素: phn
    • 音节:syllables
    • 词: #0
    • 短语: #1
    • 句子: #3
    • 句子结束: #4

6.1 MTTS的上下文标注

说明

标注文件会标记不同韵律层次所有的上下文信息,详细可以参考下面的两张表:

不同层级和对应的标注格式

层级 标注格式  
声韵母层 p1^p2-p3+p4=p5@p6@  
声调层 /A:a1-a2^a3@  
字/音节层 /B:b1+b2@b3^b4^b5+b6#b7-b8-  
词层 /C:c1_c2^c3#c4+c5+c6&  
韵律词层 /D:d1=d2!d3@d4-d5&  
韵律短语层 /E:e1 e2-e3@e4#e5&e6!e7-e8#
语句层 /F:f1^f2=f3_f4-f5!  

更加细致的划分(基元代表了不同层次的单元,可以是音素,也可以是音节,声调等)

标号 含义
p1 前前基元
p2 前一基元
p3 当前基元
p4 后一基元
p5 后后基元
p6 当前音节的元音
a1 前一音节/字的声调
a2 当前音节/字的声调
a3 后一音节/字的声调
b1 当前音节/字到语句开始字的距离
b2 当前音节/字到语句结束字的距离
b3 当前音节/字在词中的位置(正序)
b4 当前音节/字在词中的位置(倒序)
b5 当前音节/字在韵律词中的位置(正序)
b6 当前音节/字在韵律词中的位置(倒序)
b7 当前音节/字在韵律短语中的位置(正序)
b8 当前音节/字在韵律短语中的位置(倒序)
c1 前一个词的词性
c2 当前词的词性
c3 后一个词的词性
c4 前一个词的音节数目
c5 当前词中的音节数目
c6 后一个词的音节数目
d1 前一个韵律词的音节数目
d2 当前韵律词的音节数目
d3 后一个韵律词的音节数目
d4 当前韵律词在韵律短语的位置(正序)
d5 当前韵律词在韵律短语的位置(倒序)
e1 前一韵律短语的音节数目
e2 当前韵律短语的音节数目
e3 后一韵律短语的音节数目
e4 前一韵律短语的韵律词个数
e5 当前韵律短语的韵律词个数
e6 后一韵律短语的韵律词个数
e7 当前韵律短语在语句中的位置(正序)
e8 当前韵律短语在语句中的位置(倒序)
f1 语句的语调类型
f2 语句的音节数目
f3 语句的词数目
f4 语句的韵律词数目
f5 语句的韵律短语数目

以英文单词 author为例

mtts_mandarin_text

体现在代码里面的标准公式化字符串如下:

formation=[
    ' ', ' ',                                      #  开始时间,结束时间(上图中没有出现)
    '^', '-', '+', '=', '@', '@/A:',                 # 连接符
    '-', '^', '@/B:', 
    '+', '@', '^', '^', '+', '#', '-', '-/C:', 
    '_', '^', '#', '+', '+', '&/D:', 
    '=', '!', '@', '-', '&/E:', 
    '|', '-', '@', '#', '&', '!', '-', '#/F:', 
    '^', '=', '_', '-', '!']

示例中文的标注结果如下:

0 11000000 xx^xx-sil+l=v4@xx@/A:xx-xx^xx@/B:xx+xx@xx^xx^xx+xx#xx-xx-/C:xx_xx^xx#xx+xx+xx&/D:xx=xx!xx@xx-xx&/E:xx|xx-xx@xx#xx&xx!xx-xx#/F:xx^xx=xx_xx-xx!
11000000 12400000 xx^sil-l+v4=sh@v@/A:xx-4^4@/B:0+29@1^1^1+30#1-30-/C:xx_a^v#xx+1+1&/D:xx=30!xx@1-1&/E:xx|30-xx@xx#1&xx!1-1#/F:xx^30=17_1-1!
12400000 13500000 sil^l-v4+sh=ih4@v@/A:xx-4^4@/B:0+29@1^1^1+30#1-30-/C:xx_a^v#xx+1+1&/D:xx=30!xx@1-1&/E:xx|30-xx@xx#1&xx!1-1#/F:xx^30=17_1-1!'
13500000 14300000 l^v4-sh+ih4=y@ih@/A:4-4^2@/B:1+28@1^1^2+29#2-29-/C:a_v^n#1+1+2&/D:xx=30!xx@1-1&/E:xx|30-xx@xx#1&xx!1-1#/F:xx^30=17_1-1!'
...
74300000 77800000 r^an2-sil+xx=xx@xx@/A:xx-xx^xx@/B:xx+xx@xx^xx^xx+xx#xx-xx-/C:xx_xx^xx#xx+xx+xx&/D:xx=xx!xx@xx-xx&/E:xx|xx-xx@xx#xx&xx!xx-xx#/F:xx^xx=xx_xx-xx!'

以第一行和第二行为例:

0 11000000 xx^xx-sil+l=v4@xx@/A:xx-xx^xx@/B:xx+xx@xx^xx^xx+xx#xx-xx-/C:xx_xx^xx#xx+xx+xx&/D:xx=xx!xx@xx-xx&/E:xx|xx-xx@xx#xx&xx!xx-xx#/F:xx^xx=xx_xx-xx!
11000000 12400000 xx^sil-l+v4=sh@v@/A:xx-4^4@/B:0+29@1^1^1+30#1-30-/C:xx_a^v#xx+1+1&/D:xx=30!xx@1-1&/E:xx|30-xx@xx#1&xx!1-1#/F:xx^30=17_1-1!

我的博客

观点

源码