Header Logo

Guide to Streaming a Talking AI Avatar from Flask for Incoming Phone Calls

Guide to Streaming a Talking AI Avatar from Flask for Incoming Phone Calls

Build a Flask control plane for incoming phone calls with synchronized AI avatar streaming, LiveKit, and Protoface sessions.

Introduction


Streaming a talking AI avatar from Flask is mostly a systems integration problem: you need to accept an incoming call, turn it into a realtime media session, connect your voice agent to a video face, and keep latency low enough that the conversation still feels natural. The avatar is not a separate “animation layer” bolted on at the end; it is part of the realtime conversation loop, driven by the same turn-taking, audio timing, and interruption handling as the agent itself.


By the end of this post, you should understand the architecture for doing this cleanly in Flask, what the common failure modes are, and where a realtime avatar service fits into the pipeline. I’ll focus on the phone-call case because that forces the right constraints: low latency, audio-first, and a streaming transport that can survive real networks instead of a demo websocket in a local browser.


What “streaming an avatar” actually means


For an incoming phone call, the media path usually starts with PSTN or SIP, lands in your telephony provider, and then gets bridged into your application as an audio stream. From there, your agent stack performs speech recognition, dialog management, and text-to-speech. The avatar sits on top of that flow and consumes the same conversational turn events that produce the audio response.


In practice, the avatar service needs three things:


  • The agent’s response text or speech timing.

  • Audio alignment information, or an equivalent lip-sync signal.

  • A realtime transport to deliver video frames to the client that is rendering the call experience.


For phone calls, the “client” is often not the caller’s handset directly; it is a web or native UI that presents the live call. That UI can show the avatar video while the telephony leg carries the audio. The important part is that the avatar’s speaking state tracks the agent’s speaking state with minimal delay. If your TTS starts before the avatar is ready, or the avatar keeps moving after the audio has stopped, the illusion falls apart quickly.


Flask as the orchestration layer


Flask is a good fit when you want a thin HTTP control plane around an existing realtime stack. It should not be responsible for low-level media processing. Instead, use Flask to:


  1. Receive the incoming webhook from your telephony provider.

  2. Create or look up a realtime avatar session.

  3. Return the response that tells the provider where to stream audio.

  4. Expose callback endpoints for call state changes, transcript events, and cleanup.


The key design choice is to keep Flask stateless with respect to the media stream itself. Store only identifiers and metadata in your app database; let the media session live in the dedicated realtime stack. That avoids the usual trap where a WSGI process gets asked to behave like an RTP server.


A minimal Flask endpoint often looks like this:


from flask import Flask, request, jsonify
from flask import Flask, request, jsonify
from flask import Flask, request, jsonify


The exact request payload depends on the session model you use, so treat that as illustrative. The useful pattern is the same: create the session early, before the call has to wait on any expensive setup. Realtime systems get better when the control plane is boring.


Handling incoming phone calls without adding latency


Phone calls are unforgiving because the user starts talking immediately, and they notice even small delays. The practical rule is to do as little work as possible on the webhook path. If you need to fetch configuration, hydrate an agent persona, or select a voice, do it from cached state or precomputed records.


Three implementation details matter most:


  • Precreate the avatar session. Don’t wait until the first utterance. If your avatar endpoint has to warm a model or allocate a media path, do that before you connect the audio stream.

  • Keep the telephony bridge and agent loop separate. A bridge process should move audio and events; it should not also own business logic.

  • Handle interruptions explicitly. If the caller barge-ins while the agent is speaking, the avatar should stop lip motion immediately, not after the next full sentence boundary.


Also remember that phone audio is usually narrowband and noisy compared with browser audio. That affects ASR accuracy, turn detection, and the quality of the voiced response. If your avatar looks perfect but the agent misses short utterances, the user experience still feels broken. This is why a clean turn-taking model matters more than visual polish.


Using the LiveKit plugin when the agent already lives in LiveKit


If your voice agent is already running on LiveKit, you do not need to build a separate video pipeline just to add a face. The LiveKit integration path is the cleanest place to keep the avatar synchronized with the agent, because the plugin hooks into the same realtime agent lifecycle that emits speech.


