Editing a character
Change an avatar's clips and resting loop after creation: how to declare them, how each change settles, and the refusals worth telling apart.
Her clips and her resting loop are both editable after creation. The resting loop is re-directed in words; each library clip can either be rendered from a motion description or use the assetId of a pose-compatible video you uploaded. Her id, her voice and her live calls survive every change on this page.
Both are accepted immediately and rendered in the background, and she keeps serving what she has until the new take is ready. Editing never takes a live character off the air.
Her clips
Send the library you want, in full — the same shape you could have created her with. The platform reconciles: a clip whose description is unchanged is kept and keeps serving, a new or revised one is queued, one you leave out is retired.
const { plan } = await rta.setClipLibrary(avatarId, {
expectedRevision: revision, // optional compare-and-set
clips: [
{ clipId: "idle_soft", role: "idle", source: { motionPrompt: "breathing gently, a slow blink" } },
{ clipId: "listen_lean", role: "listen", source: { motionPrompt: "leans in, attentive, small nod" } },
{ clipId: "gesture_wave", role: "gesture", whenHint: "when greeting someone",
source: { motionPrompt: "raises a hand and waves warmly, then lowers it" } },
],
});
// plan → { kept: [...], queued: [...], retired: [...] }
await rta.waitForClips(avatarId);library = client.get(f"/avatars/{avatar_id}/clips").json()
revision = library["revision"] # top-level; 0 while creation still owns the library
r = client.put(f"/avatars/{avatar_id}/clips", json={
"expectedRevision": revision,
"clips": [
{"clipId": "idle_soft", "role": "idle",
"source": {"motionPrompt": "breathing gently, a slow blink"}},
],
})
# httpx does not raise on 4xx. Refusals come back as {error, status, code} with no "plan":
# 403 clip_library_not_enabled, 409 revision_conflict (body "revision" = current), 422 clip_declaration_rejected
r.raise_for_status()
plan = r.json()["plan"] # the 202 body also carries the new "revision"Twelve clips at most: up to six idle, up to two listen, the rest gestures. Per clip you also get durationSeconds (4–8) and reroll: true, which forces a fresh take of a clip whose description did not change.
Omission is deletion. This is the whole desired library, not a patch — a clip missing from the array is retired. That is also why expectedRevision exists: pass the revision you last read and a concurrent writer gets a 409 instead of silently erasing your change. Omit it to declare unconditionally. You read revision from the top level of GET /avatars/{id}/clips, beside data — it is 0 while the creation pipeline still owns the library, every accepted declaration bumps it, and the 202 body carries the new value.
For an uploaded take, upload or remotely register a video asset first, then use source: { assetId }. Uploaded clips are pose-validated against the avatar's rest-pose anchor before they can serve; the first and last frame must both return to that pose. The dashboard's Clips editor performs the same asset registration and full-library declaration without exposing a developer API key to the browser.
Her resting loop
The loop is what she plays when nothing else is happening — the same thing motionPrompt described at creation. It is not a clip: a clip with role: "idle" is a variant spliced over the loop, and declaring one never changes what she rests in.
const { servingUrl } = await rta.setLoop(avatarId, {
motionPrompt: "tilts her head, a small amused smile, settles back to centre",
});
// servingUrl = the loop she is playing RIGHT NOW, for the whole render
await rta.waitForLoop(avatarId); // throws if the render gave upresp = client.put(f"/avatars/{avatar_id}/loop", json={
"motionPrompt": "tilts her head, a small amused smile, settles back to centre",
})
resp.raise_for_status() # 409 loop_pending, 422 loop_prompt_rejected / loop_not_generatable
accepted = resp.json()
# accepted["servingUrl"] = the loop she plays for the whole render (None if she had none yet)
# Poll GET /avatars/{id} until idleVideoStatus is "ready" (or "failed").Your clip library is untouched. Clips render against the portrait, not against the loop, so re-describing the loop re-queues nothing and does not move the library's revision. Describe a closed arc — it has to end where it began, or the loop snaps every time it wraps.
Keep it small. The loop is her resting state and a separate model animates her mouth, so every description is held to a nearly-still envelope: blinks, a soft closed-lip smile, a shift in weight, a few degrees of head drift. Ask for a laugh, a head turn, or an open mouth and the render comes back inside the envelope anyway — accepted, charged, and looking very much like the loop you already had. That is the one failure here with no error code attached to it, so aim inside the envelope rather than at the edge of it.
Waiting, and how each one ends
Both return on acceptance, not completion, so both need a wait — and the intuitive version of each is wrong:
waitForClipssettles when nothing is still rendering. Waiting for every clip to reachreadyhangs forever: a clip rejected by pose validation settlesfailed, which is terminal.waitForLoopsettles onidleVideoStatusand throws if the render gave up. A failed re-direct leaves herready— she is still serving the old loop — andidleVideoStatusis the only status field guaranteed to move;errormay or may not carry the reason. PollidleVideoStatus, neverstatusorerror: a loop watchingstatuswaits for a change that never comes.
Refusals worth telling apart
| Code | Means | Do |
|---|---|---|
403 clip_library_not_enabled | a per-tenant rollout gate | ask us; nothing about the body helps |
409 revision_conflict | someone declared first | re-read, re-decide, re-declare |
409 loop_pending | a re-direct is already in flight | wait, then retry |
422 loop_not_generatable | no portrait to re-animate | terminal — do not retry |
422 clip_declaration_rejected | a clip description was refused (clips route) | rewrite it |
422 loop_prompt_rejected | the loop description was refused (loop route) | rewrite it |
409 avatar_not_ready | she is not ready (either route) | poll status until ready, then retry; the loop route also refuses a creation that settled failed |
A 503 with retryable: true (clip_screen_unavailable, loop_workflow_unavailable, loop_workflow_queue_failed) is transient — back off and send the same body again. Each refusal above is JSON { error, status, code } — a malformed body (422) or an unknown avatar (404) carries no code — so switch on code, not on the status alone.
A rejected upload is not an error response at all: the declaration is accepted and that one clip settles failed with the verdict in poseCheck. The rest of the library is untouched.
What you cannot change
Her portrait. It is the rest pose every clip is rendered to start and end on — the shared frame that makes a state switch a splice instead of a jump — so replacing it would invalidate the whole library at once. Create a new avatar instead. Everything else about her is a sentence away.