見出し画像

Aivis Cloud APIでストリーミング音声を実装する方法|低遅延なリアルタイム再生を解説

この記事はこんな方に向けた内容です。
・音声合成を生成途中からリアルタイムに再生したい方
・LLMと音声合成を組み合わせた対話システムを構築したい方
・Aivis Cloud APIのストリーミング仕様を正しく理解したい方
・ブラウザやPythonで低遅延再生を実装したい方
・音声形式やテキストの分割方法で迷っている方

⚠️ 本記事の内容は、2026年8月時点の情報です。
実装時は最新のAPIドキュメントもあわせてご確認ください。

皆さんこんにちは。
感情豊かな音声合成技術を誰もがかんたんに活用できる未来を目指す、Aivis Project です✨

音声合成をAIチャットやAITuberへ組み込むとき、音声の品質と同じくらい重要になるのが再生を始めるまでの待ち時間です。

音声データをすべて生成・受信してから再生する実装では、文章が長くなるほど無音の待ち時間も増えてしまいます。
LLMの応答生成が先にある場合は、LLMと音声合成の待ち時間が積み重なり、会話のテンポを崩す原因になります。

この待ち時間を短縮するのが、ストリーミング音声合成です。
生成済みの音声データを順次受信し、残りの音声を生成している間に再生を始めます。

Aivis Cloud APIは低遅延ストリーミングに対応しており、ベストエフォートで最速0.3秒から再生を開始できます。

この記事では、Aivis Cloud APIの正しいエンドポイントとリクエスト形式、ブラウザ・Pythonでの実装例、LLMと連携するときの設計ポイントまで順番に解説します📚


【結論】音声レスポンスは生成できた部分から順次返される

Aivis Cloud APIの音声合成では、次のように生成と配信、再生を並行して進められます。

テキストを送信
   ↓
最初の音声セグメントを生成 ─→ クライアントへ配信 ─→ 再生開始
   ↓
残りの音声を生成          ─→ 順次配信             ─→ 続けて再生

使用するエンドポイントは、次の1つです。

POST https://api.aivis-project.com/v1/tts/synthesize

ストリーミング専用の別エンドポイントや、stream: trueというリクエストパラメータはありません。 クライアント側でHTTPレスポンスを分割して読み取り、届いた音声データを再生機能へ順番に渡します。

⚠️ Aivis Cloud APIは、OpenAIの音声合成APIと同じエンドポイント・パラメータではありません。/v1/audio/speech、model、input、response_formatではなく、Aivis Cloud APIの/v1/tts/synthesize、model_uuid、text、output_formatを使用してください。

ストリーミングで短くなるのは「再生開始までの時間」

音声合成に関する時間は、ひとつの数字だけでは測れません。
実際の体感遅延は、大きく次の4段階に分かれます。

1. リクエスト送信
2. 最初の音声チャンクを受信
3. プレイヤーがデコード・バッファリング
4. 実際にスピーカーから再生開始

最初のデータが届くまでの時間と、実際に音が聞こえるまでの時間は同じではありません。ブラウザやプレイヤー側でデコードとバッファリングが発生するためです。

そのため、計測するときは次の値を分けて記録すると、遅延の原因を特定しやすくなります。

・リクエスト開始から最初のチャンクを受信するまで
・リクエスト開始から実際に再生が始まるまで
・音声データをすべて受信するまで

Aivis Cloud APIの公式リアルタイム音声合成デモでも、リクエスト送信・受信開始・再生開始・生成完了を分けて表示することが可能です。

正しいリクエスト形式を確認する

音声合成リクエストでは、APIキーをBearer形式で指定し、JSONへモデルUUIDとテキストを設定します。

curl "https://api.aivis-project.com/v1/tts/synthesize" \
  -H "Authorization: Bearer $AIVIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model_uuid": "YOUR_MODEL_UUID",
    "text": "こんにちは。ストリーミング音声合成のテストです。",
    "output_format": "mp3",
    "leading_silence_seconds": 0.0
  }' \
  --output output.mp3

この例では、リアルタイム再生と相性の良いMP3を指定しています。leading_silence_secondsを0.0にすると、音声先頭の無音を省き、再生開始後に声が聞こえるまでの時間を短縮できます。

