本文へスキップ
ガイド

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

最後には、Flutterアプリのユーザーが、ご自身で作った音声エージェントとWebSocket経由で話せるようになります。SIPトランクも電話番号も使いません。サーバーが使い捨てのセッションを発行するのでAPIキーがアプリに渡ることはなく、プロトコルはwixzel_voice Dart SDKが処理します。所要時間は約1時間で、大半はマイクとスピーカーの接続作業です。セッションは同じエンジンでの電話と同じように課金されます。Gemini Liveエンジンでは接続1分あたり$0.0491、4分のセッションで約$0.20で、前払い残高から秒単位で課金され、電話回線の費用はかかりません。

執筆 · 公開日 · 更新日

所要時間
約1時間
エンジン
gemini-live
エンジン利用時間
接続1分あたり$0.0491

始める前に何が必要ですか?

  • クレジットのあるWixzel VoiceのAPIキー。サーバーに保管します。
  • セッションを発行するための、ご自身で管理するサーバー。例ではNode.jsとExpressを使いますが、HTTPSリクエストを送れる言語なら何でも使えます。
  • 16ビットPCMをストリーミングできる録音プラグイン(できれば8 kHzモノラルでエコーキャンセル付き)と、生のPCMを再生できるプレーヤーを使うFlutterアプリ。

Wixzel Voiceでこれを構築する手順は?

  1. 01エージェントを一度だけ作成する

    このエージェントはGemini Liveを使います。聞く、考える、話すを1つでこなすリアルタイムモデルです。

    shell
    curl 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として保管しておきます。

  2. 02サーバーでセッションを発行する

    アプリがAPIキーを持つことはありません。アプリはサーバーに依頼し、サーバーがPOST /v1/realtime/sessionsでAPIにセッションを要求します。ここではTypeScript SDKを使います。

    server.mjs
    import 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はブラウザでのみ意味があるため、ネイティブアプリでは省略します。

  3. 03アプリから接続する

    flutter pub add wixzel_voice httpでSDKを追加し、サーバーが返したセッションを開きます。

    voice_call.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';
    
    /// 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です。

  4. 04エコーキャンセルを有効にし、電話並みの音質を前提にする

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

    音声は双方向ともG.711 µ-law、8 kHzで、電話回線と同じです。セッションが通話と同じメディア経路を通るためです。会話には十分ですが、広帯域の音声より帯域は狭くなります。プラグインが対応していれば8 kHzで録音し、そうでなければpcm16ToMulawの前にリサンプリングして、エージェントの音声は8 kHzモノラルで再生してください。

  5. 05セッションの記録を確認する

    すべてのセッションは、channel: "web"の通話記録になります。

    shell
    curl "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}では文字起こしも取得できます。終了時には電話と同じcallCompleted Webhookが送信されます。Webセッションは録音されません。

Wixzel Voiceでこの構築にかかる費用は?

gemini-liveエンジンで4分のセッションを行った場合のエンジン利用料で、接続1分あたり$0.0491、1セッションあたり約$0.20です。 電話回線を使わないため、電話回線の費用はかかりません。

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

セッションの料金は同じエンジンでの電話と同じで、同じ前払い残高から秒単位で課金されます。セッションの発行は無料なので、画面を開いたもののマイクを許可しなかったユーザーには費用がかかりません。

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

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

  • 電話並みの音質です。双方向ともG.711 µ-law、8 kHzで、広帯域ではありません。
  • Dart SDKは音声の録音や再生を行いません。マイクとスピーカーは、アプリが任意のプラグインで用意します。
  • セッションは電話と合わせて、アカウントの同時通話数の上限(デフォルトは5件)に数えられます。上限は申請により引き上げられます。
  • セッションの長さはデフォルトで10分、最長で1時間で、2分間無音が続くと終了します。
  • Webセッションは録音されず、ユーザーを人間の担当者に引き継ぐための別回線もありません。

よくある質問

Flutter向けの音声エージェントAPIはありますか?

Wixzel Voiceにあります。Dart SDK(pub.devのwixzel_voice)にはRealtimeConnectionが含まれ、FlutterやDartのアプリをWebSocket経由で音声エージェントに接続できます。セッションはサーバーがAPIキーで発行し、アプリが持つのは1分間だけ有効な使い捨てのシークレットだけです。

アプリ内の音声エージェントに電話番号やSIPトランクは必要ですか?

いいえ。WebアプリやモバイルアプリでのセッションはWebSocketで動き、接続しておくものは何もありません。トランクと番号が必要なのは、実際の電話との通話だけです。

エージェントが自分の発話を何度も遮ってしまうのはなぜですか?

スピーカーから出る自分の声を聞いているためです。録音プラグインでエコーキャンセルを有効にするか、ヘッドホンでテストして確かめてください。

アプリで別のエンジンを使えますか?

はい。classicとsarvamのパイプラインもソケット経由で動きます。特定のElevenLabsの音声を使いたい場合はclassic、インドの言語にはsarvamを選びます。料金は各エンジン自体の1分あたりの料金で、電話と同じです。ここで使っているGemini Liveは$0.0491です。

セッション中にアカウントのクレジットがなくなるとどうなりますか?

アプリはコードlow_balanceのsession.warningを受け取り、残りのおおよその秒数が通知されます。その後、セッションは理由insufficient_creditsで終了します。まったく料金を確保できないセッションは、クローズコード4402で拒否されます。

さらに詳しく

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

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

  • SDKドキュメント

    DartとTypeScriptのSDK(ブラウザ用クライアントを含む)。

  • 音声エンジンドキュメント

    リアルタイムの音声と組み合わせの音声、音声の一覧。

  • Webhookドキュメント

    WebセッションのcallCompletedと、指定したmetadata。

エージェント型電話のための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のAPIでGemini Liveを実際の電話につなぐ

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

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

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

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

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

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

リアルタイム通話中にエージェントの声に合わせて口を動かすマスコットを表示するFlutterの画面。APIキーはサーバーに置いたままです。

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