メインコンテンツまでスキップ

音声ボット開発

音声ボットは、@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クライアントと音声ハンドラーを結び付けます。

設定現在利用する値説明
sampleRate48000PCMのサンプルレートです。
channels1モノラル音声を指定します。
frameDurationMs201フレームの長さです。
autoStartPublish真偽値接続後に音声送信を自動で開始するかを指定します。
enableTopicAutoAdd真偽値音声トピックの自動追加を指定します。
transport.typeautolivekitmock音声トランスポートを指定します。

現行のLiveKit接続では、16ビット、48kHz、モノラルのPCMを使用します。 sampleRatechannelsframeDurationMsは上表の値から変更しないでください。 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へコミットせず、ログにも出力しないでください。 漏えいした可能性がある鍵はただちに無効化し、新しい鍵へローテーションしてください。

音声処理の基本的な流れは次のとおりです。

  1. onRemotePcmで音声フレームを受け取ります。
  2. 無音区間を検出し、発話単位にまとめます。
  3. Speech-to-Textでテキストへ変換します。
  4. LLMや業務ロジックで応答文を作ります。
  5. Text-to-Speechで16ビットPCMを生成します。
  6. 必要に応じてリサンプリングし、指定の長さへ分割してルームへ送信します。

Gemini Liveと直接音声をストリーミングする例は、公式リポジトリのexamples/voice-ai-botを参照してください。