RU ▾

API текста без цензуры для голосовых конвейеров

uncensoredhttps://api.xtts.cc/v1

TTS API Python: подготовка текста для синтеза речи с помощью SDK

Этот API не возвращает аудио. Это текстовая модель, которую вы вызываете из Python с помощью SDK OpenAI, и она расположена перед голосовым движком: она пишет сценарий, очищает его и разбивает на предложения, которые ваш голосовой этап может прочитать вслух.

Обновлено

Изометрическая иллюстрация текстовых блоков, переходящих из терминала в звуковую волну

Что делает и чего не делает этот API

Если вы искали учебник по tts api python, один момент должен быть ясен перед написанием кода: этот сервис не синтезирует речь. Он предоставляет POST /v1/chat/completions и GET /v1/models для одной текстовой модели с открытыми весами с идентификатором модели uncensored. Нет эндпоинта аудио, нет списка голосов и нет аудиофайла ни в одном ответе. Также нет вывода изображений, видео или эмбеддингов.

Так где же она вписывается в проект синтеза речи? В текстовой части конвейера. Прежде чем голосовой движок сможет прочитать сценарий, кто-то должен его написать, развернуть числа и аббревиатуры в произносимые слова, убрать разметку и разбить на части, достаточно короткие для синтеза без длинных пауз. Языковая модель делает эту работу хорошо, и вы можете вызывать её из Python с помощью того же SDK, который вы, возможно, уже используете для других сервисов, совместимых с OpenAI.

Модель настроена так, чтобы отвечать без отказов в генерации при законном использовании взрослыми. Это важно для художественной литературы, диалогов персонажей и повествования, где более строгая модель может остановиться на полуслове. Одно строгое правило: запрет на сексуальный контент с участием несовершеннолетних.

Остальная часть этой статьи создает небольшой модуль 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 отправляет токены через server-sent events, и вы можете передать каждое готовое предложение голосовому этапу, пока остальная часть все еще пишется.

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.", что является еще одной причиной нормализовать текст заранее, как показывает следующий раздел.

Нормализуйте текст для восприятия на слух

Письменный текст и устная речь различаются. «Д-р Ли купил 3 экземпляра 3/4 для 2-го издания» легко читать и неудобно произносить. Многие речевые движки обрабатывают часть этого, но результаты варьируются в зависимости от движка, поэтому очистка сценария перед тем, как он достигнет голосового этапа, устраняет класс сюрпризов. Попросите модель сделать это с явной инструкцией:

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" — стандартный пример: читать ее как 4 марта или 3 апреля? Поместите свой выбор в системный промпт, потому что модель не может угадать вашу локаль.

Стоимость этого шага

Нормализация стоит недорого. Абзац из 600 токенов на входе и около 650 токенов на выходе стоит 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 токенов. Для длинной главы книги отправляйте её частями с кратким резюме предыдущей части вместо полного текста каждый раз. Это также снижает счет за ввод.
  • Повторное использование одного глобального клиента в потоках без ограничения. Клиент безопасен для общего использования, но сервис не бесконечен. Ограничьте размер пула лимитом параллельных запросов вашего ключа.
  • Ключ, прописанный в коде. Используйте переменную окружения и смените ключ, если он когда-либо попадет в репозиторий.

Если что-то выглядит неправильно, логируйте заголовок 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 и держите все детали, специфичные для движка, внутри неё. Приведенный выше текстовый конвейер вызывает эту функцию для каждого предложения или сегмента. Когда вы меняете движки, ничего больше не меняется.

Если вы все еще выбираете между пробным и платным вариантом, цифры просты: вы платите $0,15 за миллион входных токенов и $0,50 за миллион выходных токенов из предоплаченного баланса, который не сгорает, без подписки. Пополнения от $20 до $500 можно оплатить криптовалютой (USDT в сети TRC20, USDC в сети Base).

Вопросы и ответы

Возвращает ли этот API аудиофайлы?

Нет. Он возвращает только текст. Используйте его для написания, очистки и структурирования сценариев, а затем передайте текст в отдельный голосовой движок.

Какой пакет Python выбрать?

Официальный пакет openai. Задайте base_url равным https://api.xtts.cc/v1, передайте свой ключ и используйте идентификатор модели uncensored.

Можно ли получать ответы потоком в Python?

Да. Передайте stream=True и итерируйте по фрагментам. Данные об использовании приходят в конце потока, а запрос удерживает слот параллельных запросов до завершения потока.

Что происходит при достижении лимита запросов?

Вы получаете HTTP 429 с кодом rate_limited или concurrency. Дождитесь значения Retry-After, если оно присутствует, и повторите запрос, и поддерживайте количество рабочих потоков на уровне 8 или ниже для полного ключа или 2 для пробного ключа.

До вашего ключа — одна форма

Создайте аккаунт, скопируйте ключ, измените базовый URL. Это вся настройка.