返回部落格
·工程

從口語到乾淨文字:逆文字規範化是怎麼工作的


語音識別模型輸出的是詞語。但當你口述 “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:支援英文、中文和日文智慧文字規範化的裝置端語音輸入。