なお、curl --outputは受信したデータをファイルへ保存する例です。
保存完了前から実際に再生するには、受信したチャンクをMediaSourceやストリーミング対応プレイヤーへ渡す処理が必要です。

音声形式はMP3を基本に選ぶ

Aivis Cloud APIは、WAV・FLAC・MP3・AAC・Opusの5形式に対応しています。
ただし、リアルタイム再生ではどの形式を選んでも同じというわけではありません。

ブラウザで幅広く再生するならMP3

ブラウザでのリアルタイム再生には、MP3が基本の選択肢です。
再生互換性が高く、公式デモでもMP3とMediaSourceを組み合わせています。

通信量と低遅延を優先するならOpus

Opusは圧縮効率が高く、ファイルサイズとリアルタイム性を重視する用途に適しています。
ただし、iOS Safariでの対応はiOS 18.4以降です。幅広いiPhone・iPad環境を対象にする場合は、MP3またはAACを検討してください。

保存品質を優先するならFLAC

音質を劣化させずに保存したい場合はFLACが適しています。
WAVよりもファイルサイズを抑えられます。

WAVはリアルタイム再生の第一候補ではない

WAVは無圧縮で高音質ですが、ファイルサイズが大きく、リアルタイム用途には不向きです。

また、Aivis Cloud APIのWAVストリーミングでは、配信時点でRIFFヘッダーのデータサイズが確定していません。厳密にヘッダーを検証する一部のプレイヤーでは、正常に読み込めない場合があります。

ブラウザでリアルタイム再生する

ブラウザでは、Fetch APIでレスポンスを受信し、MediaSourceへMP3チャンクを順次追加することで、すべての生成が終わる前から再生できます。

次のコードは、公式リアルタイム音声合成デモの仕組みを簡略化した実装例です。

async function playAivisStream({ apiKey, modelUuid, text }) {
  const response = await fetch(
    "https://api.aivis-project.com/v1/tts/synthesize",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model_uuid: modelUuid,
        text,
        output_format: "mp3",
        leading_silence_seconds: 0.0,
        trailing_silence_seconds: 0.1,
      }),
    },
  );

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  }
  if (!response.body) {
    throw new Error("レスポンスストリームを取得できませんでした");
  }

  const MediaSourceClass = self.MediaSource ?? self.ManagedMediaSource;
  if (!MediaSourceClass) {
    throw new Error("このブラウザはMediaSourceに対応していません");
  }

  const mediaSource = new MediaSourceClass();
  const audio = new Audio(URL.createObjectURL(mediaSource));
  audio.disableRemotePlayback = true;

  // 自動再生制限を避けるため、ボタンクリックなどの操作内で呼び出す
  audio.play().catch(console.error);

  mediaSource.addEventListener(
    "sourceopen",
    async () => {
      const sourceBuffer = mediaSource.addSourceBuffer("audio/mpeg");
      const reader = response.body.getReader();

      const waitForIdle = () =>
        sourceBuffer.updating
          ? new Promise((resolve) =>
              sourceBuffer.addEventListener("updateend", resolve, {
                once: true,
              }),
            )
          : Promise.resolve();

      for (;;) {
        const { value, done } = await reader.read();
        if (done) break;

        await waitForIdle();
        sourceBuffer.appendBuffer(value);
      }

      await waitForIdle();
      mediaSource.endOfStream();
    },
    { once: true },
  );

  return audio;
}

実際の本番実装では、長時間再生時にSourceBufferが肥大化しないよう、再生済みデータの削除や追加待ちキューの管理も必要です。
Aivis Cloud APIの公式デモでは、これらを含む実装をページ内のHTMLですべて公開しています。

⚠️ APIキーを公開WebサイトのJavaScriptへ直接書き込むと、閲覧者に取得される可能性があります。Aivis Cloud APIはブラウザからの直接利用に対応していますが、不特定多数へ公開するサービスでは、APIキーをソースコードへ埋め込まず、自社のバックエンドを経由する構成を検討してください。

Pythonで受信しながら再生する

Pythonのrequestsでストリーミングレスポンスを受け取り、標準入力から再生できるffplayへMP3データを渡す方法もあります。

