본문으로 건너뛰기
가이드

Wixzel Voice로 Flutter 앱에 에이전트 마스코트 표시하기

이 가이드를 마치면 Flutter 앱에 모든 Wixzel Voice 에이전트가 가진 애니메이션 얼굴인 에이전트 마스코트가 표시되고, 실시간 통화에 맞춰 듣고, 생각하고, 말합니다. wixzel_voice_flutter 패키지의 AgentMascotView가 마스코트를 그리고 MascotRealtimeLink가 통화와 동기화하며, 통화는 wixzel_voice Dart SDK가 처리합니다. 약 1시간이 걸리며, 대부분은 녹음기와 플레이어를 연결하는 작업입니다. 세션 요금은 같은 엔진의 전화 통화와 같습니다. Gemini Live에서 연결된 1분당 $0.0491, 4분에 약 $0.20이며, 전화 회선 요금은 없습니다.

작성 · 게시 · 업데이트

소요 시간
약 1시간
엔진 사용 시간
연결된 1분당 $0.0491
큰 초록색 마스코트가 있는 휴대폰 앱 화면. 마스코트 위의 말풍선에 인사말이 있고, 아래에 대화하기 버튼이 있습니다.

시작하기 전에 무엇이 필요한가요?

  • Wixzel Voice 에이전트와 크레딧이 있는 API 키(서버에 보관). 모든 에이전트에 마스코트가 있으므로(처음에는 기본 초록색 마스코트) 어떤 에이전트든 됩니다.
  • Flutter 3.27 이상.
  • API를 호출할 수 있는, 직접 관리하는 서버. 예제는 Node.js와 TypeScript SDK를 사용합니다.
  • 16비트 PCM을 스트리밍하는 녹음 플러그인(가능하면 8 kHz 모노, 에코 제거 지원)과 raw 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 키는 앱에 넣지 않습니다. 서버가 에이전트를 가져오고 1분짜리 일회용 세션을 발급하며, 앱은 둘 다 서버에 요청합니다.

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

    사양이 없는 에이전트는 기본 모습으로 그려집니다. 검은색이나 흰색에 가까운 마스코트는 자기 색의 배경(stage) 위에 놓이므로 어둡거나 밝은 화면에 묻히지 않습니다. 이 기능은 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 엔진에서 연결된 1분당 $0.0491로 4분 세션을 진행할 때의 엔진 사용 시간 비용: 세션당 약 $0.20. 전화 회선이 없으므로 전화 통신 비용이 없습니다.

이 가이드 빌드의 월 엔진 비용
월 세션 수연결된 시간(분)월 엔진 비용
5002,000$98.20
2,50010,000$491
10,00040,000$1,964

패키지는 무료이며 MIT 라이선스입니다. 마스코트를 그리는 데는 비용이 들지 않고, 실시간 세션만 엔진의 분당 요금으로 선불 잔액에서 초 단위로 크레딧을 사용합니다.

선불이며 구독과 무료 등급이 없습니다. $5부터 크레딧을 충전하면 사용량만큼 차감됩니다. 내 사용량으로 추정해 보기.

이 빌드에는 Wixzel Voice의 어떤 한도가 적용되나요?

  • 패키지는 오디오를 녹음하거나 재생하지 않습니다. 둘 다 앱이 직접 고른 플러그인으로 처리합니다.
  • 전화 수준의 음질: 양방향 모두 8 kHz G.711 µ-law입니다.
  • 세션은 계정의 동시 통화 한도(기본 5건)에 포함됩니다.
  • Flutter 3.27 이상이 필요합니다. 기울기 반응에는 가속도계가 필요하며, 가속도계가 없으면 마스코트가 스스로 주위를 둘러봅니다.
  • 마스코트 모습 변경은 API 키가 필요하므로 앱이 아니라 서버에서 하는 API 호출(agents.updateMascot)입니다.

자주 묻는 질문

Flutter에서 Wixzel Voice 에이전트의 마스코트를 표시하려면 어떻게 해야 하나요?

