跳到主要內容
指南

用 Wixzel Voice 透過 WebSocket 在 Flutter 中加入語音代理

完成後,您的 Flutter 應用程式能讓使用者透過 WebSocket 與您打造的語音代理對話,不需要 SIP 中繼,也不需要電話號碼。您的伺服器建立只能使用一次的工作階段,因此 API 金鑰絕不會進入應用程式,而 wixzel_voice Dart SDK 負責處理通訊協定。預計約需一小時,大部分時間花在串接麥克風和喇叭。工作階段的計費方式與同一引擎上的電話通話相同:Gemini Live 引擎每接通一分鐘 $0.0491,四分鐘的工作階段約 $0.20,從預付餘額按秒計費,沒有電信費用。

作者: · 發布於 · 更新於

所需時間
約一小時
引擎時間
每接通分鐘 $0.0491

開始之前需要準備什麼?

  • 一把有點數的 Wixzel Voice API 金鑰,保存在您的伺服器上。
  • 一台您能控制、用來建立工作階段的伺服器。範例使用 Node.js 和 Express;任何能發出 HTTPS 請求的語言都可以。
  • 一個 Flutter 應用程式,搭配能串流 16 位元 PCM 的錄音外掛(最好是 8 kHz 單聲道並開啟回音消除),以及能接受原始 PCM 的播放器。

如何在 Wixzel Voice 上逐步打造?

  1. 01先建立一次代理

    這個代理使用 Gemini Live,一個同時負責聆聽、推理和說話的即時模型:

    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);

    回應包含 socket 的 url、一個只能使用一次、有效一分鐘且僅適用此代理的 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開啟回音消除,並預期電話等級的音質

    在錄音外掛中開啟平台的回音消除。少了它,代理會透過喇叭聽到自己的聲音並打斷自己。

    雙向音訊都是 8 kHz 的 G.711 µ-law,與電話線路相同,因為工作階段與通話走同一條媒體路徑。這對語音來說足夠,但頻寬比寬頻音訊窄。如果外掛允許,請以 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"

    網頁工作階段屬於來電,沒有 from 或 to,並帶有時長、cost_micros 和您的 metadata;GET /v1/calls/{id} 會再加上逐字稿。它結束時會送出與電話通話相同的 callCompleted webhook。網頁工作階段不會錄音。

在 Wixzel Voice 上打造這個要花多少錢?

在 gemini-live 引擎上進行 4 分鐘工作階段的引擎時間,以每接通分鐘 $0.0491 計算:每個工作階段約 $0.20。 沒有電信費用,因為沒有電話線路。

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

工作階段的費用與同一引擎上的電話通話相同,按秒計費,從同一個預付餘額扣款。建立工作階段不收費,因此使用者開啟畫面後拒絕麥克風權限,不會產生任何費用。

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

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

  • 電話等級音質:雙向皆為 8 kHz 的 G.711 µ-law,不是寬頻。
  • Dart SDK 不會擷取或播放音訊。麥克風和喇叭由您的應用程式透過自選的外掛提供。
  • 工作階段與電話通話一起計入帳號的同時通話上限,預設為五通。可申請提高上限。
  • 工作階段預設持續 10 分鐘,最長一小時,靜默兩分鐘即會結束。
  • 網頁工作階段不會錄音,也沒有第二條線路可以將使用者轉接給真人。

常見問題

有 Flutter 用的語音代理 API 嗎?

Wixzel Voice 就有。它的 Dart SDK(pub.dev 上的 wixzel_voice)包含 RealtimeConnection,可透過 WebSocket 將 Flutter 或 Dart 應用程式連接到語音代理。您的伺服器以 API 金鑰建立工作階段;應用程式只會拿到一個有效一分鐘、只能使用一次的密鑰。

應用程式內的語音代理需要電話號碼或 SIP 中繼嗎?

不需要。網頁和行動應用程式中的工作階段透過 WebSocket 進行,不需要連接任何東西。只有撥打或接聽真實電話時才需要中繼和號碼。

為什麼代理一直打斷自己?

它透過喇叭聽到了自己的聲音。請在錄音外掛中開啟回音消除,或戴耳機測試加以確認。

應用程式中可以使用其他引擎嗎?

可以。classic 和 sarvam 管線也能透過 socket 執行:需要特定的 ElevenLabs 語音時選 classic,印度語言則選 sarvam。價格就是該引擎本身的每分鐘價格,與電話通話相同;這裡使用的是 Gemini Live,每分鐘 $0.0491。

工作階段進行中帳號點數用完會怎樣?

應用程式會收到代碼為 low_balance 的 session.warning,並附上大約剩餘的秒數,之後工作階段會以原因 insufficient_credits 結束。完全無法支付的工作階段會以關閉代碼 4402 被拒絕。

延伸閱讀

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

    建立工作階段、通訊協定、拒絕代碼與計費。

  • SDK文件

    Dart 和 TypeScript SDK,包括瀏覽器用戶端。

  • 語音引擎文件

    即時與組合式語音,以及語音清單。

  • Webhooks文件

    網頁工作階段的 callCompleted,附帶您的 metadata。

代理式電話通訊 API

關於 Wixzel Voice

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

建立您的第一個語音代理