NL ▾

Ongecensureerd tekst-API voor voice-pipelines

uncensoredhttps://api.xtts.cc/v1

TTS API Python: tekst voorbereiden voor spraak met de SDK

Deze API retourneert geen audio. Het is een text model dat je vanuit Python aanroept met de OpenAI SDK, en het past voor een speech engine: het schrijft het script, schonet het op en splitst het in zinnen die je voice stage hardop kan uitspreken.

Bijgewerkt

Isometrische illustratie van tekstblokken die van een terminal naar een geluidsgolf-poort stromen

Wat deze API wel en niet doet

Als je zocht naar een tts api python tutorial, dan moet één punt duidelijk zijn voordat er code komt: deze service synthetiseert geen speech. Het exposeert POST /v1/chat/completions en GET /v1/models voor één open-weight text model, met de model ID uncensored. Er is geen audio endpoint, geen voice list en geen audio bestand in welke response dan ook. Het heeft ook geen image, video of embedding output.

Dus waar past het in een speech project? In het tekstgedeelte van de pipeline. Voordat een voice engine een script kan uitspreken, moet iemand het schrijven, getallen en afkortingen omzetten in gesproken woorden, markup verwijderen en het in stukken verdelen die kort genoeg zijn om te synthetiseren zonder lange pauzes. Een language model doet dit werk goed, en je kunt het vanuit Python aanroepen met dezelfde SDK die je misschien al gebruikt voor andere OpenAI-compatible services.

Het model is getuned om te antwoorden zonder inhoudsweigeringen voor legaal gebruik door volwassenen, wat belangrijk is voor fictie, personagedialoog en narratie waar een strikter model halverwege de scène zou stoppen. Er geldt één harde regel: geen seksuele inhoud met minderjarigen.

De rest van dit artikel bouwt een kleine Python-module in vier stappen: een eerste aanroep, een streaming zinsplitser, een tekstnormalisator met JSON-output en een retry-wrapper die de rate limits respecteert.

Installeer de SDK en maak een eerste aanroep

Je hebt Python 3.8 of nieuwer nodig en het officiële openai pakket. Maak eerst een sleutel aan op de API-sleutelpagina. Nieuwe accounts kunnen daar met één klik een $0.50 proeftegoed claimen, en daar is geen kaart voor nodig.

pip install openai

Houd de sleutel buiten je broncode. Exporteer hem als een omgevingsvariabele en lees hem in code:

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)

Dat is de hele integratie. De SDK stuurt Authorization: Bearer <key> voor jou. Elk antwoord heeft ook een X-Balance-USD header, zodat je je resterende tegoed kunt lezen uit de ruwe response als je het wilt loggen. De documentatiepagina vermeldt de andere headers en parameters.

Parameters zoals temperature, top_p, stop, seed en de straffen worden doorgegeven. Voor vertelling is een temperature tussen 0,6 en 0,9 een verstandig startbereik, en een vaste seed helpt als je een opname wilt reproduceren terwijl je prompts afstelt.

Stream het script en knip het in zinnen

Speech engines werken meestal zin voor zin. Als je wacht tot een 900-woord script klaar is, wacht de luisteraar ook. Met stream: true stuurt de API tokens via server-sent events, en je kunt elke voltooide zin naar de stemfase sturen terwijl de rest nog wordt geschreven.

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)

Twee details zijn makkelijk om over het hoofd te zien. Ten eerste komen de gebruikscijfers aan het einde van de stream, in een chunk met een lege choices lijst, vandaar dat de loop chunks zonder choices overslaat. Ten tweede houdt een gestreamd verzoek zijn concurrentieslot vast totdat de stream eindigt. Als je voice engine langzaam is en je leest de stream lui, blijft het slot bezet. Haal de tekst er snel uit, zet zinnen in een wachtrij en laat de audio kant de wachtrij consumeren in zijn eigen tempo.

De bovenstaande reguliere expressie splitst op een punt of vraagteken gevolgd door witruimte. Het splitst foutief na afkortingen zoals "Dr." of "Nr.", wat nog een reden is om de tekst eerst te normaliseren, zoals de volgende sectie laat zien.

Normaliseer tekst voor het oor

Geschreven tekst en gesproken tekst verschillen. "Dr. Lee kocht 3 exemplaren op 3/4 voor de 2e editie" is makkelijk te lezen en awkward om uit te spreken. Veel speech engines verwerken dit gedeeltelijk, maar de resultaten variëren per engine, dus het opschonen van het script voordat het de stemfase bereikt, voorkomt verrassingen. Vraag het model dit te doen met een expliciete instructie:

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

Gebruik een lage temperature voor deze stap. Je wilt dat dezelfde input dezelfde output geeft, en creativiteit staat alleen maar in de weg. Beoordeel een sample van de resultaten voordat je een heel boek erdoorheen laat gaan, en besluit hoe je ambigu items wilt laten verwerken. De datum "3/4" is het standaardvoorbeeld: lees het als 4 maart of 3 april? Zet je keuze in de systeemprompt, want het model kan je landinstelling niet raden.

Kosten van deze stap

Normalisatie is goedkoop. Een paragraaf van 600 tokens in en ongeveer 650 tokens out kost 600 x $0,15 / 1.000.000 = $0,00009 voor input, plus 650 x $0,50 / 1.000.000 = $0,000325 voor output, in totaal $0,000415. Dat is ongeveer $0,42 voor duizend paragrafen. De prijspagina heeft de volledige tabel en opties voor het opwaarderen.