pubspec에 wixzel_voice와 wixzel_voice_flutter를 추가하고, 서버가 가져온 에이전트를 Agent.fromJson으로 읽은 뒤 AgentMascotView(agent: agent)로 그립니다. 코드에서 제어하려면 MascotController를 넘깁니다.

마스코트는 어떻게 에이전트 음성에 립싱크하나요?

MascotRealtimeLink가 RealtimeConnection 이벤트를 듣고 에이전트 오디오에 맞춰 입을 움직입니다. 타이밍은 플레이어 지연에 대한 자체 추정치로 맞추거나, estimatePlayback: false일 때는 플레이어가 agentAudio로 알려 주는 시각으로 맞춥니다. 상태도 설정합니다. 연결 중에는 생각하기, 사용자가 말하는 동안에는 듣기, 통화가 끝나면 대기 상태입니다.

마스코트가 웹사이트에서와 똑같이 보이나요?

네. Flutter 런타임은 웹 런타임을 옮긴 것이고, 테스트로 두 런타임이 같은 수치를 내도록 확인하므로, 같은 사양이면 사용자의 앱, Wixzel Voice 앱, 공유 링크, 웹사이트 위젯에서 똑같이 보이고 움직입니다.

웹 앱용으로도 같은 것이 있나요?

네. wixzel-voice/mascot의 mountMascot(npm의 wixzel-voice 0.10.0)이 캔버스에 마스코트를 그리고, face.connect(session)이 마이크와 재생까지 포함해 마스코트가 연결된 실시간 통화를 시작합니다.

더 읽을거리

에이전트형 전화 통신을 위한 API

Wixzel Voice 소개

Wixzel Voice는 자체 SIP 트렁크로 실제 전화를 걸고 받거나 웹·모바일 앱에서 사람들과 대화하는 AI 에이전트를 만들기 위한 음성 AI API입니다. API 키 하나와 선불 잔액 하나로 모든 음성 엔진을 사용하며, 실제 사용량에 따라 초 단위로 과금됩니다.

Wixzel Voice MCP 서버로 Claude에게 실제 전화 회선 연결하기

Claude Code나 claude.ai의 Claude가 SIP 트렁크로 실제 전화를 걸고 대화 기록을 읽어 주는 상태. 전화를 걸기 전에는 항상 확인을 거칩니다.

Wixzel Voice로 Node.js에서 Twilio AI 접수원 만들기

Node.js로 만든 AI 접수원이 기존 Twilio 번호로 오는 전화를 받고 API에 예약을 잡는 상태.

Wixzel Voice sarvam 엔진으로 만드는 힌디어·말라얄람어 전화 에이전트

Sarvam의 인도 언어 모델로 동작하며 SIP 트렁크로 전화를 받거나 거는 전화 에이전트 두 개(힌디어 하나, 말라얄람어 하나).

Wixzel Voice로 WebSocket 기반 Flutter 음성 에이전트 추가하기

API 키는 서버에 두고, 전화번호 없이 사용자가 WebSocket으로 음성 에이전트와 대화하는 Flutter 앱.

Wixzel Voice API로 실제 전화 통화에 Gemini Live 연결하기

cURL 요청 네 번으로 SIP 트렁크를 통해 실제 전화를 거는 Gemini Live 에이전트, 그리고 대화 기록과 비용이 담긴 통화 기록.

Wixzel Voice로 AI 리드 선별 캠페인 실행하기

에이전트가 SIP 트렁크로 리드 목록에 동시 통화 한도에 맞춘 배치 단위로 전화를 걸고, 모든 통화의 상태와 대화 기록을 읽어 오는 상태.

Wixzel Voice로 웹사이트에 말하는 AI 마스코트 추가하기

웹사이트 구석에서 방문자가 브라우저로 통화하는 말하는 마스코트. 지정한 도메인과 일일 한도 안에서만 동작하고, 모든 발신자가 목록에 남습니다.

첫 음성 에이전트를 만드세요