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 之间的温度是一个合理的起始范围,当你想要复现某次生成结果并在调整提示词时,固定的 seed 会有所帮助。
流式输出脚本并拆分为句子
语音引擎通常逐句工作。如果你等待 900 字的脚本完成,听众也要等。使用 stream: true,API 会通过 SSE 发送 token,你可以在其余部分仍在编写时,将每个完成的句子交给 TTS 阶段。
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."))
在此步骤中使用低温度。你希望相同的输入产生相同的输出,而创造力只会碍事。在将整本书通过此步骤之前,先审查一些结果样本,并决定如何处理模糊项。日期“3/4”是标准示例:读作 3 月 4 日还是 4 月 3 日?将你的选择放入系统提示中,因为模型无法猜测你的语言环境。
此步骤的成本
规范化成本很低。输入 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 是否是你接受的范围内的数字。如果验证失败,在放弃之前用较低的温度重试一次。将模型输出视为不可信输入,就像对待任何用户文本一样。
导致时间浪费的陷阱
当人们在语音引擎前连接语言模型时,会出现一些反复出现的错误。
- 让模型添加评论。 模型喜欢说“好的,这是你的脚本。”这句话会被朗读出来。在系统提示词末尾加上“仅返回脚本文本”,如果某行以冒号结尾,请将其剥离。
- 信任句子长度。 一些引擎难以处理非常长的句子。在提示词中添加规则,例如“句子不超过 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 个或试用密钥的 2 个及以下。
只差一张表单,即可获得密钥
创建账户,复制密钥,更改基础 URL。这就是全部设置。