Rostodocs Open the Console

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:

The manifest

A single JSON object. The only difference between the two shapes is whether base/frames hold absolute URLs or relative paths.

fieldtypemeaning
specstringalways avatar-package/1.0
namestring?display name (may be null)
style_idstringthe visual style the face was rendered in
content_hashstringsha256 of the base image; a stable identity for this face
basestringURL or relative path to the base face image
framesobject{ frame_id: url_or_path }, one entry per Action Unit
provenanceobjectgenerator, model, consent_attested, license (hosted or self-hosted)
approvedboolhosted only; false = preview, not yet publishable
masksobject{ frame_id: url_or_path } — the matte bounding what that page may paint. Required for correct rendering: without it a page repaints its neighbours
clipsobject{ frame_id: [x0, y0, x1, y1] } — the crop box for each page, as fractions of the canvas
profilestringthe style's render profile (illustration, photoreal, …); a player picks its motion defaults from it
badge, badge_urlbool, stringhosted only; whether community hosting shows the Rosto badge, and where it links
statusesobjecthosted only; { frame_id: "ok" | "skipped" } — per-frame build outcome
frames_statusstringhosted only; ready | building | absent (base only)
frames_progressobjecthosted 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
  1. Locally — open index.html (the manifest is inlined, so file:// works).
  2. Any static host — upload the folder (S3, GitHub Pages, your server) and open it.
  3. 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.

one portable format for a living face