コンテンツへ移動

テキスト読み上げAPIの統合:ストリーミング、バッチ処理、リトライ

公開日
最終更新日

聴くこの記事を聴く

テキスト読み上げ(TTS)APIの統合は簡単です。ただし、その前にいくつか具体的な判断が必要です。どの転送モードを使うか、モデルと出力形式をどう選ぶか、ストリーミングをどう行うか、同時実行数の上限を超えずに大量のリクエストを処理する方法、同じオーディオのレンダリングに二度支払わないためのキャッシュとリトライ方法、別のプロバイダーと最初のバイトを受信するまでの時間を比較するベンチマーク方法などです。

テキスト読み上げ(TTS)APIの統合を支援するため、こうしたアーキテクチャ上の判断と対応方法を一つずつ解説します。このガイドでは、ElevenLabsのテキスト読み上げ(TTS)APIを統合し、スケールする方法を紹介します。本番環境にそのまま貼り付けて使えるコードスニペットも用意しています。

ここで取り上げる概念について詳しく知りたい場合は、以下のガイドをご覧ください。オーディオストリーミングを理解するレイテンシーの最適化、およびElevenLabsモデル概要。 

概要

  • ElevenLabsのテキスト読み上げ(TTS)APIエンドポイントは1つで、バッチ変換、HTTPストリーム、stream-input WebSocketという3つの方法で利用できます。
  • HTTPでは処理中のすべてのリクエストが同時実行数の上限にカウントされますが、WebSocketではアクティブな生成のみがカウントされます。
  • 並列処理数をプランの上限より少し低く抑え、出力に影響するすべてのパラメータのハッシュをキャッシュしてください。これにより、同じテキストに二度課金されることを防げます。
  • 429と5xxには、同時実行数の上限に達する前に負荷を下げられるよう、指数バックオフとフルジッターでリトライしてください。

テキスト読み上げ(TTS)APIを統合する3つの方法

テキスト読み上げエンドポイントは1つですが、統合方法によってレイテンシー、複雑さ、コストが変わります。 

同じPOST /v1/text-to-speech/{voice_id}呼び出しを3つの形式で使え、それぞれ少し異なる用途に適しています。テキスト読み上げ(TTS)APIを統合する3つの方法を見ていきましょう。

  • バッチ(convert)は最もシンプルな統合方法です: 1回のリクエストを送信し、1つのオーディオレスポンスを受け取ります。最も複雑さが少ない一方、すべてのバイトが返る前にクリップ全体を合成するため、最初のオーディオを受信するまでの時間は最も長くなります。
  • HTTPストリーミング(stream)は同じリクエストを使い、レスポンスをチャンク化します: パスに/streamを追加してstreamメソッドを呼び出すと、オーディオがチャンク化レスポンスとして返ります。コードはほぼ同じで、体感レイテンシーは大幅に下がります。
  • WebSocket(stream-input)は永続接続を維持します: テキストを段階的に送信し、オーディオチャンクを順次受け取ります。インタラクティブなエージェントや、文が完成する前にLLMの出力をトークン生成と同時に音声へ渡す用途向けに設計されています。

ストリーミングでモデルのオーディオ生成が速くなるわけではありません。推論時間は変わりません。ストリーミングで変わるのは最初のチャンクを受け取るタイミングです。クリップ全体の完成前に送信されるため、総処理量が同じでもユーザーの待ち時間は短く感じられます。

バッチ、ストリーミング、WebSocketの選択表

この3つの方法から選ぶ際には、考慮すべき要素がいくつかあります。

簡単な目安として、オフラインレンダリングにはバッチ、ユーザーが待機している既知のテキストにはHTTPストリーミング、エージェントやリアルタイムのLLMから音声への変換にはWebSocketを選んでください。 

以下の表では、大規模運用で重要となる観点ごとのトレードオフを説明します。

Batch (convert)
Time-to-first-audio
Highest (wait for full clip)
Implementation complexity
Lowest
Text known up front?
Required
Streaming LLM output into TTS
Awkward
Concurrency cost
Each request counts fully
Best for
Offline rendering, audiobooks, caching
HTTP streaming
Time-to-first-audio
Low
Implementation complexity
Low
Text known up front?
Required
Streaming LLM output into TTS
Awkward
Concurrency cost
Each request counts fully
Best for
Web/app playback of known text
WebSocket (stream-input)
Time-to-first-audio
Lowest
Implementation complexity
Highest (connection lifecycle, framing)
Text known up front?
Not required - send incrementally
Streaming LLM output into TTS
Native fit
Concurrency cost
Only active generation counts
Best for
Voice agents, live LLM to speech

