コンテンツにスキップ

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

公開日
最終更新日

聴くこの記事を聴く

テキスト読み上げAPIの統合はシンプルですが、いくつかの具体的な決定が必要です。どの転送モードを使うか、モデルや出力フォーマットの選び方、ストリーミング方法、高負荷時に同時実行数を超えない工夫、キャッシュやリトライで同じ音声を二重に生成しない方法、他社とのTTFB(最初のバイトまでの時間)の比較などです。

テキスト読み上げAPIの統合をサポートするために、これらの設計上の判断ポイントをすべて分解し、やるべきことをまとめました。このガイドを読めば、ElevenLabsのテキスト読み上げAPIをスケールさせる方法がわかります。すぐに使えるコードスニペットも掲載しています。

ここで紹介している概念の詳しい背景は、以下のガイドもご覧ください:オーディオストリーミングの基礎レイテンシ最適化、およびElevenLabsモデル概要

まとめ

  • ElevenLabsのテキスト読み上げAPIエンドポイントは1つで、バッチ変換、HTTPストリーム、ストリーム入力WebSocketの3通りで利用できます。
  • HTTP経由では、処理中のリクエストすべてが同時実行数の上限にカウントされますが、WebSocketでは実際に音声を生成している間だけカウントされます。
  • 同時実行数はプラン上限より少し下に設定し、出力に影響するパラメータのハッシュでキャッシュすることで、同じテキストに二重課金されないようにしましょう。
  • 429や5xxエラーは指数バックオフ+ジッターでリトライし、同時実行数の壁にぶつかる前に負荷を下げてください。

テキスト読み上げAPIの統合方法は3つ

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

同じPOST /v1/text-to-speech/{voice_id}呼び出しでも、3つの形で使えます。それぞれの特徴をまとめました:

  • バッチ(変換)は最もシンプルな統合方法: 1リクエストで1つの音声レスポンスが返ります。最も簡単ですが、最初の音声が返るまでの時間は最長です(全クリップ生成後に返るため)。
  • HTTPストリーミング(stream)は同じリクエストでレスポンスを分割: パスに/streamを追加し、ストリームメソッドを呼び出すと、音声がチャンクで返ります。コードはほぼ同じで、体感レイテンシが大幅に下がります。
  • WebSocket(stream-input)は持続的な接続を維持: テキストを少しずつ送信し、その都度音声チャンクが返ります。対話型エージェントや、LLMの出力をリアルタイムで音声化する用途に最適です。

ストリーミングにしてもモデルの生成速度自体は変わりません。違いは最初のチャンクが全クリップ完成前に送られる点で、ユーザーの待ち時間が短く感じられます。

バッチ vs. ストリーミング vs. 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つで決まります。

最初からこの2つを適切に選ぶことで、レイテンシや電話システム対応など後工程もスムーズになります。

モデル

ElevenLabsでは複数のテキスト読み上げモデルを提供しています。どれが一番という順位はなく、それぞれ特徴があります。

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は大型で、1文字あたりの処理に時間をかけて高品質な出力を生成します。Flashの速度でEleven v3の品質を得る設定はありません。品質は追加計算によるものです。

リアルタイムやエージェント用途の場合は、eleven_flash_v2_5 をご利用ください。最も低遅延でMultilingual対応のオプションです。ナレーション、

電話番号や日付、通貨など発音が重要な場合は、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とインスタントボイスクローンの組み合わせが最も低遅延です。

本ガイドでは、サンプルのvoice idとしてJBFqnCBsd6RMkjVDRZzb(George)を使用しています。

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

このセクションでは、テキスト読み上げ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などのcamelCaseキー)を指定します:

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はプラン上限より少し低めに設定しましょう。余裕を持たせることで、同じAPIキーを使う他のトラフィックも吸収でき、429エラーを回避しやすくなります。

文字数制限と長文分割

各モデルには1リクエストあたりの最大文字数があります。長文を扱う場合はテキストを分割し、音声をつなぎ合わせる必要があります。

各モデルの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;
}

