今天下午,Codex 把一句測試句丟給 Qwen3.5-4B:
「報告先寄給怡安句號等她回覆」
它回來的是:
「報告先寄給怡安。」
「句號」被轉成句號,這是我要的。但「等她回覆」整段不見了。換 seed 跑四次,四次都一樣。
這一句大概就是 Voco 這個工具的全部問題,也是我到今天還在維護它的理由。
先講 Voco 是什麼
我的部落格提過 Voco 好幾次,但從來沒有一篇是在講它。
Voco 是我從 VoiceInk fork 出來的 macOS 語音輸入工具。VoiceInk 是 Pax 做的開放原始碼專案,語音辨識全部在本機跑。我用它之前是 Typeless,Typeless 出了隱私爭議之後換過去,後來乾脆自己改。
會走到自己改,理由跟一般人不一樣。我是 CP,全身能穩定操作電腦的只有右手食指。語音輸入對我不是打字打累了換一下,是主要的輸入方式。而我的肌肉張力讓發音結構跟別人不同,一般模型聽我講話,錯的方式很固定:捲舌音會掉,ㄥ 跟 ㄣ 會混,英文技術詞有一半聽不出來。
所以 fork 之後改的東西,幾乎都是為了這件事:
- ASR 核心從 Whisper 換成 Qwen3-ASR,用 MLX 在 Mac 上跑,再掛一個用我自己的錄音訓練出來的 speaker adapter。
- 中文後處理:OpenCC 簡轉正、拼音同音字修正、口語標點、重複偵測。
- Edit Mode:選一段文字,用講的改它。
- 語音指令。「全部刪除」這種話是指令,不能被當成內容修掉。
- 偵測前景 app 和視窗標題,同一句話在 Codex 裡和在 LINE 裡有不同處理。
- RNNoise 降噪、正體中文介面、拿掉付費授權。
Voco 跟上游一樣是 GPL,repo 是公開的。但我從來沒有 PR 回去。那些改動只對我有意義,PR 回去只是製造 review 負擔,這件事我在講開放原始碼社會契約那篇寫過。
聽這一層已經到邊界了
辨識這一層,adapter 能做的已經做完。日常國語句子的 CER 大約 0.02,錯的多半是標點跟簡繁。困難句的 CER 從 0.201 降到 0.174,之後再訓練幾版都降不下去。英文技術詞更慘:33 句困難句裡 35 個英文詞只認出 18 個,而且有些是聲學上就分不開的。我真的講 Brunch 的時候模型偏好 Branch,真的講 Branch 的時候模型偏好 Brunch。
這種錯再訓練也沒用,只能靠規則。
但規則要怎麼改字,才是 Voco 過去半年真正在處理的事。
現在的分層是這樣。Qwen3-ASR 加 adapter 負責聽,聽完的 raw transcript 一定保留。上面一層是 deterministic 的 auto-apply 規則,每一條規則都要有證據,也就是我確認過的錯誤紀錄,要有 context 鎖、有負例 guard,而且在動 app 之前先對整個資料庫 replay 一遍。再上面才是 LLM,而 LLM 的 prompt 六月就從「幫我修 ASR 錯誤」降級成「保守的排版器」。
六月那時我跟 Codex 講明的一件事:新的模型不能因為比較新就拿到比較高的優先權。優先權給證據跟 replay gate。
實驗室
證據跟 replay 不在 Voco 裡,在另一個 repo:VocoReplayLab。它是 private 的,裡面是我的錄音跟資料庫 snapshot,不會公開。
它做的事很單純:把 Voco 的資料庫 snapshot 拿出來,用現在的模型跟規則重跑一遍,看哪些字被改了、誰改的、改對還是改錯。有幾組固定的關卡:33 句我親自標過的困難句、22 句代名詞句、493 句日常句,加上幾筆我指定的關鍵錄音。任何 adapter 或規則要進 Voco,先過這幾關。
過不了就留在實驗室。六月那個文字清理 LoRA,有一版 held-out 分數更好,卻把「吉他譜」改成「GitHub 譜」,那一版直接擋掉;最後留下來的版本也只在實驗室做 shadow,沒有進 app。9/6 兩組新的 adapter 各只修好一句,其中一組把原本對的一句改錯,兩組都不裝。
規則也會死。9/7 發現一條規則是為了一個已經拿掉的第二次解碼路徑寫的,前後段都零觸發,就 tombstone 掉。目前 active 的規則有八百多條,每一條都能查到當初是哪一筆紀錄撐起來的。
實驗室的意義是:Voco 本體不用相信任何一個模型,它只要相信 replay 的結果。
然後上游加了 Refine
回到今天。
VoiceInk 上游這幾個月更新很多。我先把穩定性修補挑進來:錄音生命週期、音訊佇列的背壓、Esc 取消的競態、詞界判斷的 Unicode 修正。這些跟我的客製沒衝突,Codex 分兩批做完,測試過了,部署,重開。
比較難決定的是 Refine。上游用 Qwen3.5-2B 微調了一個本機文字整理模型,接在 ASR 後面,處理標點、贅詞、自我更正、口述換行。聽起來就是我要的東西。
但它是整段重新生成。模型讀進一段文字,吐出另一段文字,中間發生什麼事沒有人知道。
我沒有用上游的微調權重,直接拿官方 Qwen3.5 測。2B 會把問句變成陳述句,換 4B。BF16、8-bit、6-bit 三種都跑,結果 6-bit 跟 BF16 幾乎一樣,量化不是問題。問題就是開頭那句:「等她回覆」不見了。另一句「question mark 這個字串出現在規格裡」,它把 question mark 整個吃掉,剩「這個字串出現在規格裡」。
後來換了一個做法:模型不准重寫,只准判斷。程式先把「句號」「換行」「我我」這種可疑片段圈出來,模型回答每一段是格式指令、口吃還是正文,最後由程式在原文那個位置替換。這樣模型就沒有機會刪掉「等她回覆」。
漏文確實沒了。但判斷還是會錯。「不要換行,這是一條正文規則」被它做成真的換行;「下一段程式碼需要原樣保存」的「下一段」被吃掉;「等等,那其實」變成「等,那其實」。同一組保留測試,整段重寫反而保住 18 句,局部判斷只保住 14 句。
所以今天的結論是:不接。4B 6-bit 的模型留在硬碟上。
今天真的進 Voco 的東西
只有一個很小的改動:句子邊界的「我我」「你你」會被刪成一個。「那個那個」不動,「等等」「看看」不動,引述跟改口不動。
連這個都在 review 時抓到一個反例。Codex 原本把「可以」也加進去,Claude review 時丟了一句「可可可以喝」,「可可」被刪掉了。拿掉。
316 個測試過了才部署。
為什麼這麼保守
因為改錯的成本不對稱。
一個沒刪掉的「那個」,我看到會有點煩,但意思還在。一個被刪掉的「等她回覆」,我要用右手食指把它打回來,而且要先發現它不見了。發現這件事本身就要成本,因為整句讀起來是通的。
所以 Voco 的規則從來不是輸出要多乾淨,是每一個被改的字,都要說得出是哪一層改的、憑什麼改。adapter 改的,有 gold-33 的數字;規則改的,有 replay 紀錄跟證據 ID;LLM 改的,只准動排版。
Refine 說不出來。它的每個輸出都可能對,但你查不到是誰改的、憑什麼改。
下一步大概是做一個專門的口述格式模式,講「換行」的時候我自己知道現在在下指令,而不是讓模型猜。多一個模式要切換,這對我也是成本。還沒決定。
