# Gemini TTS Console — API 文件 後端服務封裝 `gemini-3.1-flash-tts-preview`,提供文字轉語音(單一語者/雙語者對話)功能。本文件給直接呼叫 API 的使用情境(curl、後端服務串接等),不透過網頁介面。 - **Base URL**:`http://localhost:8206`(依 `docker-compose.yaml` 設定的 port) - **認證**:不需要。Gemini API Key 已經在伺服器端 `.env` 設定好,呼叫這支 API 本身不用帶任何金鑰。 - **內容格式**:請求一律是 `application/json`;生成端點的成功回應是**音訊二進位資料**,不是 JSON;失敗回應才是 `{"detail": "..."}` 這種 JSON。 ## 端點總覽 | Method | Path | 說明 | |---|---|---| | GET | `/api/health` | 健康檢查 | | GET | `/api/voices` | 取得語音清單、語氣指令、語氣標籤 | | POST | `/api/tts` | 單一語者文字轉語音 | | POST | `/api/tts/dialogue` | 雙語者對話文字轉語音 | | GET | `/home` | 網頁介面(非 API,瀏覽器用) | --- ## GET /api/health ```bash curl http://localhost:8206/api/health ``` ```json {"status": "ok"} ``` --- ## GET /api/voices 回傳目前可用的語音、語氣指令、語氣標籤清單,供呼叫前查詢。**不需要任何參數**。 ```bash curl http://localhost:8206/api/voices ``` 回應結構: ```json { "voices": [ {"name": "Zephyr", "trait": "明亮", "gender": "female"}, {"name": "Puck", "trait": "輕快", "gender": "male"}, ... ], "directives": [ {"label": "愉快", "insert": "Say cheerfully: "}, ... ], "tags": [ {"label": "悄悄話", "insert": "[whispers] "}, ... ] } ``` - `voices[].gender` 是**社群聽感判斷**,不是 Google 官方分類;Google 只公布語音的性格特質(`trait`),刻意不分性別。僅供篩選參考。 - `directives` 是可以透過 `directive` 欄位(見下方)直接用標籤名稱套用的自然語言語氣指令。 - `tags` 是內嵌音訊標籤,**不需要透過 API 特別支援**——標籤本身就是普通文字(例如 `[whispers]`),直接寫進 `text` 裡即可,`tags` 清單只是方便查詢有哪些標籤可用。 ### 完整語音清單(30 個) | 名稱 | 特質 | 性別(社群判斷) | |---|---|---| | Zephyr | 明亮 | 女聲 | | Puck | 輕快 | 男聲 | | Charon | 知性 | 男聲 | | Kore | 堅定 | 女聲 | | Fenrir | 亢奮 | 男聲 | | Leda | 年輕 | 女聲 | | Orus | 堅定 | 男聲 | | Aoede | 輕鬆 | 女聲 | | Callirrhoe | 隨和 | 女聲 | | Autonoe | 明亮 | 女聲 | | Enceladus | 氣音 | 男聲 | | Iapetus | 清晰 | 男聲 | | Umbriel | 隨和 | 男聲 | | Algieba | 圓潤 | 男聲 | | Despina | 圓潤 | 女聲 | | Erinome | 清晰 | 女聲 | | Algenib | 沙啞 | 男聲 | | Rasalgethi | 知性 | 男聲 | | Laomedeia | 輕快 | 女聲 | | Achernar | 柔和 | 女聲 | | Alnilam | 堅定 | 男聲 | | Schedar | 平穩 | 男聲 | | Gacrux | 成熟 | 女聲 | | Pulcherrima | 直接 | 男聲 | | Achird | 親切 | 男聲 | | Zubenelgenubi | 隨性 | 男聲 | | Vindemiatrix | 溫柔 | 女聲 | | Sadachbia | 活潑 | 男聲 | | Sadaltager | 博學 | 男聲 | | Sulafat | 溫暖 | 男聲 | ### 語氣指令(`directive` 欄位可用值) | 標籤 | 實際套用的指令文字 | |---|---| | 愉快 | `Say cheerfully: ` | | 緩慢 | `Say slowly and calmly: ` | | 新聞主播腔 | `Say in the tone of a news anchor: ` | | 台灣腔 | `用台灣腔說:` | ### 語氣標籤(直接寫在 `text` 裡使用) | 標籤 | 內嵌寫法 | |---|---| | 悄悄話 | `[whispers]` | | 興奮 | `[excited]` | | 嘆氣 | `[sighs]` | | 笑聲 | `[laughs]` | | 大喊 | `[shouting]` | --- ## POST /api/tts — 單一語者 ### 請求參數 | 欄位 | 型別 | 必填 | 預設 | 說明 | |---|---|---|---|---| | `text` | string | ✅ | — | 要唸出的文字,1 ~ 4000 字元 | | `voice` | string | ✅ | — | 語音名稱,見上方語音清單的 `name` 欄位 | | `directive` | string \| null | ✗ | `null` | 語氣指令標籤(見上表),後端會把對應指令文字接到 `text` 最前面 | | `seed` | integer \| null | ✗ | `null` | 0 ~ 2147483647,見下方〈seed 與可重現性〉 | | `stream` | boolean | ✗ | `false` | 是否用串流方式回傳音訊,見下方〈串流模式〉 | ### 成功回應 **非串流**(`stream` 省略或 `false`): - Status: `200` - `Content-Type: audio/wav` - `X-Seed: <實際使用的 seed>` - Body:完整、可直接播放/儲存的 WAV 檔(16-bit PCM, 24000Hz, mono) ```bash curl -X POST http://localhost:8206/api/tts \ -H "Content-Type: application/json" \ -d '{"text": "今天天氣真好,我們一起出去走走吧!", "voice": "Kore"}' \ --output out.wav ``` **帶語氣指令**: ```bash curl -X POST http://localhost:8206/api/tts \ -H "Content-Type: application/json" \ -d '{"text": "今天天氣真好", "voice": "Kore", "directive": "愉快"}' \ --output out.wav ``` **帶內嵌音訊標籤**(不需要 `directive`,直接寫進 `text`): ```bash curl -X POST http://localhost:8206/api/tts \ -H "Content-Type: application/json" \ -d '{"text": "[whispers] 這是一個秘密", "voice": "Enceladus"}' \ --output out.wav ``` **帶固定 seed**: ```bash curl -X POST http://localhost:8206/api/tts \ -H "Content-Type: application/json" \ -d '{"text": "今天天氣真好", "voice": "Kore", "seed": 42}' \ --output out.wav ``` ### 錯誤回應 | 狀況 | Status | Body 範例 | |---|---|---| | `text` 為空字串 / 超過 4000 字 | `422` | `{"detail":[{"type":"string_too_short","loc":["body","text"],"msg":"String should have at least 1 character","input":"","ctx":{"min_length":1}}]}`(FastAPI 標準驗證錯誤格式,`type` 依實際違反的規則而不同) | | `directive` 標籤不存在 | `400` | `{"detail": "Unknown directive '不存在的標籤'. Valid labels: 愉快, 緩慢, 新聞主播腔, 台灣腔"}` | | `voice` 名稱不存在、Gemini API 呼叫失敗 | `502` | `{"detail": "Requested entity was not found."}`(訊息內容為 Gemini API 原始錯誤) | --- ## POST /api/tts/dialogue — 雙語者對話 ### 請求參數 | 欄位 | 型別 | 必填 | 預設 | 說明 | |---|---|---|---|---| | `text` | string | ✅ | — | 對話文字稿,1 ~ 4000 字元。建議寫成交替格式,例如 `Joe: How's it going?\nJane: Not bad!` | | `speaker_a` | string | ✅ | — | 語者 A 名稱,1 ~ 40 字元,需對應 `text` 裡實際使用的名字 | | `voice_a` | string | ✅ | — | 語者 A 的語音名稱 | | `speaker_b` | string | ✅ | — | 語者 B 名稱,1 ~ 40 字元 | | `voice_b` | string | ✅ | — | 語者 B 的語音名稱 | | `directive` | string \| null | ✗ | `null` | 同 `/api/tts` | | `seed` | integer \| null | ✗ | `null` | 同 `/api/tts` | | `stream` | boolean | ✗ | `false` | 同 `/api/tts` | ### 成功回應 跟 `/api/tts` 相同(WAV 或串流 PCM,見下方),一樣有 `X-Seed` header。 ```bash curl -X POST http://localhost:8206/api/tts/dialogue \ -H "Content-Type: application/json" \ -d '{ "text": "Joe: 今天天氣真好耶!\nJane: 對啊,我們出去走走吧。", "speaker_a": "Joe", "voice_a": "Kore", "speaker_b": "Jane", "voice_b": "Puck" }' \ --output dialogue.wav ``` ### 錯誤回應 同 `/api/tts`。 --- ## 串流模式(`stream: true`) 把 `stream` 設成 `true`,兩個生成端點都會改成**邊生成邊回傳**,而不是等 Gemini 產完整段音訊才一次回傳。 ### 跟非串流模式的差異 | | 非串流(預設) | 串流(`stream: true`) | |---|---|---| | `Content-Type` | `audio/wav` | `audio/l16;rate=24000;channels=1` | | Body 內容 | 完整 WAV 檔(含 44 bytes 檔頭) | **原始 PCM**,無檔頭,用 HTTP chunked transfer 分段送出 | | 何時能開始播放 | 等整包收完 | 收到第一段 chunk 就能開始播放 | | 錯誤處理 | 失敗會回傳正確的 HTTP 4xx/5xx | 只有「第一個 chunk 之前」發生的錯誤能正確回傳 502;如果錯誤發生在中途,連線會直接中斷(HTTP 狀態已經是 200,無法再改) | ### 音訊格式(串流) 原始 16-bit **little-endian** PCM、單聲道、取樣率 24000Hz,沒有任何檔頭。要播放或轉檔,需要自己組 WAV 檔頭或直接餵給支援 raw PCM 的播放工具,例如: ```bash # 用 curl 收下串流內容,再用 ffmpeg 轉成可播放的檔案 curl -X POST http://localhost:8206/api/tts \ -H "Content-Type: application/json" \ -d '{"text": "串流測試", "voice": "Kore", "stream": true}' \ --output raw.pcm ffmpeg -f s16le -ar 24000 -ac 1 -i raw.pcm out.wav ``` 網頁前端(`static/app.js`)是用瀏覽器的 **Web Audio API** 邊收 chunk 邊解碼播放,同時把所有 chunk 收集起來,串流結束後在前端組成完整 WAV blob 供下載與重播,可以參考那份程式碼作為串流播放的實作範例。 --- ## seed 與可重現性 - 不帶 `seed`:伺服器會亂數產生一個(0 ~ 2147483647)並透過 `X-Seed` response header 回傳,讓你知道這次實際用了哪個值。 - 帶固定 `seed`:Google 官方說明是「盡力而為(best-effort)」重現,**不保證**每次輸出完全一致。 實測結果(同樣文字/語音,同一個 seed 連續呼叫 3 次):3 次中有 2 次音檔位元組完全相同(同長度、同 SHA256),1 次不同。也就是說固定 seed 確實會提高重現機率,但不是 100% 保證,同一個請求還是可能拿到不同結果。 `seed` 必須放在請求的頂層欄位(本 API 已經幫你處理好轉呼叫 Gemini API 時應該放的位置),呼叫端不用煩惱 Gemini SDK 內部的參數結構。 --- ## 語氣指令 vs 語氣標籤:兩者差異 | | 語氣指令(`directive` 欄位) | 語氣標籤(寫進 `text`) | |---|---|---| | 使用方式 | 傳標籤名稱字串(如 `"愉快"`),伺服器查表接到文字前面 | 直接把 `[whispers]` 這類字串包在 `text` 裡任意位置 | | 單選/多選 | 單選(一次請求只能套一個) | 可重複、可混用多個 | | 底層機制 | 自然語言指令(例:`"Say cheerfully: "` + 你的文字) | Gemini 官方支援的內嵌音訊標籤詞彙表 | | 查詢可用清單 | `GET /api/voices` 的 `directives` | `GET /api/voices` 的 `tags`(但本質上是任意文字,不查表也能自己寫) | --- ## 其他限制 - `text` 上限 4000 字元(含 `directive` 展開後的前綴文字不計入這個上限,該前綴另外由伺服器附加)。 - `speaker_a` / `speaker_b` 上限 40 字元。 - 沒有 rate limit(由 Gemini API 本身的配額決定)。 - 沒有身份驗證機制,若要對外網開放,建議自行加一層反向代理 + 驗證。