PT ▾

API de texto sem censura para pipelines de voz

uncensoredhttps://api.xtts.cc/v1

API TTS Python: Prepare o texto para fala com o SDK

Esta API não retorna áudio. É um modelo de texto que você chama do Python com o SDK OpenAI, e ele se encaixa antes de um motor de fala: ele escreve o roteiro, o limpa e o divide em frases que seu estágio de voz pode ler em voz alta.

Atualizado

Ilustração isométrica de blocos de texto fluindo de um terminal para um portão de onda sonora

O que esta API faz e não faz

Se você procurou por um tts api python tutorial, um ponto precisa estar claro antes de qualquer código: este serviço não sintetiza fala. Ele expõe POST /v1/chat/completions e GET /v1/models para um modelo de texto de pesos abertos, com o ID do modelo uncensored. Não há endpoint de áudio, lista de vozes ou arquivo de áudio em nenhuma resposta. Também não há saída de imagem, vídeo ou incorporação.

Então, onde ele se encaixa em um projeto de fala? Na metade de texto do pipeline. Antes que um motor de voz possa ler um roteiro, alguém precisa escrevê-lo, expandir números e abreviações em palavras faladas, remover marcação e cortá-lo em pedaços curtos o suficiente para sintetizar sem longas pausas. Um modelo de linguagem faz esse trabalho bem, e você pode chamá-lo do Python com o mesmo SDK que você já pode usar para outros serviços compatíveis com OpenAI.

O modelo é ajustado para responder sem recusas de conteúdo para uso adulto legal, o que é importante para ficção, diálogo de personagens e narração onde um modelo mais rigoroso poderia parar no meio da cena. Uma regra rígida se aplica: sem conteúdo sexual envolvendo menores de idade.

O resto deste artigo constrói um pequeno módulo Python em quatro etapas: uma primeira chamada, um divisor de frases em streaming, um normalizador de texto com saída JSON e um wrapper de tentativa que respeita os limites de taxa.

Instale o SDK e faça uma primeira chamada

Você precisa do Python 3.8 ou mais recente e do pacote oficial openai. Crie uma chave na página da chave de API primeiro. Contas novas podem reivindicar um crédito de teste de $0,50 com um clique, sem necessidade de cartão.

pip install openai

Mantenha a chave fora do seu código-fonte. Exporte-a como uma variável de ambiente e leia-a no código:

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)

Essa é a integração completa. O SDK envia Authorization: Bearer <key> para você. Cada resposta também traz um cabeçalho X-Balance-USD, para que você possa ler seu crédito restante na resposta bruta se quiser registrá-lo. A página de documentação lista os outros cabeçalhos e parâmetros.

Parâmetros como temperature, top_p, stop, seed e as penalidades são passados. Para narração, uma temperatura entre 0,6 e 0,9 é uma faixa inicial sensata, e um seed fixo ajuda quando você deseja reproduzir uma versão enquanto ajusta os prompts.

Faça streaming do roteiro e divida em frases

Motores de fala geralmente funcionam frase por frase. Se você esperar um roteiro de 900 palavras terminar, o ouvinte também espera. Com stream: true, a API envia tokens via server-sent events, e você pode entregar cada frase finalizada à etapa de voz enquanto o restante ainda está sendo gerado.

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)

Dois detalhes são fáceis de perder. Primeiro, os números de uso chegam no final do stream, em um chunk que pode ter uma lista choices vazia, por isso o loop pula chunks sem choices. Segundo, uma requisição em streaming mantém sua slot de concurrency até o stream terminar. Se seu motor de voz for lento e você ler o stream de forma preguiçosa, a slot fica ocupada. Extraia o texto rapidamente, coloque frases em uma fila e deixe o lado de áudio consumir a fila no seu próprio ritmo.

A expressão regular acima divide em um ponto ou ponto de interrogação seguido por espaço. Ela dividirá erroneamente após abreviações como "Dr." ou "No.", que é mais uma razão para normalizar o texto primeiro, como a próxima seção mostra.

Normalize texto para o ouvido

Texto escrito e texto falado diferem. "Dr. Lee comprou 3 cópias em 3/4 para a 2ª edição" é fácil de ler e estranho de falar. Muitos motores de fala lidam com parte disso, mas os resultados variam por motor, então limpar o roteiro antes que ele chegue à etapa de voz remove uma classe de surpresas. Peça ao modelo para fazer isso com uma instrução explícita:

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."))

Use uma temperatura baixa para esta etapa. Você quer que a mesma entrada produza a mesma saída, e a criatividade só atrapalha. Revise uma amostra dos resultados antes de processar um livro inteiro com ela, e decida como deseja que itens ambíguos sejam tratados. A data "3/4" é o exemplo padrão: leia como 4 de março ou 3 de abril? Coloque sua escolha no system prompt, porque o modelo não pode adivinhar sua localidade.

Custo desta etapa

Normalização é barata. Um parágrafo de 600 tokens de entrada e cerca de 650 tokens de saída custa 600 x $0,15 / 1.000.000 = $0,00009 para entrada, mais 650 x $0,50 / 1.000.000 = $0,000325 para saída, totalizando $0,000415. Isso dá cerca de $0,42 para mil parágrafos. A página de preços tem a tabela completa e opções de recarga.

