音声ボット開発
音声ボットは、@metatell/bot-sdkと@metatell/bot-realtimeを組み合わせて実装します。
ルームの音声は16ビットPCMフレームとして受け取り、生成したPCMをボットの音声として送信できます。
インストール
npm install @metatell/bot-sdk @metatell/bot-realtime
音声を使う前に通常のクライアント接続を完了させてください。
音声を有効にする
import {
createMetatellClient,
enableVoice,
} from "@metatell/bot-sdk";
const client = createMetatellClient({
serverUrl: "wss://metatell.app",
roomId: "YOUR_ROOM_ID",
username: "VoiceBot",
});
await client.connect();
const voice = await enableVoice(client, {
transport: { type: "livekit" },
sampleRate: 48000,
channels: 1,
frameDurationMs: 20,
handlers: {
onRemotePcm: async (frame, { fromIdentity }) => {
console.log(fromIdentity, frame.length);
},
},
});
enableVoice()はLiveKitトランスポートへ接続し、SDKクライアントと音声ハンドラーを結び付けます。
| 設定 | 現在利用する値 | 説明 |
|---|---|---|
sampleRate | 48000 | PCMのサンプルレートです。 |
channels | 1 | モノラル音声を指定します。 |
frameDurationMs | 20 | 1フレームの長さです。 |
autoStartPublish | 真偽値 | 接続後に音声送信を自動で開始するかを指定します。 |
enableTopicAutoAdd | 真偽値 | 音声トピックの自動追加を指定します。 |
transport.type | auto、livekit、mock | 音声トランスポートを指定します。 |
現行のLiveKit接続では、16ビット、48kHz、モノラルのPCMを使用します。
sampleRate、channels、frameDurationMsは上表の値から変更しないでください。
1フレームは20ms、960サンプルです。
mockはテスト用です。
通常のルーム接続ではautoまたはlivekitを使用してください。
標準環境以外では、環境に対応するLiveKit URLをMETATELL_REALTIME_URLへ設定できます。
値は運用環境の管理者から入手してください。
受信PCMを処理する
onRemotePcmにはInt16Arrayと送信元メタデータが渡されます。
handlers: {
onRemotePcm: async (frame, meta) => {
await speechToText.write(frame, {
sampleRate: 48000,
channels: 1,
identity: meta.fromIdentity,
});
},
}
Speech-to-Textへ転送するときは、APIが要求するフレーム長、サンプルレート、エンディアンをそろえてください。 同意やプライバシーの要件を確認し、必要のない音声を保存しないでください。
送信用PCMストリームを提供する
getLocalPcmStreamは、送信するInt16Arrayを返す非同期イテレーターです。
async function* localPcm(): AsyncIterable<Int16Array> {
// 16bit、48kHz、モノラルのPCMフレームを順に返します。
// 1フレームは20ms(960サンプル)です。
}
const voice = await enableVoice(client, {
handlers: {
getLocalPcmStream: localPcm,
},
sampleRate: 48000,
channels: 1,
frameDurationMs: 20,
});
音声を送る場合は、getLocalPcmStreamからPCMを供給する方法を使用してください。
このハンドラーがあると、enableVoice()が音声送信を開始します。
PCMユーティリティ
pcm.resample()はPCMストリームのサンプルレートを変 換します。
pcm.chunk()は指定サンプル数へ分割します。
import { pcm } from "@metatell/bot-sdk";
async function* localPcm(
sourcePcm24k: AsyncIterable<Int16Array>,
): AsyncIterable<Int16Array> {
const pcm48k = pcm.resample(sourcePcm24k, 24000, 48000, 1);
for await (const frame of pcm.chunk(pcm48k, 960)) {
yield frame;
}
}
const voice = await enableVoice(client, {
handlers: { getLocalPcmStream: localPcm },
sampleRate: 48000,
channels: 1,
frameDurationMs: 20,
});
48kHz、モノラル、20msの場合、1フレームは960サンプルです。
終了処理
音声接続とルーム接続を順番に閉じます。
await voice.detach();
await client.disconnect();
例外やプロセス終了時にも実行できるように、finallyまたは終了シグナルのハンドラーへ組み込んでください。
Google Speech-to-TextとText-to-Speech
公式のspeech-to-speech-botは、Google CloudのSpeech-to-TextとText-to-Speechを組み合わせる実装例です。
ローカル開発でサービスアカウントJSONを使う場合は、そのパスをGOOGLE_APPLICATION_CREDENTIALSへ設定します。
Google Cloud上では、実行リソースにサービスアカ ウントを割り当ててください。
Google Cloud外ではWorkload Identity Federationを優先し、いずれもJSON鍵を使わない構成にしてください。
サービスアカウント鍵をSecret Managerなどの秘密ストアへ保存する方式も、鍵自体をなくせないため推奨しません。
ローカル開発でJSON鍵が避けられない場合は、必要最小限の権限を持つ専用のサービスアカウントを使い、鍵をリポジトリ外へ保管してください。
ファイル名や配置先のパターンを.gitignoreへ追加してください。
認証情報ファイルとその内容をGitへコミットせず、ログにも出力しないでください。
漏えいした可能性がある鍵はただちに無効化し、新しい鍵へローテーションしてください。
音声処理の基本的な流れは次のとおりです。
onRemotePcmで音声フレームを受け取ります。- 無音区間を検出し、発話単位にまとめます。
- Speech-to-Textでテキストへ変換します。
- LLMや業務ロジックで応答文を作ります。
- Text-to-Speechで16ビットPCMを生成します。
- 必要に応じてリサンプリングし、指定の長さへ分割してルームへ送信します。
Gemini Liveと直接音声をストリーミングする例は、公式リポジトリのexamples/voice-ai-botを参照してください。