為什麼要自己跑 TTS?
市面上的 TTS API 不缺——ElevenLabs、OpenAI TTS、Azure Speech。但如果你想要的是用自己的聲音說話,而且不想每個月付錢、不想把錄音傳到別人的 server,那選擇就少很多了。
我的需求很簡單:讓我的 AI agent(跑在 OpenClaw 上)能用我的聲音回覆語音訊息。Agent 跑在 Oracle Cloud 的 ARM VPS 上,沒有 GPU。但家裡有一台 Windows 桌機,裝了 RTX 4070 Ti。
所以架構很明確:VPS 負責 agent 邏輯,Windows 桌機負責 GPU 推理,中間用 SSH tunnel 串起來。
聽起來簡單。實際上花了三代 TTS 模型、無數次 WSL 踩坑,才到今天穩定運作的狀態。
第一代:Fish Speech(2025 年底)
Fish Speech 是最早嘗試的方案。它支援 voice cloning,品質不錯,社群也活躍。
部署在 WSL 上,port 8880,透過 autossh reverse tunnel 讓 VPS 能連到。一開始跑得還行,但遇到幾個問題:
- 模型更新頻繁,API 不太穩定
- VRAM 吃得多,跟其他任務搶資源
- 後來有更好的選擇出現,就換了
Fish Speech 的功勞是:它驗證了整個架構是可行的——WoL 喚醒、WSL systemd、autossh tunnel、VPS 呼叫腳本這一整套 pipeline。後面換模型只需要改 server 端,其他都能重用。
第二代:CosyVoice2(2026-02-13)
阿里的 CosyVoice2 看起來是很好的升級:0.5B 參數、3.2GB VRAM、zero-shot voice cloning。
順利的部分
安裝本身不難。conda 環境、pip install、下載模型。Server 啟動後 VRAM 佔用合理。Tunnel 也沿用 Fish Speech 的設定。
不順利的部分
模型檔 sha256 不符。 flow.pt 下載了好幾次,integrity check 一直 fail。最後用了一個 workaround 才跑起來。
首次推理超級慢。 CosyVoice2 的 warmup 包含 CUDA compilation,第一次呼叫要等好幾分鐘。設了 120 秒 timeout 還是不夠。
soundfile 不吃 SpooledTemporaryFile。 FastAPI 的 UploadFile 底層用 SpooledTemporaryFile,但 soundfile 開不了。得先寫到暫存檔再讀。
Reference audio 要 16kHz。 原始錄音是 48kHz,直接丟進去會 crash。需要先 ffmpeg 轉檔。
CosyVoice3 的插曲
既然 2 能跑了,當然想試 3。結果撞到一個已知 bug(GitHub #1422):hifigan 的 f0_predictor kernel size 是 4,但某些情況下 mel frames 只有 3,直接 RuntimeError。
RuntimeError: Calculated padded input size per channel: (3).
Kernel size: (4). Kernel size can't be greater than actual input size
官方沒修。退回 CosyVoice2 穩定運行。
第三代:Qwen3-TTS(2026-02-19)
Qwen3-TTS 是目前的方案。選它的原因:
- 3.9GB VRAM,4070 Ti 綽綽有餘
- Zero-shot voice cloning,只需要一段 reference audio
- FastAPI server,API 乾淨
- 阿里出品,跟 CosyVoice 系出同門但更穩定
部署
# WSL 上
pip install qwen-tts
# 啟動 server(已做成 systemd user service)
systemctl --user start qwen3-tts
Server 在 port 8880,沿用之前的 autossh tunnel,VPS 端完全不用改。
API
兩個 endpoint:
| Endpoint | 用途 |
|---|---|
POST /tts | 用 server 預設的 reference audio |
POST /tts/clone | 自帶 ref_audio + ref_text |
/tts/clone 的參數:
curl -X POST "http://localhost:8880/tts/clone" \
-F "text=要合成的文字" \
-F "language=Chinese" \
-F "ref_text=參考音頻的逐字稿" \
-F "[email protected]" \
-o output.wav
Reference Audio 的 Best Practices
根據阿里雲官方文件和實測:
| 項目 | 建議 |
|---|---|
| 時長 | 10-20 秒(太短學不到特徵,太長引入雜訊) |
| 取樣率 | ≥ 24 kHz |
| 聲道 | Mono |
| 內容 | 連續清晰語音,無背景噪音 |
| ref_text | 必須和錄音內容完全一致 |
我實測了三個版本的 reference audio:12.7 秒、25 秒、29 秒。結果 12.7 秒的效果最好,剛好落在官方建議的甜蜜點。更長的反而聲紋模仿度下降。
WSL 踩坑大全
這套架構最痛苦的部分不是 TTS 模型,是 WSL。
1. WSL 不會自己啟動
Windows 開機後,WSL 不會自動跑。你需要:
- Windows Task Scheduler 建一個 AtLogon 任務(不是 AtStartup)
- 執行
wsl -d Debian -- bash -c "sleep infinity" - 必須用使用者帳號,SYSTEM 帳號無法啟動 WSL
2. systemd 要額外開啟
WSL 預設沒有 systemd。在 /etc/wsl.conf 加:
[boot]
systemd=true
command=/usr/bin/sleep infinity
sleep infinity 是為了防止 WSL 在沒有前台進程時自動關閉。
3. User service 需要 linger
TTS server 跑在 systemd user service 裡(不需要 root),但 WSL 的 user session 可能會被回收:
loginctl enable-linger mark
4. WoL 不能走 WireGuard
VPS 到家裡有 WireGuard tunnel,但 WoL 是 Layer 2 broadcast,WireGuard 是 Layer 3 tunnel,送不了。
解法:透過 Home Assistant 的 wake_on_lan integration,讓同 LAN 的 HA 代發 WoL。
# VPS 上的 wake-gpu.sh
curl -X POST "http://<ha-host>:8123/api/services/wake_on_lan/send_magic_packet" \
-H "Authorization: Bearer $HA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mac": "XX:XX:XX:XX:XX:XX"}'
5. autossh tunnel 要處理斷線
GPU 機器不是 24/7 開機,tunnel 會斷。用 autossh + systemd 自動重連:
# cosyvoice-tunnel.service
[Service]
ExecStart=/usr/bin/autossh -M 0 -N \
-R 8880:localhost:8880 \
-R 2222:localhost:22 \
-o "ServerAliveInterval=30" \
-o "ServerAliveCountMax=3" \
vps-host
Restart=always
RestartSec=5
完整架構圖
┌─────────────────────────────────────────────┐
│ VPS (Oracle Cloud Singapore, ARM) │
│ │
│ OpenClaw Agent │
│ → scripts/qwen3-tts.sh │
│ → curl localhost:8880/tts │
│ → message tool (Telegram voice) │
│ │
│ autossh reverse tunnel ←────────────────┐ │
│ localhost:8880 → WSL:8880 │ │
│ localhost:2222 → WSL:22 │ │
│ │ │
│ WireGuard → Home Assistant │ │
│ → WoL magic packet │ │
└──────────────────────────────────────────┘ │
│
┌──────────────────────────────────────────────┘
│ Windows Desktop (RTX 4070 Ti)
│
│ Task Scheduler (AtLogon)
│ → wsl -d Debian -- sleep infinity
│
│ WSL Debian
│ → systemd user services:
│ - qwen3-tts.service (port 8880)
│ - cosyvoice-tunnel.service (autossh)
│ → loginctl enable-linger mark
└──────────────────────────────────────────────┘
啟動流程:VPS 呼叫 wake-gpu.sh → HA 發 WoL → Windows 開機 → 自動登入 → WSL 啟動 → systemd services 啟動 → tunnel 建立 → VPS 可呼叫 TTS API。
整個過程約 55 秒。
給想自己搞的人
- 先驗證架構再選模型。 WoL、tunnel、WSL systemd 這些搞定後,換模型只是改 server 端。
- Reference audio 不是越長越好。 10-20 秒是甜蜜點。
- WSL + systemd 是可行的,但坑很多。
enable-linger、sleep infinity、AtLogon 而非 AtStartup——這些都是踩過才知道的。 - 用 Home Assistant 做 WoL 中繼是最省事的方案,如果你家本來就有的話。
- autossh > 手動 SSH tunnel。 斷線自動重連,不用操心。
現狀
Qwen3-TTS + WSL + autossh tunnel,穩定運行中。AI agent 可以隨時用我的聲音說話,延遲大概 10-15 秒(含推理),VRAM 佔 3.9GB。
三代 TTS 模型,無數次 WSL debug,最後得到的是一個不太酷但很穩的方案。有時候 boring infrastructure 就是最好的 infrastructure。
