数字转越南语词组实现方法

先说明一下背景:
近日,公司在自研TTS服务的阿拉伯数字转换成越南语音频的功能,在网上查询了相关资料,没有发现可直接使用的库或者代码片段。于是,自己就开发了一套。事后,经过整理、总结分享给有相同需求的同学。

TTS 就是文本转语音,语音合成(Text To Speech)。
在现实生活中,随处可见的智能设备(天猫精灵、小爱同学等)要想说话就需要用到TTS了,相当于嘴。

转换流程

经过分析发现,阿拉伯数字转换成越南语音频,大致需要两个步骤:

  1. 先把数字转换成越南语词组序列
  2. 再把越南词组序列转换成对应的音频

第一步的实现是本文讲解的重点。
第二步的实现由于涉及机密问题,就说一下大致思路:把第一步中获得的越南词组序列映射到事先录好的基准词音频,然后使用拼接方法,把这些基准词音频拼接在一起即可。

所谓基准词音频就是指转换成的词组序列中的最小单位对应的音频。
例如:123,汉语读做一百二十三,其中的1、2、3为最小的不可拆分的词,这些词对应的音频就叫做基准词音频。
百、十则是单位。使用基准词+单位进行拼接就可以得到最终的结果。例如:123就是一+百+二+十+三
123 越南语词组序列:[‘một’, ‘trăm’, ‘hai’, ‘mươi’, ‘ba’]

越南语的基准词 0~10 及 部分单位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 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ônglinhlẻ 表达。
一般的读法是, 零在十位上读 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ộtmươ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较为常用)即:与mươi(十)结合使用时,读作 bốn

例如:
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开发,具体如下:

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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
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 对应的序列。

1
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 改成你想要的数字。感兴趣的话,来测试一下吧。