HTTPでは、バッチでもストリーミングでも、処理中の各リクエストが期間全体にわたってプランの同時実行数上限にカウントされます。WebSocketでは、モデルが実際にオーディオを生成している時間だけがカウントされます。接続中でもアイドル状態のソケットはほとんどコストがかかりません。

カスケード型音声エージェントでは、会話全体を通じて接続を維持しつつ、オーディオを生成するのはエージェントの発話時だけです。この違いは大きく、エージェントを構築する際にWebSocketを使う主な理由です。プロトコルの詳細は、リアルタイムのテキスト読み上げWebSocketガイドに記載されています。

モデルと出力形式の選び方

TTS API統合で返されるオーディオは、2つの選択によって決まります。1つ目は品質と速度を決めるモデル、2つ目はコンテナ、ビットレート、サンプルレートを決める出力形式です。

この2つを最初から適切に選べば、レイテンシーや電話システムとの互換性など、後続の要素もスムーズに整います。

モデル

複数のテキスト読み上げモデルを提供しています。優劣で順位付けされているのではなく、それぞれ異なるトレードオフがあります。

Best for
eleven_flash_v2_5
Real-time, agents, bulk throughput (~75ms model inference)
eleven_flash_v2
Real-time, English only (~75ms)
eleven_multilingual_v2
Highest stable fidelity, narration
eleven_v3
Most expressive, widest language range
Languages
eleven_flash_v2_5
32
eleven_flash_v2
English
eleven_multilingual_v2
29
eleven_v3
70+
Character limit
eleven_flash_v2_5
40,000
eleven_flash_v2
30,000
eleven_multilingual_v2
10,000
eleven_v3
5,000

補足すると、約75msという数値はネットワークとアプリケーションのレイテンシーを除いた、代表的な条件下でのモデル推論時間です。入力が長い場合や負荷が高い場合は増加します。ベンチマーク値ではなく、必ずアプリケーションから計測してください。

Flashモデルは小型で、推論時間を短縮するためにより積極的な近似を採用しています。Eleven v3とMultilingual v2はより大きなモデルで、豊かな出力を生成するために文字ごとにより多くの時間をかけます。Eleven v3の品質をFlashの速度で得られる設定はありません。その品質には追加の計算が必要だからです。

リアルタイムまたはエージェント用途には、最も低レイテンシーの多言語オプションであるeleven_flash_v2_5を使用してください。ナレーション、オーディオブック、マーケティング用ボイスオーバーには、安定した高忠実度が必要ならeleven_multilingual_v2を、最大限の表現力と感情の幅が必要ならeleven_v3を使用してください。 

電話番号、日付、通貨など発音が重要な場合は、テキストがAPIに届く前にアプリケーション側で数値を正規化してください。希望する読み方をそのまま書き出します。 

アプリケーション側で正規化すれば、モデル間で発音の予測可能性を保て、変更される可能性があるモデル固有のデフォルト設定に依存せずに済みます。

出力形式

output_formatパラメータは、返されるオーディオのコンテナ、サンプルレート、ビットレートを制御します。主に使用する値は次のとおりです。

Use case
mp3_44100_128
General playback, downloads, highest mp3 quality shown here
mp3_22050_32
Lower-bandwidth playback, smaller files
pcm_24000 / pcm_16000
Raw PCM for your own audio pipeline or further processing
ulaw_8000
Telephony - the format used with Twilio and similar systems
Languages
mp3_44100_128
32
mp3_22050_32
English
pcm_24000 / pcm_16000
29
ulaw_8000
70+
Character limit
mp3_44100_128
40,000
mp3_22050_32
30,000
pcm_24000 / pcm_16000
10,000
ulaw_8000
5,000

音声設定

以下の設定で、生成された音声の話し方を制御できます。

  • Stability: 一貫性と表現力のバランスを制御します。値を下げると変化に富んだ表現豊かな音声になり、値を上げると安定した予測可能な話し方になります。
  • SimilarityBoost: 出力が参照音声にどの程度忠実に従うかを制御します。
  • Style: 値を上げると、音声本来の話し方のスタイルを強調します。
  • useSpeakerBoost: わずかなレイテンシーを代償に、元の話者への類似性を高めます。
  • Speed: デフォルト値の1.0を基準に話す速さを調整します。

これらの設定のうち、通常はStabilityが体感品質に最も大きく影響します。低い値では表現力は高まりますが一貫性は下がり、高い値では一貫性と予測可能性を優先します。

