本文へスキップ
ガイド

Wixzel VoiceのエージェントのマスコットをFlutterアプリに表示する

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

執筆 · 公開日 · 更新日

所要時間
約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でこれを構築する手順は?

  1. 01パッケージを追加する

    wixzel_voice_flutterがマスコットを描画し、wixzel_voiceはその土台となるDart SDKです。

    pubspec.yaml
    dependencies:
      wixzel_voice: ^0.8.0
      wixzel_voice_flutter: ^0.1.0
      http: ^1.2.0

    このパッケージはsensors_plusに依存しており、マスコットの目がスマートフォンの傾きを追えるようになっています。またvisibility_detectorにも依存しており、画面に表示されている間だけアニメーションします。音声の録音や再生は行いません。

  2. 02サーバーからアプリにエージェントとセッションを渡す

    APIキーはアプリに入れません。サーバーがエージェントを取得し、1分間有効な使い捨てのセッションを発行し、アプリはその両方をサーバーに要求します。

    server.mjs
    import 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が含まれます。セッションの発行は無料で、課金はアプリが接続した時点から始まります。

  3. 03マスコットを描画する

    Agent.fromJsonがサーバーから送られた内容を読み込み、AgentMascotViewが描画します。

    agent_face.dart
    import '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')のように名前を指定して動きを再生できます。

  4. 04マスコットを通話につなぐ

    Dart SDKのRealtimeConnectionで接続し、それをMascotRealtimeLinkに渡します。このリンクにより、マスコットは通話の接続中は考え、あいさつで手を振り、ユーザーが話している間は聞き、エージェントの音声に合わせて口を動かします。

    talk.dart
    import '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)を使います。通話が動かすのと同じコントローラーです。画面を閉じるときはコントローラーを破棄してください。

    録音側でプラットフォームのエコーキャンセルを有効にしてください。そうしないと、エージェントがスピーカーから自分の声を聞いて、自分の発話を遮ってしまいます。

  5. 05プレーヤーが音声をスケジュールするなら、口の動きを正確に合わせる

    デフォルトでは、リンクはプレーヤーが各チャンクを受け取ってから約120 ms後に再生を始めると想定しています。プレーヤーが各チャンクの再生時刻を正確に把握している場合は、代わりにそれをリンクに伝えます。

    exact_timing.dart
    import '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です。 電話回線を使わないため、電話回線の費用はかかりません。

このガイドの構築にかかる月額のエンジン費用
月間のセッション数接続時間(分)月額のエンジン費用
5002,000$98.20
2,50010,000$491
10,00040,000$1,964

パッケージは無料で、MITライセンスです。マスコットの描画は無料で、クレジットを消費するのは実際のセッションだけです。エンジンの1分あたりの料金で、前払い残高から秒単位で課金されます。

前払い制で、サブスクリプションも無料プランもありません。$5からクレジットを追加すると、使用量に応じて消費されます。自分の使い方で見積もる。

この構築に関係するWixzel Voiceの制限は?

  • このパッケージは音声の録音や再生を行いません。どちらもアプリが任意のプラグインで用意します。
  • 電話並みの音質です。双方向ともG.711 µ-law、8 kHzです。
  • セッションは、アカウントの同時通話数の上限(デフォルトは5件)に数えられます。
  • Flutter 3.27以降が必要です。傾きの追従には加速度センサーが必要で、ない場合はマスコットが自分であたりを見回します。
  • マスコットの見た目の変更は、APIキーが必要なため、アプリからではなくサーバーからのAPI呼び出し(agents.updateMascot)で行います。

よくある質問

Wixzel VoiceのエージェントのマスコットをFlutterで表示するにはどうすればよいですか?

pubspecにwixzel_voiceとwixzel_voice_flutterを追加し、サーバーが取得したエージェントをAgent.fromJsonで読み込み、AgentMascotView(agent: agent)で描画します。コードから動かすにはMascotControllerを渡します。

マスコットはどのようにエージェントの声に口の動きを合わせるのですか?

MascotRealtimeLinkがRealtimeConnectionのイベントを受け取り、エージェントの音声に合わせて口を動かします。タイミングは、プレーヤーの遅延についての独自の推定か、estimatePlayback: falseの場合はプレーヤーがagentAudioで報告する時刻に基づきます。状態の切り替えも行い、接続中は考える状態、ユーザーが話している間は聞く状態、通話が終わるとアイドル状態にします。

マスコットの見た目はWebサイトと同じですか?

はい。FlutterのランタイムはWeb版を移植したもので、テストで両者が同じ数値になることを確認しています。そのため同じspecなら、あなたのアプリ、Wixzel Voiceアプリ、共有リンク、Webサイトウィジェットのどこでも、同じ見た目と動きになります。

Webアプリ向けにも同じものはありますか?

はい。wixzel-voice/mascotのmountMascot(npmのwixzel-voice 0.10.0)がキャンバスにマスコットを描画し、face.connect(session)でマスコットを組み込んだリアルタイム通話を開始します。マイクと再生も含まれます。

さらに詳しく

  • SDKドキュメント

    DartとTypeScriptのSDKと、マスコットのウィジェット。

  • マスコットドキュメント

    spec、見た目の変更、Webサイトからの生成。

  • リアルタイム:Webとモバイルドキュメント

    セッションの発行、プロトコル、課金。

エージェント型電話のためのAPI

Wixzel Voiceについて

Wixzel Voiceは、自社のSIPトランク経由で実際の電話を発信・着信するAIエージェントや、Webアプリやモバイルアプリの中で人と話すAIエージェントを構築するための音声AI APIです。1つのAPIキーと1つの前払い残高ですべての音声エンジンを利用でき、実際の使用量に応じて秒単位で課金されます。

Wixzel VoiceのMCPサーバーでClaudeに実際の電話回線を持たせる

Claude Codeまたはclaude.aiのClaudeが、ご自身のSIPトランク経由で実際に電話をかけ、文字起こしを読み上げます。発信の前には必ず確認が入ります。

Wixzel VoiceとNode.jsでTwilioのAI受付を作る

Node.jsで作った、既存のTwilio番号に応答してAPIに予約を登録するAI受付。

Wixzel Voiceのsarvamエンジンで作るヒンディー語とマラヤーラム語の電話エージェント

Sarvamのインドの言語向けモデルで動く、ヒンディー語とマラヤーラム語の2つの電話エージェント。ご自身のSIPトランク経由で着信に応答したり発信したりします。

Wixzel VoiceでWebSocket経由のFlutter音声エージェントを追加する

ユーザーがWebSocket経由で音声エージェントと話せるFlutterアプリ。APIキーはサーバーに置いたままで、電話番号は使いません。

Wixzel VoiceのAPIでGemini Liveを実際の電話につなぐ

4つのcURLリクエストで、ご自身のSIPトランク経由で実際の電話を鳴らすGemini Liveのエージェントと、文字起こしと料金を含むその通話記録。

Wixzel VoiceでAIによるリード選別キャンペーンを実行する

ご自身のSIPトランク経由で、同時通話数の上限に合わせたバッチでエージェントが発信したリードの一覧と、各通話のステータスと文字起こし。

Wixzel VoiceでWebサイトに話すAIマスコットを追加する

Webサイトの隅にいて、訪問者がブラウザから通話できる話すマスコット。指定したドメインと1日の上限の範囲内で動き、すべての発信者が一覧されます。

最初の音声エージェントを構築