チャンクを順番にレンダリングし、音声を連結します。各チャンクが独立している長文ナレーションの場合は、splitTextの出力を上記のバウンデッドプールに渡すだけでOKです。

キャッシュと冪等性

テキスト読み上げの出力は十分決定的なので、同じテキスト・ボイス・モデル・設定で再生成するのは無駄です。音声に影響する入力のハッシュをキーにキャッシュし、リトライ時の冪等性トークンとしても使いましょう。

その方法を以下に紹介します。

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");
}

より多くの余裕が必要な場合は、プランのアップグレードをご検討ください。エンタープライズのお客様はアカウントマネージャー経由で上限引き上げをリクエストできます。

レイテンシとTTFB(最初のバイトまでの時間)のベンチマーク

レイテンシは地域・入力・負荷状況によって変動するため、信頼できる数値はご自身の環境で計測したものだけです。

このセクションでは、FlashストリーミングエンドポイントのTTFB計測方法を紹介します。同じ仕組みで他社サービスも比較できます。

これは手法の紹介であり、公式な性能値ではありません。1回の計測結果は保証になりません。

テキスト読み上げAPI統合のレイテンシをベンチマークする際の注意点:

  • ネットワーク往復時間を含める:TTFBは地理的条件やプロバイダーの最寄りクラスタに依存するため、実際にサーバーを運用する場所からテストしてください。
  • ウォームアップ実行を捨てる:コールド接続への最初のリクエストは遅くなりやすく、数値が偏ります。
  • 入力を固定する:入力長・ボイス・モデル・負荷で結果が変わるため、プロバイダー間で条件を揃えてください。
  • 分布で報告する:計測ごとに数値は変動するため、中央値やp95(95パーセンタイル)を公開しましょう。

これらを踏まえれば、ベンチマークの準備は万全です。

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(ms単位)を報告してください。

公正な比較には変数のコントロールが重要です。

両プロバイダーを同じマシン・ネットワーク(できれば実際の運用リージョンのサーバー)で実行し、同じ入力テキスト・短めの音声で計測してください。モデル推論時間が主な要素となるようにし、複数回の中央値・p95を報告しましょう。1回の計測はノイズです。

パブリックインターネット経由のTTFBには、モデルとは無関係な20~200msのネットワーク往復が含まれます。ElevenLabsは北米・欧州・東南アジアにクラスタがあり、最寄りにルーティングされます。テストクライアントも同じ地域に配置しないと、実質的にデータセンターまでの距離を測ることになります。

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

本番環境での

これらを押さえれば、他の部分も自然にうまくいきます:

  • 用途ごとにモデルを選ぶ: インタラクティブ用途はFlash v2.5、高音質が必要な場合はMultilingual v2やElevenv3を。レイテンシが気にならないオフラインレンダリング向けです。
  • ユーザーが待つ場面ではストリーミングを使う: 既知テキストはHTTPストリーミング、エージェント用途はWebSocketで、アイドル時間を同時実行数に含めないようにしましょう。
  • 同時実行数はプラン上限に合わせて制御: 同時リクエスト数は上限より少し下に抑え、出力に影響するパラメータのハッシュでキャッシュし、同じ音声に二重課金されないようにしましょう。
  • 429や5xxは指数バックオフ+ジッターでリトライ: 429や5xxはフルジッター付きでバックオフし、同時実行数ヘッダーで上限に近いか確認しましょう。
  • 長文は文単位で分割: 各モデルの文字数制限内で文単位に分割し、イントネーションの自然さを保ちましょう。

さらに詳しく知りたい方は、ストリーミングの使い方オーディオストリーミングの概念認証クライアントサイド用ワンタイムトークンもご覧ください。

ElevenAPIでテキスト読み上げ統合を構築しよう

このガイドを読めば、本番環境向けテキスト読み上げAPI統合に必要なパターンがすべて揃います。ストリーミング、バッチ処理、キャッシュ、リトライ、ベンチマークまで、すぐに運用を始められます。

まずはテキスト読み上げAPIについて詳しく知るか、サインアップ で最初のリクエストを実行してみましょう

テキスト読み上げAPI統合に関するFAQ

関連記事

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