音声を選ぶ際、最も低レイテンシーな組み合わせはFlashとInstant ボイスクローン、またはデフォルト音声です。プロフェッショナルボイスクローンは優れた音質ですが、生成ごとのオーバーヘッドを考慮する必要があります。

このガイドでは、サンプル音声IDとしてJBFqnCBsd6RMkjVDRZzb(George)を使用します。

ストリーミング統合(HTTPとWebSocket)

このセクションでは、テキスト読み上げ(TTS)API統合の実践的な核心を扱います。SDKのインストール、ストリームの開始、届いたオーディオの消費を解説します。HTTPはほとんどのWeb・アプリ再生を対象とし、WebSocketはエージェントとリアルタイムのLLM出力を対象とします。

どちらの方法も、以下のようにElevenLabsクライアントを初期化済みであることを前提としています。

npm install @elevenlabs/elevenlabs-js
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";

const elevenlabs = new ElevenLabsClient({
  apiKey: process.env.ELEVENLABS_API_KEY,
});

ストリーミングではストリームを開き、到着したチャンクを消費します。voiceIdが最初の位置引数で、その後にキャメルケースのキー(modelId、outputFormat、voiceSettings)を持つオプションオブジェクトを指定します。

const stream = await elevenlabs.textToSpeech.stream("JBFqnCBsd6RMkjVDRZzb", {
  text,
  modelId: "eleven_flash_v2_5",
  outputFormat: "mp3_44100_128",
  voiceSettings: { stability: 0, similarityBoost: 1.0, style: 0, useSpeakerBoost: true, speed: 1.0 },
});

for await (const chunk of stream) {
  // chunk is a Buffer; feed it to the player as it arrives
}

WebSocket版では、wss://api.elevenlabs.io/v1/text-to-speech/{voice_id}/stream-inputに接続します。まず音声設定と先頭のスペースを含むメッセージを送信し、利用可能になったテキストを順次送信します。audioフィールドにBase64エンコードされたチャンクを持つJSONフレームを読み取ります。

高スループットに対応するバッチ処理と同時実行数の上限

高スループットの統合は、同時にオーディオを生成するリクエスト数である同時実行数に左右されます。各プランにはモデルファミリーごとの上限があります。 

各プランにはそれぞれ異なる同時実行数上限があります。

  • 無料: Flashリクエストを4件まで同時実行。
  • スターター: Flashリクエストを6件まで同時実行。
  • クリエイター: Flashリクエストを10件まで同時実行。
  • プロ: Flashリクエストを20件まで同時実行。
  • スケールとビジネス: Flashリクエストを30件まで同時実行。エンタープライズの上限は個別設定です。

Multilingual v2の上限は上記のおよそ半分です。

境界付きプールを使うと、同時に実行するリクエスト数を制限できます。

// Set MAX_CONCURRENCY at or below your plan's Flash concurrency limit.
const MAX_CONCURRENCY = 8;

