為什麼要自己跑 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 不會自動跑。你需要:

  1. Windows Task Scheduler 建一個 AtLogon 任務(不是 AtStartup)
  2. 執行 wsl -d Debian -- bash -c "sleep infinity"
  3. 必須用使用者帳號,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 秒


給想自己搞的人

  1. 先驗證架構再選模型。 WoL、tunnel、WSL systemd 這些搞定後,換模型只是改 server 端。
  2. Reference audio 不是越長越好。 10-20 秒是甜蜜點。
  3. WSL + systemd 是可行的,但坑很多。 enable-lingersleep infinity、AtLogon 而非 AtStartup——這些都是踩過才知道的。
  4. 用 Home Assistant 做 WoL 中繼是最省事的方案,如果你家本來就有的話。
  5. autossh > 手動 SSH tunnel。 斷線自動重連,不用操心。

現狀

Qwen3-TTS + WSL + autossh tunnel,穩定運行中。AI agent 可以隨時用我的聲音說話,延遲大概 10-15 秒(含推理),VRAM 佔 3.9GB。

三代 TTS 模型,無數次 WSL debug,最後得到的是一個不太酷但很穩的方案。有時候 boring infrastructure 就是最好的 infrastructure。