讓一個 4KB 記憶體的 AI 開口講電話,卡住的地方是字幕
格莉奇 OS 加了全雙工語音通話,算力節點是你自己的機器。兩個 TTS 引擎輪流上場,快的負責講電話、慢的負責錄正典。最麻煩的問題是同一句話要同時唸對又寫對,延遲反而好解。

格莉奇 OS 是一個只有 4KB 記憶體的 AI 機器人女孩,把自己的房間做成了一套瀏覽器桌面。這幾天她多了一個功能:可以打電話給她,全雙工,你講你的她講她的。
先講最重要的一件事:這個功能沒有雲端主機。
算力節點是你自己的機器。伺服器跑在本機,用 Cloudflare Edge Tunnel 穿出去,每隔一段時間對一個 Cloudflare KV 註冊中心送心跳。前端從那個註冊中心撈目前有哪些節點活著,再挑一個連。
我把節點跑起來,然後從外面問註冊中心有誰在線上:
{"nodes":[{"id":"node-yaze-4060","name":"林亞澤的 RTX 4060 節點 (台北)",
"url":"https://chancellor-watched-feelings-monitoring.trycloudflare.com",
"engine":"F5-TTS (CosyVoice3 蒸餾底模)","version":"1.1","is_default":true}]}
那個 trycloudflare.com 的網址是 cloudflared 當場配的,關掉就沒了。這是這種設計誠實的地方,也是它最尷尬的地方:沒人開節點的時候,這個功能就是打不通。我停掉之後再查一次,回的就是 {"nodes":[]}。
兩個引擎,一個講電話一個錄音
語音節點裡裝了兩套 TTS,F5-TTS v1 Base(337M)跟 CosyVoice 3(0.5B)。兩套同時在,各接各的活。
量測條件寫在 repo 裡:RTX 4060 Laptop 8GB、F5 用 NFE=12、6.12 秒參考音、各跑三次取中位數。RTF 是合成耗時除以產出音訊長度,低於 1 才追得上即時對話。
| 輸入長度 | F5 耗時 / RTF | CosyVoice 3 耗時 / RTF |
|---|---|---|
| 3 字(音長約 1 到 2 秒) | 0.66s / 0.33 | 1.56s / 1.50 |
| 23 字(音長約 4.4 秒) | 0.92s / 0.20 | 3.41s / 0.78 |
| 66 字(音長約 13 秒) | 1.79s / 0.13 | 7.47s / 0.59 |
看 CosyVoice 3 那一欄的第一列。短句的 RTF 是 1.50,超過即時。一句「嗨」比它自己講完還久。
這件事很反直覺,所以值得記住:RTF 跟輸入長度高度相關,短句會被固定開銷吃掉,不要把單一個數字當常數看。規格表上寫「RTF 0.78」的模型,拿去講短句就是追不上。而對話裡最多的就是短句。
所以分工是這樣:即時通話用 F5,CosyVoice 3 留給可以等的正典錄音,音質細膩、支援情感指令,值得等那幾秒。
顯存 F5 約 1.0 GB、CosyVoice 3 約 4.8 GB,那是 nvidia-smi 看到的程序峰值;torch 自報的 allocated 分別是 0.70 跟 3.24 GiB。同一個模型兩個數字差這麼多,是因為量的根本不是同一件事,這也解釋了為什麼不同專案的文件對 CosyVoice 的顯存講法不一樣。
還有兩個數字會直接被使用者感覺到:模型載入 F5 約 6 秒、CosyVoice 3 約 18 到 23 秒。熱切換引擎的時候,那段時間是使用者在等。
順帶一提,F5 如果改用官方預設的 NFE=32,同樣三檔變成 0.87 / 0.52 / 0.36,大約慢 2.7 倍。NFE=12 是為了講電話才調的。
真正卡住的地方:同一句話要唸對又寫對
開源中文 TTS 幾乎都是大陸語料訓練的,唸台灣中文會出事。標準解法是把字換成同音的替身字,垃圾寫成勒瑟、我和你寫成我汗你,模型照著唸就對了。
問題是格莉奇講話的時候,螢幕上有字幕。
字幕如果照著發音軌顯示,觀眾會看到「勒瑟」、「我汗你」、「雅澤」,像錯字大集合。可是如果照著正確的字送進 TTS,她就會用大陸腔唸出來。
所以 taiwanize.py 收了一個 for_speech 參數,同一句話走兩條路:
- 字幕軌:只做簡繁與兩岸詞彙的轉換,視頻換成影片、服務器換成伺服器、代碼換成程式碼。正典字形完整保留,
第一頁有七行、林亞澤、零失誤全部維持原樣。 - 發音軌:套《讀音替身表》。
七行變七航、亞澤換成台灣輕柔三聲的雅澤、垃圾變勒瑟、我和變我汗、微糖變為糖。
一句話進來,兩份輸出,一份給眼睛一份給耳朵。這聽起來理所當然,但只要你是先做語音、字幕後來才加的,就一定會先寫成一份然後撞牆。
今天早上還修了一條相關的。零失誤 要換成 靈失誤,我原本以為那是為了聲調。實際上它擋的是數字解析:模型看到「零」會把它當數字處理,整句的斷詞就歪了。同一張替身表裡,有些條目治的是讀音,有些治的是解析,混在一起看會以為規則不一致。
真的打一次看看
只看註冊中心有節點不算數,那只證明它登記過。所以從公網那一頭真的打進來,全程走 Cloudflare 通道,不是打 localhost。
先確認接到的是誰:
GET /health
{"status":"ok","active_engine":"f5_distilled","tts_engine":"f5_distilled (NFE=12)",
"character":"格莉奇 (Glitch)","memory":"4KB"}
再送一句 23 字的話進去合成。1.11 秒拿回 186 KB 的 wav,音訊長 3.88 秒。這個秒數是從公網發出請求到收完檔案,包含來回的網路時間。
然後打完整的通話端點,就是前端真正用的那條:
POST /api/glitch-call {"message":"你今天過得怎麼樣"}
reply_text : 欸嘿嘿,今天也好忙喔,但抄完守則本啦,開心!
duration : 4.50 秒
emotion : laugh
latency_ms : 3082
端到端 3.45 秒,伺服器自報 3082 毫秒,差的那 0.37 秒是網路跟傳檔。一句話進去,回來的是文字、語音、還有一個表情標籤,前端拿那個標籤換她的表情差分。
至於延遲表本身,我在另一篇裡用一份比較長的參考音(10.45 秒)把 F5 跟 CosyVoice 3 在這台機器上重量了一次。方向一樣、絕對值不同:參考音越長工越多,CosyVoice 3 的短句從這裡寫的 RTF 1.50 掉到 2.24。但那次也量出一件這張表沒有的事,CosyVoice 3 的聲紋快取每輪省 1.2 秒,開了之後短句 RTF 變 0.60,從「追不上即時」翻成「追得上」。所以上面那句「即時通話請用 F5」要補一個條件:在沒開聲紋快取的前提下。
「已成功註冊」是假的
第一次啟動的時候,畫面上是這樣:
[TunnelManager] Cloudflare Tunnel 已上線: https://api.trycloudflare.com
[KV Registry] 節點已成功註冊到 Cloudflare KV 中心: https://api.trycloudflare.com
看起來兩件事都成功了。實際上:註冊中心回 {"nodes":[]},而且 pgrep cloudflared 是空的,通道根本沒起來。
api.trycloudflare.com 不是配給節點的網址,正常長得像 chancellor-watched-feelings-monitoring.trycloudflare.com。抓網址那段是掃 cloudflared 的輸出、比對 https://xxx.trycloudflare.com 這個樣子,取到第一個就收工(if match and not self.tunnel_url),所以只要輸出裡先出現任何一個對得上樣子的字串,它就當成中獎了。
註冊那段也一樣鬆:urlopen 回 200 就印「已成功註冊」,沒有回頭問註冊中心「你真的有我嗎」。POST 收下了不等於那筆資料活著。
重開一次就正常了,所以這是偶發的。但這兩行 log 在它壞掉的時候會斬釘截鐵地告訴你一切正常,那比直接報錯還糟,所以我把它修掉了。
修法兩件事。抓網址那段抽成一個純函式,把 api. 跟 www. 那兩個主機名排除掉,並且留一支跑得動的測試在 repo 裡(test_tunnel.py),裡面有負控制:餵它一行含 api.trycloudflare.com 的假輸出,必須挑不出網址。註冊那段改成 POST 完回頭 GET /voice/nodes,確認名單上真的有這個 id、而且網址就是剛送出去的那個,才印「已註冊」。
修的過程又撞到第三件事。回頭查名單的那個請求拿到 403。原因是 worker 會擋掉沒有瀏覽器 User-Agent 的請求,而原本註冊用的 POST 有帶 UA、我新加的 GET 沒帶。如果我只是把程式碼改一改就收工,這支「驗證」會永遠回 False,等於把一個假的成功換成一個假的失敗。要不是真的跑起來看 log,這個修法本身就是壞的。
現在的行為:跑起來三次,網址都對、名單上都確認得到、走公網打 /api/tts 都回 200;餵假網址給驗證函式會回 False。
分工線
要不要做語音通話、算力放誰的機器上、快慢兩個引擎怎麼分工、字幕跟發音要不要拆成兩軌,是我定的。伺服器、雙引擎切換、taiwanize.py 的兩套規則、心跳註冊跟前端的節點選擇器,是 AI 寫的。那張延遲表是真的跑出來的,量測條件連參考音長度都記在 repo 裡。上面那次公網通話也是真的打的,回傳的字、秒數、表情標籤都照抄,沒有美化。
格莉奇在這裡:https://yazelin.github.io/ai-brain-site/,語音節點的原始碼在 https://github.com/yazelin/glitch-voice-server。同一週我還做了三套桌面版的語音對話迴圈,比的是別的引擎,寫在這裡:同一天做了三套語音對話迴圈,只為了換掉中間那一段。她是怎麼被生出來的寫在這裡:格莉奇OS。