Retourneer gestructureerde segmenten met JSON-modus

Sommige stem engines accepteren per-segment instellingen: een pauzelengte, een spreeksnelheid, een andere stem voor een ander personage. Je kunt het model die instructies als JSON laten produceren en ze in Python inlezen. Stel response_format in op {"type": "json_object"} en beschrijf ook de vorm in de prompt, omdat JSON-modus geldige JSON garandeert, niet jouw schema.

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

Valideer wat er terugkomt. Controleer of segments bestaat, of elk item een bekende speaker heeft, en of pause_ms een getal is in een bereik dat je accepteert. Als de validatie faalt, probeer het één keer opnieuw met een lagere temperature voordat je het opgeeft. Behandel model output als onbetrouwbare input, net zoals je dat zou doen met elke gebruikerstekst.

Valkuilen die tijd kosten

Een paar fouten komen steeds terug als mensen een taalmodel voor een spraakengine draaien.

  • Het model commentaar laten toevoegen. Modellen zeggen graag "Zeker, hier is je script." Die zin zou hardop worden gelezen. Eindig je systeemprompt met "Geef alleen de scripttekst terug" en verwijder een leidende regel als deze eindigt op een dubbele punt.
  • Vertrouwen op zinslengte. Sommige engines worstelen met zeer lange zinnen. Voeg een regel toe zoals "geen zin langer dan 25 woorden" aan de prompt, en dwing dit ook af in Python door te splitsen op komma's als een zin nog steeds te lang is.
  • Het contextvenster vergeten. Prompt en completion delen 100.000 tokens. Stuur een lang boekhoofdstuk in secties met een korte samenvatting van de vorige sectie in plaats van de volledige tekst elke keer. Dat houdt de inputkosten ook laag.
  • Eén globale client hergebruiken over threads zonder limiet. De client is veilig om te delen, maar de service is niet oneindig. Stel een limiet voor je pool in op de concurrentie van je sleutel.
  • De sleutel hardcoderen. Gebruik een omgevingsvariabele en draai de sleutel om als deze ooit in een repository terechtkomt.

Als iets verkeerd lijkt, log dan de X-Request-Id header met de prompt die het veroorzaakte. Als je hulp nodig hebt, is die ID de snelste manier om naar één specifieke call te wijzen, en de docs leggen uit waar je het kunt vinden.

Behandel rate limits en retries

Een volledige sleutel staat 8 gelijktijdige verzoeken en 300 verzoeken per minuut toe. Een proefsleutel staat 2 gelijktijdige en 60 per minuut toe. Als je die limiet overschrijdt, krijg je HTTP 429, met de error code rate_limited voor verzoeken per minuut of concurrency voor te veel in één keer. Bij tijdelijke overload antwoordt de API ook met 429 en stuurt Retry-After: 5.

De SDK probeert het zelf opnieuw, maar een expliciete wrapper geeft je controle en maakt het gedrag zichtbaar in logs. Combineer het met een thread pool die is afgestemd op je sleutel, en je zult zelden een 429 zien:

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

Mislukte of geweigerde verzoeken worden niet in rekening gebracht, dus opnieuw proberen na een fout kost je niets voor de mislukte poging. Een verzoek dat compleet is, wordt normaal in rekening gebracht, dus vermijd blinde retries bij trage maar succesvolle calls.

Geef de tekst aan je speech engine

De laatste stap ligt buiten deze API. Python heeft verschillende manieren om audio te produceren uit tekst: open-source speech modellen die je zelf kunt draaien, desktop speech services ingebouwd in een besturingssysteem, en hosted speech APIs van andere bedrijven. Elk heeft zijn eigen licentie, kwaliteit en prijs, dus controleer hun huidige documentatie en voorwaarden voordat je kiest. Deze site levert dat deel niet.

Een schone grens houdt je code makkelijk aanpasbaar. Schrijf één functie met de signature speak(text: str) -> None en houd elke engine-specific detail erin. De text pipeline hierboven roept die functie aan voor elke zin of segment. Als je engines wisselt, verandert er niets anders.

Als je nog twijfelt tussen een trial en een betaalde setup, zijn de cijfers simpel: je betaalt $0,15 per miljoen input tokens en $0,50 per miljoen output tokens, van prepaid credit dat nooit vervalt, zonder abonnement. Top-ups lopen van $20 tot $500 en kunnen worden betaald met crypto (USDT op TRC20, USDC op Base).

Vragen en antwoorden

Retourneert deze API audiobestanden?

Nee. Het retourneert alleen tekst. Gebruik het om scripts te schrijven, op te schonen en te structureren, en stuur de tekst daarna naar een aparte speech engine.

Welk Python-pakket moet ik gebruiken?

Het officiële openai-pakket. Stel base_url in op https://api.xtts.cc/v1, voer je sleutel in en gebruik de model ID uncensored.

Kan je antwoorden streamen in Python?

Ja. Geef stream=True door en itereer over de chunks. Gebruik wordt aan het einde van de stream gerapporteerd, en het verzoek houdt zijn concurrency slot vast totdat de stream klaar is.

Wat gebeurt er als ik de rate limit raak?

Je krijgt HTTP 429 met de code rate_limited of concurrency. Wacht op de Retry-After waarde indien aanwezig en probeer het opnieuw, en houd je worker count op of onder de 8 bij een volledige sleutel of 2 bij een proefsleutel.

Je sleutel is nog maar één formulier verwijderd

Maak een account aan, kopieer de sleutel, pas de base URL aan. Dat is de hele setup.