跳到主要內容
指南

用 Wixzel Voice 在 Flutter 應用程式中顯示代理的吉祥物

完成本指南後,您的 Flutter 應用程式會顯示代理的吉祥物(每個 Wixzel Voice 代理都有的動態臉孔),它會隨著進行中的通話聆聽、思考和說話。wixzel_voice_flutter 套件以 AgentMascotView 繪製吉祥物,並以 MascotRealtimeLink 讓它與通話同步;通話本身由 wixzel_voice Dart SDK 負責。預計約需一小時,大部分時間花在串接錄音器和播放器。工作階段的計費方式與同一引擎上的電話通話相同:Gemini Live 每接通一分鐘 $0.0491,四分鐘約 $0.20,沒有電話線路費用。

作者: · 發布於 · 更新於

所需時間
約一小時
引擎時間
每接通分鐘 $0.0491
A phone app screen with a large green mascot, a greeting in a speech bubble above it, and a Talk button below

開始之前需要準備什麼?

  • 一個 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 金鑰絕不放進應用程式。由您的伺服器取得代理並建立一個有效一分鐘、只能使用一次的工作階段,應用程式再向您的伺服器索取這兩者:

    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) 顯示它,使用與通話驅動的同一個控制器。畫面關閉時請釋放(dispose)該控制器。

    在錄音器中開啟平台的回音消除,否則代理會透過喇叭聽到自己的聲音並打斷自己。

  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 分鐘工作階段的引擎時間,以每接通分鐘 $0.0491 計算:每個工作階段約 $0.20。 沒有電信費用,因為沒有電話線路。

本指南所打造內容的每月引擎費用
每月工作階段數接通分鐘數每月引擎費用
5002,000$98.20
2,50010,000$491
10,00040,000$1,964

這些套件免費,並採用 MIT 授權。繪製吉祥物不收費;只有進行中的工作階段會消耗點數,依引擎的每分鐘價格按秒計費,從預付餘額扣款。

預付制,沒有訂閱,也沒有免費方案:從 $5 起儲值點數,使用時從中扣除。估算您自己的使用組合。

這個實作適用哪些 Wixzel Voice 限制?

  • 這個套件不會錄製或播放音訊。兩者都由您的應用程式透過自選的外掛提供。
  • 電話等級音質:雙向皆為 8 kHz 的 G.711 µ-law。
  • 工作階段計入帳號的同時通話上限,預設五通。
  • 需要 Flutter 3.27 或更新版本。傾斜功能需要加速度計;沒有的話,吉祥物會自己四處張望。
  • 更改吉祥物外觀需要從您的伺服器呼叫 API(agents.updateMascot),而不是從應用程式,因為這需要您的 API 金鑰。

常見問題

如何在 Flutter 中顯示 Wixzel Voice 代理的吉祥物?

將 wixzel_voice 和 wixzel_voice_flutter 加入 pubspec,用 Agent.fromJson 讀取伺服器取得的代理,再用 AgentMascotView(agent: agent) 繪製。傳入 MascotController 即可用程式碼驅動它。

吉祥物如何與代理對嘴?

MascotRealtimeLink 會監聽 RealtimeConnection 的事件,並隨代理的音訊動嘴,時間依據它對播放器延遲的估算;若設定 estimatePlayback: false,則依您的播放器透過 agentAudio 回報的時間。它也會設定狀態:連線時思考、使用者說話時聆聽、通話結束時閒置。

吉祥物看起來和網站上的一樣嗎?

一樣。Flutter 執行環境是由網頁版移植而來,並以測試確保兩者使用相同的數值,因此同一個 spec 在您的應用程式、Wixzel Voice 應用程式、分享連結和網站小工具中,外觀和動作都相同。

網頁應用程式也有同樣的東西嗎?

有。wixzel-voice/mascot 中的 mountMascot(npm 上的 wixzel-voice 0.10.0)會在 canvas 上繪製吉祥物,face.connect(session) 則會開始即時通話並接好吉祥物,包括麥克風和播放。

延伸閱讀

  • SDK文件

    Dart 和 TypeScript SDK,以及吉祥物小工具。

  • 吉祥物文件

    spec、更改外觀,以及從網站產生吉祥物。

  • 即時:網頁與行動裝置文件

    建立工作階段、通訊協定與計費。

代理式電話通訊 API

關於 Wixzel Voice

Wixzel Voice 是一個語音 AI API,用於打造 AI 代理,透過您自己的 SIP 中繼撥打和接聽真實的電話,或在您的網頁和行動應用程式中與人對話。一把 API 金鑰和一份預付餘額即可使用所有語音引擎,依實際用量按秒計費。

建立您的第一個語音代理