Conceptually, you drop the avatar into the agent as a service/provider and let the plugin handle the video side of the session. Your existing agent code remains in charge of conversation logic, while the avatar mirrors the speaking state. That reduces the surface area for drift bugs.


from livekit.agents import WorkerOptions, JobContext<p></p>
from livekit.agents import WorkerOptions, JobContext<p></p>
from livekit.agents import WorkerOptions, JobContext<p></p>


That snippet is intentionally schematic. The important part is the integration model: the avatar is attached to the same realtime session as the voice agent, so speaking state, interruptions, and reconnects are all coordinated in one place. If you are already using LiveKit Agents, this is usually less work than inventing a custom media router.


When to use the REST API or Python SDK directly


If you are building a Flask app that manages sessions itself, the REST API and Python SDK are the tools you want. Use them when you need to provision avatars, create sessions, inspect usage, or wire avatar lifecycle into your own database and job queue. That fits well when Flask is the control plane and another component handles audio transport.


A direct API call is useful for debugging and for backend-to-backend orchestration:


curl -X POST <a href="https://api.protoface.com/sessions" data-framer-link="Link:{"url":"https://api.protoface.com/sessions","type":"url"}">https://api.protoface.com/sessions</a> 
curl -X POST <a href="https://api.protoface.com/sessions" data-framer-link="Link:{"url":"https://api.protoface.com/sessions","type":"url"}">https://api.protoface.com/sessions</a> 
curl -X POST <a href="https://api.protoface.com/sessions" data-framer-link="Link:{"url":"https://api.protoface.com/sessions","type":"url"}">https://api.protoface.com/sessions</a> 


If you prefer Python, the SDK gives you the same control with less boilerplate. Use it from your Flask service or from a background worker that prepares sessions before the call connects. The SDK and REST layer are especially useful for idempotency, retries, and auditability; those concerns are easier to manage outside the request/response path.


For people who want a more opinionated starting point, the public quickstarts linked from the main repository are worth skimming because they show the shape of the realtime setup rather than just the avatar piece. The implementation details vary by telephony stack, but the lifecycle is consistent: create session, connect audio, stream events, clean up on hangup.


Protoface in this architecture


This is exactly the kind of integration Protoface is meant for: you keep Flask as your orchestration layer, while the avatar service handles realtime video synthesis and session management. In a phone-call flow, that means your app can create a session from the backend, hand the session off to the media layer, and avoid exposing credentials in the browser or on the call side.


The practical advantage is not just convenience. It is separation of concerns. Your Flask code stays focused on call control, routing, persistence, and auth; the avatar system stays focused on synchronized visual output. That makes failure handling easier too, because a transient media error should not take down the rest of your application.


If you want to dig into the exact request/response shapes, session fields, and integration examples, the documentation is the right place to go: docs.protoface.com.


Gotchas and production notes


A few issues show up repeatedly in production:


  • Session startup time: create sessions before the user is waiting. A phone call can feel broken if the avatar appears after a long silent pause.

  • Clock and jitter mismatch: if audio and video are driven by different clocks, lip sync degrades under load. Keep them tied to one realtime session.

  • Cleanup: end avatar sessions on hangup, timeout, or agent failure. Leaked sessions become a cost problem quickly.

  • Backpressure: if your webhook retries are aggressive, make your session creation idempotent so you do not mint duplicate sessions for one call.

  • Observability: log call IDs, session IDs, agent state transitions, and disconnect reasons. Without that, debugging realtime issues is guesswork.


If you are using a vendor telephony bridge, read its webhook retry and timeout behavior carefully. Many “my avatar is slow” issues are actually “my webhook returned too late, so the provider retried, so I created two sessions, so the agent attached to the wrong one.” Realtime systems punish imprecision.


Conclusion


To stream a talking AI avatar from Flask for incoming phone calls, treat Flask as the control plane, not the media plane. Create sessions early, keep audio and video synchronized through one realtime conversation loop, and make interruptions, cleanup, and retries first-class concerns. If your agent is already on LiveKit, the plugin path is usually the shortest route; otherwise, the REST API and Python SDK give you the backend primitives to wire everything together cleanly.


For implementation details, session shapes, and quickstarts, start with the docs at docs.protoface.com. If you want concrete examples, the public repos linked from the docs and the quickstart collection are the fastest way to adapt the pattern to your telephony stack.


Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.