コンテンツにスキップ

音声API実装ガイド

音声チャンクについて

概要

Marionetteの音声APIは、リアルタイムストリーミング音声合成をサポートしています。
音声データは、WebSocketのevent.responseメッセージを通じてチャンク形式で配信されます。

音声データ形式

PCMフォーマット仕様

項目 仕様
エンコーディング Base64
データ形式 Float32 Little Endian (PCM)
サンプリングレート 24,000 Hz
チャンネル数 1(モノラル)
ビット深度 32-bit floating point

Base64デコード方法

function decodeFloat32(base64String) {
  const binaryString = atob(base64String);
  const bytes = new Uint8Array(binaryString.length);
  for (let i = 0; i < binaryString.length; i++) {
    bytes[i] = binaryString.charCodeAt(i);
  }
  return new Float32Array(bytes.buffer);
}

メッセージ構造

通常の音声チャンク

audioデータを含むチャンクです。

Note

ドラマ作成者の設計次第ではaudio以外のキーが使われる可能性もあります。動作不良の場合はドラマ作成者にキー名を確認してください。

{
  "op": "event.response",
  "reply_to": "msg-123",
  "data": {
    "audio": {
      "audio": "base64_encoded_pcm_data...",
      "chunk_id": 0,
      "synthesis_id": "ae8afd57-abee-4fa8-a497-f9bbd69cf7b2",
      "audio_seconds": 0.69,
      "exp_delay": 0.0,
      "is_last": false,
      "order": 1
    }
  }
}

最終チャンク(is_last=true)

最終チャンクは、以下の2つのパターンで送信される可能性があります。両方に対応してください

パターン1: audioデータと共に送信
{
  "op": "event.response",
  "reply_to": "msg-123",
  "data": {
    "audio": {
      "audio": "base64_encoded_pcm_data...",
      "chunk_id": 5,
      "synthesis_id": "ae8afd57-abee-4fa8-a497-f9bbd69cf7b2",
      "audio_seconds": 0.84,
      "is_last": true
    }
  }
}
パターン2: audioデータなし(最終マーカーのみ)
{
  "op": "event.response",
  "reply_to": "msg-123",
  "data": {
    "audio": {
      "is_last": true,
      "total_audio_seconds": 4.91,
      "total_chunks": 6,
      "chunk_id": 6,
      "synthesis_id": "ae8afd57-abee-4fa8-a497-f9bbd69cf7b2"
    }
  }
}

Warning

is_last=trueのチャンクにaudioフィールドが存在しない場合でも、必ず処理してください
このチャンクは、音声ストリームの終了を示す重要なマーカーです。

フィールド仕様

フィールド 必須 説明
audio string 条件付き Base64エンコードされたPCMデータ。通常のチャンクでは必須、最終マーカーでは省略可能
chunk_id integer Yes チャンクID(0から始まる連番)
synthesis_id string Yes 音声合成の一意ID。同じIDを持つチャンクは1つの音声ストリームを構成します
audio_seconds float No このチャンクの音声の長さ(秒)
exp_delay float No 期待される遅延時間(秒)。初期バッファリング時間の調整に使用可能
is_last boolean Yes このチャンクが最後かどうか。trueの場合、このsynthesis_idの音声ストリームは完了
order integer No **サーバーが設定する**再生順序。詳細は後述
total_audio_seconds float 条件付き ストリーム全体の音声の長さ(秒)。is_last=trueの場合のみ
total_chunks integer 条件付き ストリーム全体のチャンク数。is_last=trueの場合のみ

クライアント実装要件

必須実装

1. synthesis_idごとのキュー管理

複数の音声ストリームが並列で送信される可能性があります。

  • synthesis_idごとに独立したキューを作成してください
  • 同じsynthesis_idのチャンクは、chunk_id順に並べて保管してください
  • 異なるsynthesis_idのチャンクが混在して到着しても、正しく処理できるようにしてください
2. is_last処理

is_last=trueは、ストリームの終了を示します。

  • audioフィールドがある場合:通常通り再生し、このsynthesis_idのストリームを完了としてマーク
  • audioフィールドがない場合:最終マーカーとして処理し、このsynthesis_idのストリームを完了

両方のパターンに対応してください。

3. 再生順序の制御

現在再生中のsynthesis_idのis_last=trueが完全に再生終了するまで、次のsynthesis_idの再生を開始しないでください。

これにより、音声が混在せず、クリアに聞こえます。

必要に応じて、音声間に0.5秒程度の間を挟むことで、より自然に聞こえる場合もあります。
畳み掛け気味の場合は検討してください。

再生順序の決定方法

次にどのsynthesis_idを再生するかは、以下の優先順位で決定してください:

ケース1: orderフィールドがある場合

サーバーがorderフィールドを設定している場合、その値の小さい順に再生してください。

// order=1 → 最初に再生
{ "synthesis_id": "aaa", "order": 1, ... }

// order=2 → 2番目に再生  
{ "synthesis_id": "bbb", "order": 2, ... }

// order=3 → 3番目に再生
{ "synthesis_id": "ccc", "order": 3, ... }
  • 同じsynthesis_id内の全てのチャンクは、同じorder値を持ちます
  • order値が小さいものから順に再生してください

Note

ドラマ作成者の設計次第ではorder以外のキーが使われる可能性もあります。ドラマ作成者にキー名を確認してください。

ケース2: orderフィールドがない場合

orderがない場合は、chunk_id=0の到着タイムスタンプが早いものから順に再生してください。

これにより、先に到着した音声から自然に再生されます。
一般的には、このケースで問題なく再生できるはずです。

推奨実装

キュー間の待ち時間

もし、音声が畳み掛け気味になる場合、あるsynthesis_idの再生が完了してから、次のsynthesis_idの再生を開始するまでに、適切な待ち時間(推奨: 500ms)を挿入してください。