async function synthMany(texts: string[]): Promise<Buffer[]> {
  const results: Buffer[] = [];
  for (let i = 0; i < texts.length; i += MAX_CONCURRENCY) {
    const batch = texts.slice(i, i + MAX_CONCURRENCY);
    results.push(...(await Promise.all(batch.map(eachSingleRequest)))); // never more than MAX_CONCURRENCY in flight
  }
  return results;

MAX_CONCURRENCYはプランの上限と同じ値ではなく、少し低く設定してください。この余裕により、同じキーを共有する他のトラフィックを吸収し、429が返されるしきい値を下回れます。

文字数制限と長いテキストの分割

各モデルには、1回のリクエストで受け付ける文字数の上限があります。長文を扱う統合では、テキストを分割し、オーディオをつなぎ合わせる必要があります。 

モデルごとのリクエストあたりの文字数上限は次のとおりです。

  • Flash v2.5: 1リクエストあたり最大40,000文字。
  • Flash v2: 1リクエストあたり最大30,000文字。
  • Multilingual v2: 1リクエストあたり最大10,000文字。
  • Eleven v3: 1リクエストあたり最大5,000文字。

これを超えるテキストは複数のリクエストに分割する必要があります。チャンクのつなぎ目でもプロソディを維持できるよう、文の境界で分割するようにしてください。

function splitText(text: string, maxChars: number): string[] {
  const sentences = text.trim().split(/(?<=[.!?])\s+/);
  const chunks: string[] = [];
  let current = "";
  for (let sentence of sentences) {
    if (current.length + sentence.length + 1 > maxChars) {
      if (current) chunks.push(current.trim());
      // A single sentence longer than the limit is hard-split.
      while (sentence.length > maxChars) {
        chunks.push(sentence.slice(0, maxChars));
        sentence = sentence.slice(maxChars);
      }
      current = sentence;
    } else {
      current = `${current} ${sentence}`.trim();
    }
  }
  if (current) chunks.push(current.trim());
  return chunks;
}

チャンクを順番にレンダリングし、オーディオを連結します。各チャンクが独立した長文ナレーションでは、この2つをそのまま組み合わせられます。splitTextの出力を上記の境界付きプールに渡し、残りの処理を任せてください。

キャッシュと冪等性

テキスト読み上げの出力は十分に決定的なので、同じテキストを同じ音声、モデル、設定で再レンダリングするのは無駄です。オーディオに影響する入力のハッシュをキーにして結果をキャッシュしてください。同じキーをリトライ時の冪等性トークンとしても使えます。

両方を実現する方法は次のとおりです。

import { createHash } from "node:crypto";

function cacheKey(text: string, voiceId: string, modelId: string,
                  outputFormat: string, settings: object): string {
  // Every parameter that changes the audio must be in the key.
  const payload = JSON.stringify({ text, voiceId, modelId, outputFormat, settings });
  return createHash("sha256").update(payload).digest("hex");
}

async function cachedSynth(text: string, voiceId: string, modelId: string,
                           outputFormat: string, settings: object): Promise<Buffer> {
  const key = cacheKey(text, voiceId, modelId, outputFormat, settings);
  const cached = await cacheGet(key);          // e.g. read from disk or S3
  if (cached) return cached;

  const audio = await elevenlabs.textToSpeech.convert(voiceId, { text, modelId, outputFormat });
  await cachePut(key, audio);                   // store the bytes under the key
  return audio;
}

これを機能させるには、outputFormatと音声設定を含め、オーディオを変更するすべてのパラメータをキーに含める必要があります。正しく実装すれば、同じキーが冪等性トークンとしても機能します。すでに成功したリクエストをクライアントがリトライした場合は、再生成せずにキャッシュ済みのバイト列を返します。

エラー処理とレート制限(429)

本番環境のクライアントには、バックオフとジッターを伴うリトライに加え、ステータスコードに応じた処理が必要です。リトライする価値のある失敗と、そうでない失敗があるためです。 

以下の表は各ステータスに適した対応を示し、このセクションでは429がハードな壁ではなくソフトリミットである理由を説明します。

Meaning
401
Authentication failed
422
Invalid request
429
Concurrency exceeded
5xx
Transient server error
Action
401
Do not retry. Check the xi-api-key header and key validity.
422
Do not retry. Fix the payload (bad voice id, unsupported format, text over limit).
429
Retry with exponential backoff and jitter.
5xx
Retry with backoff.
Character limit
401
40,000
422
30,000
429
10,000
5xx
5,000

429はハードな壁ではなく、その仕組みを理解しておくと役立ちます。同時実行数の上限を超えると、リクエストはまず優先度に応じてキューに入り、通常は約50msの遅延が追加されます。それでも容量を超えている場合にのみ429が返されます。 

レスポンスには、現在の余裕を示すcurrent-concurrent-requestsおよびmaximum-concurrent-requestsヘッダーも含まれます。これらを読み取り、上限に達する前に負荷を下げられます。

const RETRYABLE = new Set([429, 500, 502, 503, 504]);

async function synthWithRetry(text: string, voiceId: string, maxRetries = 5): Promise<Buffer> {
  let delay = 500; // ms, base for exponential backoff
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await elevenlabs.textToSpeech.convert(voiceId, {
        text, modelId: "eleven_flash_v2_5", outputFormat: "mp3_44100_128",
      });
    } catch (err: any) {
      const status = err.statusCode;
      // 401/422 and exhausted retries are not recoverable here.
      if (!RETRYABLE.has(status) || attempt === maxRetries) throw err;
      // Exponential backoff with full jitter.
      await new Promise((r) => setTimeout(r, Math.random() * delay));
      delay = Math.min(delay * 2, 8000);
    }
  }
  throw new Error("unreachable");
}

リトライ動作の改善ではなく、より大きな余裕が必要な場合はプランをアップグレードしてください。エンタープライズのお客様は、アカウントマネージャーを通じて上限の引き上げをリクエストできます。

レイテンシーと最初のバイトを受信するまでの時間のベンチマーク

レイテンシーはリージョン、入力、現在の負荷に依存します。つまり、信頼できるレイテンシー値は自身の環境から計測したものだけです。 

