中文 ▾

面向语音管道的无审查文本 API

uncensoredhttps://api.xtts.cc/v1

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。这就是全部设置。