Building
Companion packs
Ship 3D and 2.5D (Live2D) characters with their own motions, expressions, thinking sounds and behaviour. Data only, no code.
This is the full reference. To make your first companion, start with Create a 3D companion or Create a 2D companion.
Companion mode gives an agent a face and a voice: a character in the browser that listens, looks thoughtful while the agent works, and answers out loud with lip-sync and gestures timed to the words — while still being the same agent, with its tools, files and crew.
A companion pack is a plugin that ships the looks and the characters for that, as data. No code runs.
| Capability | Folder | What the user gets |
|---|---|---|
| Model (a look) | companion-models/<key>/ |
an avatar — 3D (VRM, GLB) or 2.5D (a Live2D Cubism model) — with its own motions (idle, talking, listening, thinking, gestures, reactions, and for 3D, extra body parts like tails and ears) and its own facial expressions |
| Character | companion-characters/<key>/ |
a ready-made face in the Companions gallery: one look + a suggested personality, voice, thinking sounds and behaviour profile |
| Item | companion-items/<key>/ |
a prop any 3D companion can wear: a mug for the hand, a hat for the head. The agent picks it up with [hold:…], the user ticks it in the wardrobe menu |
Every look is drawn by the Unity renderer, the only one; a pack ships data and never a renderer.
Voices come from voice providers — see Voice and speech-to-text. A character only suggests voices.
A complete pack
readme.md
companion-models/sample-seed/model.json
companion-models/sample-seed/seed-san.vrm
companion-models/sample-seed/thumbnail.png
companion-models/sample-seed/background.jpg
companion-models/sample-seed/idle-neutral.vrma
companion-models/sample-seed/talk-idle.vrma
companion-models/sample-seed/listen.vrma
companion-models/sample-seed/think.vrma
companion-models/sample-seed/wave.vrma
companion-models/sample-seed/laugh.vrma
companion-characters/sample-seed-character/character.json
companion-characters/sample-rose-character/character.json
(infrastructure/plugins/sample-companion-pack — walkthrough: Sample Companion Pack)
An item pack looks the same, with companion-items/<key>/item.json, its .glb model and an optional thumbnail per item
(infrastructure/plugins/sample-companion-items).
Keys are unique across the whole package. A character can't reuse its model's key — use fox-model and fox.
Every binary file must be referenced by one of the JSON files, or the publish is refused naming the file.
Model — model.json
A model is a look: the avatar plus everything that says how it moves and how its face is driven. The motions live inside the look — there is no separate motion-set capability.
{
"name": "Sample Seed-san",
"description": "VirtualCast's sample VRM avatar Seed-san, with a small set of its own motions.",
"avatar": "seed-san.vrm",
"thumbnail": "thumbnail.png",
"motions": [
{ "id": "file:idle-neutral.vrma", "roles": ["idle"], "name": "Idle",
"description": "A long, unhurried neutral standing idle loop." },
{ "id": "file:talk-idle.vrma", "roles": ["talkIdle"], "name": "Talk idle",
"description": "Idle loop with light gestures while speaking." },
{ "id": "file:wave.vrma", "roles": ["gesture"], "name": "Wave",
"description": "Waves a hand — greeting or farewell.", "cues": ["waving"], "strokeSeconds": 0.9 }
]
}
| Field | Rules |
|---|---|
name, description |
display text; name 1–100 characters |
avatar |
required: .vrm or .glb (3D), or a .zip Live2D Cubism model (2.5D, see below), ≤ 100 MB. VRM gets the full treatment (facial expressions, visemes, spring-bone physics) |
thumbnail |
optional PNG, JPEG or WebP, ≤ 2 MB — shown in the gallery and the look picker |
outfits |
optional, 3D looks only — the clothes she can switch between, by showing and hiding parts of the model (below) |
background |
optional PNG, JPEG or WebP, ≤ 2 MB — a picture shown blurred behind her on the Companion tab (below) |
rig |
optional bone map for a plain GLB humanoid — see Bone maps. Not allowed on a VRM |
motions |
the look's motions (below). At least one idle, one talkIdle and one gesture or the publish is refused |
face |
optional face map — which of the model's own morph targets or expressions drive each emotion, mouth shape, blink, smile, brows (raise, and browLower for a frown) and blush, plus an optional jawBone (a bone of the avatar). Every control must exist on the avatar |
framing |
optional — what the Face, Upper body and Full body buttons show for this look, which one a new companion starts on, and for a 3D look its camera (below) |
cloth |
optional, 3D looks only — the skirt or cape chains to simulate as cloth (below). Left out, the stage finds a skirt on its own; [] means no cloth |
pacing |
optional, 3D looks only — whether she steps sideways across the floor on her own, how fast and how far (below). Needs a walk motion |
isLooseChainPhysicsEnabled |
optional boolean, default true — for a model without spring bones, false keeps its loose bone chains on a gentle sway instead of physics (below) |
The whole manifest, as the platform stores it, may be at most 64 KB. An unknown key anywhere in model.json refuses the publish.
Framing
By default the stage guesses each preset from where it thinks her head is. A look can say exactly what it wants instead:
"framing": {
"defaultPreset": "fullBody",
"face": { "bottom": 0.73, "top": 1.0 },
"upperBody": { "bottom": 0.45, "top": 1.0, "centerX": 0.5 }
}
defaultPreset(face,upperBodyorfullBody) is where a new companion starts; without it, Upper body.face,upperBody,fullBodyare windows, each optional. A preset without a window keeps the stage's guess: the stage measures her head (a 2.5D look: from itsHead/Facehit area, else from the parts that move as far as her eyes whenParamAngleXturns, else the top 13% of the canvas), then shows about 1.5 head heights below the head for Face and 4.5 for Upper body, and the whole model plus a 4% margin for Full body. If the head can't be found, every preset shows the whole model.- A window is
bottomandtop, as fractions of the model's height (0 = its bottom edge, 1 = its top edge), and an optionalcenterX(0 = left edge, 1 = right edge). WithoutcenterXthe crop is centred on her head. For a 3D look the model is its rest-pose box (hat and hair included), measured at its front; for a 2.5D look it is the canvas the artist drew in. - A window guarantees height only. At zoom 1 the stage's height shows exactly
bottomtotop, on any stage shape. The width is whatever the stage allows: a wide stage shows more space beside her, a narrow one crops her sides aroundcenterX.centerXmoves the crop, it cannot widen it. - Values may go a little past 0 and 1 (
"bottom": -0.04leaves air under her feet), between -1 and 2, andtopmust be at least 0.02 abovebottom. An unknown key or an out-of-range value refuses the publish with the reason.
A user who uploads their own look gets the same thing from the Use this view for Face button in the companion's Frame mode.
For a 3D look, framing can also carry a camera that says where the camera rests, how far a viewer can turn it and how it follows her:
"framing": { "camera": { "defaultYawDegrees": 20, "yawRange": [-45, 45], "isDriftEnabled": true, "followSeconds": 0.6 } }
| Field | Rules |
|---|---|
defaultYawDegrees |
-75 to 75, default 0 — the resting turn of the camera around her |
yawRange |
optional [minimum, maximum], both within -75 to 75, defaultYawDegrees inside it; default -75 to 75 |
isDriftEnabled |
default true — the camera drifts a little while she idles |
followSeconds |
0.2 to 2, default 0.6 — how softly the camera follows her across the floor |
A 2.5D look with a camera is refused.
Pacing and the walk motion
With pacing on, she now and then takes a step to the side on her own: while idle every 12 to 30 seconds, and sometimes
between sentences of a long reply. She turns three-quarters toward the direction she walks (60 degrees), plays her walk
loop on her legs while she travels, squares up to the viewer before the last step and stops to face the user when they start
talking or typing.
"pacing": { "isEnabled": true, "speed": 0.35, "stepDistance": 0.25 },
"motions": [
{ "id": "file:walk.vrma", "roles": ["walk"], "name": "Walk", "description": "An in-place walking loop for stepping across the floor." }
]
| Field | Rules |
|---|---|
isEnabled |
default true |
speed |
body heights per second, 0.1 to 1, default 0.35 |
stepDistance |
how far one step goes, in body heights, 0.05 to 0.6, default 0.25 |
- Distances are in body heights, so a tall and a short model pace alike.
- The
walkclip must be an in-place loop (no travel in the clip; the stage moves her). The stage measures how far the left foot travels in the loop and plays it faster or slower (0.5x to 2x) to matchspeed; a clip it can't measure plays at 1x. - Without a
walkmotion she never paces, whateverpacingsays:pacingalone does nothing. A user with reduced motion on never sees her pace either. - Without a
pacingblock the defaults above apply; she paces if the look has awalkmotion. - She only steps when there is floor to step on. If the current framing (a close-up Face view, say) leaves less visible floor than one step, the step is skipped.
- A character turns it off or tunes it with
behaviour.behaviours.pacing({ "enabled": false }or{ "strength": 0.75 }). - A
[pace:left],[pace:right]or[pace:home]tag in a reply makes her step on cue, and[camera:30](a whole number of degrees within -75 to 75),[camera:front]or[camera:reset]turns the camera. On a 3D look the agent's briefing teaches the camera tags, and the pace tags when the look has awalkmotion. A plugin that edits the reply can write them too. An invalid argument drops the tag. - Pacing is for 3D looks; a 2.5D look with
pacingis refused.
Outfits
An outfit is a named set of the model's parts to show. She wears one outfit at a time, by design: "what is she wearing" is a single answer for the agent and the user. (Independent toggles such as "glasses on or off" are left out on purpose; make a second outfit instead.)
"outfits": [
{ "key": "default", "name": "Casual", "meshes": ["Tshirts", "Bottoms", "Shoes"], "shapes": [{ "mesh": "Body", "shape": "Hip1", "weight": 1 }] },
{ "key": "dress", "name": "Dress", "meshes": ["Dress", "Dress_Shoes"] },
{ "key": "swim", "name": "Swimwear", "meshes": ["SwimB_L", "Bottoms_L002"], "materials": ["Mat_Swim"] }
]
| Field | Rules |
|---|---|
key |
required, a-z0-9-, 1–40, unique in the look — what [outfit:key] and the stored choice refer to |
name |
required, 1–60 — shown in the wardrobe menu |
meshes |
mesh node names the outfit shows, ≤ 50 parts per outfit |
materials |
material names the outfit shows — for models that merge several garments into one mesh and split them only by material (the VRoid exports do) |
shapes |
blend-shape weights the outfit sets, each { "mesh", "shape", "weight" } with weight 0–1 — to shrink the body under the clothes; ≤ 64 per outfit, and they don't count as parts |
- The union rule. A part that any outfit lists is hidden unless the worn outfit lists it; a part no outfit lists (body, face, hair, a tail) always shows. So an outfit only names its own clothes.
- Shapes follow the same rule. A (mesh, shape) pair that any outfit names is 0 unless the worn outfit sets it; a pair no outfit names is never touched. The mesh must be one of the avatar's and the shape one of its shape keys.
- The first outfit is the default, and what a companion wears until someone changes it. At most 20 outfits per look.
- Write mesh names the way three.js spells them (the same rule as bone maps):
Bottoms_L.002becomesBottoms_L002. Material names are written as they are. The publish is refused when the avatar has no part of that name, and tells you the spelling when it differs. - A VRoid export draws the top, skirt and shoes as one
Bodymesh split by material: list the clothing materials inmaterialsinstead. VRoid has no skin under its clothes, so hiding a garment leaves a hole; give such an outfit its own mesh in the file, or keep the garments in all of them. - A material shared between a garment and the body hides both. That is the author's to avoid.
- Outfits are for 3D looks. A Live2D look with
outfitsis refused.
Hair, cloth and what a garment covers
All of this is 3D only and needs no code, but the model has to carry the right things. What the stage does with what you leave out:
| You supply | The stage does | Left out |
|---|---|---|
| Spring bones in the VRM for hair, hoods, ribbons and tails | simulates them against a body rig (a ball for the head, capsules for neck, torso, arms, hands, hips, legs) fitted at load to the innermost layer of the mesh, which it treats as her skin, so garments don't widen it | the stage falls back to the loose-chain rule below |
| A body mesh under the clothes (the innermost layer is the skin) | hides the skin and underwear that a garment covers, and the garment parts that poke through another one, from the geometry alone: no mesh names to list. The face is never hidden, and a thin border of skin stays next to every kept part so a pose shows a sliver of skin, never a hole | nothing covers anything: skin shows through, or the layers clip |
| Loose bone chains (non-humanoid bones that skin the mesh) in a model without spring bones: a GLB, or a VRM that declares none | simulates the chains that hold at least 20 vertices and don't skin the face, up to 48 | the chains don't move; set isLooseChainPhysicsEnabled: false to keep a model's chains on a gentle sway instead of physics |
| A skirt as a ring or fan of bone chains | finds it on its own: at least 4 chain roots under the hips (not on the legs, not above the waist) that spread over more than 180 degrees around her, grouped under one parent (a panel of at least 3 chains per parent also counts); the largest group becomes one closed-loop cloth that holds its shape and stays out of the legs | no skirt is found, and the skirt hangs rigid or follows the loose-chain rule above |
cloth in model.json, when detection picks the wrong thing or finds nothing |
simulates exactly the cloths you list | a look with no cloth finds its skirt automatically |
cloth is a list of up to 4 pieces:
"cloth": [
{ "chains": ["Skirt_F1", "Skirt_F2", "Skirt_R1", "Skirt_B1", "Skirt_B2", "Skirt_L1"], "isClosedLoop": true, "stiffness": 0.5, "drag": 0.4, "gravity": 1 }
]
| Field | Rules |
|---|---|
chains |
required, 2 to 32 root bone names, ordered around the body, no bone used twice. Each must be a non-humanoid bone of the avatar, written the way three.js spells it (the same rule as bone maps); otherwise the publish names the bone |
isClosedLoop |
default true — the last chain links back to the first (a skirt does, a cape doesn't) |
stiffness |
0 to 1, default 0.5 — higher holds its shape |
drag |
0 to 1, default 0.4 |
gravity |
0 to 2, default 1 |
"cloth": []on the look means no cloth at all. A skirt-less look needs nothing.- An outfit can carry its own
cloth(same rules). A list, even an empty one, replaces the look's cloth while that outfit is worn; an outfit withoutclothuses the look's, or the detected skirt. Yuzuki gives Casual, Loungewear and Swimwear"cloth": []so only Dress uses the detected skirt. - Cloth on a 2.5D look is refused.
- An outfit change shows at once; the covered skin hides a moment later (the stage waits at most a quarter of a second for it, and remembers the result on that device).
Background
background is a picture that sits behind her on the Companion tab, blurred more the closer the framing (Face most,
Full body least), so she stays the focus. It belongs to her stage, not the page, and it is not drawn in previews. Use a
soft, out-of-focus room picture (1920×1080, a JPEG under 1 MB is plenty). It keeps its proportions: on a stage of another
shape it is scaled to cover and cropped at the edges, never stretched. It works for 2.5D looks too.
Each motion:
| Field | Rules |
|---|---|
id |
required, unique in the look. file:<path> — a clip file in this model's folder (.vrma, .glb or .fbx, ≤ 32 MB); or avatar:<name> — an animation embedded in the avatar file (for a 2.5D look, the .motion3.json path inside the zip). library: ids are refused |
roles |
required, 1–2 distinct roles — see below |
name, description |
required — name 1–60 characters, description 1–300. The description is what the gesture picker reads, so describe the motion and its mood |
rig |
a bone map for a body clip in a .glb without VRM humanoid data, or in an .fbx |
cues |
up to 10 tone words that trigger this motion (see below) |
strokeSeconds |
0–30 — when in the clip the "beat" lands, so it can be timed to a word |
isFullBody |
optional boolean |
canPlayWhileSpeaking |
optional boolean, default true — false pauses her voice at a requested move's tag while the motion plays, then resumes; automatic gestures never pick it while she speaks |
isBodyAnimated and partBones are worked out from the clip itself — writing them refuses the publish.
Roles:
| Role | Plays |
|---|---|
idle |
standing still, the default loop |
talkIdle |
the loop while speaking |
listen |
while the user is talking |
think |
while the agent is working |
gesture |
a movement chosen per sentence while speaking |
reaction |
an emotional beat, e.g. a laugh on a [laughing] line |
partsIdle |
an always-on loop for extra body parts (tail, ears, wings) |
partsGesture |
an extra-parts movement chosen while speaking (a tail wag on a happy line) |
walk |
an in-place walking loop the stage plays on the legs while she paces (3D only, see Pacing); never chosen by the agent |
postureShift |
a shift of weight and a small turn, either side — the clip must end in the shifted pose; she holds it, then eases back. Used every 20–60 s, while she thinks and between sentences |
leanIn |
a lean or turn toward the viewer that ends in that pose — used while the user talks to her |
Posture roles are optional (a look with none just does not shift), never chosen by the agent, and a look's own posture
clips replace the standard set's. walk is optional too: without it she does not pace.
An extra-parts role needs a .glb clip (or an avatar animation) that moves at least one named bone the avatar has.
A body clip on a humanoid avatar must map humanoid bones: a .vrma always does; a .glb or .fbx needs a rig.
Cues tie motions to the tone of what the agent says. When the agent speaks aloud it can mark lines with stage
directions such as [happy] or [laughing]; a motion whose cues include that word becomes the natural pick.
Allowed words are the emotions (happy, sad, excited, calm, curious, surprised, proud, grateful,
confused, … — about fifty), the sound effects (laughing, chuckling, sighing, gasping, crying,
yawning, …) and the stage actions waving (her greeting), stretching, yawning, humming and looking-around — a motion cued with one of the last four is played when she is idle for a long while, so a look can bring its own idle habits. An unknown cue refuses the publish, naming it.
2.5D looks (Live2D)
Set avatar to a .zip of a Live2D Cubism 5 model: one .model3.json, its .moc3, textures, motions, expressions and
physics. Publish opens the archive and refuses a missing file, a hostile path or a broken .moc3. A 2.5D look is a painted
character whose layers bend and slide, and it can only play its own motions — so list them as avatar:<path>.motion3.json
(the path inside the zip, leaving out the model's own folder if you zipped the folder) with roles idle, talkIdle, listen, think, gesture, reaction, postureShift and leanIn. Extra-parts roles, walk and
rig do not apply, and a 2.5D look with cloth, pacing or a framing camera is refused. The face map names the model's own parameters (ParamMouthOpenY, ParamEyeLSmile, ParamCheek, …);
anything it leaves out uses the standard Cubism parameters. Working example: infrastructure/plugins/hello-live2d.
A 2.5D look takes a background like a 3D one, but no outfits and no items: the artwork is one painted piece.
What the 2.5D look should supply, and what happens when it doesn't:
- A head hit area. Name a hit area in the
.model3.jsonwith "head" or "face" in its name. The stage uses its drawable to find her head for framing, clicks and the backdrop shift. Without one, the stage measures the head when the model loads: the parts that move at least 60% as far as her eyes do whenParamAngleXturns. A model withoutParamAngleXgets a guessed head (the top 13% of the canvas) and no backdrop shift. Declaredframingwindows replace the guess for the buttons. ParamAngleXandParamAngleY. They carry the head turn, nods and the shift of the picture against the backdrop.- The look is drawn on its own layer and lit with the backdrop's tint, rim light, soft shadow and glow; the artist's colours are not changed, and there are no per-model settings for it.
- A model must fit the budget the stage keeps for it: on a desktop 448 MB and four 4096-pixel textures, on a phone 192 MB and two.
Face map: smiling eyes and closed lips
Besides emotions, mouth shapes, blink, smile and brows, a face map can drive:
eyeSmile(leftandrightlists, likesmile) — the squint of a smile. It follows her happiness at full strength, even while she speaks, so her eyes keep smiling as she talks; thesmilemouth dial is capped while she speaks so it doesn't fight the lip-sync.- Two more
visemes:pp(closed lips, for "p", "b", "m") andss(teeth together, for "s", "z"), next toaa,ih,ou,eeandoh.
When the face map has any mouth shape, the VRM's own aa…oh expressions are not driven at the same time, so the two never fight. Auto-fill in the look editor suggests these names, and a pack author can start from them:
| Model | Names |
|---|---|
| ARKit blendshapes (most avatar tools) | eyeSmile = cheekSquintLeft/Right + eyeSquintLeft/Right; pp = mouthClose 1, mouthPressLeft/Right 0.5; ss = mouthStretchLeft/Right 0.6, jawOpen 0.1 |
VRoid (Fcl_*) |
eyeSmile = Fcl_EYE_Joy_L/R; visemes Fcl_MTH_A/I/U/E/O; pp = Fcl_MTH_Close; blink Fcl_EYE_Close_L/R; smile = Fcl_MTH_Fun; browRaise = Fcl_BRW_Surprised; browLower = Fcl_BRW_Angry |
| Oculus / Ready Player Me | viseme_aa/I/U/E/O, viseme_PP, viseme_SS |
Calm and lively talk loops
A talkIdle motion can carry emotion cues ("cues": ["calm"], ["happy"]). A reply that is mostly calm (or sad) prefers a
talkIdle cued with a calm word; a happy, excited reply prefers one cued with a happy word. A look with only one
talkIdle just plays it. Gestures also run a little faster and bigger in a lively reply.
Looks are followed live. A companion wearing your look points at it — nothing is copied. When a user's installation moves to a new version of your plugin, every companion wearing the look moves the new way, and open Companion tabs update. If a version drops a look, companions wearing it show "Model missing" with one-click replacements until a version brings it back. If the user uninstalls your plugin, they do too.
Item — item.json
An item is a small prop model with an instruction for where it sits. Any 3D companion with a humanoid look can wear any installed item, whatever pack its look came from.
{
"name": "Mug",
"description": "A warm mug of coffee.",
"model": "mug.glb",
"thumbnail": "thumbnail.png",
"attach": { "bone": "rightHand", "position": [-0.03, 0, 0.03], "rotation": [0, 0, -90], "scale": 1 }
}
| Field | Rules |
|---|---|
name |
required, 1–100 — also what [hold:…] calls it (see below) |
description |
optional, ≤ 300 — what the agent reads about the item |
model |
required, a .glb in the item's folder, ≤ 3 MB, with at least one mesh. Every texture inside it must be ≤ 2048 px on its longest side, embedded (no external URIs), and PNG, JPEG or WebP (no KTX2) |
thumbnail |
optional PNG, JPEG or WebP, ≤ 2 MB — shown in the wardrobe menu |
attach.bone |
required — one of the 15 humanoid bones: hips, spine, head, leftUpperArm, leftLowerArm, leftHand, rightUpperArm, rightLowerArm, rightHand, leftUpperLeg, leftLowerLeg, leftFoot, rightUpperLeg, rightLowerLeg, rightFoot |
attach.position |
3 numbers in metres, each within -2 to 2 — the item's origin, in the bone's own frame, for the reference body |
attach.rotation |
3 numbers in degrees (X, Y, Z order), each within -360 to 360 |
attach.scale |
optional, 0.01–100, default 1 |
The reference body is AvatarSample_A (the VRoid Studio sample, freely downloadable from VRoid Hub). Author the
position,rotationandscaleagainst it (the bone frames are the VRM's normalized humanoid, arms in a T-pose, model facing +Z, so her right side is -X). The browser scales the item to each body by the length of the body part it sits on:scale × (her segment ÷ the reference segment), and the position by the same factor. The reference segments, in metres:Bone Segment Length headhead height 0.240 spinespine to neck 0.330 hipship width 0.308 leftUpperArm,rightUpperArmupper arm 0.240 leftLowerArm,rightLowerArmforearm 0.234 leftHand,rightHandwrist to middle finger base 0.057 leftUpperLeg,rightUpperLegthigh 0.364 leftLowerLeg,rightLowerLegshin 0.406 leftFoot,rightFootankle to toes 0.111 Items are rigid props. They move with the bone; there is no skinning and no animation inside an item.
An item in a hand makes that hand curl around it.
One item per body part. Wearing a second item on the same bone replaces the first.
Worn budgets, enforced together (a save or a
[hold:…]that would break one is refused): at most 8 items at a time, 12 MB of item models in total, and 16 million texture pixels in total.Items are not drawn on 2.5D looks.
Character — character.json
{
"name": "Sample Seed",
"vibe": "A test character with a plugin look that brings its own motions.",
"look": "sample-seed",
"soul": "You are Sample Seed, a cheerful test companion. You keep answers short and friendly.",
"instructions": "You talk with the person out loud. Keep replies to a sentence or two and avoid lists.",
"voice": {
"description": "A bright, friendly young voice.",
"suggestions": [
{ "provider": "fish-audio", "voiceId": "8ef4a238714b45718ce04243307c57a7", "voiceName": "Sample voice" },
{ "provider": "elevenlabs", "voiceId": "21m00Tcm4TlvDq8ikWAM", "voiceName": "Rachel", "models": ["eleven_v3"] }
]
}
}
A character appears in the Companions gallery as an equal of the built-in ones, tagged with your plugin's name. Talk creates a new agent from it; Talk as… puts the face on an existing agent (optionally adopting the suggested soul). Each character becomes at most one companion per account.
The file is read strictly: an unknown field (a typo like "sugestions") refuses the publish. The old top-level
model and motions are gone — a character names its one look, and a look's motions live in its model.json.
| Field | Rules |
|---|---|
name |
required, 1–100 |
vibe |
required, 1–120 — the one line on the gallery card |
soul |
required, 1–8,000 — the suggested persona for the agent |
instructions |
required, 1–8,000 — the suggested operating instructions |
look |
required — a model key from this package ("look": "<model key>") or { "builtIn": "<built-in model key>" }. A character has exactly one look |
fillers |
optional, ≤ 20 short lines she says while she thinks or is amused: { "key": "hmm", "text": "Hmm.", "purpose": "thinking" } — keep thinking lines to a sound ("Hmm.", "Mm."): she says them before she knows the answer, so words like "interesting" give the game away — key is a-z0-9- (1–40, unique), text 1–120, purpose is thinking or amused. A companion's own lines win; then these; then the built-in lines. They update live with your plugin |
behaviour |
optional, data only: temperament (energy, eyeContact, gestureRate, blinkRate, expressiveness, each 0.5–1.5, default 1), behaviours (per engine behaviour { "enabled": false } or { "strength": 0.75 }, 0.5–1.5), wordReactions (phrases → smile/browRaise/glance/soften, {name} is her own name) and toolActivity (when → reading/glanceAside/aha). Tools are named by stable identity: builtin:<id>, tool:<capabilityKey> for a tool in this same package, agent.delegated, agent.reported. At most 30 rules, 20 entries per rule, phrases ≤ 40 characters. Default word lists are English, so add your own language's. An unknown key or reaction refuses the publish with its path. It updates live with your plugin. A top-level temperament is refused: it moved inside behaviour |
thumbnail |
optional PNG/JPEG/WebP ≤ 2 MB, inside the character's folder |
voice.description |
≤ 300 characters, shown to the user |
voice.suggestions |
≤ 8, one per provider: provider (elevenlabs, fish-audio, deepgram, openai-compatible, or a plugin provider's kindId), voiceId (1–128), optional voiceName (≤ 100) and models (≤ 8 model ids, each ≤ 200) |
How the voice is picked: the companion takes the first suggestion whose provider the user has set up. None
matching means the user's default voice; with no voice set up at all, captions only, with a notice offering to add a
provider. For ElevenLabs, fish.audio and Deepgram a voice id works on any of their models; for OpenAI-compatible and
plugin providers a voice id belongs to certain models, so list them in models, preferred first — the suggestion then
applies only when the user has one of them.
Soul and instructions are suggestions: the personality always belongs to the agent. The agent's soul also steers which gestures the companion picks.
How the agent changes her wardrobe
The agent changes what she wears by putting tags in what she says aloud, the same way it writes [happy]:
| Tag | Does |
|---|---|
[outfit:dress] |
puts on the outfit with that key |
[hold:mug] |
picks up the item, in whatever hand or place its attach.bone says (taking off any item on that bone) |
[hold:none] |
takes every item off |
- A tag happens where it is written:
"[outfit:swim] Ready for the beach! [hold:mug]"keeps the old outfit until the first word, switches, and adds the mug at the end. - The change is permanent. It is saved when the reply is saved, so a reload, another tab and the next reply all see it. The agent's briefing lists the outfits she has and the items installed, and what she wears now.
- Item tags are the item's
key(the folder name). When two installed item packs share a key, the tag iskey-pluginslug(for examplecoffee-cozy-props); the long form always works, so older history keeps matching. Tags are case-insensitive. A tag that names nothing she has, or that would break a limit, is ignored. - A plugin can write these tags too — see
assistantMessageOutbound— but thecompanionPerformancehook never sees them. - The picture follows the voice: the outfit and items change on screen as her voice reaches the tag. Replaying an old reply does not change her clothes again.
When the user changes her wardrobe
The agent is told when the user changes what she wears in the Companion tab. A run of clicks produces one platform notice, about 4 seconds after the last click, worded like "The user just changed what you are wearing in the Companion tab. Before: Casual, nothing extra. Now: Swimwear, with Mug (right hand)." — "Before" is what she wore when the run began, so clicking back to where she started produces no notice. The agent can react out loud if it fits the moment. Only changes made in the Companion tab are reported, never the agent's own tags.
Preparing a VRM
A VRM straight out of an authoring tool can be tens of megabytes. infrastructure/scripts/dev/companion-model-shrink.mjs
compresses any VRM for the web while keeping its humanoid bones, expressions, spring bones (hair and tail physics) and
MToon toon shading; it turns PNG textures into WebP and meshopt-compresses the geometry:
npm i --no-save @gltf-transform/core@4 @gltf-transform/extensions@4 @gltf-transform/functions@4 meshoptimizer@1 sharp
node infrastructure/scripts/dev/companion-model-shrink.mjs in.vrm out.vrm [maxTextureSize]
maxTextureSize (pixels, optional) caps the longest side of every texture; leave it out and no texture is resized (2048
takes an 88 MB model with 4K textures down to about 6 MB). The script refuses a model whose expressions use material-colour or
texture-transform binds, or whose first-person annotations or thumbnail point at meshes or images, and writes nothing in that case.
Bone maps (rig)
A body clip in an FBX file, or in a GLB without VRM humanoid data, needs a bone map from humanoid bone names to the file's node names:
"rig": {
"version": 1,
"bones": {
"hips": "mixamorigHips", "spine": "mixamorigSpine", "head": "mixamorigHead",
"leftUpperArm": "mixamorigLeftArm", "leftLowerArm": "mixamorigLeftForeArm", "leftHand": "mixamorigLeftHand",
"rightUpperArm": "mixamorigRightArm", "rightLowerArm": "mixamorigRightForeArm", "rightHand": "mixamorigRightHand",
"leftUpperLeg": "mixamorigLeftUpLeg", "leftLowerLeg": "mixamorigLeftLeg", "leftFoot": "mixamorigLeftFoot",
"rightUpperLeg": "mixamorigRightUpLeg", "rightLowerLeg": "mixamorigRightLeg", "rightFoot": "mixamorigRightFoot"
}
}
version must be 1; those 15 bones are required.
Write every bone name the way three.js spells it. Browsers load these files with three.js, which rewrites node
names: whitespace becomes _, and [ ] . : / are removed. Names in your JSON must already be in that form — the
publish refuses a raw name and tells you the correct spelling:
| In the source file | In your JSON |
|---|---|
Tail.001 |
Tail001 |
mixamorig:Hips |
mixamorigHips |
Ear L |
Ear_L |
A file where two nodes would end up with the same name after that rewrite is refused.
Limits
| Package | 256 MB |
| Avatar model (3D file or Live2D zip) | 100 MB |
| Clip file | 32 MB |
| Thumbnail, background, item thumbnail | 2 MB each (PNG, JPEG, WebP) |
| Outfits per look | 20 (≤ 50 parts and ≤ 64 shapes per outfit) |
| Item model | 3 MB .glb, textures ≤ 2048 px on the longest side |
| Worn at once | 8 items, 12 MB of item models, 16 million texture pixels, one item per body part |
| Motions per look | 100 (≤ 30 gestures, ≤ 10 extra-parts gestures) |
| Filler lines per character | 20 |
| Cues per motion | 10 |
| Framing window | bottom/top/centerX within -1 to 2, top at least 0.02 above bottom |
| Cloths per look (or outfit) | 4, each 2 to 32 chains |
| Pacing | speed 0.1 to 1 body heights per second, stepDistance 0.05 to 0.6 body heights |
| Camera | defaultYawDegrees and yawRange within -75 to 75, followSeconds 0.2 to 2 |
model.json once parsed |
64 KB |
| Motion name / description | 60 / 300 characters |