この例を動かすには、FFmpegに含まれるffplayを事前にインストールしてください。

import os
import subprocess

import requests


API_URL = "https://api.aivis-project.com/v1/tts/synthesize"
API_KEY = os.environ["AIVIS_API_KEY"]


def play_streaming(text: str, model_uuid: str) -> None:
    response = requests.post(
        API_URL,
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "model_uuid": model_uuid,
            "text": text,
            "output_format": "mp3",
            "leading_silence_seconds": 0.0,
        },
        stream=True,
        timeout=(5, 120),
    )
    response.raise_for_status()

    player = subprocess.Popen(
        [
            "ffplay",
            "-nodisp",
            "-autoexit",
            "-loglevel",
            "error",
            "-i",
            "pipe:0",
        ],
        stdin=subprocess.PIPE,
    )

    try:
        if player.stdin is None:
            raise RuntimeError("ffplayの標準入力を開けませんでした")

        for chunk in response.iter_content(chunk_size=16 * 1024):
            if chunk:
                player.stdin.write(chunk)
                player.stdin.flush()
    finally:
        if player.stdin is not None:
            player.stdin.close()
        player.wait()


play_streaming(
    text="こんにちは。生成できた音声から順番に再生しています。",
    model_uuid="YOUR_MODEL_UUID",
)

requestsのstream=Trueは、Aivis Cloud APIへストリーミングを要求する指定ではありません。
Python側でレスポンス全体を一括取得せず、少しずつ読み取るための設定です。

チャンクサイズは、ネットワークや再生環境に応じて調整します。
小さくすれば必ず速くなるわけではなく、細かすぎる読み書きは処理回数を増やします。
固定値を絶対的な推奨値とせず、最初の再生までの時間と途切れの有無を実環境で計測してください。

LLMと連携するときは、文が完成した段階で音声合成へ渡す

LLMと音声合成を直列につなぎ、回答全文の生成を待ってから音声合成を始めると、LLMの待ち時間と音声合成の待ち時間がそのまま加算されます。

低遅延にするには、LLMの出力を受け取りながら、意味のまとまった文やフレーズが完成した段階で音声合成へ渡すパイプラインを構成します。

LLMが最初の文を生成
   ↓
最初の文をAivis Cloud APIへ送信 ─→ 音声チャンクを受信・再生

再生中にLLMが次の文を生成
   ↓
次の文を音声合成キューへ追加    ─→ 前の音声に続けて再生

細かく分割しすぎない

LLMのトークンを1文字ずつ音声合成へ送ることはできません。
単語や短いフレーズごとに分割すると、リクエスト数と処理のオーバーヘッドが増え、感情表現や音声のつながりも不自然になりやすくなります。

句点が現れたら必ず分割するのではなく、会話として自然な意味のまとまりを保つことが重要です。

長い文章を1行にまとめすぎない

Aivis Cloud APIは、改行や特定のSSMLタグで区切られたセグメントごとに音声を生成し、順次配信します。
1行に長いテキストをすべて書くと、その行の生成を待つ時間が長くなり、ストリーミングの効果が弱くなります。

一方、細かく区切りすぎても自然さと生成速度が低下します。
段落や文脈の区切りを目安に改行し、実際の音声を聞きながら調整するのが確実です。

再生順序をキューで管理する

複数の文を並列で音声合成すると、後から送った短い文が先に完成する可能性があります。
対話システムでは、生成した順番を保持するキューを用意し、音声が前後しないようにしてください。

本番実装で押さえたい4つのポイント

1. 再生途中のリトライで音声を重複させない

最初のチャンクを受信する前なら、429や一時的なサーバーエラーに対してリトライできます。しかし、すでに音声を再生した後で同じリクエストを最初からやり直すと、聞き手には同じ文章が重複して聞こえます。

ストリーミング開始後に通信が切れた場合は、無条件にリクエスト全体を再試行せず、再生済みのセグメントを記録して次の文から続けるか、音声を停止して再生成するかを決めておきましょう。

2. ユーザーの割り込みで古い音声を停止する

