PL ▾

API tekstu bez cenzury dla potoków głosowych

uncensoredhttps://api.xtts.cc/v1

TTS API Python: Przygotuj tekst do syntezy mowy za pomocą SDK

To API nie zwraca dźwięku. Jest to model tekstowy, który wywołujesz z Pythona za pomocą OpenAI SDK, i pasuje przed silnik syntezatora: pisze scenariusz, czyści go i dzieli na zdania, które twój etap głosu może przeczytać na głos.

Zaktualizowano

Izometryczna ilustracja bloków tekstu płynących z terminala do bramy fal dźwiękowych

Co to API robi i czego nie robi

Jeśli szukałeś samouczka tts api python, jedna rzecz musi być jasna przed jakimkolwiek kodem: ta usługa nie syntetyzuje mowy. Udostępnia POST /v1/chat/completions i GET /v1/models dla jednego modelu tekstowego o otwartych wagach, z identyfikatorem modelu uncensored. Nie ma endpointu audio, listy głosów ani pliku audio w żadnej odpowiedzi. Nie ma również wyjścia obrazu, wideo ani embeddingów.

Gdzie więc pasuje to do projektu głosowego? W tekstowej części potoku. Zanim silnik głosowy przeczyta tekst do syntezy, ktoś musi go napisać, rozwinąć liczby i skróty na słowa mówione, usunąć znaczniki i podzielić go na fragmenty wystarczająco krótkie, aby synteza nie zawierała długich pauz. Model językowy wykonuje tę pracę dobrze, a możesz go wywołać z Pythona za pomocą tego samego SDK, którego już używasz do innych usług kompatybilnych z OpenAI.

Model jest dostrojony do odpowiadania bez odmów treści dla prawnego użytku dorosłego, co ma znaczenie dla fikcji, dialogów postaci i narracji, gdzie bardziej rygorystyczny model mógłby zatrzymać się w połowie sceny. Jedna surowa zasada obowiązuje: brak treści seksualnych z udziałem nieletnich.

Reszta tego artykułu buduje mały moduł Pythona w czterech krokach: pierwsze zapytanie, strumieniowy splitter zdań, normalizator tekstu z wyjściem JSON i opakowanie powtórzeń szanujące limity zapytań.

Zainstaluj SDK i wykonaj pierwsze zapytanie

Wymagany jest Python 3.8 lub nowszy oraz oficjalny pakiet openai. Najpierw utwórz klucz na stronie klucza API. Nowe konta mogą odebrać darmowy kredyt próbny w wysokości $0,50 jednym kliknięciem, bez konieczności podawania karty.

pip install openai

Trzymaj klucz z dala od kodu źródłowego. Wyeksportuj go jako zmienną środowiskową i odczytuj w kodzie:

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)

To cała integracja. SDK wysyła Authorization: Bearer <key> za Ciebie. Każda odpowiedź zawiera również nagłówek X-Balance-USD, więc możesz odczytać pozostały kredyt z surowej odpowiedzi, jeśli chcesz go zalogować. Na stronie dokumentacji znajdziesz inne nagłówki i parametry.

Parametry takie jak temperature, top_p, stop, seed i kary są przekazywane dalej. Dla narracji temperatura między 0,6 a 0,9 jest rozsądnym zakresem startowym, a stały seed pomaga, gdy chcesz odtworzyć ujęcie przy strojeniu promptów.

Strumieniuj scenariusz i podziel go na zdania

Silniki syntezatora zazwyczaj działają zdanie po zdaniu. Jeśli poczekasz na zakończenie scenariusza 900 słów, słuchacz też czeka. Ze stream: true API wysyła tokeny przez zdarzenia wysyłane przez serwer, a możesz przekazać każde ukończone zdanie etapowi głosu, podczas gdy reszta jest wciąż pisana.

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)

Dwa szczegóły łatwo przeoczyć. Po pierwsze, liczby zużycia pojawiają się na końcu strumienia, w fragmencie, który może zawierać pustą listę choices, dlatego pętla pomija fragmenty bez choices. Po drugie, zapytanie strumieniowe rezerwuje slot równoległości do momentu zakończenia strumienia. Jeśli silnik głosowy jest wolny, a Ty leniwie przetwarzasz strumień, slot pozostaje zajęty. Wyciągnij tekst szybko, umieść zdania w kolejce, a strona audio niech pobiera dane z kolejki we własnym tempie.

