从口语到干净文本:逆文本规范化是怎么工作的
语音识别模型输出的是词语。但当你口述 “the meeting is on January fifteenth at two thirty PM and the budget is three thousand dollars” 时,你并不想看到这些词逐字展开。你真正想看到的是: “The meeting is on January 15th at 2:30 PM and the budget is $3,000.”
把口语形式转换成书面形式,这件事叫做 inverse text normalization, 简称 ITN。它正好和文本转语音(把书面文本转换成口语形式)相反。 而且它就是那种“做对了没人注意,做错了会非常烦人”的能力。
ITN 覆盖什么
ITN 处理的转换范围,比大多数人意识到的要广得多:
- 数字。 "forty two" → "42", "three point one four" → "3.14", "negative seven" → "-7"
- 货币。 "three thousand dollars" → "$3,000", "fifty euros" → "€50"
- 日期和时间。 "January fifteenth twenty twenty six" → "January 15th, 2026", "two thirty PM" → "2:30 PM"
- 序数。 "the third item" → "the 3rd item"
- 单位。 "five kilometers" → "5 km", "twenty degrees celsius" → "20°C"
- 标点。 "comma" → ",", "period" → ".", "question mark" → "?"
- 语音命令。 "new line" → 实际换行, "colon" → ":"
这些单独看都不复杂,真正的复杂度来自歧义。“one” 到底是数字 1, 还是代词?“May” 指的是月份,还是情态动词?“dash” 到底应该变成连字符,还是保留单词 “dash”?
为什么我们要自己做一套引擎
大多数 ASR 提供商都会在云端管线里附带基础 ITN。但如果你做的是设备端处理, 那你同样需要设备端 ITN,而现成选择其实很少。现有的开源 ITN 库大多基于 Python,偏批处理设计,而且主要聚焦英文。
我们需要的是另一种东西:
- 实时性能。 ITN 会运行在每一个流式转写 chunk 上。 它必须在微秒级处理文本,而不是毫秒级。
- CJK 支持。 中文和日文拥有完全不同的数字系统、 标点习惯和格式规则。“三千美元” 也必须像 “three thousand dollars” 一样被规范成 “$3,000”。
- 原生集成。 我们需要一个能在 Swift macOS 应用中直接运行的库,而不是得打包 Python 运行时或通过子进程桥接。
有限状态转导器
我们的 ITN 引擎建立在有限状态转导器(finite state transducers,FST) 之上。FST 是一种状态机:读取输入序列,产生输出序列。对 ITN 来说, 输入是一串口语词语,输出则是规范化后的书面形式。
相比正则或基于规则的字符串替换,FST 的关键优势是可组合性。 你可以为单个转换构建小型 FST,比如一个处理基数词、一个处理日期、 一个处理货币,然后把它们组合成一个统一转导器, 在规则重叠时还能通过确定性的优先级顺序来处理冲突。
我们用 Zig 写了 FST 运行时库 libfst。Zig 在这个场景里有几个非常关键的点: C ABI 兼容(这样库可以直接链接进 Swift 应用)、状态机转移的零成本抽象, 以及对内存分配的精确控制,不会在实时文本处理过程中冒出垃圾回收暂停。
规则编译管线
ITN 规则本身是用 Python 以声明式转换规范来编写的。 每条规则描述一个模式(输入词语)和一个替换结果(输出文本)。 Python 工具链会把这些规则编译成二进制 FST 文件, 也就是紧凑、优化后的转导器表示,供 Zig 运行时加载和执行。
这种拆分让我们能很快迭代规则。Python 很适合表达语言学模式, 而运行时仍然足够快。想新增一种数字格式或货币符号, 只需要改 Python 规则文件并重新编译,运行时二进制本身不用变。
目前我们提供中文、日文和英文的编译后 FST 规则文件。 每种语言都有自己的一套规则,因为规范化约定差异非常大。 日文使用全角标点和不同的量词;中文数字按万位分组,而不是千位; 英文则在序数词和 12 小时制时间上有自己的细节。
语音命令
ITN 还负责处理语音命令,也就是那些映射成键盘动作而不是字面文字的口语短语。 当你在听写时说出 “new line”,你要的是真正的换行,而不是单词 “new line”。 说 “comma” 时,你要的是标点符号。
这就带来了一个有意思的歧义消解问题。ITN 引擎需要判断 “new line” 到底是一条语音命令,还是句子的一部分,例如 “this is a new line of products”。我们的处理方式是结合上下文:语音命令通常出现在特定句法位置, 比如停顿之后或分句边界处,而 FST 的优先级顺序会确保在这些模糊位置优先采用命令解释。
对于 CJK 语言,语音命令也是本地化的。中文用户会说 “换行” 表示 newline, 说 “逗号” 表示 comma。FST 规则是按语言分别定义的,因此每个 locale 都有自己的自然命令词汇。
看不见的功能
好的 ITN 是看不见的。你自然说话,出来的文字自然正确。 数字就是数字,日期就是日期,标点出现在它该在的位置。 一旦你不得不手动修正 “$three thousand”,或者删掉文本里字面出现的 “new line”,幻觉就破了。
我们在一个“做得完美时没人会注意”的功能上投入了大量工程精力。 这正是重点。最好的语音输入体验,就是让你忘记自己并不是在打字。
试试 OnType:支持英文、中文和日文智能文本规范化的设备端语音输入。