Wixzel VoiceでWebSocket経由のFlutter音声エージェントを追加する
最後には、Flutterアプリのユーザーが、ご自身で作った音声エージェントとWebSocket経由で話せるようになります。SIPトランクも電話番号も使いません。サーバーが使い捨てのセッションを発行するのでAPIキーがアプリに渡ることはなく、プロトコルはwixzel_voice Dart SDKが処理します。所要時間は約1時間で、大半はマイクとスピーカーの接続作業です。セッションは同じエンジンでの電話と同じように課金されます。Gemini Liveエンジンでは接続1分あたり$0.0491、4分のセッションで約$0.20で、前払い残高から秒単位で課金され、電話回線の費用はかかりません。
執筆 Aqeel Shamsudheen · 公開日 · 更新日
- 所要時間
- 約1時間
- エンジン
- gemini-live
- エンジン利用時間
- 接続1分あたり$0.0491
始める前に何が必要ですか?
- クレジットのあるWixzel VoiceのAPIキー。サーバーに保管します。
- セッションを発行するための、ご自身で管理するサーバー。例ではNode.jsとExpressを使いますが、HTTPSリクエストを送れる言語なら何でも使えます。
- 16ビットPCMをストリーミングできる録音プラグイン(できれば8 kHzモノラルでエコーキャンセル付き)と、生のPCMを再生できるプレーヤーを使うFlutterアプリ。
Wixzel Voiceでこれを構築する手順は?
01エージェントを一度だけ作成する
このエージェントはGemini Liveを使います。聞く、考える、話すを1つでこなすリアルタイムモデルです。
shellcurl https://api.voice.wixzel.com/v1/agents \ -H "Authorization: Bearer $WIXZEL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "In-app assistant", "system_prompt": "You are the voice assistant inside the Acme app. Answer in one or two short sentences.", "opening_message": "Hi, what can I help you with?", "voice": { "realtime": { "model": "google/gemini-live", "voice": "Kore" } } }'このエージェントにはアプリ固有の設定はありません。同じエージェントが電話番号に応答することもできます。
idはサーバーにAGENT_IDとして保管しておきます。02サーバーでセッションを発行する
アプリがAPIキーを持つことはありません。アプリはサーバーに依頼し、サーバーが
POST /v1/realtime/sessionsでAPIにセッションを要求します。ここではTypeScript SDKを使います。server.mjsimport express from 'express'; import { WixzelVoice } from 'wixzel-voice'; const wixzel = new WixzelVoice({ apiKey: process.env.WIXZEL_API_KEY }); const app = express(); // Replace with your own authentication. It must set req.userId. function requireUser(req, res, next) { req.userId = 'demo-user'; next(); } app.post('/voice/session', requireUser, async (req, res) => { const session = await wixzel.realtime.createSession({ agent_id: process.env.AGENT_ID, max_duration_seconds: 300, metadata: { user_id: req.userId }, }); res.json(session); }); app.listen(3000);レスポンスには、ソケットの
url、このエージェントに対して1分間に1回だけ使えるclient_secret、audio_format: "mulaw_8000"が含まれます。発行は無料で、課金はアプリが接続した時点から始まります。max_duration_secondsは会話の長さの上限で、10〜3600秒の範囲で指定でき、デフォルトは600秒です。metadataは通話記録にそのまま返され、モデルに送られることはありません。allowed_originsはブラウザでのみ意味があるため、ネイティブアプリでは省略します。03アプリから接続する
flutter pub add wixzel_voice httpでSDKを追加し、サーバーが返したセッションを開きます。voice_call.dartimport 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'package:http/http.dart' as http; import 'package:wixzel_voice/wixzel_voice.dart'; /// Whatever your playback plugin offers, behind a small interface. abstract class PcmPlayer { void add(Int16List samples); void clear(); } class VoiceCall { VoiceCall({required this.player, required this.onLine}); final PcmPlayer player; final void Function(String role, String text) onLine; RealtimeConnection? _conn; StreamSubscription<Uint8List>? _mic; final List<int> _pending = []; /// [micBytes] is 16-bit little-endian PCM, 8 kHz mono, from your recording plugin. Future<void> start(Stream<Uint8List> micBytes) async { final res = await http.post(Uri.parse('https://api.example.com/voice/session')); final status = res.statusCode; if (status != 200) throw Exception('Could not start a voice session: HTTP $status'); final session = RealtimeSession.fromJson(jsonDecode(res.body) as Map<String, dynamic>); final conn = RealtimeConnection.forSession(session); _conn = conn; conn.events.listen((event) { switch (event) { case RealtimeAudio(:final mulaw): player.add(mulawToPcm16(mulaw)); case RealtimeAudioClear(): player.clear(); // the user interrupted: drop queued audio case RealtimeTranscript(:final role, :final text): onLine(role, text); case RealtimeError(:final code, :final message): onLine('error', '$code: $message'); case RealtimeEnded(:final reason): onLine('system', 'ended: $reason'); _mic?.cancel(); default: } }); await conn.ready; _mic = micBytes.listen(_send); } void _send(Uint8List bytes) { final data = ByteData.sublistView(bytes); for (var i = 0; i + 1 < bytes.length; i += 2) { _pending.add(data.getInt16(i, Endian.little)); } // 160 samples is 20 ms at 8 kHz, the frame size the server expects. while (_pending.length >= 160) { final frame = Int16List.fromList(_pending.sublist(0, 160)); _pending.removeRange(0, 160); _conn?.sendAudio(pcm16ToMulaw(frame)); } } Future<void> end() async { await _mic?.cancel(); await _conn?.end(); } }RealtimeConnectionが扱うのはプロトコルだけです。マイクやスピーカーは開かないため、SDKはプラグインに依存せず、すでに使っている音声プラグインをそのまま使えます。pcm16ToMulawとmulawToPcm16は、16ビットPCMと通信で送る形式を相互に変換します。先にマイクの権限を求めてください。Androidでは
RECORD_AUDIO、iOSではNSMicrophoneUsageDescriptionです。04エコーキャンセルを有効にし、電話並みの音質を前提にする
録音プラグインで、プラットフォームのエコーキャンセルを有効にしてください。これがないと、エージェントがスピーカーから自分の声を聞いて、自分の発話を遮ってしまいます。
音声は双方向ともG.711 µ-law、8 kHzで、電話回線と同じです。セッションが通話と同じメディア経路を通るためです。会話には十分ですが、広帯域の音声より帯域は狭くなります。プラグインが対応していれば8 kHzで録音し、そうでなければ
pcm16ToMulawの前にリサンプリングして、エージェントの音声は8 kHzモノラルで再生してください。05セッションの記録を確認する
すべてのセッションは、
channel: "web"の通話記録になります。shellcurl "https://api.voice.wixzel.com/v1/calls?channel=web&limit=5" \ -H "Authorization: Bearer $WIXZEL_API_KEY"Webセッションは着信として記録され、
fromとtoはなく、通話時間、cost_micros、指定したmetadataを含みます。GET /v1/calls/{id}では文字起こしも取得できます。終了時には電話と同じcallCompletedWebhookが送信されます。Webセッションは録音されません。
Wixzel Voiceでこの構築にかかる費用は?
gemini-liveエンジンで4分のセッションを行った場合のエンジン利用料で、接続1分あたり$0.0491、1セッションあたり約$0.20です。 電話回線を使わないため、電話回線の費用はかかりません。
| 月間のセッション数 | 接続時間(分) | 月額のエンジン費用 |
|---|---|---|
| 500 | 2,000 | $98.20 |
| 2,500 | 10,000 | $491 |
| 10,000 | 40,000 | $1,964 |
セッションの料金は同じエンジンでの電話と同じで、同じ前払い残高から秒単位で課金されます。セッションの発行は無料なので、画面を開いたもののマイクを許可しなかったユーザーには費用がかかりません。
前払い制で、サブスクリプションも無料プランもありません。$5からクレジットを追加すると、使用量に応じて消費されます。自分の使い方で見積もる。
この構築に関係するWixzel Voiceの制限は?
- 電話並みの音質です。双方向ともG.711 µ-law、8 kHzで、広帯域ではありません。
- Dart SDKは音声の録音や再生を行いません。マイクとスピーカーは、アプリが任意のプラグインで用意します。
- セッションは電話と合わせて、アカウントの同時通話数の上限(デフォルトは5件)に数えられます。上限は申請により引き上げられます。
- セッションの長さはデフォルトで10分、最長で1時間で、2分間無音が続くと終了します。
- Webセッションは録音されず、ユーザーを人間の担当者に引き継ぐための別回線もありません。
よくある質問
Flutter向けの音声エージェントAPIはありますか?
wixzel_voice)にはRealtimeConnectionが含まれ、FlutterやDartのアプリをWebSocket経由で音声エージェントに接続できます。セッションはサーバーがAPIキーで発行し、アプリが持つのは1分間だけ有効な使い捨てのシークレットだけです。アプリ内の音声エージェントに電話番号やSIPトランクは必要ですか?
エージェントが自分の発話を何度も遮ってしまうのはなぜですか?
アプリで別のエンジンを使えますか?
セッション中にアカウントのクレジットがなくなるとどうなりますか?
low_balanceのsession.warningを受け取り、残りのおおよその秒数が通知されます。その後、セッションは理由insufficient_creditsで終了します。まったく料金を確保できないセッションは、クローズコード4402で拒否されます。さらに詳しく
- リアルタイム:Webとモバイルドキュメント
セッションの発行、プロトコル、拒否コード、課金。
- SDKドキュメント
DartとTypeScriptのSDK(ブラウザ用クライアントを含む)。
- 音声エンジンドキュメント
リアルタイムの音声と組み合わせの音声、音声の一覧。
- Webhookドキュメント
WebセッションのcallCompletedと、指定したmetadata。
Wixzel Voiceについて
Wixzel Voiceは、自社のSIPトランク経由で実際の電話を発信・着信するAIエージェントや、Webアプリやモバイルアプリの中で人と話すAIエージェントを構築するための音声AI APIです。1つのAPIキーと1つの前払い残高ですべての音声エンジンを利用でき、実際の使用量に応じて秒単位で課金されます。
その他のガイド
Claude Codeまたはclaude.aiのClaudeが、ご自身のSIPトランク経由で実際に電話をかけ、文字起こしを読み上げます。発信の前には必ず確認が入ります。
Node.jsで作った、既存のTwilio番号に応答してAPIに予約を登録するAI受付。
Sarvamのインドの言語向けモデルで動く、ヒンディー語とマラヤーラム語の2つの電話エージェント。ご自身のSIPトランク経由で着信に応答したり発信したりします。
4つのcURLリクエストで、ご自身のSIPトランク経由で実際の電話を鳴らすGemini Liveのエージェントと、文字起こしと料金を含むその通話記録。
ご自身のSIPトランク経由で、同時通話数の上限に合わせたバッチでエージェントが発信したリードの一覧と、各通話のステータスと文字起こし。
Webサイトの隅にいて、訪問者がブラウザから通話できる話すマスコット。指定したドメインと1日の上限の範囲内で動き、すべての発信者が一覧されます。
リアルタイム通話中にエージェントの声に合わせて口を動かすマスコットを表示するFlutterの画面。APIキーはサーバーに置いたままです。
最初の音声エージェントを構築

