小黃開發日記 04|搬回家,長出聲音和臉(2026-07-26~08-23)

總結摘要
從 Railway 搬回 Mac 常駐,接上語音,把延遲從 27 秒壓到 7 秒。也在這裡立下最重要的一條紅線:能力優先於速度。

7 月 26 日,她從 Railway 搬回家,在一台 Mac 上用 launchd 常駐。資料都在本機,也終於能碰到麥克風和喇叭。接下來一個月,她長出了耳朵、嘴巴和臉:v1.1.0 是語音和 Mac 桌寵,v1.2.0 是第二個殼 iPhone app。

ZOE 搬回家了。

語音管線

1
2
3
麥克風 → VAD(判斷你講完沒)→ whisper.cpp 本機轉文字
      → 分流器 → 主腦(同一個小黃、同一份記憶)
      → TTS 逐句串流合成 → 喇叭
  • 聲音不上傳:辨識在本機跑 whisper.cpp。
  • 喚醒詞:喊「小黃」或「Zoe」就開始聽。喚醒用 small 模型,指令用 large-v3-turbo。tiny 太不穩,同一段錄音每次解出來都不一樣。
  • VAD 門檻:拿五段真實錄音掃六個門檻,定在 0.1,靜音判斷從 500ms 拉到 900ms。我一度推薦 0.15,結果門檻調高會吃掉你的字。
  • 追問視窗:她回完後短時間內可以直接接著講,不用每輪重喊名字。

延遲:從 27 秒到 7 秒

第一版語音,「你講完到她開口」要 24 到 27 秒,根本沒辦法對話。量時序 log 才找到真兇:

分流器想了 418 個字,然後輸出 7 個字。

分流器只需要輸出一個角色代號,prompt 也寫了「不要解釋」,但 cli 路徑從來沒關過 extended thinking,SDK 預設開著。主腦也一樣,為了「介紹一下自己」想了 409 字,你最後只聽得到 60 到 90 個字。

最終成果:

之前之後
分流7~14 秒2.5~3.3 秒
第一句出聲3.75 秒1.14 秒
整輪24~27 秒7~14 秒

四件有效的改動裡,三件是量出來的。我自己想的三個方向,全部被數字打掉。

最重要的一課:能力優先於速度

關掉分流器的思考後,真機三題立刻錯兩題。「你喜歡看 Spider-Man 嗎」被分到個股分析,她戴著分析師的帽子聊電影。

原來思考本來在幫兩個洞擦屁股:

  1. 分類器全程看不到上下文:history 一路傳進去,卻沒被用上。
  2. prompt 只有規則,沒有例子也沒有反例。小模型看到專有名詞就往股票猜。

把最近 4 則前文餵給分類器,補上「作品名、人名不是標的」等反例之後,又快又準。這件事變成專案寫在 CLAUDE.md 裡的紅線:

「變慢」使用者可以接受,「她變得很奇怪」不行。

純浪費的開銷儘管砍;會動到她能力的(拿掉工具、砍上下文、換小模型、關思考),就算非動不可,也要留 env 旋鈕能切回來、真機驗證沒退步,並想清楚原本那個開銷在補什麼洞。

一個美麗的意外

ElevenLabs 的 eleven_v3 看到 *laughs* 這種動作描述,剝掉星號後她會真的笑出來,而不是把那個字念出來。這件事記在備忘裡,標註「別順手修掉」。

launchd 拿不到麥克風,所以她有了臉

常駐的行程由 launchd 啟動,在 macOS 上沒有麥克風權限:授權綁在執行檔上、叫不出授權視窗、python 也沒有用途說明。

解法是做一個 Mac 桌寵 app(Swift):桌面上一張圓臉,顯示她在想、在聽還是在講話,點她就能打字。它同時也是她的麥克風和喇叭(VOICE_AUDIO=app)。腦還在常駐行程裡,殼只負責耳朵和嘴巴。「同一顆腦,耳朵長在殼上」這個架構,後來長成了 iPhone 和 Stack-chan。

第二個殼:iPhone(v1.2.0)

手機只是另一個殼,鎖屏放口袋裡喊一聲她就醒。順手幫殼和腦之間的通道加上共享密鑰。這段的坑:

  • iPhone 必須戴耳機用:內建麥克風在手拿距離下,連續人聲偵測到 0ms,戴耳機是 2190ms。能量式 VAD 救不回來。
  • 模擬器測語音沒聲音:Mac 的預設輸入被藍牙耳機佔著,用的是 16kHz 的通話模式,剛好是她要的取樣率,所以格式檢查完全沒發現異狀。
  • 播完長回覆就開不了錄音:PortAudio 回 -9986,退避重試 33 次全失敗。

(iPhone app 後來因為沒在用,在 v2.0.0 封存了。)

下一篇:她從投資 bot,變成一個有專業的人。