The Avatar Package
One portable format for a living avatar: a base face, a set of FACS expression frames, and a manifest that ties them together. No runtime AI.
What it is
avatar-package/1.0 is a small, stable descriptor plus its image assets.
It comes in two shapes, same schema:
- Hosted:
https://studio.rosto.ai/pub/TOKEN/manifest.json. Paths are absolute CDN URLs; the public token is a revocable capability. This is what an embed loads. - Standalone: the downloadable bundle. Paths are relative; the whole avatar is self-contained and self-hostable, with zero dependency on Rosto.
The manifest
A single JSON object. The only difference between the two shapes is whether
base/frames hold absolute URLs or relative paths.
| field | type | meaning |
|---|---|---|
spec | string | always avatar-package/1.0 |
name | string? | display name (may be null) |
style_id | string | the visual style the face was rendered in |
content_hash | string | sha256 of the base image; a stable identity for this face |
base | string | URL or relative path to the base face image |
frames | object | { frame_id: url_or_path }, one entry per Action Unit |
provenance | object | generator, model, consent_attested, license (hosted or self-hosted) |
approved | bool | hosted only; false = preview, not yet publishable |
masks | object | { frame_id: url_or_path } — the matte bounding what that page may paint. Required for correct rendering: without it a page repaints its neighbours |
clips | object | { frame_id: [x0, y0, x1, y1] } — the crop box for each page, as fractions of the canvas |
profile | string | the style's render profile (illustration, photoreal, …); a player picks its motion defaults from it |
badge, badge_url | bool, string | hosted only; whether community hosting shows the Rosto badge, and where it links |
statuses | object | hosted only; { frame_id: "ok" | "skipped" } — per-frame build outcome |
frames_status | string | hosted only; ready | building | absent (base only) |
frames_progress | object | hosted only; { done, total } while building |
allowed_domains is deliberately NOT a manifest field. Domain
locking is enforced on the server at request time; publishing the allow-list to the page that
is being checked would tell an attacker exactly what to spoof, and a client cannot enforce
it anyway.
Standalone bundle:
{
"spec": "avatar-package/1.0",
"name": "Ava",
"style_id": "studio-clean",
"content_hash": "9f2c…",
"base": "base.png",
"frames": { "smile_warm": "frames/smile_warm.png", "brows_up": "frames/brows_up.png" },
"provenance": { "generator": "avatar-studio@…", "consent_attested": true, "license": "self-hosted" }
}
Hosted (embedded) manifest:
{
"spec": "avatar-package/1.0",
"base": "https://studio.rosto.ai/pub/TOKEN/9f2c….png",
"frames": { "smile_warm": "https://studio.rosto.ai/pub/TOKEN/1a4e….png", "brows_up": "…" }
}
Frames are Action Units
Each frames entry is one identity-preserving edit of the base along a single
FACS channel: a raised brow, a warm smile, a viseme mouth shape, a gaze direction.
The rig composes emotions and speech from these at runtime; blinks, breath, gaze
saccades and lip-sync are procedural, so no extra assets are needed. To animate without
our rig, composite base with each frame through its mask (masks),
one page per region at a time — a frame is fully on or fully off, never blended. Two
pages up in one region is a superposition: the viewer sees the base's feature AND the
frame's, which is the double mouth and the second brow. The ids name the channels; see
the spec for the region each one owns.
Driving it — the rig API
Mount the manifest, get a controller back. Framework-free; a plain browser global.
<div id="avatar"></div>
<script src="./avatar-rig.js"></script>
<script>
fetch("./manifest.json").then(r => r.json()).then(m => {
const rig = AvatarRig.mount(document.getElementById("avatar"), m, {size: 320});
rig.direct("await"); // present and waiting
// ONE CALL PER LINE. The drive call is:
// rig.direct({ intent, say, audio, visemes })
// intents: greet · await · attend · ponder · empathize · … (AvatarRig.INTENTS)
// visemes: POST https://studio.rosto.ai/visemes -> AvatarRig.trackFromAudio(buf, units)
});
</script>
rig.setState("listening"); // idle · listening · thinking · speaking · acknowledging · holding
rig.setMood("warm"); // a persistent baseline affect
rig.express("genuine-joy"); // AvatarRig.EMOTIONS lists all 10
rig.react("delight"); // a brief impulse that decays back to mood
rig.speak("Hello there!"); // browser-TTS demo path — see #voice for the real one
rig.command({ state:"speaking", emotion:"curious", speak:"Tell me more?" });
Self-hosting the bundle
The standalone bundle runs anywhere. This is the "you own it" guarantee.
rosto-<name>/
├─ manifest.json avatar-package/1.0 descriptor (relative paths)
├─ base.png the stylized base face
├─ frames/ the FACS expression frames (Action Units)
├─ avatar-rig.js the runtime — a browser global, 67 KB gzipped (185 KB raw), no dependencies
├─ index.html a runnable demo (double-click, works offline)
├─ README.md human quickstart + this protocol
└─ AGENTS.md instructions your AI agent reads to wire it in- Locally — open
index.html(the manifest is inlined, sofile://works). - Any static host — upload the folder (S3, GitHub Pages, your server) and open it.
- In your app — copy the folder into your served assets and mount the rig (above).
Let your AI agent integrate it
The bundle ships an AGENTS.md. Drop the folder into your project and tell your
coding agent (Claude Code, Cursor, Windsurf, Copilot) "integrate this avatar." It reads
AGENTS.md and writes the wiring for your framework.
To create or manage avatars programmatically, point any MCP-capable agent at the Rosto MCP server:
{ "mcpServers": { "rosto": { "url": "https://mcp.rosto.ai/mcp" } } }
// tools: start_free · create_avatar · build_frames · approve · get_embed_guide
Every image is invisibly watermarked to its owner; generated media is provenance-stamped; consent is attested at creation.