HeyGen Voice 把数字和多音字念错、在文本里怎么标注才让它读对?

文章导读
HeyGen Voice 这类语音生成工具通常没有可依赖的“读音标注语法”可以直接照抄,所以稳妥的顺序是:先在提交文本之前把数字、单位和多音字改成你希望听到的中文写法,再用同一句话的两种写法各生成一遍对照试听。插入空格、加分隔符、平台提供的发音词典或 SSML 之类的做法能不能生效,需要在该语言和该音色下单独验证;只有验证通过才继续用,验证不通过就换同义词规避,而不是反复点重新生成、指望某一次碰巧
📋 目录
  1. 一 先把读错的词逐个记下来,按数字、单位、多音字分三类
  2. 二 在生成前用替换表改文本:把数字写成想听到的读法
  3. 三 同一句话用原写法和替换后写法各生成一遍,对照听差异
  4. 四 把确认有效的替换规则存成词表,下次生成前先跑一遍
A A

HeyGen Voice 这类语音生成工具通常没有可依赖的“读音标注语法”可以直接照抄,所以稳妥的顺序是:先在提交文本之前把数字、单位和多音字改成你希望听到的中文写法,再用同一句话的两种写法各生成一遍对照试听。插入空格、加分隔符、平台提供的发音词典或 SSML 之类的做法能不能生效,需要在该语言和该音色下单独验证;只有验证通过才继续用,验证不通过就换同义词规避,而不是反复点重新生成、指望某一次碰巧读对。

数字和多音字被念错,优先在文本层解决:生成前把 2024 写成“二零二四”、把 3.5% 写成“百分之三点五”、把“重启”换成“重新启动”,然后两种写法各生成一遍对照试听。两次结果稳定一致才算规则有效;只碰巧读对一次,或者替换时顺带换了音色、语速、语言设置,都不能当结论。发音标注能否使用,要在同一音色下用小样本单独确认,确认前不要批量替换。

先把读错的词逐个记下来,按数字、单位、多音字分三类

零散地听到“这里读错了”没有可操作性,建议先建一张表,把每次听出来的问题写进去。字段至少要覆盖:原写法、听到的读法、期望读法、出现次数、类型、上下文片段、初步处理方向。出现次数不是考核指标,它的作用是帮你排序:出现 5 次的错读值得写一条规则,只出现 1 次的可以手工改。

原写法听到的读法期望读法次数类型处理方向
2024年两千零二十四年二零二四年多次数字改写法:逐位读
A10086一万零八十六幺零零八六多次数字改写法:逐位读
3.5%three point five percent百分之三点五多次单位符号改写法:符号展开并调语序
25℃二十五 C二十五摄氏度偶尔单位符号改写法:符号展开
重启zhòng qǐchóng qǐ多次多音字换词:重新启动
行长xíng zhǎngháng zhǎng偶尔多音字换词:负责人

分类判定看错在哪一层:如果错的是“整个字符串该怎么读”(年份逐位还是按数值、单号要不要读成数字),归到数字;如果符号被忽略或被读成英文(% 读 percent、℃ 读 C),归到单位符号;如果字形没变但声母或声调错了(重、行、长、差、还、了),归到多音字。这个分类决定了后面的处理方式:数字和单位基本靠替换表能解决,多音字通常换词比加标注更稳,因为同形异音本身就是分词和词义判断问题,标注不一定被引擎采纳。

在生成前用替换表改文本:把数字写成想听到的读法

替换发生在“文本交给生成器之前”,所以它适用于两种入口:界面里手动粘贴,就先把脚本输出复制进输入框;接口批量提交,就把替换放在构造请求体之前,并同时保留原文和替换后文本,方便回溯。下面这张表是常见改写方向,按业务确认后写进规则。

