用 Wixzel Voice 透過 WebSocket 在 Flutter 中加入語音代理
完成後,您的 Flutter 應用程式能讓使用者透過 WebSocket 與您打造的語音代理對話,不需要 SIP 中繼,也不需要電話號碼。您的伺服器建立只能使用一次的工作階段,因此 API 金鑰絕不會進入應用程式,而 wixzel_voice Dart SDK 負責處理通訊協定。預計約需一小時,大部分時間花在串接麥克風和喇叭。工作階段的計費方式與同一引擎上的電話通話相同:Gemini Live 引擎每接通一分鐘 $0.0491,四分鐘的工作階段約 $0.20,從預付餘額按秒計費,沒有電信費用。
作者:Aqeel Shamsudheen · 發布於 · 更新於
- 所需時間
- 約一小時
- 引擎時間
- 每接通分鐘 $0.0491
開始之前需要準備什麼?
- 一把有點數的 Wixzel Voice API 金鑰,保存在您的伺服器上。
- 一台您能控制、用來建立工作階段的伺服器。範例使用 Node.js 和 Express;任何能發出 HTTPS 請求的語言都可以。
- 一個 Flutter 應用程式,搭配能串流 16 位元 PCM 的錄音外掛(最好是 8 kHz 單聲道並開啟回音消除),以及能接受原始 PCM 的播放器。
如何在 Wixzel Voice 上逐步打造?
01先建立一次代理
這個代理使用 Gemini Live,一個同時負責聆聽、推理和說話的即時模型:
shellcurl 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保存在您的伺服器上。02在您的伺服器上建立工作階段
應用程式絕不持有您的 API 金鑰。它向您的伺服器請求,您的伺服器再以
POST /v1/realtime/sessions向 API 請求工作階段,這裡透過 TypeScript SDK 進行:server.mjsimport 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只對瀏覽器有意義,因此原生應用程式不需要設定。03從應用程式連線
用
flutter pub add wixzel_voice http加入 SDK,然後開啟伺服器回傳的工作階段:voice_call.dartimport '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。04開啟回音消除,並預期電話等級的音質
在錄音外掛中開啟平台的回音消除。少了它,代理會透過喇叭聽到自己的聲音並打斷自己。
雙向音訊都是 8 kHz 的 G.711 µ-law,與電話線路相同,因為工作階段與通話走同一條媒體路徑。這對語音來說足夠,但頻寬比寬頻音訊窄。如果外掛允許,請以 8 kHz 錄音,否則在
pcm16ToMulaw之前重新取樣,並以 8 kHz 單聲道播放代理的音訊。05事後讀取工作階段
每個工作階段都是一筆
channel: "web"的通話紀錄:shellcurl "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}會再加上逐字稿。它結束時會送出與電話通話相同的callCompletedwebhook。網頁工作階段不會錄音。
在 Wixzel Voice 上打造這個要花多少錢?
在 gemini-live 引擎上進行 4 分鐘工作階段的引擎時間,以每接通分鐘 $0.0491 計算:每個工作階段約 $0.20。 沒有電信費用,因為沒有電話線路。
| 每月工作階段數 | 接通分鐘數 | 每月引擎費用 |
|---|---|---|
| 500 | 2,000 | $98.20 |
| 2,500 | 10,000 | $491 |
| 10,000 | 40,000 | $1,964 |
工作階段的費用與同一引擎上的電話通話相同,按秒計費,從同一個預付餘額扣款。建立工作階段不收費,因此使用者開啟畫面後拒絕麥克風權限,不會產生任何費用。
預付制,沒有訂閱,也沒有免費方案:從 $5 起儲值點數,使用時從中扣除。估算您自己的使用組合。
這個實作適用哪些 Wixzel Voice 限制?
- 電話等級音質:雙向皆為 8 kHz 的 G.711 µ-law,不是寬頻。
- Dart SDK 不會擷取或播放音訊。麥克風和喇叭由您的應用程式透過自選的外掛提供。
- 工作階段與電話通話一起計入帳號的同時通話上限,預設為五通。可申請提高上限。
- 工作階段預設持續 10 分鐘,最長一小時,靜默兩分鐘即會結束。
- 網頁工作階段不會錄音,也沒有第二條線路可以將使用者轉接給真人。
常見問題
有 Flutter 用的語音代理 API 嗎?
wixzel_voice)包含 RealtimeConnection,可透過 WebSocket 將 Flutter 或 Dart 應用程式連接到語音代理。您的伺服器以 API 金鑰建立工作階段;應用程式只會拿到一個有效一分鐘、只能使用一次的密鑰。應用程式內的語音代理需要電話號碼或 SIP 中繼嗎?
為什麼代理一直打斷自己?
應用程式中可以使用其他引擎嗎?
工作階段進行中帳號點數用完會怎樣?
low_balance 的 session.warning,並附上大約剩餘的秒數,之後工作階段會以原因 insufficient_credits 結束。完全無法支付的工作階段會以關閉代碼 4402 被拒絕。延伸閱讀
- 即時:網頁與行動裝置文件
建立工作階段、通訊協定、拒絕代碼與計費。
- SDK文件
Dart 和 TypeScript SDK,包括瀏覽器用戶端。
- 語音引擎文件
即時與組合式語音,以及語音清單。
- Webhooks文件
網頁工作階段的 callCompleted,附帶您的 metadata。
關於 Wixzel Voice
Wixzel Voice 是一個語音 AI API,用於打造 AI 代理,透過您自己的 SIP 中繼撥打和接聽真實的電話,或在您的網頁和行動應用程式中與人對話。一把 API 金鑰和一份預付餘額即可使用所有語音引擎,依實際用量按秒計費。
更多指南
Claude 在 Claude Code 或 claude.ai 中透過您的 SIP 中繼撥打真實電話並讀回逐字稿,撥號前一律先徵求確認。
AI 接待員接聽您現有的 Twilio 號碼,並透過 API 安排預約,以 Node.js 打造。
兩個電話代理,一個說印地語、一個說馬拉雅拉姆語,使用 Sarvam 的印度語言模型,透過您的 SIP 中繼接聽或撥出電話。
透過四個 cURL 請求,讓 Gemini Live 代理經由您的 SIP 中繼撥打真實電話,並取得包含逐字稿與費用的通話紀錄。
由您的代理透過您的 SIP 中繼撥打一份潛在客戶名單,依同時通話上限分批進行,並讀回每通電話的狀態和逐字稿。
網站角落有一個會說話的吉祥物,訪客可從瀏覽器撥打給它,僅限您的網域並受每日上限約束,每位來電者都會列出。
一個有代理吉祥物的 Flutter 畫面,吉祥物在即時通話中隨代理的語音動嘴,API 金鑰保存在您的伺服器上。

