Header Logo

Building an Accessible Realtime Avatar Interface in Flask with WebSockets

Building an Accessible Realtime Avatar Interface in Flask with WebSockets

Build an accessible realtime avatar interface in Flask with WebSockets, session control, and synced audio/video streaming.

Introduction


Adding a realtime avatar to a Flask app sounds straightforward until you try to do it for real: audio arrives in small chunks, the model or voice agent needs to respond with low latency, the avatar video must stay synchronized with speech, and the browser still expects a responsive UI. If you try to push all of that through ordinary HTTP request/response cycles, you end up fighting buffering, timing, and state management.


This post shows one practical way to structure that problem in Flask with WebSockets: keep HTTP for page delivery and control-plane actions, use WebSockets for realtime session events, and treat the avatar stream as a separate media pipeline. By the end, you should have a clear mental model for how to wire up a realtime avatar interface, what the backend is responsible for, what the browser should own, and where the edge cases usually show up.


Why Flask plus WebSockets is the right boundary


Flask is still a good choice when your app is mostly conventional web application logic, but the moment you add a conversational avatar you need a bidirectional channel. WebSockets give you that without forcing you into a full realtime framework.


The key architectural split is:


  • HTTP for static pages, auth, session creation, and admin actions.

  • WebSockets for live state changes: connection status, token issuance, session lifecycle, transcript events, and control messages like mute/unmute or interrupt.

  • Media transport for the actual audio/video stream, which should be handled by a purpose-built realtime layer rather than by your Flask process.


This matters because the avatar is not “just video.” It is usually the visible end of a realtime speech pipeline: browser mic capture or agent audio, streaming ASR, turn-taking, TTS, lip sync, and then a video renderer or face generator. Latency accumulates across those steps, so the interface needs to keep control-plane traffic lightweight and let the media plane do its job independently.


Designing the session lifecycle


A useful way to think about the system is as a session state machine. The browser connects to your Flask app, gets an application-specific session record, and then moves through a small set of states: initialized, connecting, active, interrupted, and closed.


In practice, you want the backend to own the authoritative session metadata: user identity, avatar selection, quality tier, rate limits, expiry, and any custom prompt or voice configuration. The frontend should only hold ephemeral connection details and render status.


A minimal lifecycle looks like this:


  1. User loads the page.

  2. Flask creates or looks up a realtime avatar session.

  3. The server returns a short-lived connection payload to the browser.

  4. The browser establishes the realtime connection and subscribes to status events.

  5. The UI updates as the avatar connects, starts speaking, pauses, or ends.


Two implementation details are worth calling out:


  • Do not expose long-lived API keys to the browser. If the browser needs to join a realtime session, have Flask mint short-lived session credentials or a constrained embed token.

  • Keep session state server-side. The frontend can show “connected” or “speaking,” but the backend should decide whether the session is still valid and whether the user is allowed to continue.


Flask WebSocket structure in practice


With Flask, the simplest pattern is to keep the HTTP routes thin and add a WebSocket layer for live updates. The exact library is up to you, but the important thing is message shape and event discipline.


For example, your server might emit messages like:


{
}
{
}
{
}


{
}
{
}
{
}


{
}
{
}
{
}


The browser can then update a status badge, enable a “Reconnect” button, or show an error overlay without polling. This is especially useful when you need to coordinate several moving parts: the avatar stream, the transcript, and the user interface.


One practical rule: keep WebSocket messages idempotent and small. If you send high-frequency transcript fragments or audio timing events, prefer compact event names and simple payloads. The browser should reconstruct UI state locally rather than rely on a server push for every visual change.


Managing media sync and latency


The hardest part of an avatar interface is sync, not rendering. If the user hears speech before the face starts moving, or the face starts lip movement before audio is audible, the experience feels broken immediately. That means you need a pipeline that keeps audio timestamps, model response timing, and avatar animation aligned.


There are a few practical techniques that help:


  • Stream, don’t batch. Small audio chunks reduce perceived latency and let the avatar start animating earlier.

  • Use a single source of truth for turn state. Decide when the agent is “speaking” in one place, and have both the audio pipeline and avatar renderer consume that state.

  • Handle interruption explicitly. If the user starts speaking over the agent, the browser should be able to send an interrupt event and the backend should stop playback and transition the avatar back to listening.

  • Instrument the round trip. Measure mic-to-first-token, first-token-to-first-audio, and first-audio-to-first-frame. Without those metrics, you are guessing.


On the client side, don’t block UI rendering on a media handshake. Show a connection state immediately, then swap in the avatar once the realtime path is ready. On the server side, keep timeouts explicit; a dead session should fail closed rather than hang forever.


Session control from Flask


Most production setups need a control plane separate from the media plane. Flask is a reasonable home for that control plane because it can authenticate the user, allocate resources, and call out to the avatar platform as needed.


For instance, you might use a Python SDK to create a session before the browser connects:


from protoface import Client

print(session.id, session.url)
from protoface import Client

print(session.id, session.url)
from protoface import Client

print(session.id, session.url)


The exact SDK shape depends on the package version, but the pattern is the same: keep your API key on the server, create or manage sessions there, and send only ephemeral connection data to the frontend.


If you prefer a raw HTTP control path, the same pattern applies with a bearer token:


curl https://api.protoface.com/v1/sessions \
}'
curl https://api.protoface.com/v1/sessions \
}'
curl https://api.protoface.com/v1/sessions \
}'


That division of responsibility makes the Flask app easier to secure. The browser never sees your secret key, and your backend remains the only place that can create, update, or terminate sessions.


Where Protoface fits


This is where Protoface is useful: it gives you the avatar/session layer so you do not have to build realtime face rendering, lip sync, or session management from scratch. For a Flask app, the most relevant surface is the REST API plus the Python SDK, because they let your backend create avatars and sessions, then hand the browser only short-lived connection data.


If you are integrating with a voice agent stack, the LiveKit Agents plugin can drop a synchronized avatar into an existing agent pipeline without changing your browser architecture. If you want to validate the pieces before wiring them into Flask, the docs at docs.protoface.com are the right place to confirm the exact request fields and session parameters, since those details change faster than the architectural pattern.


Browser UX and accessibility details


An accessible avatar interface is more than a face on screen. You still need the same interaction affordances you would provide for any realtime communication UI.


Concretely:


  • Expose connection state textually, not just with animation.

  • Provide captions or transcript text alongside the avatar.

  • Make mute, stop, and reconnect controls keyboard accessible.

  • Support reduced-motion preferences by limiting unnecessary animation.

  • Preserve semantic controls even if the avatar is embedded in an iframe.


That last point matters. A good avatar experience should not require the user to infer system state from motion alone. The video face is the conversational surface, but the interface still needs explicit status, errors, and controls. If the avatar fails to connect, the user should see why. If the agent is listening, that should be indicated in text as well as visually.


For embedded deployments, customer-managed iframes are often the cleanest option because they keep the browser integration simple and avoid exposing secrets. You can still enforce origin allowlists, per-embed instructions, and rate limits without asking the host app to become a media platform.


Conclusion


The simplest mental model is to treat the avatar as a realtime subsystem, not a widget. Flask can own authentication, session allocation, and application state; WebSockets can carry live control events; and the media stack can handle audio/video transport and synchronization.


If you keep those boundaries clean, the result is much easier to reason about, debug, and secure. Start by building a thin Flask control plane, add a WebSocket channel for live state, and test the session lifecycle before worrying about visual polish. From there, use the docs at docs.protoface.com and the relevant SDK or plugin repository to wire the avatar into your actual agent 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.