API TTS en Python: prepara texto para el habla con el SDK
Esta API no devuelve audio. Es un modelo de texto que llamas desde Python con el SDK de OpenAI, y se coloca delante de un motor de voz: escribe el guion, lo limpia y lo divide en oraciones que tu motor de voz puede leer en voz alta.
Actualizado

Qué hace y qué no hace esta API
Si buscaste un tutorial de API TTS en Python, un punto debe quedar claro antes de cualquier código: este servicio no sintetiza voz. Expone POST /v1/chat/completions y GET /v1/models para un modelo de texto de pesos abiertos, con el ID de modelo uncensored. No hay endpoint de audio, ni lista de voces ni archivo de audio en ninguna respuesta. Tampoco tiene salida de imagen, video ni embeddings.
¿Dónde encaja en un proyecto de voz? En la parte de texto del flujo de trabajo. Antes de que un motor de voz pueda leer un guion, alguien tiene que escribirlo, expandir números y abreviaturas en palabras habladas, eliminar el marcado y dividirlo en fragmentos lo suficientemente cortos para sintetizar sin pausas largas. Un modelo de lenguaje hace ese trabajo bien, y puedes invocarlo desde Python con el mismo SDK que ya usas para otros servicios compatibles con OpenAI.
El modelo está ajustado para responder sin rechazos de contenido para uso adulto lícito, lo cual importa para ficción, diálogos de personajes y narración donde un modelo más estricto podría detenerse a mitad de escena. Una regla estricta aplica: sin contenido sexual que involucre menores.
El resto de este artículo construye un pequeño módulo en Python en cuatro pasos: una primera llamada, un divisor de oraciones en streaming, un normalizador de texto con salida JSON, y un envoltorio de reintento que respeta los límites de peticiones.
Instala el SDK y haz una primera llamada
Necesitas Python 3.8 o superior y el paquete oficial openai. Crea una clave en la página de claves de API primero. Las cuentas nuevas pueden reclamar un crédito de prueba de $0,50 allí con un clic, y no se necesita tarjeta para ello.
pip install openai
Mantén la clave fuera de tu fuente. Expórtala como variable de entorno y léela en el 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)
Esa es toda la integración. El SDK envía Authorization: Bearer <key> por ti. Cada respuesta incluye el encabezado X-Balance-USD, así que puedes leer tu crédito restante en la respuesta cruda si deseas registrarla. La página de documentación lista los demás encabezados y parámetros.
Parámetros como temperature, top_p, stop, seed y las penalizaciones se pasan a través. Para narración, una temperatura entre 0,6 y 0,9 es un rango de inicio sensato, y un seed fijo ayuda cuando quieres reproducir una toma mientras ajustas prompts.
Transmite el guion y divídelo en oraciones
Los motores de voz suelen funcionar oración por oración. Si esperas a que termine un guion de 900 palabras, el oyente también espera. Con stream: true la API envía tokens sobre eventos enviados por el servidor, y puedes pasar cada oración terminada a la etapa de voz mientras el resto aún se está escribiendo.
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)
Dos detalles son fáciles de pasar por alto. Primero, los números de uso llegan al final del streaming, en un fragmento que puede tener una lista choices vacía, por eso el bucle salta los fragmentos sin choices. Segundo, una petición en streaming mantiene su slot de concurrencia hasta que el streaming termine. Si tu motor de voz es lento y lees el streaming de forma perezosa, el slot permanece ocupado. Extrae el texto rápidamente, pon oraciones en una cola y deja que el lado de audio consuma la cola a su propio ritmo.
La expresión regular anterior divide en un punto o signo de interrogación seguido de espacio. Dividirá incorrectamente después de abreviaturas como "Dr." o "N.", lo cual es otra razón para normalizar el texto primero, como muestra la siguiente sección.
Normaliza el texto para el oído
El texto escrito y el texto hablado difieren. "Dr. Lee compró 3 copias el 3/4 para la 2ª edición" es fácil de leer y incómodo de decir. Muchos motores de voz manejan parte de esto, pero los resultados varían según el motor, así que limpiar el guion antes de que llegue a la etapa de voz elimina una clase de sorpresas. Pídele al modelo que lo haga con una instrucción 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."))
Usa una temperatura baja para este paso. Quieres que la misma entrada dé la misma salida, y la creatividad solo estorba. Revisa una muestra de resultados antes de pasar un libro entero por ello, y decide cómo quieres que se manejen los elementos ambiguos. La fecha "3/4" es el ejemplo estándar: ¿se lee como 4 de marzo o 3 de abril? Pon tu elección en el prompt del sistema, porque el modelo no puede adivinar tu región.
Costo de este paso
La normalización es barata. Un párrafo de 600 tokens de entrada y unos 650 tokens de salida cuesta 600 x $0,15 / 1.000.000 = $0,00009 para entrada, más 650 x $0,50 / 1.000.000 = $0,000325 para salida, un total de $0,000415. Eso es unos $0,42 por mil párrafos. La página de precios tiene la tabla completa y opciones de recarga.
Devuelve segmentos estructurados con modo JSON
Algunos motores de voz aceptan configuraciones por segmento: una longitud de pausa, una velocidad de habla, una voz diferente para un personaje diferente. Puedes hacer que el modelo produzca esas instrucciones como JSON y léelas en Python. Establece response_format a {"type": "json_object"} y también describe la forma en el prompt, ya que el modo JSON garantiza JSON válido, no tu 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"])
Valida lo que devuelve. Verifica que segments exista, que cada elemento tenga un hablante conocido, y que pause_ms sea un número en un rango que aceptes. Si la validación falla, reintenta una vez con una temperatura más baja antes de rendirte. Trata la salida del modelo como entrada no confiable, igual que harías con cualquier texto de usuario.
Errores que cuestan tiempo
Algunos errores aparecen una y otra vez cuando la gente conecta un modelo de lenguaje delante de un motor de voz.
- Dejar que el modelo añada comentarios. A los modelos les gusta decir "Claro, aquí tienes tu guion." Esa oración se leería en voz alta. Termina tu prompt del sistema con "Devuelve solo el texto del guion" y elimina una línea inicial si termina con dos puntos.
- Confiar en la longitud de la oración. Algunos motores tienen dificultades con oraciones muy largas. Añade una regla como "ninguna oración más larga de 25 palabras" al prompt, y también aplícala en Python dividiendo por comas cuando una oración siga siendo demasiado larga.
- Olvidar la ventana de contexto. El prompt y la completación comparten 100.000 tokens. Para un capítulo largo de libro, envíalo en secciones con un resumen corto de la sección anterior en lugar del texto completo cada vez. Eso también mantiene la factura de entrada baja.
- Reutilizar un cliente global único entre hilos sin un límite. El cliente es seguro para compartir, pero el servicio no es infinito. Limita tu pool a la concurrencia de tu clave.
- Codificar la clave en el código. Usa una variable de entorno y rota la clave si alguna vez termina en un repositorio.
Cuando algo parece incorrecto, registra el encabezado X-Request-Id con el prompt que lo causó. Si necesitas pedir ayuda, esa ID es la forma más rápida de señalar una llamada específica, y la documentación explica dónde encontrarla.
Maneja los límites de peticiones y reintentos
Una clave completa permite 8 solicitudes concurrentes y 300 solicitudes por minuto. Una clave de prueba permite 2 concurrentes y 60 por minuto. Al superar cualquiera de esos límites obtienes HTTP 429, con el código de error rate_limited para solicitudes por minuto o concurrency para demasiadas a la vez. En caso de sobrecarga temporal, la API también responde con 429 y envía Retry-After: 5.
El SDK reintenta automáticamente, pero un envoltorio explícito te da control y hace visible el comportamiento en los registros. Combínalo con un pool de hilos dimensionado a tu clave, y rara vez verás un 429 en absoluto:
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))
Las peticiones fallidas o rechazadas no se cobran, así que reintentar después de un error no te cuesta por el intento fallido. Una petición que se completa se cobra normalmente, así que evita reintentos ciegos en llamadas lentas pero exitosas.
Pásale el texto a tu motor de voz
El último paso vive fuera de esta API. Python tiene varias formas de producir audio a partir de texto: modelos de voz de código abierto que puedes ejecutar tú mismo, servicios de voz de escritorio integrados en un sistema operativo, y APIs de voz alojadas de otras empresas. Cada una tiene su propia licencia, calidad y precio, así que revisa su documentación y términos actuales antes de elegir. Este sitio no proporciona esa parte.
Un límite limpio mantiene tu código fácil de cambiar. Escribe una función con la firma speak(text: str) -> None y mantén todos los detalles específicos del motor dentro de ella. El pipeline de texto anterior llama a esa función para cada oración o segmento. Cuando cambias motores, nada más cambia.
Si aún estás decidiendo entre una prueba y una configuración pagada, los números son simples: pagas $0,15 por millón de tokens de entrada y $0,50 por millón de tokens de salida, desde crédito prepago que no caduca, sin suscripción. Las recargas van de $20 a $500 y se pueden pagar con cripto (USDT en TRC20, USDC en Base).
Más guías
Preguntas y respuestas
¿Esta API devuelve archivos de audio?
No. Solo devuelve texto. Úsala para escribir, limpiar y estructurar guiones, y luego pasa el texto a un motor de síntesis de voz independiente.
¿Qué paquete de Python debo usar?
El paquete oficial de openai. Establece base_url en https://api.xtts.cc/v1, pasa tu clave y usa el ID de modelo uncensored.
¿Puedo hacer streaming de las respuestas en Python?
Sí. Pasa stream=True e itera sobre los chunks. El uso se reporta al final del stream, y la solicitud mantiene su slot de concurrencia hasta que el stream finalice.
¿Qué sucede cuando alcanzas el límite de peticiones?
Obtienes HTTP 429 con el código rate_limited o concurrency. Espera el valor de Retry-After si está presente y reintenta, y mantén la cantidad de workers en o por debajo de 8 en una clave completa o 2 en una clave de prueba.
Tu clave está a un formulario de distancia
Crea una cuenta, copia la clave, cambia la URL base. Esa es toda la configuración.