音声対話では、AIが話している途中にユーザーが次の質問を始めることがあります。
このとき古いHTTPリクエストと音声再生を止めなければ、前の回答と新しい回答が重なります。

ブラウザではAbortControllerでリクエストを中断し、使用中のAudio要素とMediaSourceを破棄できるようにしておくと管理しやすくなります。

3. 429・402・5xxを分けて扱う

429はレート制限、402はクレジット不足または月間利用上限への到達を示します。
429や一時的な5xxは待機後の再試行を検討できますが、402はクレジットや設定を変更しない限り回復しません。

4. 実際の再生開始時間を計測する

サーバーから最初のチャンクが届く時間だけでなく、プレイヤーのplayingイベントなどを使って、ユーザーに音が聞こえ始めた時間を計測してください。

ネットワーク、端末、ブラウザ、音声形式によって結果は変わります。公式の「最速0.3秒」はすべての環境で保証される固定値ではないため、対象環境での実測が重要です。

よくある質問(FAQ)

Q. ストリーミングを有効にするパラメータはありますか?

ありません。POST /v1/tts/synthesizeの音声レスポンスをクライアント側で順次読み取ります。
Pythonのrequestsで指定するstream=Trueは、レスポンスを一括取得しないためのクライアント側の設定です。

Q. ブラウザでのリアルタイム再生には、どの形式がおすすめですか?

幅広いブラウザやOSを対象にする場合はMP3がおすすめです。
iOS対応が不要で圧縮効率を重視する場合はOpusも選べます。

Q. 公式デモのコードを自分のサービスで使えますか?

はい。
「リアルタイム音声合成デモ」のソースコードはHTML内にすべて含まれており、パブリックドメインとして公開しています。
ただし、APIキーの管理やエラー処理は、ご自身のサービス構成に合わせて設計してください。

Q. Firefoxでも公式デモは動きますか?

2026年8月時点の「リアルタイム音声合成デモ」は、PC Chrome・Android Chrome・Mac Safari・iOS Safariでの動作を主な対象としており、Firefoxなどその他ブラウザでの動作は保証しておりません。
独自に実装する場合も、対象ブラウザでMediaSourceと使用する音声形式の対応状況をご確認ください。

Q. 長いテキストは、1文ずつ別のAPIリクエストにすべきですか?

必ずしも1文ごとに分ける必要はありません。
細かすぎる分割は、音声の自然さと生成速度に影響します。

1回のリクエスト内で段落や文脈の区切りに改行を入れる方法と、LLMの生成に合わせて複数リクエストへ分ける方法を用途に応じて使い分けてください。

Q. 音声をファイルへ保存するだけでも、ストリーミングにする意味はありますか?

ファイルの完成を待ってから再生するだけなら、体感遅延を短縮する効果はありません。
ただし、データを一括でメモリへ載せず、受信しながらファイルへ書き込めるため、長い音声を扱う際のメモリ使用量を抑えられます。

まとめ

・Aivis Cloud APIの音声合成エンドポイントはPOST /v1/tts/synthesize
・専用のstream: trueパラメータはなく、音声レスポンスをクライアント側で順次読み取る
・ブラウザでのリアルタイム再生は、MP3とMediaSourceの組み合わせが基本
・LLM連携では、意味のまとまりごとに音声合成へ渡し、再生順序をキューで管理する
・細かすぎる文章の分割と、長すぎる1行はどちらも避ける
・再生開始後のリトライでは、同じ音声を重複再生しないように設計する
・最初のチャンク受信だけでなく、実際に音が聞こえ始めるまでの時間を計測する

ストリーミング音声合成の目的は、単にデータを細かく受信することではありません。
生成と再生を並行させ、ユーザーが声を聞くまでの待ち時間を短縮することが本質です。

Aivis Cloud APIの「リアルタイム音声合成デモ」では、実際の速度や音声品質を確認できるほか、MediaSourceを使った実装コードも確認できます。
まずはデモで動作を試し、そのコードを自分のサービス構成に合わせて調整してみてください✨

以上で、この記事はおしまいです。

Aivis ProjectやAivis Cloud APIに関してご不明点やご相談がある場合には、お問い合わせフォームよりお気軽にご連絡ください。

いいなと思ったら応援しよう!