Docs
Living avatars,
in a few lines.
One consenting selfie becomes an avatar that breathes, speaks and feels, animated live in any browser, with no AI at runtime.
Embed an avatar
Already have a public token? This is the whole integration: drop it anywhere. The token is a capability, so rotating it revokes every embed at once.
<div id="rosto"></div>
<script src="https://studio.rosto.ai/rig/avatar-rig.js"></script>
<script>
fetch("https://studio.rosto.ai/pub/PUBLIC_TOKEN/manifest.json").then(r => r.json()).then(m => {
const rig = AvatarRig.mount(document.getElementById("rosto"), m, {size: 320});
rig.setState("idle"); // idle · listening · thinking · speaking
// rig.speak("Hello!"); rig.express("genuine-joy"); rig.setMood("warm");
});
</script>
Create one via the API
Authenticate with x-api-key (get a key from the Console by
redeeming an invite). Consent is required. Your photo is kept encrypted only until you accept the avatar, then deleted.
# 1 — create an avatar (get a key from the Console: redeem an invite)
curl -sX POST https://studio.rosto.ai/v1/avatars \
-H "x-api-key: $ROSTO_KEY" -H "content-type: application/json" \
-d '{"image_base64": "'"$(base64 -i selfie.jpg)"'", "mime_type": "image/jpeg", "consent_attested": true}'
# -> { "avatar": { "id": "...", "public_token": "TOKEN" } }
# 2 — build the expression set
curl -sX POST https://studio.rosto.ai/v1/avatars/$AVATAR_ID/frames -H "x-api-key: $ROSTO_KEY"
# 3 — the runtime loads the public manifest (no key; the token is the capability)
curl -s https://studio.rosto.ai/pub/$TOKEN/manifest.json
Drive the rig
The avatar animates in the browser, with no model calls at runtime. Command it directly, or hand it a JSON command stream and let an LLM drive it automatically.
rig.setState("listening"); // idle · listening · thinking · speaking · acknowledging\nrig.setMood("warm");\nrig.express("genuine-joy"); // the manifest lists what this avatar supports\nrig.speak("Great to meet you."); // viseme lip-sync\nrig.command({ state: "speaking", emotion: "curious", say: "Tell me more?" });
Or skip the animator vocabulary entirely. rig.direct(cue) speaks in the terms your
app already knows — conversational intents. Your logic detects the situation; one call performs it
(mood + state + emotion beat + head language + speech, choreographed):
rig.direct("await"); // friendly, present, waiting\n// the feedback turned negative:\nrig.direct({ intent: "empathize", say: "I completely understand. We will fix this." });\n// the joke landed:\nrig.direct({ intent: "laugh", say: "Okay, that was a good one." });\n// with your TTS audio: the voice locks to the lips (starts on its play event)\nrig.direct({ intent: "reassure", say: "All fixed.", audio: new Audio(url) });\n// AvatarRig.INTENTS lists all of them: greet · await · attend · ponder · empathize ·\n// apologize · reassure · laugh · celebrate · agree · disagree · surprised · curious · farewell
Full vocabulary and dynamics: the control protocol.
Adding voice
Rosto animates the face; it does not include a voice. You bring the audio, which keeps you free to use any provider (or none). Two ways to run it:
- Silent — skip
speak; the face reacts while your user reads (states and emotions). - Voiced — call
rig.speak(text)the moment your audio starts, so the lips move with it.
// THE RULE: lips start WITH the audio, never with the request.
// TTS generation takes seconds — call rig.speak too early and the voice
// lands on a face that already finished talking.
// Free, client-side — perfect for a demo:
const say = (t) => {
const u = new SpeechSynthesisUtterance(t);
u.onstart = () => rig.speak(t); // lips start exactly when the voice does
speechSynthesis.speak(u);
};
// Production — fetch your TTS first, then let speakWith handle the sync:
async function speak(text) {
const url = await myTTS(text); // your provider returns an audio URL
rig.speakWith(text, new Audio(url)); // starts playback, lips on its 'play'
} // event; still talks if audio is blocked
// speakWith gives you four sync layers automatically:
// 1. START lock — lips begin on the audio's 'play' event, never earlier
// 2. LENGTH lock — the viseme timeline is paced to the real audio duration
// 3. LIVE follow — an analyser tracks the actual sound: pauses close the
// mouth, stressed syllables open it (fails open — cadence — if the
// browser blocks the audio tap)
// 4. ALIGNMENT — the letter clock pauses with the voice and resumes on the
// next word, re-anchoring the mouth-shape sequence at every pause; when
// the audio ends, so do the lips
// Perfect per-phoneme sync (providers with character timestamps, e.g.
// ElevenLabs): drive rig.setViseme(...) from the timestamp stream instead.
For production, any text-to-speech works (ElevenLabs, OpenAI, Google, Azure). Start rig.speak(text)
together with playback for the tightest sync; it is provider-agnostic.
Connect an agent (MCP)
Rosto ships a remote MCP server so an agent can go from selfie to embedded avatar on its own.
// Point any MCP client at the Rosto server (JSON-RPC 2.0, streamable HTTP):
{ "mcpServers": { "rosto": { "url": "https://mcp.rosto.ai/mcp" } } }
// Then the agent walks:
// start_free(invite_code) -> create_avatar(api_key, image_base64, mime_type, consent)
// -> build_frames(api_key, avatar_id) -> get_embed_guide(public_token)
Reference
- Avatar Package spec (avatar-package/1.0)→
- OpenAPI schema→
- Interactive API explorer→
- llms.txt (for agents)→
- Console — bring a face to life→
Every avatar is consented; the photo is kept encrypted only until it's accepted, then permanently deleted; generated media is provenance-stamped; tokens are revocable to the CDN edge.