摘要: OpenRouter 在 2026 年 9 月 11 日發布的 TTS 教學指出,同一端點成功時回傳原始音訊 bytes,失敗時回傳 JSON,因此必須先檢查狀態碼與 Content-Type,才把回應寫入檔案。模型與 voice 是綁定供應商的配對,長文本應按句子或段落分段,並記錄 X-Generation-Id 與格式等資訊。重試限於 429 與特定 5xx;400、401、402 應先修正請求、憑證或額度,而不是退避重送。
統一介面的代價:驗證得自己來
OpenRouter 於 2026 年 9 月 11 日發布的教學,把 text-to-speech 收斂到單一 OpenAI 相容端點 POST /api/v1/audio/speech。要送出的欄位並不多:model、input,再加上絕大多數模型都需要的 voice。日後想換供應商,端點位址、認證方式與回應處理流程都不必更動,唯一要同步更換的,就是 model 與 voice 這一組配對。
聽起來像是省了不少工,實際上卻是把責任往下挪了一層。同一個端點,成功時吐回 raw audio bytes,失敗時吐回 JSON。換句話說,若在寫檔之前沒先看狀態碼與 Content-Type,就直接把 response.content 存成 output.mp3,那麼落到磁碟上的內容十之八九是錯誤訊息,而不是音訊。
驗證過了才落盤
教學的 Python 範例把先後順序示範得很清楚:先 raise_for_status(),讓 4xx/5xx 直接拋出;接著比對 Content-Type 是否為 audio/mpeg;兩關都過,最後才執行 write_bytes。cURL 版本走的是另一條路,用 --fail-with-body 讓失敗請求以非零退出碼結束,同時提醒:一旦失敗,得先用 cat output.mp3 把錯誤讀出來,刪掉檔案,然後才能重試。
JavaScript 版本在此之上又多一層處理:察覺內容型別不符時,立刻呼叫 response.body?.cancel(),免得串流留下半截。這些檢查單看都很細碎,但差別就在於整條 pipeline 最後是偶爾冒出一個 JSON 假音檔,還是乾淨俐落地失敗。
格式選項與模型支援度互相牽制
response_format 與 speed 都屬選填,不過教學建議把格式講明,原因是各模型的支援範圍並不一致。沒指定時,端點會以 PCM 作為預設;偏偏 Mistral Voxtral Mini TTS 只吃 MP3,對它要求 PCM 只會換來 400。PCM 的回應型別是 audio/pcm,可以附帶 rate 與 channels 參數,但若以為把副檔名改成 .mp3 就等於轉檔,那是行不通的。
voice 同樣不是跨供應商通用的識別碼。Grok Voice TTS 1.0 的內建聲音有五個:eve、ara、rex、sal、leo;一旦換供應商,model 與 voice 必須一起更新。Microsoft MAI-Voice-2 採用的是 Azure 風格的聲音名稱,style 與 styledegree 要透過 provider.options.azure 傳入,speed 的可接受範圍是 0.5 到 2.0。這些欄位都屬於供應商專屬設定,遇到不支援的供應商,很可能被直接忽略。
教學另外指出,截至 2026 年 9 月,live catalog 中並未收錄 OpenAI 的語音模型,因此想依賴某個供應商專屬欄位之前,最好先確認當下的模型清單。
重試要先分類,別無差別退避
教學給的生產環境做法相當具體:長文本先依句子或段落切成數段,依序送出請求,最後用看得懂格式的工具把音訊接回來;附帶的好處是第一段能更早拿到。每一段都得經過同一套檢查,並且要把 X-Generation-Id 與 model、voice、格式、自家的 request ID 一起留下紀錄。
重試的範圍僅限 429、502、503、524、529,而且有 Retry-After 就照著走,沒有才改用有次數上限的指數退避。至於 400、401、402,把它們丟進退避迴圈毫無意義,該做的是回頭修正請求、憑證或額度。計價方式是按輸入字元數計算,費率則隨模型與供應商而異。
這對建置順序意味著什麼
若你正在評估要不要做語音功能,這個端點確實讓「接第二家 TTS」變得便宜,但省下來的成本會轉嫁到格式與錯誤處理的複雜度上,得由你自己扛。這與我們先前在挑選 AI 語音代理平台時談到的順序一致:部署層級與成本該先拆解,功能面留到後面再談。
落到實務,下一步並不複雜:先執行 curl "https://openrouter.ai/api/v1/models?output_modalities=speech" 查出當前可用的語音模型,接著把「狀態碼檢查、Content-Type 檢查、空回應檢查、generation ID 記錄」這幾項包成一個共用函式,完成後再動手接 UI。少了這四件事,未來每次換模型都只會讓問題被放大。