场景原写法替换写法说明
年份2024年二零二四年逐位读,避免数值读法
单号、电话A10086A幺零零八六逐位读;含 0 时按习惯选“零”或“幺”
数量1500人一千五百人按数值读
小数3.5三点五小数点必须写成“点”
百分比3.5%百分之三点五符号展开后语序要提前
温度25℃二十五摄氏度符号展开
时间9:30九点三十分冒号在部分引擎里会被跳过
区间10-20十到二十连字符可能被读成“杠”或直接吞掉
速度单位60km/h每小时六十公里单位整体改成中文表述
多音字重启重新启动换同义词,不依赖标注

手工改几句没问题,句子一多就必须用脚本,而且要能打印出“改了哪里”。下面是一个通用骨架,把规则集中在一处,运行后先输出逐条 diff,人工确认后再提交生成。注意最后一条区间的规则会误命中日期 2024-05-06,这类反例要写进规则注释或干脆按字段单独处理。

HeyGen Voice 把数字和多音字念错、在文本里怎么标注才让它读对?
# replace_before_tts.py
import re

DIGITS = "零一二三四五六七八九"
UNITS = ["", "十", "百", "千"]

def int2cn(n: int) -> str:
    if n == 0:
        return "零"
    if n >= 10000:
        return "".join(DIGITS[int(c)] for c in str(n))  # 超范围退化为逐位读,按业务改
    s, out, zero = str(n), "", False
    for i, ch in enumerate(s):
        d, pos = int(ch), len(s) - i - 1
        if d == 0:
            zero = True
        else:
            if zero and out:
                out += "零"
            out += DIGITS[d] + UNITS[pos]
            zero = False
    return out[1:] if out.startswith("一十") else out

def num2cn(s: str) -> str:
    if "." in s:
        a, b = s.split(".", 1)
        return num2cn(a) + "点" + "".join(DIGITS[int(c)] for c in b)
    return int2cn(int(s))

def read_digit(s: str) -> str:
    return "".join(DIGITS[int(c)] if c.isdigit() else c for c in s)

RULES = [
    # (规则名, 正则, 替换函数或字符串)
    ("年份逐位读", r"(?<!\d)(\d{4})(?=年)", lambda m: read_digit(m.group(1))),
    ("编号逐位读", r"(?<=[A-Za-z])(\d{4,})", lambda m: read_digit(m.group(1))),
    ("百分比展开", r"(\d+(?:\.\d+)?)%", lambda m: "百分之" + num2cn(m.group(1))),
    ("摄氏度展开", r"(\d+(?:\.\d+)?)\s*℃", lambda m: num2cn(m.group(1)) + "摄氏度"),
    ("区间连字符", r"(?<=\d)-(?=\d)", lambda m: "到"),  # 反例:日期 2024-05-06
    ("多音字规避", r"重启", "重新启动"),
]

def apply(text: str):
    hits = []
    for name, pattern, repl in RULES:
        def _sub(m, name=name, repl=repl):
            new = repl(m) if callable(repl) else repl
            hits.append((name, m.group(0), new))
            return new
        text = re.sub(pattern, _sub, text)
    return text, hits

def run(path: str):
    lines = open(path, encoding="utf-8").read().splitlines()
    report = []
    for i, line in enumerate(lines, 1):
        new, hits = apply(line)
        if hits:
            report.append(f"--- 行{i} 原:{line}")
            report.append(f"+++ 行{i} 新:{new}")
            for name, a, b in hits:
                report.append(f"    [{name}] {a} -> {b}")
    out = "\n".join(report)
    open("diff_report.txt", "w", encoding="utf-8").write(out)
    print(out)

if __name__ == "__main__":
    run("script.txt")

验证方式不是看脚本有没有报错,而是看 diff 内容对不对:每一条命中的原串、新串都要人工过一遍,重点看三类误伤——本来该按数值读的数量被逐位读了、日期被当成区间、英文字母后面的编号被截断。确认无误后,把替换后的文本提交生成;同时把 diff_report.txt 留一份,后面判断“这次读对了是因为替换还是因为碰巧”时要用到它。

