
先说明一下背景:
近日,公司在自研TTS服务的阿拉伯数字转换成越南语音频的功能,在网上查询了相关资料,没有发现可直接使用的库或者代码片段。于是,自己就开发了一套。事后,经过整理、总结分享给有相同需求的同学。
TTS 就是文本转语音,语音合成(Text To Speech)。
在现实生活中,随处可见的智能设备(天猫精灵、小爱同学等)要想说话就需要用到TTS了,相当于嘴。
转换流程
经过分析发现,阿拉伯数字转换成越南语音频,大致需要两个步骤:
- 先把数字转换成越南语词组序列
- 再把越南词组序列转换成对应的音频
第一步的实现是本文讲解的重点。
第二步的实现由于涉及机密问题,就说一下大致思路:把第一步中获得的越南词组序列映射到事先录好的基准词音频,然后使用拼接方法,把这些基准词音频拼接在一起即可。
所谓
基准词音频
就是指转换成的词组序列中的最小单位对应的音频。
例如:123,汉语读做一百二十三
,其中的1、2、3
为最小的不可拆分的词,这些词对应的音频就叫做基准词音频。
百、十
则是单位。使用基准词+单位
进行拼接就可以得到最终的结果。例如:123
就是一+百+二+十+三
。
123 越南语词组序列:['một', 'trăm', 'hai', 'mươi', 'ba']
越南语的基准词 0~10 及 部分单位。
# 0~10的映射关系
int_pronounce_map = [
"không", # 0
"một", # 1
"hai", # 2
"ba", # 3
"bốn", # 4
"năm", # 5
"sáu", # 6
"bảy", # 7
"tám", # 8
"chín", # 9
"mười", # 10
]
# 部分单位
units = {
"mười", # [十] 单独一个十的发音
"mươi" # [十] 特殊读法十 20~90
"trăm", # [百]
"nghìn", # [千]
"triệu", # [百万]
"tỷ", # [十亿]
}
注意:越南语是存在音调的,例如:10的发音为 "mười",20~90中的10发音为 "mươi"。仔细看他们,是有区别的哦!
越南语特点
咨询了一些懂得越南语的同事得知,越南语使用的应该是类似拉丁语系的字符构成。其数字读法有别于汉语读法,但是有一部分又类似汉语读法。
越南语一到四位数的读法与汉语相同,例如:11=10+1,读作mười một
但有几个数量词在与其他数量词组合时发生音变, 1 至 19 的数字只有 15 有变音,15 不读作mười năm
,而是读作mười lăm
。
一到四位数读法
虽说越南语一到四位数的读法与汉语相同,但1001,汉语会省略百位不读
,读成:一千零一。而越南语不会省略百位
,读作một nghìn không trăm linh một
(一千零百零一,即一千零一)。
注意:越南语可以省略十位不读,例如:101(一百零一)读作một trăm linh một。
五位数及以上读法
越南语一到四位数读法与汉语相同,五位数及以上读法与汉语就不同,反而是与英语的读数规则相同。
越南语四位数以后不用“万”、“亿”为计算单位,而是用千(nghìn
)、百万(triệu
)、十亿(tỷ
)为计算单位。
例如:87000,此时越南语5位数字读法与英语相似,可以以三位数为一个段位对其进行分段,即为87,000 (英语读法: eighty-seven thousand
)读作:tám mươi bảy nghìn
(八十七千,即八万七千)。
同理,可分段为 987,000 (英语读法:nine hundred and eighty-seven thousand
)读作:chín trăm tám mươi bảy nghìn
注意:英语在百位与十位之间要加 and,而越南语都不用
一些特殊读法
0的特殊读法
多位数中有零时,越南语用 không
、 linh
或 lẻ
表达。
一般的读法是, 零在十位上读 linh
(比较常用linh
) 或 lẻ
,零在百位上读 không
。
例如:
303(汉语读法:三百零三) 可读作 ba trăm linh ba
1001(汉语读法:一千零一) 可读作 một nghìn không trăm linh một
(一千零百零一,即一千零一)
10的特殊读法
mười
(十)在 hai
(二)到 chín
(九)这些数量词之后变成 mươi
(十)。
例如:
20(汉语读法:二十)读作 hai mươi
,而不读作 hai mười
90(汉语读法:九十)要读作 chín mươi
而不读作 chín mười
1的特殊读法
một
在 mươi
之后变成 mốt
(即与mươi
结合使用时,“一”读作mốt
)
例如:
21(汉语读法:二十一)读作 hai mươi mốt
,而不读作 hai mươi một
4的特殊读法
bốn
(四) 在 mươi
(十)之后读成 bốn
或 tư
(tư
较为常用)即:与mươi
(十)结合使用时,读作 bốn
或tư
)
例如:
24(汉语读法:二十四)可读作 hai mươi tư
5的特殊读法
năm
(五)在 mười
(十)之后要变成 lăm
,在 mươi
(十)之后要变成 lăm
(比较常用 lăm
)或 nhăm
(与mươi
(十)或 mười
(十)结合使用时,(五)均可读作 lăm
。
例如:
15(汉语读法:十五)读作 mười lăm
25(汉语读法:二十五)可说 hai mươi lăm
而不说 hai mươi năm
65(汉语读法:六十五)可说 sáu mươi lăm
而不说 sáu mươi năm
代码实现
基于以上规则以及特殊读法,实现了相关逻辑。代码使用Python开发,具体如下:
import re
# 0~10 数字的发音
int_pronounce_map = [
"không", # 0
"một", # 1
"hai", # 2
"ba", # 3
"bốn", # 4
"năm", # 5
"sáu", # 6
"bảy", # 7
"tám", # 8
"chín", # 9
"mười", # 10
]
# 判断是否为数字
def can_convert_to_float(text):
try:
float(text)
return True
except ValueError:
return False
# 检查数字大小是否超过阈值
def is_num_enable(num):
# (10 ** 12) = 1,000,000,000,000 = 1万亿
if num >= (10 ** 12): # 最大数值为 1万亿-1
return False
return True
# 字符串按照固定长度拆分为一组
# 按照 lens 个字符拆分为一组
#
# text_arr.append(text[(len(text_arr) * lens):]) 剩余的为最后一组字符
def cut_text(text, lens):
text_arr = re.findall('.{' + str(lens) + '}', text)
# 最后一个字符串
last = text[(len(text_arr) * lens):]
if len(last) > 0:
text_arr.append(last)
return text_arr
# 字符换翻转
def str_rev(strings: str):
return strings[::-1]
# 字符串尝试是否为3位
def is_lens_three(num_string):
return True if len(num_string) == 3 else False
# 数字字符串转换成越南语词组序列
#
# 比较重要,三个数字为一组进行处理
def number_to_word(num_string: str):
# 字符串长度
lens_number = len(num_string)
if lens_number > 3: # 字符串长度大于3个,则抛出异常
raise Exception(f"number:{num_string} error, lens more than 3")
# 字符串长度是否等于3
is_len_3 = is_lens_three(num_string)
# 字符串转换的数字为 0,则返回空
int_number = int(num_string)
# 0
if int_number == 0:
return []
# 数字 1~10
if int_number <= 9:
# 如果字符串长度为3,但是数字却是个位数,则读作:零百零x。否则原样读出
words_list = ["không", "trăm", "linh"] if is_len_3 else []
# 单独一个数字的读法
words_list += one_digital(int_number)
return words_list
# 数字 11~99
if 10 <= int_number <= 99:
# 如果字符串长度为3,数字在11~99之间,读作:零百xx。否则原样读出
words_list = ["không", "trăm"] if is_len_3 else []
words_list += two_digital(int_number)
return words_list
# 数字 100~999
# 百位数不为 0。不需要添加 零百xx 之类的读法
# if 100 <= int_number <= 999:
hundreds = int_number // 100 # 百位
exclude_hundreds = int_number % 100 # 十位 & 个位
# 读法:x百 开头
words_list = [int_pronounce_map[hundreds], "trăm"]
# x0x
# 十位&个位数字为 1~9 之间,则读作:x百零x
if 1 <= exclude_hundreds <= 9:
words_list.append("linh")
words_list.append(int_pronounce_map[exclude_hundreds])
return words_list
# x百xx
words_list += two_digital(exclude_hundreds)
return words_list
# 1个数字
def one_digital(int_number):
return [int_pronounce_map[int_number]]
# 2位 (十位&个位) 数字
# 0
# 0~10
# 11~19
# 20~99
def two_digital(int_number):
# 00
if int_number == 0:
return []
if 0 < int_number <= 10:
return [int_pronounce_map[int_number]]
decade = int_number // 10 # 十位数
mod = int_number % 10 # 个位数
# 数字 11 ~ 19
if decade == 1:
lists = ["mười", "lăm" if mod == 5 else int_pronounce_map[mod]] # 特殊发音的10
return lists
if 2 <= decade <= 9:
# 数字 20 ~ 99
# 读作:x十x
#
lists = [int_pronounce_map[decade], "mươi"]
if mod == 1: # 个位为1的特殊读法,21~91
lists.append("mốt")
elif mod == 4: # 个位为4的特殊读法,24~94
lists.append("tư")
elif mod == 5: # 个位为5的特殊读法,25~95
lists.append("lăm")
else:
if mod != 0: # 其他非0、非特殊的个位数
lists.append(int_pronounce_map[mod])
return lists
# 获取字符串需要的所有单位
def get_units(item_count):
# 只有1组数字,不需要单位
if item_count == 1:
return []
# 2组数字包含的所有单位:千位
if item_count == 2:
return ["nghìn"]
# 3组数字包含的所有单位:百万、千位
if item_count == 3:
return ["triệu", "nghìn"]
# 4组数字包含的所有单位:十亿、百万、千位
if item_count == 4:
return ["tỷ", "triệu", "nghìn"]
# 超出数字限制,抛出异常
raise Exception("number too big, is not support")
# 数字字符串转换为越南语发音词汇序列
# num 为字符串,主要是为了进行3个一组的拆分
def convert_number_2_vietnam_words(num: str):
# 数字大于最大范围
if not is_num_enable(int(num)):
return None
# 思路:
# 例如:1234567
# 字符串翻转 "7654321"
# 3个一组进行拆分 = ['765', '432', '1']
# 数组翻转 = ['1', '432', '765']
# 在翻转数组的每一项 = ['1', '234', '567']
#
# 拆分的数组为 str_arr = ['1', '234', '567'],则需要追加 len(str_arr)-1 个单位
# 至于需要的单位,由于数字长度优先,可以枚举出来不同的组数需要多少个单位
#
# 字符串长度大于3,才进行3个一组拆分,最终获得多组字符串(每一组字符串3位)
# 否则单独为一组
if len(num) > 3:
# 1. 先翻转字符串。3个字符为一组进行截取,最后不足3个字符的为一组
str_arr = cut_text(str_rev(num), 3)
# 2. 数组翻转
str_arr = str_arr[::-1]
# 再次把每一组字符串翻转过来
for inx, str_number in enumerate(str_arr):
str_arr[inx] = str_number[::-1]
else: # 字符串长度小于等于3,则为单独的一组
str_arr = [num]
#
# 最终的越南语词组序列
ret_words = []
# 所有需要的单位序列
units = get_units(len(str_arr))
# print(f"final units:{units}")
# 处理每一组字符串数字
for inx, str_number in enumerate(str_arr):
# 数字字符串转换为越南语词汇序列
words_list = number_to_word(str_number)
# 转换的词组序列个数大于0,则需要加单位
if len(words_list) > 0:
ret_words += words_list
# todo 判断单位,并加上单位
# 单位数组不为空 && 除了最后一组数字字符串之外,前面的每一组都需要追加一个单位
if 0 < len(units) and inx < len(units):
ret_words.append(units[inx])
# print(f"inx:{inx} unit:{units[inx]}")
# 打印输出整个数字字符串的拆分情况以及最终转换成的词汇序列
print(f"{num}:{str_arr} words:{ret_words}\n")
return ret_words
# 测试
if __name__ == "__main__":
convert_number_2_vietnam_words("1234567889")
上面代码直接复制下来,在Python环境中直接运行就会得到数字 1234567889 对应的序列。
1234567889:['1', '234', '567', '889'] words:['một', 'tỷ', 'hai', 'trăm', 'ba', 'mươi', 'tư', 'triệu', 'năm', 'trăm', 'sáu', 'mươi', 'bảy', 'nghìn', 'tám', 'trăm', 'tám', 'mươi', 'chín']
当然,你也可以把 1234567889 改成你想要的数字。感兴趣的话,来测试一下吧。
本文链接:https://www.ivansli.com/archives/268/
本文系原创作品,版权所有(禁止转载)