このセクションではFlashストリーミングエンドポイントの最初のバイトを受信するまでの時間(TTFB)を扱います。同じハーネスを別のプロバイダーにも向け、同一条件で比較できる構成です。

これは公開結果ではなく、手法として捉えてください。1回の実行結果で何かが保証されるわけではありません。 

テキスト読み上げ(TTS)API統合のレイテンシーをベンチマークする際に重要な注意点をいくつか紹介します。

  • ネットワーク往復時間を含める: TTFBは地域とプロバイダーの最寄りのクラスターに依存するため、通常サーバーを稼働させる場所からテストを実行してください。
  • ウォームアップ実行を除外する: コールド接続への最初のリクエストは遅く、数値を歪める可能性があります。
  • 入力を固定する: 入力の長さ、音声、モデル、負荷はいずれも結果に影響するため、プロバイダー間で同一に保ってください。
  • 分布を報告する: 数値は実行ごとに変動するため、単一の値ではなく中央値とp95を公開してください。

これらを踏まえたら、ベンチマークの準備は完了です。

const TEXT = "This is a fixed benchmark sentence used for every provider.";

async function measureElevenLabs(): Promise<number> {
  const start = performance.now();
  const res = await fetch(
    "https://api.elevenlabs.io/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb/stream?output_format=mp3_44100_128",
    {
      method: "POST",
      headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY!, "Content-Type": "application/json" },
      body: JSON.stringify({ text: TEXT, model_id: "eleven_flash_v2_5" }),
    },
  );
  for await (const _ of res.body!) {
    return performance.now() - start; // first chunk received
  }
  throw new Error("no audio returned");
}

別のプロバイダーと比較するには、同じ形式の関数を作成してください。次に、小さなランナーで両方を実行します。ウォームアップ呼び出しを1回除外し、相互に競合しないよう間隔を空けて約20件の時間計測サンプルを取り、中央値とp95をミリ秒で報告します。

公平な比較には、変数を制御することが重要です。 

両方のプロバイダーを同じマシンとネットワークから実行してください。家庭用ブロードバンドのノートPCではなく、実際にデプロイするリージョンのサーバーが理想的です。同じ入力テキストを使用し、生成時間ではなくモデル推論が数値の中心となるよう、オーディオは短く保ちます。単一の計測値はノイズなので、複数回の実行で中央値とp95を報告してください。 

パブリックインターネット上のTTFBには、モデルとは無関係な20〜200msのネットワーク往復時間が含まれる点に注意してください。北米、欧州、東南アジアのクラスターから最寄りのクラスターへルーティングしているため、テストクライアントもそれに合わせて配置してください。そうしないと、主にデータセンターまでの距離をベンチマークすることになります。

テキスト読み上げ(TTS)API統合の重要ポイント

本番環境向けのテキスト読み上げAPI統合は、影響の大きいいくつかの判断に集約されます。

これらを適切に行えば、他の要素も自然に整います。

  • 用途に応じてモデルを選ぶ: インタラクティブな用途にはFlash v2.5を使い、Multilingual v2やEleven v3のような高忠実度モデルは、レイテンシーの重要性が低いオフラインレンダリングに使用してください。
  • ユーザーが待っている場合はストリーミングを使う: 既知のテキストにはHTTPストリーミングを、エージェントにはWebSocketを使用し、アイドル時間が同時実行数の予算に含まれないようにします。
  • 並列処理数をプラン上限内に抑える: 同時リクエスト数をプラン上限より少し低く制限し、出力に影響するすべてのパラメータのハッシュをキャッシュしてください。同じオーディオに二度課金されることを防げます。
  • 429と5xxを指数バックオフとフルジッターでリトライする: 429と5xxではフルジッター付きでバックオフし、同時実行数ヘッダーで上限までの余裕を確認してください。
  • 長いテキストは文の境界で分割する: 各モデルの文字数上限内で文の境界で分割し、プロソディをつなぎ目で維持してください。

さらに詳しく知りたい場合は、ストリーミングのハウツーオーディオストリーミングの概念認証、およびクライアントサイド用の単回使用トークンをご覧ください。

ElevenAPIでテキスト読み上げ統合を構築する

このガイドを読めば、本番環境向けのテキスト読み上げ(TTS)API統合に必要なすべてのパターンを習得できます。ストリーミング、バッチ処理、キャッシュ、リトライ、ベンチマークまで、運用に移す準備は万全です。 

テキスト読み上げ(TTS)APIの詳細を確認するか、登録して、今日からElevenAPIで最初の呼び出しを行いましょう。

テキスト読み上げ(TTS)API統合FAQ

関連記事

最高品質のAIオーディオで創造する