Note

待ち時間は、 再生完了後 に挿入してください。待機中のキューがない状態で新しいチャンクが到着した場合は、即座に再生を開始してください(最速応答のため)。

// 良い実装例
if (待機中のキューがある) {
  // 前のsynthesis_idの再生完了 → 待ち時間 → 次のsynthesis_id開始
  setTimeout(() => startNextQueue(), 500);
} else {
  // 待機中のキューなし → 新しいチャンク到着時に即座に再生
}
exp_delayの活用

exp_delayフィールドは、初期バッファリング時間の調整に使用できます。
これは過去数分間の音声合成の統計から、リアルタイム音声合成から遅延した秒数を推定したものです。
この時間だけ待ってから、1チャンク目の音声再生を開始することで、音声再生の途切れを軽減することができます。

exp_delay=0のケースも存在しますが、それはサーバー側での遅延が無く、高速に音声合成ができていることを示します:

const bufferMs = Math.max(initialBufferMs, exp_delay * 1000);

実装例

完全な実装例

以下の実装例では、Marionetteが提供する公式の音声プレイヤーライブラリを使用しています。

📥 ダウンロード: ttsAudioPlayer.js (右クリック → 名前を付けて保存)

import { initTTSPlayer, decodeFloat32 } from '/static/js/ttsAudioPlayer.js';

// TTSプレイヤーの初期化
const ttsPlayer = initTTSPlayer({
  queueTransitionDelayMs: 500  // キュー切り替え時の待ち時間
});

// WebSocketメッセージハンドラー
function handleEventResponse(message) {
  const data = message.data || {};

  // 音声データの取得(キー名は設定可能)
  const audioKey = 'audio';  // または設定から取得
  if (data[audioKey]) {
    handleAudioChunk(data[audioKey]);
  }
}

function handleAudioChunk(audioData) {
  if (!audioData) return;

  try {
    const chunkId = audioData.chunk_id !== undefined ? audioData.chunk_id : 0;
    const synthesisId = audioData.synthesis_id || 'default';
    const expDelayMs = audioData.exp_delay ? Math.round(audioData.exp_delay * 1000) : 0;
    const order = audioData.order !== undefined ? audioData.order : null;
    const isLast = audioData.is_last === true;

    // パターン1: audioデータありの場合
    if (audioData.audio && audioData.audio.length > 0) {
      const pcm = decodeFloat32(audioData.audio);
      ttsPlayer.enqueueChunk(pcm, chunkId, synthesisId, expDelayMs, order, isLast);
    }
    // パターン2: audioなしでis_last=trueの場合(最終マーカー)
    else if (isLast) {
      // 空のPCMデータでis_lastフラグを伝える
      ttsPlayer.enqueueChunk(new Float32Array(0), chunkId, synthesisId, expDelayMs, order, isLast);
    }
  } catch (error) {
    console.error('音声チャンクの処理に失敗:', error);
  }
}

クライアント実装チェックリスト

実装時に、以下の項目を確認してください:

  • synthesis_idごとに独立したキューを管理している
  • is_last=trueの2つのパターン(audioあり/なし)に対応している
  • orderフィールドがある場合、値の小さい順に再生している
  • orderフィールドがない場合、到着タイムスタンプ順に再生している
  • 現在のsynthesis_idのis_last=trueが完全に再生終了するまで、次のsynthesis_idを待機させている
  • キュー間に適切な待ち時間を挿入している(推奨: 500ms)
  • 待機中のキューがない場合、新しいチャンクを即座に再生開始している

トラブルシューティング

音声が再生されない

症状: 最初の音声は再生されるが、2つ目以降が再生されない

原因: is_last=trueが正しく処理されていない可能性があります

確認事項:

  • audioフィールドがないis_last=trueチャンクもスキップせずに処理していますか?
  • is_last=trueの検出ロジックはaudioData.is_last === trueで正しく判定していますか?

音声が重なって聞こえる

症状: 複数のsynthesis_idの音声が同時に再生される

原因: キュー切り替えのタイミングが不適切です

確認事項:

  • 前のsynthesis_idの 再生完了を待ってから 次のキューを開始していますか?
  • 待ち時間をオーディオタイムライン上で管理していますか?(実時間のみではNG)

再生順序が意図と異なる

症状: orderを設定しているのに、順序がバラバラ

原因: order処理ロジックが不適切です

確認事項:

  • orderフィールドの値を正しく読み取っていますか?
  • orderの小さい順にソートして再生していますか?
  • orderがnullやundefinedの場合の処理は適切ですか?

技術的詳細

synthesis_id の役割

synthesis_idは、音声合成サーバーが生成する一意のIDです:

  • サーバー側で並列生成される複数の音声を区別するために使用されます。クライアント側では、このIDごとにキューを管理してください
  • 同じsynthesis_idを持つチャンクは、必ず順序を保って再生してください

order の役割

orderは、サーバー側が決定する再生順序です:

  • サーバーが意図的に再生順序を制御したい場合に設定されます。クライアント側では、この値に従って再生順序を決定してください
  • orderが設定されていない場合は、到着順で再生してください

exp_delay の活用

exp_delayは、サーバーが計算した期待遅延時間です:

  • 音声再生に対し合成が追いつかなくなる生成遅延の予想時間を示します。初期バッファリング時間の調整に使用できます
  • initialBufferMsexp_delay * 1000の大きい方を使用することを推奨します

実装時に留意するべき可変パラメータ

以下のパラメータについては、ドラマ作成者のポリシーによってデフォルト値以外が使われる可能性があるため、ドラマ作成者に確認してください。

設定項目 デフォルト値
音声データのキー名 audio
再生順序のキー名 order