Wyrażenie regularne powyżej dzieli się po kropce lub znaku zapytania, po których następuje spacja. Błędnie podzieli po skrótach takich jak "Dr." lub "Nr.", co jest kolejnym powodem do znormalizowania tekstu najpierw, jak pokazuje następny rozdział.

Znormalizuj tekst dla ucha

Tekst pisany i tekst mówiony różnią się. „Dr Lee kupił 3 egzemplarze 3/4 dla 2. wydania” jest łatwy do przeczytania, ale niezręczny do wypowiedzenia. Wiele silników głosowych radzi sobie z częścią takich przypadków, ale wyniki zależą od silnika, dlatego wyczyszczenie tekstu przed etapem głosowym eliminuje pewną klasę niespodzianek. Poproś model o wykonanie tego za pomocą wyraźnej instrukcji:

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

Użyj niskiej temperatury dla tego kroku. Chcesz, aby to samo wejście dało to samo wyjście, a kreatywność tylko przeszkadza. Przeglądaj próbkę wyników przed uruchomieniem całej książki przez to, i zdecyduj, jak chcesz obsługiwać elementy niejednoznaczne. Data "3/4" jest standardowym przykładem: czytaj ją jako 4 marca czy 3 kwietnia? Włóż swój wybór do promptu systemowego, ponieważ model nie może zgadnąć twojej lokalizacji.

Koszt tego kroku

Normalizacja jest tania. Akapit o długości 600 tokenów na wejściu i około 650 tokenów na wyjściu kosztuje 600 × $0.15 / 1 000 000 = $0.00009 za dane wejściowe, plus 650 × $0.50 / 1 000 000 = $0.000325 za dane wyjściowe, razem $0.000415. To około $0.42 za tysiąc akapitów. Strona z cenami zawiera pełną tabelę i opcje doładowania.

Zwróć strukturalne segmenty w trybie JSON

Niektóre silniki głosowe akceptują ustawienia dla każdego segmentu: długość pauzy, prędkość mówienia, inny głos dla innej postaci. Możesz poprosić model o wygenerowanie tych instrukcji w formacie JSON i odczytać je w Pythonie. Ustaw response_format na {"type": "json_object"} i opisz również kształt w prompcie, ponieważ tryb JSON gwarantuje poprawny JSON, a nie Twój schemat.

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

Zweryfikuj to, co wraca. Sprawdź, czy segments istnieje, czy każdy element ma znanego mówcę, i czy pause_ms jest liczbą w zakresie, który akceptujesz. Jeśli walidacja się nie powiedzie, powtórz raz z niższą temperaturą przed poddaniem się. Traktuj wyjście modelu jako niezaufane wejście, tak samo jak dowolny tekst użytkownika.

Pułapki kosztujące czas

Kilka błędów pojawia się ponownie i ponownie, gdy ludzie łączą model językowy przed silnikiem syntezatora.

  • Pozwalanie modelowi dodawać komentarze. Modele lubią mówić "Oczywiście, oto twój scenariusz." To zdanie byłoby przeczytane na głos. Zakończ swój prompt systemowy "Zwróć tylko tekst scenariusza" i usuń wiodącą linię, jeśli kończy się dwukropkiem.
  • Ufanie długości zdania. Niektóre silniki mają trudność z bardzo długimi zdaniami. Dodaj regułę taką jak "żadne zdanie dłuższe niż 25 słów" do promptu i również wymuś ją w Pythonie, dzieląc po przecinkach, gdy zdanie jest wciąż za długie.
  • Pominięcie okna kontekstu. Prompt i uzupełnienie dzielą 100 000 tokenów. Dla długiego rozdziału książki wysyłaj go w sekcjach, dołączając krótkie streszczenie poprzedniej sekcji zamiast pełnego tekstu za każdym razem. To również obniża koszt tokenów wejściowych.
  • Ponowne użycie jednego globalnego klienta między wątkami bez limitu. Klient jest bezpieczny do udostępnienia, ale usługa nie jest nieskończona. Ogranicz swoją pulę do współbieżności twojego klucza.
  • Sztywno zakodowany klucz. Użyj zmiennej środowiskowej i obróć klucz, jeśli kiedykolwiek wyląduje w repozytorium.

