wixzel_voice_flutter 0.1.1
wixzel_voice_flutter: ^0.1.1 copied to clipboard
Flutter widgets for Wixzel Voice. Draw an agent's animated mascot in your app and make it move and lip-sync with a live realtime call.
example/lib/main.dart
// An agent's mascot, and a sketch of a talk with it.
//
// flutter run --dart-define=BACKEND_URL=https://your-server.example.com
//
// Your API key stays on your server. This app asks your server for two
// things, and your server answers with the API's own JSON:
//
// GET /agent -> await client.agents.retrieve(agentId)
// POST /session -> await client.realtime.createSession(
// CreateRealtimeSession(agentId: agentId))
//
// Without BACKEND_URL it shows the default mascot, and Talk is off.
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.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 = String.fromEnvironment('BACKEND_URL');
void main() => runApp(const MaterialApp(home: AgentScreen()));
/// An agent made locally, for when there is no backend to ask.
Agent demoAgent() => Agent.fromJson({
'id': 'agt_demo',
'name': 'Front desk',
'system_prompt': '',
'voice': const <String, dynamic>{},
'language': 'en',
'appointment_booking_enabled': false,
'created_at': '2026-10-21T00:00:00Z',
'mascot': {'spec': defaultMascot, 'updated_at': '2026-10-21T00:00:00Z'},
});
class AgentScreen extends StatefulWidget {
const AgentScreen({super.key});
@override
State<AgentScreen> createState() => _AgentScreenState();
}
class _AgentScreenState extends State<AgentScreen> {
final mascot = MascotController();
Agent agent = demoAgent();
RealtimeConnection? connection;
MascotRealtimeLink? link;
StreamSubscription<RealtimeEvent>? events;
String caption = 'Tap the mascot, or press Talk.';
@override
void initState() {
super.initState();
if (backendUrl.isNotEmpty) unawaited(_loadAgent());
}
Future<void> _loadAgent() async {
final res = await http.get(Uri.parse('$backendUrl/agent'));
final loaded = Agent.fromJson(jsonDecode(res.body) as Map<String, dynamic>);
if (mounted) setState(() => agent = loaded);
}
Future<void> _talk() async {
// 1. Your server mints a one-minute, single-use session.
final res = await http.post(Uri.parse('$backendUrl/session'));
final session =
RealtimeSession.fromJson(jsonDecode(res.body) as Map<String, dynamic>);
// 2. Connect, and let the mascot follow the call.
final conn = RealtimeConnection.forSession(session);
link = MascotRealtimeLink(mascot, conn);
events = conn.events.listen((event) {
switch (event) {
case RealtimeTranscript(:final role, :final text):
setState(() => caption = role == 'user' ? 'You: $text' : text);
case RealtimeAudio(:final mulaw):
// 3. Play the agent's voice: 16-bit PCM at 8 kHz.
_play(mulawToPcm16(mulaw));
case RealtimeAudioClear():
// The user spoke over the agent: flush your player's queue.
case RealtimeEnded():
_hangUp();
default:
}
});
setState(() => connection = conn);
// 4. Send the microphone: 16-bit PCM at 8 kHz, in 160-sample frames.
// From a recorder plugin (e.g. record), for each frame:
// conn.sendAudio(pcm16ToMulaw(frame));
// link.micAudio(frame); // the mascot leans in while you talk
}
/// Feed your PCM player here, e.g. the flutter_pcm_sound plugin. The
/// mascot's mouth already follows this audio through the link.
void _play(Int16List pcm) {}
Future<void> _hangUp() async {
final conn = connection;
if (conn == null) return;
setState(() => connection = null);
await events?.cancel();
await link?.dispose();
await conn.end();
}
@override
void dispose() {
unawaited(_hangUp());
mascot.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final talking = connection != null;
return Scaffold(
appBar: AppBar(title: Text(agent.name)),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AgentMascotView(agent: agent, controller: mascot, size: 220),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(caption, textAlign: TextAlign.center),
),
const SizedBox(height: 24),
FilledButton(
onPressed:
backendUrl.isEmpty ? null : (talking ? _hangUp : _talk),
child: Text(talking ? 'Hang up' : 'Talk'),
),
],
),
),
);
}
}