Wixzel VoiceのエージェントのマスコットをFlutterアプリに表示する
このガイドを終えると、Flutterアプリにエージェントのマスコットが表示されます。マスコットはWixzel Voiceのすべてのエージェントが持つアニメーションの顔で、実際の通話に合わせて聞き、考え、話します。wixzel_voice_flutterパッケージがAgentMascotViewで描画し、MascotRealtimeLinkで通話と同期させます。通話そのものはwixzel_voice Dart SDKが担います。所要時間は約1時間で、大半は録音とプレーヤーの接続作業です。セッションは同じエンジンでの電話と同じように課金されます。Gemini Liveでは接続1分あたり$0.0491、4分で約$0.20で、電話回線は不要です。
執筆 Aqeel Shamsudheen · 公開日 · 更新日
- 所要時間
- 約1時間
- エンジン
- gemini-live
- エンジン利用時間
- 接続1分あたり$0.0491

始める前に何が必要ですか?
- Wixzel Voiceのエージェントと、クレジットのあるAPIキー(サーバーに保管)。どのエージェントでも使えます。すべてのエージェントにマスコットがあり、最初はデフォルトの緑色のマスコットです。
- Flutter 3.27以降。
- APIを呼び出せる、ご自身で管理するサーバー。例ではNode.jsとTypeScript SDKを使います。
- 16ビットPCMをストリーミングできる録音プラグイン(できれば8 kHzモノラルでエコーキャンセル付き)と、生のPCMを再生できるプレーヤー。たとえば
recordとflutter_pcm_soundです。
Wixzel Voiceでこれを構築する手順は?
01パッケージを追加する
wixzel_voice_flutterがマスコットを描画し、wixzel_voiceはその土台となるDart SDKです。pubspec.yamldependencies: wixzel_voice: ^0.8.0 wixzel_voice_flutter: ^0.1.0 http: ^1.2.0このパッケージは
sensors_plusに依存しており、マスコットの目がスマートフォンの傾きを追えるようになっています。またvisibility_detectorにも依存しており、画面に表示されている間だけアニメーションします。音声の録音や再生は行いません。02サーバーからアプリにエージェントとセッションを渡す
APIキーはアプリに入れません。サーバーがエージェントを取得し、1分間有効な使い捨てのセッションを発行し、アプリはその両方をサーバーに要求します。
server.mjsimport express from 'express'; import { WixzelVoice } from 'wixzel-voice'; const wixzel = new WixzelVoice({ apiKey: process.env.WIXZEL_API_KEY }); const app = express(); const AGENT_ID = process.env.AGENT_ID; // Put your own sign-in check in front of both routes. app.get('/agent', async (req, res) => { res.json(await wixzel.agents.retrieve(AGENT_ID)); }); app.post('/session', async (req, res) => { res.json(await wixzel.realtime.createSession({ agent_id: AGENT_ID, max_duration_seconds: 300 })); }); app.listen(3000);エージェントのJSONには、マスコットを描画するための小さな仕様である
mascot.specが含まれます。セッションの発行は無料で、課金はアプリが接続した時点から始まります。03マスコットを描画する
Agent.fromJsonがサーバーから送られた内容を読み込み、AgentMascotViewが描画します。agent_face.dartimport 'dart:convert'; import 'package:flutter/widgets.dart'; import 'package:http/http.dart' as http; import 'package:wixzel_voice/wixzel_voice.dart'; import 'package:wixzel_voice_flutter/wixzel_voice_flutter.dart'; const backendUrl = 'https://api.example.com'; Future<Agent> loadAgent() async { final res = await http.get(Uri.parse('$backendUrl/agent')); return Agent.fromJson(jsonDecode(res.body) as Map<String, dynamic>); } Widget agentFace(Agent agent, MascotController controller) => AgentMascotView(agent: agent, controller: controller, size: 220); // In a list, draw one frame and run no animation: Widget agentRow(Agent agent) => AgentMascotView(agent: agent, size: 40, still: true);specのないエージェントにはデフォルトの見た目が使われます。黒に近い、または白に近いマスコットは、暗い画面や明るい画面に埋もれないよう、自分の色のステージの上に表示されます。これは
stage: falseでオフにできます。タップすると表情が再生され、MascotControllerではmascot.play('hello')のように名前を指定して動きを再生できます。04マスコットを通話につなぐ
Dart SDKの
RealtimeConnectionで接続し、それをMascotRealtimeLinkに渡します。このリンクにより、マスコットは通話の接続中は考え、あいさつで手を振り、ユーザーが話している間は聞き、エージェントの音声に合わせて口を動かします。talk.dartimport 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'package:http/http.dart' as http; import 'package:wixzel_voice/wixzel_voice.dart'; import 'package:wixzel_voice_flutter/wixzel_voice_flutter.dart'; /// Your audio plugins, behind a small interface. abstract class PcmPlayer { void add(Int16List samples); // 16-bit PCM, 8 kHz mono void clear(); } class MascotCall { MascotCall(this.mascot, this.player); final MascotController mascot; final PcmPlayer player; RealtimeConnection? _conn; MascotRealtimeLink? _link; StreamSubscription<Int16List>? _mic; /// [micFrames] is 16-bit PCM at 8 kHz in 160-sample (20 ms) frames. Future<void> start(Stream<Int16List> micFrames) async { final res = await http.post(Uri.parse('https://api.example.com/session')); final session = RealtimeSession.fromJson(jsonDecode(res.body) as Map<String, dynamic>); final conn = RealtimeConnection.forSession(session); _conn = conn; _link = MascotRealtimeLink(mascot, conn); conn.events.listen((event) { switch (event) { case RealtimeAudio(:final mulaw): player.add(mulawToPcm16(mulaw)); // the mouth follows this audio through the link case RealtimeAudioClear(): player.clear(); // the user spoke over the agent case RealtimeEnded(): unawaited(end()); default: } }); await conn.ready; _mic = micFrames.listen((frame) { conn.sendAudio(pcm16ToMulaw(frame)); _link?.micAudio(frame); // it leans in while the user talks }); } Future<void> end() async { await _mic?.cancel(); await _link?.dispose(); // leaves the mascot idle; does not end the call await _conn?.end(); _mic = null; _link = null; _conn = null; } }表示には
AgentMascotView(agent: agent, controller: mascot)を使います。通話が動かすのと同じコントローラーです。画面を閉じるときはコントローラーを破棄してください。録音側でプラットフォームのエコーキャンセルを有効にしてください。そうしないと、エージェントがスピーカーから自分の声を聞いて、自分の発話を遮ってしまいます。
05プレーヤーが音声をスケジュールするなら、口の動きを正確に合わせる
デフォルトでは、リンクはプレーヤーが各チャンクを受け取ってから約120 ms後に再生を始めると想定しています。プレーヤーが各チャンクの再生時刻を正確に把握している場合は、代わりにそれをリンクに伝えます。
exact_timing.dartimport 'dart:typed_data'; import 'package:wixzel_voice/wixzel_voice.dart'; import 'package:wixzel_voice_flutter/wixzel_voice_flutter.dart'; MascotRealtimeLink exactLink(MascotController mascot, RealtimeConnection conn) => MascotRealtimeLink(mascot, conn, estimatePlayback: false); // In your player, each time it schedules a chunk, on the player's own clock: void scheduled(MascotRealtimeLink link, Int16List pcm, double playAt, double now) { link.agentAudio(pcm, playAt: playAt, currentTime: now, sampleRate: 8000); }
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 |
パッケージは無料で、MITライセンスです。マスコットの描画は無料で、クレジットを消費するのは実際のセッションだけです。エンジンの1分あたりの料金で、前払い残高から秒単位で課金されます。
前払い制で、サブスクリプションも無料プランもありません。$5からクレジットを追加すると、使用量に応じて消費されます。自分の使い方で見積もる。
この構築に関係するWixzel Voiceの制限は?
- このパッケージは音声の録音や再生を行いません。どちらもアプリが任意のプラグインで用意します。
- 電話並みの音質です。双方向ともG.711 µ-law、8 kHzです。
- セッションは、アカウントの同時通話数の上限(デフォルトは5件)に数えられます。
- Flutter 3.27以降が必要です。傾きの追従には加速度センサーが必要で、ない場合はマスコットが自分であたりを見回します。
- マスコットの見た目の変更は、APIキーが必要なため、アプリからではなくサーバーからのAPI呼び出し(
agents.updateMascot)で行います。
よくある質問
Wixzel VoiceのエージェントのマスコットをFlutterで表示するにはどうすればよいですか?
wixzel_voiceとwixzel_voice_flutterを追加し、サーバーが取得したエージェントをAgent.fromJsonで読み込み、AgentMascotView(agent: agent)で描画します。コードから動かすにはMascotControllerを渡します。マスコットはどのようにエージェントの声に口の動きを合わせるのですか?
MascotRealtimeLinkがRealtimeConnectionのイベントを受け取り、エージェントの音声に合わせて口を動かします。タイミングは、プレーヤーの遅延についての独自の推定か、estimatePlayback: falseの場合はプレーヤーがagentAudioで報告する時刻に基づきます。状態の切り替えも行い、接続中は考える状態、ユーザーが話している間は聞く状態、通話が終わるとアイドル状態にします。マスコットの見た目はWebサイトと同じですか?
Webアプリ向けにも同じものはありますか?
wixzel-voice/mascotのmountMascot(npmのwixzel-voice 0.10.0)がキャンバスにマスコットを描画し、face.connect(session)でマスコットを組み込んだリアルタイム通話を開始します。マイクと再生も含まれます。さらに詳しく
- SDKドキュメント
DartとTypeScriptのSDKと、マスコットのウィジェット。
- マスコットドキュメント
spec、見た目の変更、Webサイトからの生成。
- リアルタイム:Webとモバイルドキュメント
セッションの発行、プロトコル、課金。
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トランク経由で着信に応答したり発信したりします。
ユーザーがWebSocket経由で音声エージェントと話せるFlutterアプリ。APIキーはサーバーに置いたままで、電話番号は使いません。
4つのcURLリクエストで、ご自身のSIPトランク経由で実際の電話を鳴らすGemini Liveのエージェントと、文字起こしと料金を含むその通話記録。
ご自身のSIPトランク経由で、同時通話数の上限に合わせたバッチでエージェントが発信したリードの一覧と、各通話のステータスと文字起こし。
Webサイトの隅にいて、訪問者がブラウザから通話できる話すマスコット。指定したドメインと1日の上限の範囲内で動き、すべての発信者が一覧されます。
最初の音声エージェントを構築

