中文語音合成:文本規範化與工程 是做什麼的?
這個 skill 來自一個天天在說話的 AI 桌寵(見文末「來源與維護」):Claude 串流回覆,逐句交給本機 TTS 唸出來。下面的規則都有測試,數字都是量過的。
一句話原則:唸法在自己這層轉好,引擎只管發音。 交給引擎的話,換一個引擎唸法就變;整個換成現成的 TN 庫,也有幾處會變差。現成庫(wetext、PaddleSpeech)拿來對照、補規則。
流程
模型的串流文字
→ ① 過濾:去掉裝飾、emoji、列表符號;程式碼區塊、表格、鏈接換成一句話(串流時要能認出被拆開的)
→ ② 轉唸法:數字、時間、單位、符號、唸法替換表(串流時扣住邊界)
→ ③ 切句:第一段切到第一個逗號,之後超過 40 字在逗號處切
→ ④ 合成:只比唸到的地方提前約 20 秒;打斷時真的停掉推理
順序不能換:先轉唸法,再切句。 先切句、後清理時實測出三個錯:
| 原文 | 先切句的結果 | 問題 |
|---|---|---|
下午14:30開會 | 下午14: / 30開會 | 冒號被當成逗號,時間被切開 |
看這裡:https://…/pull/7 就行 | https: / //…/pull/7 就行 | 鏈接被切成兩半,地址被逐字唸 |
大概3~5天 | 大概35天 | ~ 被當裝飾去掉,唸成「三十五天」 |
半角 : 只在後面跟空白時才當逗號。
串流要扣住邊界
整句轉對了,串流不一定對。逐字串流時「工作了26年」「80年代」被唸成「二六年」「八零年代」,因為數字後面的字還沒到;-10°C 那段被當成英文原樣唸了。
- 數字後面再等 2 個字才放出去;單位隔著空格也要能認(
跑了 5先扣住,等km)。 - 轉下一段時帶上上一段末尾幾個漢字(判斷「了 26 年」要看到前面的「了」)。
- 最多扣 200 字,程式碼區塊、表格最多扣 4000 字,到上限就放。
- 每條測試都按「逐字餵」和「整段餵」各跑一遍,結果要一樣。
規則速查(詳見 references/rules.md)
| 類 | 例子 | 唸法 |
|---|---|---|
| 年份 | 2026年、2026屆、26年見 | 逐位:二零二六年、二零二六屆、二六年見 |
| 年數 | 工作了26年、80年代、26年前 | 照數量:二十六年、八十年代、二十六年前 |
| 號碼 | 電話、驗證碼、編號27149、房間1201 | 逐位,1 唸「幺」,橫槓和空格處停一下 |
| 五位數 | 95588 / 價格 12999 | 沒量詞、不以 0 結尾、前面不是說錢的 → 號碼;否則照數量 |
| 網路用語 | 666、2333、520 單獨出現 / 666個讚、花了888 | 逐位 / 照數量(只認一張固定單子) |
| 單位前的 2 | 2個、2L、2℃ | 兩個、兩升;溫度除外:二攝氏度 |
| 單位 | 60km/h、3kWh、5min、10mg | 每小時六十公里、三度電、五分鐘、十毫克 |
| 不轉的單位 | 4g網路、5A景區、16GB | 原樣交給引擎 |
| 範圍、百分比 | 3~5%、3-5km、-1.5~2 | 百分之三到五、三到五公里、負一點五到二 |
| 時間 | 14:30、13:00、9:05 | 十四點半、十三點、九點零五(24 小時制) |
| 日期、分數 | 10/26 交 / 有7/12的觀眾 | 十月二十六號 / 十二分之七(前面是「有、佔、約」才當分數) |
| 符號 | #15、2x、x3、100w | 十五號、兩倍、乘三、一百萬 |
用對照用例檢查
references/golden-cases.json 有 115 條整段用例和 24 條串流用例,都是原項目驗收過的唸法。scripts/check-tn.mjs 拿它們測你的實現:
node scripts/check-tn.mjs ./my-tn.mjs # 模組匯出 normalize(text) 和(可選)createStream()
node scripts/check-tn.mjs ./my-tn.mjs --only 號碼 # 只看某一組
- 比較時忽略空白和常見標點,只比唸法。帶
note的用例是「參考」,只報出來不算失敗。 - 新寫 TN 時把它當驗收線;改規則時跑一遍,防止改好一處、壞了另一處。
- 有爭議的唸法(比如「這台電腦 12999」會被當成號碼)原項目是接受了的。你的產品想要別的唸法,就改用例,並在用例旁寫為什麼。
唸錯了怎麼查
分四類:唸之前沒寫成唸法;多音字是模型猜錯;參考錄音有問題;隨機的(重抽一次就好)。
用當前聲音和另外兩個聲音各唸同一句:每個聲音都在同一個字上錯,就加唸法替換表;只有一個聲音怪,就換聲音。每次把原文和轉出來的唸法一起記進日誌(jsonl),回頭查得到。
工程(詳見 references/engines.md)
- 首句延遲: 第一段只切到第一個逗號。Matcha 第一個字到達後 0.65 秒出第一段音訊。
- 最有效的提速是快取: 沒合成過的句子 CosyVoice 首塊 2.6–3 秒,命中快取 6 毫秒。使用者一開口就預合成墊話(「嗯」「我想想」)。
- 取消要真的停掉推理: vLLM 連線一斷就
abort_request,否則下一句要等 4.9 秒(做了以後 0.8–1.7 秒)。 - 英文會被靜默跳過: 有的引擎不唸
GitHub、PR、%;依賴升級前重新驗證英文。 - 備援音源: 主音源沒就緒或一開口就出錯,就換備援;唸到一半出錯不重唸。服務的 HTTP 起來了不等於模型載入好了,遇到 503 要重試。
來源與維護
- 來源: Cirra(AI 桌寵,RedCarpG/cirra-deskpet)和它的語音服務 cirra-voice,2026-10 至 2026-11。
- 程式碼:
sidecar/src/tts/speech/(numbers.ts、units.ts、links.ts、filter.ts、overrides.ts、speech.test.ts)、sidecar/src/tts/splitter.ts、sidecar/src/tts/pronunciations.ts。 - 文件:
docs/speech.md、docs/speech-tn.md(119 句三方對照)、docs/tts.md、docs/voices.md、docs/voice-chat.md、docs/voice-chat-2.md、docs/voice-install.md、docs/perf-checkup.md。 - 經驗:私人 repo 的
lessons/chinese-tts.md。
- 程式碼:
- 源頭: RedCarpG/redcarpg-skills 的
skills/chinese-tts/。~/.claude/skills/裡的是安裝出來的副本,不要直接改。references/golden-cases.json是 2026-11-10 從 Cirra 的speech.test.ts用腳本抽出來的,兩邊不會自動同步。 - 怎麼改:
- 在 redcarpg-skills 裡改,走 PR;
manifest.json版本加一,node scripts/validate.mjs通過後發版(tagchinese-tts-v<版本>),再重裝本機副本。先看lessons/chinese-tts.md有沒有新條目可以收進來。 - Cirra 的 TN 規則有變化時,重新抽一次用例(抽的辦法:讀
speech.test.ts裡readNumbers那組和「串流」那組的[原文, 唸法]),並用 Cirra 編好的sidecar/dist/tts/接一個適配模組跑check-tn.mjs,應該全過。 - 引擎的數字(延遲、即時率、視訊記憶體)是 2026-10 在一台 8 GB 視訊記憶體、CPU 沒有 VNNI 的電腦上量的,換了硬體要重新量。
- 在 redcarpg-skills 裡改,走 PR;
- 更新記錄:
- 2026-11-12 · 1.0.0,搬進 redcarpg-skills 公開發佈。改掉本機絕對路徑,來源項目寫成連結;內容不變。
- 2026-11-10 · Cirra · 初版:流程和順序、串流邊界、規則速查和細則、唸錯排查、引擎工程;139 條對照用例和
check-tn.mjs(用 Cirra 的實現跑過,全過);evals/裡兩個測試提示。