# Companion packs

This is the full reference. To make your first companion, start with [Create a 3D companion](create-3d-companion.md) or [Create a 2D companion](create-2d-companion.md).

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](plugin-voice-and-speech.md). 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](example-sample-companion-pack.md))*

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.

```json
{
  "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](#bone-maps-rig). 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:

```json
"framing": {
  "defaultPreset": "fullBody",
  "face": { "bottom": 0.73, "top": 1.0 },
  "upperBody": { "bottom": 0.45, "top": 1.0, "centerX": 0.5 }
}
```

- `defaultPreset` (`face`, `upperBody` or `fullBody`) is where a new companion starts; without it, Upper body.
- `face`, `upperBody`, `fullBody` are windows, each optional. A preset without a window keeps the stage's guess: the stage
  measures her head (a 2.5D look: from its `Head`/`Face` hit area, else from the parts that move as far as her eyes when
  `ParamAngleX` turns, 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 `bottom` and `top`, as fractions of the model's height (0 = its bottom edge, 1 = its top edge), and an
  optional `centerX` (0 = left edge, 1 = right edge). Without `centerX` the 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 `bottom` to `top`, 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 around
  `centerX`. `centerX` moves the crop, it cannot widen it.
- Values may go a little past 0 and 1 (`"bottom": -0.04` leaves air under her feet), between -1 and 2, and `top` must be
  at least 0.02 above `bottom`. 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:

```json
"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.

```json
"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 `walk` clip 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 match `speed`; a clip it can't measure plays at 1x.
- **Without a `walk` motion she never paces**, whatever `pacing` says: `pacing` alone does nothing. A user with reduced motion on
  never sees her pace either.
- **Without a `pacing` block** the defaults above apply; she paces if the look has a `walk` motion.
- **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 a `walk` motion. 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 `pacing` is 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.)

```json
"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](#bone-maps-rig)): `Bottoms_L.002`
  becomes `Bottoms_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 `Body` mesh** split by material: list the clothing materials
  in `materials` instead. 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 `outfits` is 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:

```json
"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](#bone-maps-rig)); 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 without `cloth` uses 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.json` with "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 when `ParamAngleX` turns. A model without `ParamAngleX` gets a guessed head
  (the top 13% of the canvas) and no backdrop shift. Declared `framing` windows replace the guess for the buttons.
- **`ParamAngleX` and `ParamAngleY`.** 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` (`left` and `right` lists, like `smile`) — the squint of a smile. It follows her happiness at full strength, even while she speaks, so her eyes keep smiling as she talks; the `smile` mouth dial is capped while she speaks so it doesn't fight the lip-sync.
- Two more `visemes`: `pp` (closed lips, for "p", "b", "m") and `ss` (teeth together, for "s", "z"), next to `aa`, `ih`, `ou`, `ee` and `oh`.

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.

```json
{
  "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`, `rotation` and `scale` against 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 |
  | --- | --- | --- |
  | `head` | head height | 0.240 |
  | `spine` | spine to neck | 0.330 |
  | `hips` | hip width | 0.308 |
  | `leftUpperArm`, `rightUpperArm` | upper arm | 0.240 |
  | `leftLowerArm`, `rightLowerArm` | forearm | 0.234 |
  | `leftHand`, `rightHand` | wrist to middle finger base | 0.057 |
  | `leftUpperLeg`, `rightUpperLeg` | thigh | 0.364 |
  | `leftLowerLeg`, `rightLowerLeg` | shin | 0.406 |
  | `leftFoot`, `rightFoot` | ankle 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`

```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 is
  `key-pluginslug` (for example `coffee-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`](plugin-middleware.md) — but the `companionPerformance` hook 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:

```bash
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:

```json
"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 |
