Skip to main content

Avatar Control

@charivo/avatar is optional. It turns your model's expressions and motions into a catalog the model can call as tools — so the character decides when to smile, wave, or look somewhere — and bridges successful calls back into the avatar:* events @charivo/render already listens for.

It is a separate install, and the same tools serve both conversation paths: LLMManager and RealtimeManager take the same ToolRegistration[].

pnpm add @charivo/avatar

Do You Need It?​

Not if your app decides what the avatar does. A button, a sentiment pass, or any rule of your own can drive the avatar by emitting the events directly:

charivo.emit("avatar:expression", { expressionId: "F01" });
charivo.emit("avatar:motion", { group: "TapBody", index: 0 });
charivo.emit("avatar:gaze", { x: 0.2, y: -0.1 });

Emit on the bus rather than calling the renderer, so RenderManager stays the single owner of expression state — including the auto-release in Rendering.

Reach for @charivo/avatar when you want the model to decide. What it adds over raw events is vocabulary and guard rails: the tool schema pins expressionId and group to enums built from your catalog, the handlers re-check arguments and fail the call rather than forwarding an invented id, and the instruction builder tells the model what those ids mean.

What @charivo/avatar Owns​

  • catalog-constrained setExpression, playMotion, and lookAt tool definitions
  • catalog-aware instruction text
  • a result projector that turns successful calls into avatar:expression, avatar:motion (asking for muteSound, so a motion's baked-in sample clip does not talk over the character), and avatar:gaze

It owns no rendering and no session state. It builds on @charivo/core's neutral ToolRegistration / ToolResultProjector contracts and sits beside the manager packages rather than inside either one.

Building The Catalog​

expressions and motions belong to the loaded model, so read them off the renderer instead of hand-maintaining a list. A renderer that implements getAvatarControlCatalog() hands over the whole catalog — names, counts and, where the model carries them, descriptions; prefer it when present. @charivo/render-iki implements it with the descriptions the .iki file declares for every expression and motion. Otherwise assemble the catalog from the two getters and descriptions you write:

import type { AvatarControlCatalog } from "@charivo/core";

const catalog: AvatarControlCatalog = renderer.getAvatarControlCatalog?.() ?? {
expressions: renderer.getAvailableExpressions?.() ?? [],
motions: renderer.getAvailableMotionGroups?.() ?? {},
expressionDescriptions,
motionDescriptions,
};

An empty section removes its tool rather than registering an unusable one: setExpression appears only when expressions is non-empty, playMotion only when motions is, and lookAt is always registered — its handler clamps the coordinates to [-1, 1]. A catalog read before the model finishes loading therefore yields a gaze-only tool surface.

Rebuild it when the model changes — readAvatarCatalog in examples/web/src/app/hooks/useCharivoChat.ts is the assembled form wired to a model switch.

When the model carries no descriptions, they are the part you write, and they decide whether the model picks a face or picks the right one.

Writing Descriptions​

Cubism sample models name expressions F01, exp_01, and so on. The ids carry no meaning, there is no authoritative published mapping for them, and mappings that circulate online get them wrong. Derive them yourself:

  1. Play each expression and motion in your own app and watch it. This is the only step that establishes meaning; everything else only confirms it.
  2. Cross-check against the .exp3.json parameter deltas — but do not trust deltas alone. Haru's F04 ("sad") and F08 ("unimpressed, deadpan") set the identical ParamMouthForm: -1.76; they differ in the brows and eyes.
  3. Write what you saw into your app config, not into the model. Renaming ids inside model3.json does not survive: those are vendor sample assets, and the next SDK refresh replaces them wholesale.
  4. Pin the order you observed with a test. Motion indices are positional, so a reordered motion file silently reassigns every description after it — examples/web/src/app/config/characters/index.test.ts shows the shape.

Two things worth knowing before spending long on wording. An expression reads weakly while the character speaks, because lip sync owns the mouth; the ones that change or close the eyes are the ones that still read. And a motion group usually holds several unrelated reactions, which is why motionDescriptions is positional per index rather than one sentence per group.

Wiring It Up​

Both managers register the same tools, and each guide carries its own recipe:

Alternatives​

  • Skip avatar control entirely when the character only needs to talk — or drive it from the app instead, as Do You Need It? describes.

References​