同一句话用原写法和替换后写法各生成一遍,对照听差异

这一步是为了排除巧合:有些文本第一次读错、第二次读对,原因可能是分段方式变了、上下文变了,而不是你的处理起效。做法是固定音色、语速、语言设置,只改文本写法,各生成一遍,然后按下面的表记录。

句子原写法结果替换写法结果是否稳定判定
S1两次都读成“两千零二十四”两次都读成“二零二四”稳定规则有效,入词表
S2第一次错、第二次对两次都对原写法不稳原写法不可依赖,保留替换
S3两次都错一次对一次错不稳定替换无效,按排查顺序查
S4—两次读法一致但仍不对稳定但错误替换词选错,换同义词

判定标准就两条:同一设置下,替换后的文本重复生成结果一致;并且这个结果与期望读法一致。两个条件缺一不可。如果替换后读法稳定但依然不是想要的,说明问题不在“要不要替换”,而在“替换成了什么”,这时换同义词往往比继续加标注更快。

确认替换无效时,按这个顺序排查,先便宜的、先能证伪的:

HeyGen Voice 把数字和多音字念错、在文本里怎么标注才让它读对?
  1. 确认提交的确实是替换后的文本——打印请求体或对照输入框内容,排除复制漏行、脚本没跑到这一行。
  2. 确认音色、语言、语速设置与上一次完全相同,中英混排的文本在部分音色下会整段按另一种语言读。
  3. 确认平台没有做二次规范化,比如把中文数字又转回阿拉伯数字、把符号还原成缩写。
  4. 确认标点、换行、空格没有把词切断,加空格有时会改变分词,反而制造新错读,这一点要靠对照试听确认,不能凭感觉。
  5. 确认平台是否支持发音词典或 SSML 类能力,若支持,用最小样本单独测,测通再接入。
  6. 以上都不成立,就换词规避,别在同一条规则上反复调。

把确认有效的替换规则存成词表,下次生成前先跑一遍

对照试听通过之后,把这条规则从“临时改动”升级成词表条目。建议用 CSV 或 YAML 存,字段包含:规则名、正则、替换内容、类型(数字/单位/多音字)、示例输入、示例输出、反例、状态(待验证/启用/停用)、备注。反例这一列不要省,它是防止下一个人把规则写宽的护栏;停用比删除好,出问题时能快速回滚。

冲突处理有三条实用约定:同一段原串只允许有一条启用规则;正则尽量加边界,比如数字前用 (?<!\d)、年份后加 (?=年),避免“3.5%”的规则吃掉“v3.5%”;多条规则同时命中时按“长匹配优先、具体优先于通用”的顺序执行,并在 diff 里全部打印出来,不要静默合并。新增条目先进“待验证”,跑完对照试听再改成“启用”。

脚本跑完不要直接提交,先看输出报告。可以把下面这段当作检查清单,逐项确认:

== 替换运行报告 ==
输入文件:script.txt,共 42 行
启用规则:7 条,命中 19 处
命中了停用规则:0 处          # 大于 0 需检查状态字段
替换后仍含阿拉伯数字:3 处(行 5、11、12)   # 逐行确认是有意保留还是漏规则
替换后仍含 % ℃ / : 等符号:1 处(行 8)
替换前后长度变化超过 20% 的行:2 处          # 人工确认语序没被改乱
多音字规则命中但未见反例说明:1 条          # 建议补反例再启用
diff 已写入 diff_report.txt

这份报告的作用是把“我记得改过”变成可核对的记录:哪些行改了、还剩哪些字符没处理、有没有规则被停用却仍被别的规则间接命中。报告里的每一项都来自脚本实际处理结果,不需要额外统计工具。只要保持“先跑词表、看 diff、再提交生成、抽听几句”这个顺序,同一个错读通常只需要修一次,后面都是在已有词表上追加条目。