Retorne segmentos estruturados com modo JSON

Alguns motores de voz aceitam configurações por segmento: um tempo de pausa, uma velocidade de fala, uma voz diferente para um personagem diferente. Você pode pedir ao modelo para produzir essas instruções em JSON e lê-las no Python. Defina response_format como {"type": "json_object"} e também descreva a estrutura no prompt, já que o modo JSON garante JSON válido, não o seu esquema.

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"])

Valide o que retorna. Verifique se segments existe, se cada item tem um falante conhecido, e se pause_ms é um número em uma faixa que você aceita. Se a validação falhar, tente novamente uma vez com uma temperatura menor antes de desistir. Trate a saída do modelo como entrada não confiável, da mesma forma que faria com qualquer texto de usuário.

Armadilhas que custam tempo

Alguns erros aparecem novamente e novamente quando as pessoas conectam um modelo de linguagem na frente de um motor de fala.

  • Deixando o modelo adicionar comentários. Modelos adoram dizer "Claro, aqui está seu roteiro." Essa frase seria lida em voz alta. Finalize seu system prompt com "Retorne apenas o texto do roteiro" e remova uma linha inicial se ela terminar com dois pontos.
  • Confiando no tamanho da frase. Alguns motores têm dificuldade com frases muito longas. Adicione uma regra como "nenhuma frase com mais de 25 palavras" ao prompt, e também a imponha no Python dividindo por vírgulas quando uma frase ainda estiver muito longa.
  • Esquecendo a janela de contexto. Prompt e resposta compartilham 100.000 tokens. Para um capítulo longo de livro, envie-o em seções com um breve resumo da seção anterior em vez do texto completo a cada vez. Isso também mantém a conta de entrada baixa.
  • Reutilizando um cliente global único entre threads sem um limite. O cliente é seguro para compartilhar, mas o serviço não é infinito. Limite seu pool à concorrência da sua chave.
  • Hardcoding a chave. Use uma variável de ambiente e rotacione a chave se ela alguma vez acabar em um repositório.

Quando algo parecer errado, registre o cabeçalho X-Request-Id junto com o prompt que o causou. Se precisar pedir ajuda, esse ID é a maneira mais rápida de apontar para uma chamada específica, e a documentação explica onde encontrá-lo.

Lide com limites de taxa e tentativas

Uma chave completa permite 8 requisições simultâneas e 300 requisições por minuto. Uma chave de teste permite 2 simultâneas e 60 por minuto. Ao ultrapassar qualquer limite, você recebe HTTP 429, com o código de erro rate_limited para requisições por minuto ou concurrency para muitas de uma vez. Em sobrecarga temporária, a API também responde 429 e envia Retry-After: 5.

O SDK tenta novamente automaticamente, mas um wrapper explícito dá a você controle e torna o comportamento visível nos logs. Combine-o com um thread pool dimensionado para sua chave, e você raramente verá um 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))

Requisições falhas ou recusadas não são cobradas, então tentar novamente após um erro não custa a você pela tentativa falhada. Uma requisição que é concluída é cobrada normalmente, então evite tentativas cegas em chamadas lentas, mas bem-sucedidas.

Entregue o texto ao seu motor de fala

A última etapa vive fora desta API. Python tem várias maneiras de produzir áudio a partir de texto: modelos de fala de código aberto que você pode executar sozinho, serviços de fala de desktop integrados a um sistema operacional e APIs de fala hospedadas de outras empresas. Cada uma tem sua própria licença, qualidade e preço, então verifique sua documentação e termos atuais antes de escolher. Este site não fornece essa parte.

Uma fronteira limpa mantém seu código fácil de alterar. Escreva uma função com a assinatura speak(text: str) -> None e mantenha todos os detalhes específicos do motor dentro dela. O pipeline de texto acima chama essa função para cada frase ou segmento. Quando você troca de motores, nada mais muda.

Se você ainda está decidindo entre um teste e uma configuração paga, os números são simples: você paga $0,15 por milhão de tokens de entrada e $0,50 por milhão de tokens de saída, a partir de crédito pré-pago que nunca expira, sem assinatura. Recargas variam de $20 a $500 e podem ser pagas com cripto (USDT na TRC20, USDC na Base).

Perguntas e respostas

Esta API retorna arquivos de áudio?

Não. Retorna apenas texto. Use para escrever, limpar e estruturar roteiros, depois envie o texto para um motor de fala separado.

Qual pacote Python devo usar?

O pacote oficial openai. Defina base_url para https://api.xtts.cc/v1, passe sua chave e use o ID do modelo uncensored.

Posso fazer streaming das respostas em Python?

Sim. Passe stream=True e itere sobre os chunks. O uso é relatado no final do stream, e a requisição mantém seu slot de concurrency até o stream terminar.

O que acontece ao atingir o limite de requisições?

Você recebe HTTP 429 com o código rate_limited ou concurrency. Aguarde o valor Retry-After, se presente, e tente novamente, mantendo sua contagem de workers em ou abaixo de 8 em uma chave completa ou 2 em uma chave de teste.

Sua chave está a um formulário de distância

Crie uma conta, copie a chave e altere a URL base. Essa é toda a configuração.