音声API実装ガイド
音声チャンクについて¶
なぜストリーミング再生が重要なのか¶
Marionetteでは、最速で音声を届けるために、ストリーミング再生の実装を強く推奨します。
従来方式 vs ストリーミング方式¶
音声データは生成に時間がかかるため、全体を待ってから再生を開始すると、ユーザーは長時間待たされることになります。
sequenceDiagram
participant C as クライアント
participant S as サーバー
Note over C,S: ❌ 従来方式(非推奨)
C->>S: リクエスト
Note over S: 音声生成中...
Note over S: (3秒経過)
S->>C: 音声データ全体
Note over C: 再生開始(3秒後)
Note over C,S: ✅ ストリーミング方式(推奨)
C->>S: リクエスト
Note over S: 生成開始
S->>C: チャンク1
Note over C: 即座に再生開始!
S->>C: チャンク2
S->>C: チャンク3
Note over C: 継続して再生
S->>C: チャンク4 (最終) 体感速度の違い¶
下の図は、同じ3秒間の音声に対する体感速度の違いを示しています:
【従来方式】
サーバー: ████████████████████ 生成完了
クライアント: ▶ 再生開始(3秒後)
【ストリーミング方式】
サーバー: ████ ████ ████ ████ ████ チャンク送信
クライアント: ▶────────────────────── 0.2秒後から再生開始!
到着するたびに即座に再生
ストリーミング再生のメリット
- 初回応答までの時間を大幅短縮:最初のチャンク(約0.2〜0.5秒分)が到着次第、即座に再生開始
- 待ち時間の体感を軽減:音声生成と再生が並行して行われる
- 自然な会話体験:ユーザーがリクエストしてからほぼ瞬時に応答が始まる
実装の基本方針¶
flowchart LR
subgraph Client[クライアント側]
direction TB
A1[チャンク1 到着] --> B1[キューに追加] --> C1[再生開始]
A2[チャンク2 到着] --> B2[キューに追加] --> C2[連続再生]
A3[...] --> B3[...] --> C3[...]
AN[最終チャンク] --> BN[キューに追加] --> CN[再生完了]
end 要点:チャンクが到着したら、キューに追加し、すぐに再生を開始する。全体のダウンロード完了を待たない。
概要¶
Marionetteの音声APIは、リアルタイムストリーミング音声合成をサポートしています。
音声データは、WebSocketのevent.responseメッセージを通じてチャンク形式で配信されます。
音声データ形式¶
PCMフォーマット仕様¶
| 項目 | 仕様 |
|---|---|
| エンコーディング | Base64 |
| データ形式 | Float32 Little Endian (PCM) |
| サンプリングレート | 24,000 Hz |
| チャンネル数 | 1(モノラル)または 2(ステレオ・バイノーラル) |
| ビット深度 | 32-bit floating point |
バイノーラル音声(Binaural)の場合
ドラマに Binaural コンポーネントが含まれている場合、音声は ステレオ(2チャンネル) で配信されます。バイノーラル処理により、3D空間での音源位置が再現されたHRTFベースのステレオ音声です。デコード時は Float32Array の長さを2で割って左右チャンネルに分離してください(interleaved L/R 形式)。
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);
}
// バイノーラル(ステレオ)の場合: interleaved L/R 形式
// Float32Array を [L0,R0,L1,R1,...] から左右に分離
function decodeFloat32Stereo(base64String) {
const mono = decodeFloat32(base64String);
const n = mono.length / 2;
const left = new Float32Array(n);
const right = new Float32Array(n);
for (let i = 0; i < n; i++) {
left[i] = mono[i * 2];
right[i] = mono[i * 2 + 1];
}
return { left, right };
}
メッセージ構造¶
通常の音声チャンク¶
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のケースも存在しますが、それはサーバー側での遅延が無く、高速に音声合成ができていることを示します:
実装例¶
完全な実装例¶
以下の実装例では、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は、サーバーが計算した期待遅延時間です:
- 音声再生に対し合成が追いつかなくなる生成遅延の予想時間を示します。初期バッファリング時間の調整に使用できます
initialBufferMsとexp_delay * 1000の大きい方を使用することを推奨します
実装時に留意するべき可変パラメータ¶
以下のパラメータについては、ドラマ作成者のポリシーによってデフォルト値以外が使われる可能性があるため、ドラマ作成者に確認してください。
| 設定項目 | デフォルト値 |
|---|---|
| 音声データのキー名 | audio |
| 再生順序のキー名 | order |
音声デコレーション(AudioDecoratorChain)¶
ドラマに AudioDecoratorChain が含まれている場合、各音声チャンクに decoration フィールドが付加されます。
このフィールドにはリップシンク(口の開閉・母音分類)と感情推定のメタデータが含まれており、3Dアバターやキャラクターアニメーションの制御に活用できます。
decoration フィールドの構造¶
{
"op": "event.response",
"data": {
"audio": {
"audio": "base64_encoded_pcm_data...",
"chunk_id": 0,
"synthesis_id": "ae8afd57-abee-4fa8-a497-f9bbd69cf7b2",
"audio_seconds": 0.69,
"is_last": false,
"decoration": {
"synthesis_id": "ae8afd57-abee-4fa8-a497-f9bbd69cf7b2",
"chunk_id": 0,
"frame_interval_sec": 0.1,
"frames": [
{
"time_offset_sec": 0.0,
"mouth_open": 0.72,
"vowel": "a",
"vowel_scores": { "a": 0.85, "i": 0.02, "u": 0.01, "e": 0.08, "o": 0.04 },
"emotion": {
"type": "happy",
"intensity": 0.82,
"scores": { "happy": 0.82, "sad": 0.31, "angry": 0.15 }
}
},
{
"time_offset_sec": 0.1,
"mouth_open": 0.45,
"vowel": "i",
"vowel_scores": { "a": 0.05, "i": 0.78, "u": 0.03, "e": 0.10, "o": 0.04 },
"emotion": {
"type": "happy",
"intensity": 0.79,
"scores": { "happy": 0.79, "sad": 0.28, "angry": 0.12 }
}
}
]
}
}
}
}
decoration フィールド仕様¶
| フィールド | 型 | 説明 |
|---|---|---|
decoration.synthesis_id | string | 音声合成ID。親チャンクのsynthesis_idと一致 |
decoration.chunk_id | integer | チャンクID。親チャンクのchunk_idと一致 |
decoration.frame_interval_sec | float | フレーム間隔(秒)。デフォルト 0.1 |
decoration.frames | array | 解析フレームの配列 |
frames 配列の各要素¶
| フィールド | 型 | 値の範囲 | 説明 |
|---|---|---|---|
time_offset_sec | float | 0.0〜 | チャンク先頭からの時間オフセット(秒) |
mouth_open | float | 0.0〜1.0 | 口の開き具合。0.0=閉じている、1.0=最大に開いている |
vowel | string | a / i / u / e / o / silence / N | 推定された母音。silence=無音、N=母音分類無効時 |
vowel_scores | object | 各値 0.0〜1.0 | 各母音の確信度スコア(softmax正規化済み) |
emotion | object / null | - | 感情推定結果。テンプレート未登録・無音時はnull |
emotion オブジェクト¶
| フィールド | 型 | 値の範囲 | 説明 |
|---|---|---|---|
type | string | - | 最も類似度の高い感情テンプレートのタグ名 |
intensity | float | 0.0〜1.0 | コサイン類似度。1.0に近いほど確信度が高い |
scores | object | 各値 0.0〜1.0 | 全テンプレートとの類似度スコア |
decoration が存在しない場合
ドラマに AudioDecoratorChain が含まれていない場合、decoration フィールドは存在しません。クライアント側では decoration の有無をチェックしてから利用してください。
クライアント実装例¶
以下は、decoration データを使ってアバターの口の動きを制御する実装例です。
function handleAudioChunk(audioData) {
// 音声再生処理...
// decoration がある場合、リップシンク・感情を処理
if (audioData.decoration && audioData.decoration.frames) {
const frames = audioData.decoration.frames;
const intervalMs = (audioData.decoration.frame_interval_sec || 0.1) * 1000;
frames.forEach((frame, i) => {
setTimeout(() => {
// 口の開閉アニメーション
setMouthOpen(frame.mouth_open);
// 母音に応じた口の形状
setMouthShape(frame.vowel);
// 感情に応じた表情
if (frame.emotion) {
setExpression(frame.emotion.type, frame.emotion.intensity);
}
}, i * intervalMs);
});
}
}
decoration 実装チェックリスト¶
-
decorationフィールドの有無をチェックしている -
frames配列をframe_interval_secの間隔でスケジューリングしている -
mouth_openの値(0.0〜1.0)を口の開閉アニメーションに反映している -
vowelの値に応じて口の形状を変更している(任意) -
emotionがnullの場合を正しくハンドリングしている -
emotion.typeとemotion.intensityを表情制御に反映している(任意)