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

ivansli 2021/11/20 540℃ 0

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

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

转换流程

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

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

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

所谓基准词音频就是指转换成的词组序列中的最小单位对应的音频。
例如: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ô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开发,具体如下:

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

Python

评论啦~