Gdy coś wygląda nieprawidłowo, zaloguj nagłówek X-Request-Id wraz z promptem, który go spowodował. Jeśli potrzebujesz pomocy, to ID jest najszybszym sposobem na wskazanie konkretnej rozmowy, a dokumentacja wyjaśnia, gdzie go znaleźć.

Obsłuż limity zapytań i powtórzenia

Pełny klucz pozwala na 8 równoległych zapytań i 300 zapytań na minutę. Klucz próbny pozwala na 2 równoległe i 60 na minutę. Po przekroczeniu któregoś z limitów otrzymasz HTTP 429, z kodem błędu rate_limited dla zapytań na minutę lub concurrency za zbyt wiele naraz. Przy tymczasowym przeciążeniu API również odpowiada 429 i wysyła Retry-After: 5.

SDK sam powtarza próby, ale jawne opakowanie daje ci kontrolę i sprawia, że zachowanie jest widoczne w logach. Połącz go z pulą wątków dostosowaną do twojego klucza, a rzadko zobaczysz 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))

Nieudane lub odrzucone zapytania nie są rozliczane, więc ponawianie po błędzie nie kosztuje cię za nieudaną próbę. Zapytanie, które się kończy, jest rozliczane normalnie, więc unikaj ślepych powtórzeń przy wolnych, ale udanych wywołaniach.

Przekaż tekst do swojego silnika syntezatora

Ostatni krok znajduje się poza tym API. Python ma kilka sposobów na generowanie audio z tekstu: otwartoźródłowe modele mowy, które możesz uruchomić samodzielnie, usługi mowy w systemach operacyjnych oraz hostowane API mowy od innych firm. Każde ma własną licencję, jakość i cenę, więc sprawdź ich aktualną dokumentację i warunki przed wyborem. Ta strona nie dostarcza tej części.

Czysta granica utrzymuje twój kod łatwym do zmiany. Napisz jedną funkcję z sygnaturą speak(text: str) -> None i utrzymaj każdą szczegółowość specyficzną dla silnika wewnątrz niej. Powyższy potok tekstowy wywołuje tę funkcję dla każdego zdania lub segmentu. Gdy zamieniasz silniki, nic innego się nie zmienia.

Jeśli nadal wahasz się między próbą a płatnym ustawieniem, liczby są proste: płacisz $0,15 za milion tokenów wejściowych i $0,50 za milion tokenów wyjściowych, z przedpłaconego kredytu, który nigdy nie wygasa, bez subskrypcji. Doładowania wynoszą od $20 do $500 i mogą być opłacone kryptowalutami (USDT na TRC20, USDC na Base).

Pytania i odpowiedzi

Czy to API zwraca pliki audio?

Nie. Zwraca tylko tekst. Użyj go do pisania, czyszczenia i strukturyzowania scenariuszy, a następnie przekaż tekst do osobnego silnika syntezatora.

Który pakiet Pythona powinienem użyć?

Oficjalny pakiet openai. Ustaw bazowy URL na https://api.xtts.cc/v1, przekaż swój klucz i użyj identyfikatora modelu uncensored.

Czy mogę używać strumieniowania odpowiedzi w Pythonie?

Tak. Przekaż stream=True i iteruj po kawałkach. Zużycie jest raportowane na końcu strumienia, a zapytanie zajmuje slot współbieżności aż do zakończenia strumienia.

Co się stanie, gdy osiągnę limit zapytań?

Otrzymasz HTTP 429 z kodem rate_limited lub concurrency. Poczekaj na wartość Retry-After, jeśli występuje, i ponów próbę, utrzymując liczbę wątków na poziomie 8 lub mniej dla pełnego klucza lub 2 dla klucza próbnego.

Twój klucz jest o jeden formularz stąd

Utwórz konto, skopiuj klucz, zmień bazowy URL. To cała konfiguracja.