Header Logo

OpenAI Realtime API Guide: Dynamically Switching Avatar Styles, Outfits, and Facial Expressions

OpenAI Realtime API Guide: Dynamically Switching Avatar Styles, Outfits, and Facial Expressions

Guide to switching realtime avatar styles, outfits, and expressions in OpenAI/LiveKit sessions via stateful control and WebRTC updates.

Introduction


Realtime avatars are useful only if they stay in sync with the conversation state. If you’re building a voice agent, that means more than lip sync: you need to switch visual style, outfit, and facial expression based on the user’s intent, conversation phase, or app state without breaking the stream.


This post focuses on the practical side of that problem: how to model avatar appearance as runtime state, how to change it safely in a live session, and what the transport constraints look like when the avatar is streaming over WebRTC or a similar low-latency media path. By the end, you should be able to design a state machine for avatar presentation, implement style/expression changes without tearing the session down, and decide where those controls belong in your stack.


Think of avatar appearance as session state, not static configuration


The first mistake teams make is treating “avatar style” as a one-time creation-time property. In a realtime system, appearance is usually a mutable part of the session state, alongside voice, prompt, and participant context.


A useful mental model is:


  • Avatar identity: the base face/character or model you want the user to recognize.

  • Presentation style: clothing, background, camera framing, lighting, or brand theme.

  • Facial expression: transient state such as neutral, listening, smiling, surprised, concerned.

  • Behavioral state: speaking, idle, listening, interrupted, thinking.


These should not all be updated at the same frequency. Identity changes are rare. Presentation style changes are usually app-driven and event-based. Expressions should be cheap and responsive, because they often track dialog state in real time. If your system conflates them, you’ll end up with jittery transitions or API calls that are too expensive to make on every token or every audio chunk.


What actually changes in a live realtime avatar session


Under the hood, the avatar is usually receiving a stream of audio and state updates, then producing synchronized video frames or a video track. If the platform supports dynamic style changes, those updates are typically applied as metadata on the live session rather than by creating a new session.


The practical rule is: keep the media pipeline alive, and update the avatar’s render state in place.


That has a few implications:


  • Use low-frequency control messages for style and outfit changes. These are discrete state transitions, not continuous signals.

  • Use high-frequency signals sparingly for expressions. If you drive expression directly from every transcript token, you will overfit the animation to the language model’s output.

  • Debounce state changes if the source is noisy. For example, “smile while speaking” can be derived from the agent’s speaking state, but “look surprised” should usually be triggered by a user event or a strong semantic cue.


In practice, a good control loop might look like this:


  1. User starts speaking.

  2. Avatar switches to a listening expression.

  3. Agent starts responding.

  4. Avatar returns to a speaking expression, maybe with a different outfit or brand theme if the conversation enters a new workflow.

  5. When the session ends, the avatar returns to neutral or is torn down entirely.


Designing the state machine for styles, outfits, and expressions


You’ll get the best results if you explicitly model avatar presentation as a state machine. That keeps business logic out of your rendering code and makes transitions testable.


For example, a support bot might have states like:


  • idle: neutral face, default outfit

  • listening: attentive expression, same outfit

  • explaining: neutral or friendly speaking expression

  • escalated: more serious expression, maybe a different visual theme

  • resolved: relaxed expression, celebratory visual cue


Keep the transition rules deterministic. If you have multiple upstream signals, define precedence. For example, a “critical error” expression should override “smile” until it is cleared, while a temporary outfit change should not override the current speaking/listening state.


A simple implementation pattern is to store the current presentation as a small object and apply only deltas:


{
}
{
}
{
}


The backend then updates only the fields that changed. That matters because many realtime systems are optimized for small, explicit control messages. If the transport has to recompute the entire avatar config on every update, you’ll add latency and increase the odds of visible glitches.


Implementation details: latency, synchronization, and failure modes


Dynamic visual changes are only useful if they line up with the conversation state the user experiences. The main sources of drift are network latency, async agent timing, and audio/video buffering.


Two guidelines matter most:


1. Drive expression from agent state, not from raw model output. For example, wait until the assistant has actually started speaking before switching into the speaking expression. If you trigger the change at token emission time, the face may move before any audio is audible, which looks off.


2. Treat style changes as idempotent. If you send the same outfit twice, the result should be the same as sending it once. This lets you safely retry after transient network failures.


Also watch for these failure modes:


  • Out-of-order updates: if “neutral” arrives after “smile,” the avatar may regress visually. Include a version, timestamp, or monotonic sequence in your app logic.

  • Unsupported transitions: not every avatar asset supports every outfit or expression. Validate the target state before sending it to the realtime session.

  • Over-updating: changing visual state on every conversation turn is fine; changing it on every token is usually too much.

  • Transport teardown: if you rebuild the whole session for a cosmetic update, the user will see a media interruption. Avoid this unless you are intentionally changing the avatar identity.


