JA ▾

音声パイプライン向け無検閲テキストAPI

uncensoredhttps://api.xtts.cc/v1

TTS API Python: 音声用のテキストを準備する

このAPIは音声を返しません。OpenAI SDKを使用してPythonから呼び出すテキストモデルであり、音声エンジンの前に配置できます。スクリプトを作成し、前処理を行い、音声ステージが読み上げられるように文に分割します。

更新日:

ターミナルから音波ゲートへ流れるテキストブロックの等角図

このAPIの機能と制限

tts api pythonのチュートリアルをお探しの場合、コードを書く前に1点明確にしておくべきことがあります。このサービスは音声を合成しません。モデルIDがuncensoredのオープンウェイトテキストモデルに対して、POST /v1/chat/completionsとGET /v1/modelsを公開しています。音声エンドポイント、音声リスト、レスポンス内の音声ファイルはありません。画像、動画、埋め込み出力もありません。

音声プロジェクトでどこに位置づけられるでしょうか?パイプラインのテキスト側です。音声エンジンがスクリプトを読み上げる前に、誰かがそれを作成し、数字や略語を音声用単語に展開し、マークアップを削除し、長い沈黙なしで合成できる十分な短さに分割する必要があります。言語モデルはこの作業を適切に行い、すでに他のOpenAI互換サービスに使用しているSDKと同じものを使用してPythonから呼び出すことができます。

このモデルは、合法的な成人向け用途に対してコンテンツ拒否なしで回答するように調整されています。これは、より厳格なモデルがシーン途中で停止する可能性があるフィクション、キャラクターの会話、ナレーションにおいて重要です。ただし、1つの厳格なルールがあります:未成年者を含む性的コンテンツを禁止します。

この記事の残りの部分では、4つのステップで小さな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ヘッダーが含まれ、ログ記録用に生レスポンスから残高クレジットを確認できます。docs ページに他のヘッダーとパラメータを記載しています。

temperature、top_p、stop、seedなどのパラメータやペナルティはそのまま渡されます。ナレーションの場合、temperatureは0.6から0.9が適切な開始範囲です。プロンプトを調整しながら生成結果を再現したい場合、固定されたseedが役立ちます。

スクリプトをストリーミングして文に分割する

音声エンジンは通常、文ごとに処理されます。900語のスクリプトが完了するのを待つと、リスナーも待たされます。stream: trueを使用すると、APIはサーバー送信イベント(SSE)でトークンを送信し、残りが生成中でも、完成した文を音声処理段階に渡すことができます。

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)

見落としがちな2つの詳細があります。まず、使用量データはストリームの末尾に、空のchoicesリストを持つチャンクとして届くため、ループはchoicesのないチャンクをスキップします。第二に、ストリーミングリクエストは同時実行数のスロットをストリーム終了まで保持します。音声エンジンが遅く、ストリームの処理を遅延させる場合、スロットはビジー状態のままになります。テキストを素早く取得し、文をキューに入れ、音声側がキューを自身のペースで消費するようにしてください。

上記の正規表現は、空白に続くピリオドまたは疑問符で分割します。"Dr."や"No."などの略語の後に誤って分割されるため、次のセクションで示すように、まずテキストを正規化する理由の1つになります。

耳に最適化されたテキストに正規化する

書き言葉と話し言葉は異なります。「リー博士は第2版のために3/4に3部購入しました」は読みやすくはありますが、発音するには不自然です。多くの音声合成エンジンはこれを処理できますが、エンジンによって結果が異なるため、音声処理段階に到達する前にスクリプトを前処理すると、予期せぬ事象を減らせます。明示的な指示でモデルにこれを行わせてください。

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

このステップでは低いtemperatureを使用してください。同じ入力が同じ出力を生むようにし、創造性(ランダム性)が邪魔になります。結果のサンプルを確認してから全体を処理し、曖昧な項目の処理方法を決めてください。日付「3/4」は標準的な例です: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かかります。これは段落1000件あたり約$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が許容範囲内の数値であることを確認します。検証に失敗した場合は、あきらめる前にtemperatureを下げて1回再試行してください。モデルの出力は信頼できない入力として扱い、ユーザーテキストと同じように扱います。

時間を要する落とし穴

言語モデルを音声エンジンの前に接続する際に、いくつかのミスが繰り返し発生します。

  • モデルによる解説の追加。 モデルは「もちろん、スクリプトはこちらです。」と言いたがるものです。この文は音声として読み上げられます。システムプロンプトの最後に「スクリプトのテキストのみを返してください」と記述し、コロンで終わる先頭の行を削除してください。
  • 文の長さへの信頼。 一部のエンジンは非常に長い文の処理が苦手です。プロンプトに「文は25語を超えないように」というルールを追加し、文がまだ長すぎる場合はPythonでカンマで分割して適用してください。
  • コンテキストウィンドウの忘却。 プロンプトと補完は100,000トークンを共有します。長い書籍の章の場合、毎回全文を送るのではなく、前のセグメントの短い要約とともにセクションごとに送信してください。これにより、入力料を抑えることができます。
  • 制限なしでスレッド間で1つのグローバルクライアントを再利用する。 クライアントは共有可能ですが、サービスは無限ではありません。プールをキーの同時実行数で制限してください。
  • キーのハードコーディング。環境変数を使用し、キーがリポジトリに漏れた場合に回転してください。

何かおかしい場合は、プロンプトとともにX-Request-Idヘッダーをログに記録してください。ヘルプを依頼する必要がある場合、そのIDは特定の1つの呼び出しを指し示す最も速い方法であり、ドキュメントにはその見つけ方が記載されています。

レート制限とリトライを処理する

完全なキーでは8つの同時リクエストと1分あたり300件のリクエストが可能です。トライアルキーでは2つの同時と1分あたり60件です。どちらかの制限を超えるとHTTP 429エラーが発生し、1分あたりのリクエスト数に対してはエラーコード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というシグネチャを持つ関数を1つ書き、エンジン固有の詳細をその内部に保持します。上記のテキストパイプラインは、各文またはセグメントに対してその関数を呼び出します。エンジンを切り替えても、それ以外の変更はありません。

まだトライアルと有料セットアップの間で迷っている場合、数字はシンプルです:前払いクレジットから、100万入力トークンあたり$0.15、100万出力トークンあたり$0.50を支払います。クレジットは期限が切れず、サブスクリプションも不要です。チャージは$20から$500まで可能で、暗号資産(TRC20上のUSDT、Base上のUSDC)で支払えます。

質問と回答

このAPIは音声ファイルを返しますか?

いいえ。テキストのみを返します。スクリプトの作成、整理、構造化に使用し、その後テキストを別の音声エンジンに渡してください。

どのPythonパッケージを使用すべきですか?

公式のopenaiパッケージです。base_urlをhttps://api.xtts.cc/v1に設定し、キーを渡して、モデルID uncensoredを使用してください。

Pythonでレスポンスをストリーミングできますか?

はい。stream=Trueを渡してチャンクをイテレートしてください。使用量はストリームの終了時に報告され、リクエストはストリーミングが完了するまでその同時実行枠を保持します。

レート制限に達するとどうなりますか?

エラーコードがrate_limitedまたはconcurrencyのHTTP 429を受信します。Retry-Afterの値があればそれを待ってからリトライし、完全なキーではワーカー数を8以下、トライアルキーでは2以下に保ってください。

キーはフォーム 1 つで手に入ります

アカウントを作成し、キーをコピーし、ベースURLを変更します。これでセットアップは完了です。