Guides / Creating avatars
Creating avatars
Bring your own face: upload a custom image or video and get a streaming avatar you can drive from your app. Do it once in the portal, or entirely through the API.
From the portal
Sign in at /portal/avatars, paste an API key, and drop in a photo or a short video. A short video gives the most lifelike result: real head motion, captured from you. A still photo works too, and becomes a talking photo: the mouth moves in sync with the audio while the head stays still. When it finishes preparing you get an avatar id and a snippet to stream it. Manage your keys at /portal/keys.
From the API
Create the same avatar programmatically. A video source gives real head motion; a still photo gives a talking photo: moving mouth, still head.
from zeli import ZeliClient, AvatarConfig, ClientOptions
client = ZeliClient(
api_key="YOUR_API_KEY",
options=ClientOptions(server_url="https://avatar.zelibot.xyz"),
)
status = await client.create_avatar("founder.mp4", name="founder")
print(status.avatar_id, status.status) # e.g. "founder" "preparing"create_avatar returns an AvatarStatus (preparing or ready). Poll
list_avatars until it's ready:
from zeli._avatars import avatar_is_ready
while not avatar_is_ready(await client.list_avatars(), status.avatar_id):
await asyncio.sleep(2.5)Where the build runs
Everything a conversation touches stays on your box in Sydney. Building an avatar is the one step with a choice in it, because the driving clips a tone is built from can come from either of two generators.
The built-in generator runs on your own GPU, and it is what you get unless you ask for something else. Nothing leaves the machine.
The other is Google's Veo, which produces more lifelike motion and is served from
us-central1 only, so selecting it means the uploaded photograph is sent to
Google in the United States, once, while the avatar is being built. Because that
crosses a border it is not something you can switch on by accident:
AVATAR_DRIVING_CLIP_ENGINE=veochooses the engine.ALLOW_OFFSHORE_FACE_PROCESSING=1acknowledges what the choice means. Without it the build refuses and tells you why, rather than quietly sending the photo.
The second switch is deliberately not the same one that authorises copying a prepared avatar between regions. Setting an Australian Vertex location is not a way around it either, because Veo is not served from one, so the refusal names the decision instead of suggesting a region.
Streaming is unaffected by this choice. Once an avatar is built, the speech
recognition, the transcript, the reasoning and the rendered video all run on your
GPU in ap-southeast-2.
Natural motion from a photo (in development)
Today a still photo gives you a talking photo: moving mouth, still head. Giving a still natural idle motion (subtle sway, blinks, a look to camera) is a separate pipeline, and it is off unless a server is set up to run it. Two things have to be true: the feature flag is on, and the server can verify consent for the face you send. Neither is the default.
POST /v1/photo_avatar.create answers 501 feature_disabled unless the
server is started with ZELI_PHOTO_AVATAR_ENABLED. With the flag on, the
request still has to clear a consent gate, and a server told nothing about
consent records refuses every request: the last gate in the chain denies by
default, on purpose. What clears it is a face the server can account for, so
a box configured with a portraits table admits an approved generated portrait
or an upload the owner attested to in the portal, and asks the deny-all gate
only about files nothing else can explain. Use create_avatar above for a
working avatar from a still.
Building tone clips from the portal is not a separate implementation. That
flow and /v1/photo_avatar.create both run the same builder behind the same
consent gate and the same feature flag, so a consent setup that refuses one
refuses the other. The two differ only in which route you call.
The SDK method and its request/response shape are settled, and the call carries the server-side consent verification the pipeline requires:
created = await client.create_avatar_from_photo(
"headshot.jpg", avatar_id="founder", tones=["neutral", "warm"],
consent_token="<from your consent flow>",
)
print(created.avatar_id, [c.tone for c in created.clips])If consent isn't verified you get a ConsentError (with .decision); a rejected
key raises AuthenticationError. On a server without the feature enabled, the
501 surfaces as a ZeliError with code
feature_disabled.
Then stream it
Once an avatar is ready, drive it like any built-in avatar. Reference it by id:
client = ZeliClient(
avatar_config=AvatarConfig(avatar_id="founder"),
api_key="YOUR_API_KEY",
options=ClientOptions(server_url="https://avatar.zelibot.xyz"),
)
async with client.connect() as session:
await session.talk("Hi, welcome to the demo.")
async for frame in session.video_frames():
img = frame.to_ndarray(format="rgb24")See Connecting and Driving the avatar for the full streaming loop.