If you are coordinating with a voice agent, a clean separation is:


  • The agent decides what should happen semantically.

  • The presentation controller decides how the avatar should look right now.

  • The transport layer applies the update to the live session.


Example: controlling an avatar session from Python


If you want to orchestrate avatar updates from backend code, the Python SDK is the cleanest place to do it. The exact method names and payload fields are documented in the SDK docs, but the shape is straightforward: authenticate with an API key, fetch or create a session, then patch the live presentation state.


from protoface import Client

)
from protoface import Client

)
from protoface import Client

)


That example is intentionally schematic. The important part is the pattern: create the realtime session once, then issue small update calls as the conversation context changes. In your own code, keep a local copy of the last-applied state so you can avoid redundant updates and make retries safe.


If you want to see the SDK surface in more detail, the Python client is a good starting point: https://github.com/protoface-ai/protoface-sdk-python.


When the voice agent owns the timing: the LiveKit path


For teams using LiveKit Agents, the most natural place to trigger expression changes is inside the agent runtime, because that’s where speaking state, interruptions, and turn-taking already exist. The livekit-plugins-protoface plugin drops the avatar into the agent pipeline so the voice agent gets a synchronized talking video face without you hand-wiring media plumbing.


In that setup, your agent can emit presentation changes when its own state changes. For example, you might switch to a “listening” expression when the agent is waiting for user input, then swap to a “speaking” expression as soon as audio generation starts. For maintainability, keep the presentation logic adjacent to the agent state machine, not scattered across UI events.


# Illustrative only; check the plugin docs for exact constructor and update APIs.

await avatar.update(expression="smile")
# Illustrative only; check the plugin docs for exact constructor and update APIs.

await avatar.update(expression="smile")
# Illustrative only; check the plugin docs for exact constructor and update APIs.

await avatar.update(expression="smile")


For examples and package details, start with the plugin repository: https://github.com/protoface-ai or the package page if you are installing it directly in a LiveKit/Python stack.


How to use Protoface without exposing control logic to the browser


For web apps, it’s often better to keep avatar control server-side and render the avatar in an iframe. That gives you a clean security boundary: the browser gets a self-contained interactive avatar, but your API key never reaches client code.


This model is especially useful if you want to switch styles or expressions based on user actions in your own app while still enforcing origin allowlists, rate limits, and custom instructions on the embed itself. The app sends semantic events to your backend; your backend updates the live session; the iframe reflects the change.


That separation is simpler to operate than a fully custom frontend integration, and it reduces the chance that presentation state leaks into the client in an uncontrolled way. If you need the browser to request a change directly, have it call your backend, not the avatar API.


REST workflow: update live state from your server


If you prefer explicit HTTP control, the REST API gives you a straightforward server-side integration point. A typical flow is:


  1. Create an avatar and/or session.

  2. Start the realtime media connection.

  3. Patch presentation fields as the session evolves.


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


curl -X PATCH https://api.protoface.com/v1/sessions/session_123 \
}'
curl -X PATCH https://api.protoface.com/v1/sessions/session_123 \
}'
curl -X PATCH https://api.protoface.com/v1/sessions/session_123 \
}'


The exact endpoint names and fields belong in the docs, not in your application assumptions. The important design point is that these are control-plane requests layered on top of a live media session. They should be fast, infrequent, and safe to retry.


Practical heuristics for production


If you want the behavior to feel natural, these heuristics help:


  • Change expressions on semantic boundaries, not every sentence fragment.

  • Keep outfit/style changes tied to coarse app states: onboarding, support, purchase flow, escalation, resolution.

  • Prefer optimistic local state with server confirmation if your UI needs immediate feedback.

  • Log presentation transitions separately from audio events so you can debug timing issues later.

  • Validate asset compatibility before applying a style change to a live session.


In other words: make the avatar’s visuals predictable. Users will tolerate a slight delay better than a face that changes mood unpredictably.


Conclusion


Dynamically switching avatar styles, outfits, and facial expressions is mostly a state-management problem, not a graphics problem. Model appearance as mutable session state, update it with small idempotent control messages, and keep those updates aligned with your voice agent’s actual speaking/listening state.


If you’re implementing this in a production app, the next step is to read the docs, pick the integration surface that matches your stack, and prototype the state machine before you wire it into the UI. Start with https://docs.protoface.com, then use the LiveKit plugin, Python SDK, or REST API depending on where your orchestration logic lives.

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.