TTS API Python:使用 SDK 準備語音文本
此 API 不返回音訊。它是透過 OpenAI SDK 從 Python 呼叫的文本模型,位於語音引擎之前:它負責撰寫腳本、清理內容並將其拆分為語音階段可朗讀的句子。
更新於

此 API 的功能與限制
如果你搜尋了 tts api python 教學,在程式碼之前必須釐清一點:此服務不合成語音。它提供 POST /v1/chat/completions 和 GET /v1/models 給一個開放權重文本模型,模型 ID 為 uncensored。沒有音訊端點、沒有語音列表,任何回應中都沒有音訊檔案。它也沒有圖像、影片或嵌入輸出。
那麼它在語音專案中扮演什麼角色?在文本處理階段。在語音引擎朗讀腳本之前,必須有人撰寫它、將數字和縮寫擴展為口語詞彙、去除標記,並將其切割成足夠短的片段以進行合成而不會產生長暫停。語言模型能很好地完成這項工作,你可以使用與用於其他 OpenAI 相容服務的相同 SDK 從 Python 呼叫它。
該模型經過調整,可針對合法成人用途無內容拒絕地回答,這在較嚴格的模型可能於場景中途停止的虛構作品、角色對話與敘事中尤為重要。有一項硬性規定:禁止涉及未成年人的性內容。
本文的其餘部分將分四步構建一個小型 Python 模組:首次呼叫、串流句子分割器、帶有 JSON 輸出的文本正規化器,以及尊重速率限制的重試包裝器。
安裝 SDK 並進行首次呼叫
你需要 Python 3.8 或更新版本以及官方 openai 套件。先在 API 金鑰頁面 建立金鑰。新帳戶可以點擊一次領取 $0.50 免費試用額度,無需信用卡。
pip install openai
請將金鑰保留在程式碼之外。將其匯出為環境變數並在程式碼中讀取:
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.xtts.cc/v1",
api_key=os.environ["XTTS_API_KEY"],
)
resp = client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": "You write short narration scripts. Plain sentences only."},
{"role": "user", "content": "Write a 60-word intro for a podcast about night trains."},
],
temperature=0.8,
)
script = resp.choices[0].message.content
print(script)
print(resp.usage.prompt_tokens, resp.usage.completion_tokens)
這就是完整的整合方式。SDK 會自動為您傳送 Authorization: Bearer <key>。每個回應也會包含 X-Balance-USD 標頭,因此如果您想記錄剩餘額度,可以從原始回應中讀取該標頭。文件頁面列出了其他標頭與參數。
temperature、top_p、stop、seed 等參數以及懲罰因子會直接傳遞。對於敘事,0.6 到 0.9 之間的 temperature 是一個合理的起始範圍,而固定的 seed 有助於在調整提示詞時重現結果。
串流輸出腳本並將其分割為句子
語音引擎通常逐句運作。如果你等待 900 字的腳本完成,聽眾也會等待。使用 stream: true,API 會透過伺服器發送事件(SSE)傳送 token,你可以在其餘部分仍在撰寫時,將每個完成的句子交給語音階段。
import os
import re
from openai import OpenAI
client = OpenAI(base_url="https://api.xtts.cc/v1", api_key=os.environ["XTTS_API_KEY"])
SENTENCE_END = re.compile(r"(?<=[.?])\s+")
def stream_sentences(topic):
stream = client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": "Write spoken narration. One idea per sentence."},
{"role": "user", "content": topic},
],
stream=True,
)
buffer = ""
for chunk in stream:
if not chunk.choices:
continue # the final chunk can carry usage only
buffer += chunk.choices[0].delta.content or ""
parts = SENTENCE_END.split(buffer)
for sentence in parts[:-1]:
yield sentence.strip()
buffer = parts[-1]
if buffer.strip():
yield buffer.strip()
for line in stream_sentences("Explain how a tide table is read."):
print(line) # replace with: your_voice_engine.say(line)
兩個細節容易被忽略。首先,用量數據會在串流輸出結束時到達,該區塊可能包含空的 choices 清單,這就是迴路會跳過沒有 choices 區塊的原因。其次,串流請求會在其串流結束前佔用並行槽位。如果你的語音引擎處理緩慢且你懶惰地讀取串流,該槽位會保持忙碌。請快速提取文字,將句子放入佇列,讓音訊端以自己的速度消耗佇列。
上述的正則表達式在句號或問號後跟隨空白時分割。它會在縮寫(如 "Dr." 或 "No.")後錯誤分割,這也是為什麼要先正規化文本的原因,如下節所示。
為聽覺優化文本
書面文字與口語文字不同。"Dr. Lee bought 3 copies on 3/4 for the 2nd edition" 易讀但朗讀時不順暢。許多語音引擎能處理部分情況,但結果因引擎而異,因此在進入語音階段前清理腳本可消除一類意外狀況。請以明確指示要求模型執行:
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.xtts.cc/v1", api_key=os.environ["XTTS_API_KEY"])
NORMALIZE = (
"Rewrite the user's text so it can be read aloud. "
"Spell out numbers, currency, dates and abbreviations as words. "
"Remove markdown, URLs and emoji. Keep the meaning and the order. "
"Return only the rewritten text."
)
def normalize(text):
r = client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": NORMALIZE},
{"role": "user", "content": text},
],
temperature=0.2,
)
return r.choices[0].message.content.strip()
print(normalize("Dr. Lee bought 3 copies on 3/4 for the 2nd edition."))
此步驟使用低 temperature。你希望相同的輸入產生相同的輸出,而創造力只會礙事。在將整本書通過此步驟之前,請審查結果樣本,並決定如何處理歧義項目。日期 "3/4" 是標準範例:讀作三月四日還是四月三日?將你的選擇放入系統提示詞中,因為模型無法猜測你的地區設定。
此步驟的成本
正規化很便宜。輸入 600 個 token,輸出約 650 個 token,成本為 600 x $0.15 / 1,000,000 = $0.00009(輸入),加上 650 x $0.50 / 1,000,000 = $0.000325(輸出),總計 $0.000415。這大約是每千個段落 $0.42。定價頁面 有完整的表格和儲值選項。
以 JSON 模式返回結構化區段
某些語音引擎接受分段設定:暫停長度、語速、不同角色的不同聲音。你可以讓模型產生這些指示的 JSON,並在 Python 中讀取它們。將 response_format 設為 {"type": "json_object"},並在提示詞中描述結構,因為 JSON 模式保證產生有效的 JSON,而非你的結構定義。
import json
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.xtts.cc/v1", api_key=os.environ["XTTS_API_KEY"])
prompt = (
"Split this scene into segments. Return JSON like "
'{"segments": [{"speaker": "narrator", "text": "...", "pause_ms": 300}]}. '
"Speakers are narrator, mara, or tom."
)
r = client.chat.completions.create(
model="uncensored",
response_format={"type": "json_object"},
messages=[
{"role": "system", "content": prompt},
{"role": "user", "content": "Mara opens the door. Tom says he is late again. She sighs and lets him in."},
],
temperature=0.3,
)
data = json.loads(r.choices[0].message.content)
for seg in data["segments"]:
print(seg["speaker"], seg["pause_ms"], seg["text"])
驗證返回的內容。檢查 segments 是否存在,每個項目是否有已知的說話者,以及 pause_ms 是否在可接受的範圍內為數字。如果驗證失敗,在放棄前以較低的 temperature 重試一次。將模型輸出視為不可信的輸入,就像你處理任何使用者文本一樣。
耗時的陷阱
當人們將語言模型連接在語音引擎之前時,會出現一些反覆出現的錯誤。
- 讓模型添加評論。 模型喜歡說「好的,這是你的腳本。」這句話會被朗讀出來。在系統提示詞末尾加上「僅返回腳本文本」,並刪除以冒號結尾的首行。
- 信任句子長度。 某些引擎難以處理非常長的句子。在提示詞中加入規則,例如「句子不超過 25 個詞」,並在 Python 中透過在逗號處分割來強制執行,以防句子仍然太長。
- 遺忘上下文視窗。 提示詞與補全內容共用 100,000 個 token。對於長篇章節,請分段發送,並附上前一段的簡短摘要,而非每次都發送全文。這也能降低輸入費用。
- 在沒有上限的情況下跨執行緒重用單一全域用戶端。 用戶端可以安全共享,但服務並非無限。將你的連線池上限設為金鑰的並行限制。
- 硬編碼金鑰。 請使用環境變數,如果金鑰意外進入程式碼庫,請將其輪替。
當發現異常時,請記錄導致問題的提示詞以及 X-Request-Id 標頭。如果需要尋求協助,該 ID 是最快指向特定請求的方式,文件則說明在哪裡可以找到它。
處理速率限制與重試
完整金鑰允許 8 個並行請求和每分鐘 300 個請求。試用金鑰允許 2 個並行請求和每分鐘 60 個請求。超過任一限制時,您會收到 HTTP 429,每分鐘請求逾限時的錯誤代碼為 rate_limited,同時請求過多時的錯誤代碼為 concurrency。在暫時過載時,API 也會回應 429 並傳送 Retry-After: 5。
SDK 會自動重試,但明確的包裝器可讓你控制行為並在日誌中顯示該行為。將其與根據你的金鑰調整大小的執行緒池結合,你幾乎不會看到 429:
import os
import time
from concurrent.futures import ThreadPoolExecutor
from openai import OpenAI, RateLimitError
client = OpenAI(
base_url="https://api.xtts.cc/v1",
api_key=os.environ["XTTS_API_KEY"],
max_retries=0,
)
def ask(prompt, tries=5):
for attempt in range(tries):
try:
r = client.chat.completions.create(
model="uncensored",
messages=[{"role": "user", "content": prompt}],
)
return r.choices[0].message.content
except RateLimitError as err:
wait = int(err.response.headers.get("Retry-After", 5))
time.sleep(wait * (attempt + 1))
raise RuntimeError("still rate limited after retries")
prompts = ["Narrate a sunrise in two sentences."] * 20
with ThreadPoolExecutor(max_workers=8) as pool: # use 2 on a trial key
results = list(pool.map(ask, prompts))
print(len(results))
失敗或被拒絕的請求不會收費,因此在錯誤後重試不會因失敗的嘗試而產生費用。完成的請求會正常計費,因此避免對緩慢但成功的呼叫進行盲目重試。
將文本交給你的語音引擎
最後一步位於此 API 之外。Python 有多種從文字產生音訊的方法:你可以自行運行的開源語音模型、作業系統內建的桌面語音服務,以及其他公司的託管語音 API。每種都有其各自的授權、質量和價格,因此在選擇前請檢查它們的當前文件和條款。本網站不提供該部分。
清晰的邊界讓你的程式碼更易於修改。撰寫一個具有 speak(text: str) -> None 簽名的函式,並將所有引擎特定的細節保留在其中。上述文字管線會對每個句子或段落呼叫該函式。當你切換引擎時,其他部分不會改變。
如果你仍在試用和付費設定之間猶豫,數字很簡單:你為每百萬個輸入 token 支付 $0.15,為每百萬個輸出 token 支付 $0.50,費用來自永不過期的預付額度,無訂閱。儲值金額從 $20 到 $500 不等,可透過加密貨幣(TRC20 上的 USDT、Base 上的 USDC)支付。
更多指南
問答
此 API 會返回音訊檔案嗎?
不會。它僅返回文本。用它來撰寫、清理並結構化腳本,然後將文本傳遞給獨立的語音引擎。
我應該使用哪個 Python 套件?
官方 openai 套件。將 base_url 設為 https://api.xtts.cc/v1,傳入你的金鑰,並使用模型 ID uncensored。
我可以在 Python 中串流輸出回應嗎?
是的。傳入 stream=True 並遍歷區塊。用量會在串流結束時回報,且請求會佔用並行請求槽位,直到串流結束為止。
當我達到速率限制時會發生什麼事?
你會收到 HTTP 429,代碼為 rate_limited 或 concurrency。若存在 Retry-After 值,請等待後重試,並將工作程序數量保持在完整金鑰的 8 以下或等於 8,或試用金鑰的 2 以下或等於 2。
只差一張表單,即可取得金鑰
建立帳號、複製金鑰、更改基礎 URL。這就是整個設定過程。