# Unreleased: say plain audio (the line player builds the line itself)

- `MinimojiLinePlayer.say(audioFile:words:transcript:mood:)` and
  `say(pcm:sampleRate:words:transcript:mood:)` (new) take your audio, plus
  any timed words or transcript you have. The player builds the line with
  `SpeechLine.build`, off the main actor, then plays it: lip-sync, mood cues
  and captions, with no line building in your app. Built files are cached per
  file, words, transcript and mood (the last `clipCacheSize`, 8).
  `prepare(audioFile:…) async throws` builds ahead of time, so the say that
  follows starts at once, and `speak(audioFile:…) async` waits for the end.
  `preparing` is true while a line is being built.
- A clip that can't be built (unreadable, empty, over 60 s) never plays and
  never hangs. The head stays as it is and the say ends `.stopped`; the new
  `lastFailure: LineFailure?` (cleared by the next say) and `onFailure`
  callback give the reason. `EndReason` keeps its two cases, so existing
  switches still compile; a future major may fold this into a `.failed` case.
- MinimojiMetal now depends on MinimojiLive (the builder; it links Accelerate
  and opens no microphone or audio session). Prebuilt lines
  (`say(_:audio:)`) play exactly as before. No bake change.
# Unreleased: line player polish (exact character posters, phrase captions, quiet moods)

- **Captions now show phrases by default** (`MinimojiCaptions`,
  `SpeechLine.captionWindow`): up to four words, the word being said lit,
  the first phrase before the voice and the last after it. Pass
  `style: .window` to keep 9.44's sliding window.
- `MinimojiCaptions(player:)` observes the player itself: a host no longer
  needs `@ObservedObject` for the words to move. The last phrase stays a
  second after the line ends (`MinimojiCaptions.hold`), and a new phrase
  pushes up.
- `MinimojiCaptions(maxLines:height:)`: under Reduce Motion the whole line
  fits a set slot, shrunk to fit (to half size) and then truncated.
- `MinimojiHeadPoster(character:)`: a curated character's still is placed
  exactly on a cold load, from measures the Kit keeps for every catalogue
  character (`HeadFit.characters`; D.A.S.H. under `.face` was about 10% small
  before). `placementHint:` places a still of your own framed your own way.
  `HeadFit` and `HeadCamera.fitted(to: HeadFit…)` are public;
  `HeadPosterPlacement.render(for:)` takes either.
- `SpeechLine.build(…mood:)` sets the opening face quietly (intensity
  `SpeechLine.setIntensity`, 0.3, under the quiet threshold), so no emote
  bursts at each line's start. Audio tags in the words or transcript
  (`[excited]`, `[curious]`…) leave the captions and become quiet face
  changes (`SpeechLine.tagMoods`); `[laughs]` plays the laugh. Lines built by
  the engine's `buildLine` / `line-from-words.mjs` match.
- No bake change.

# Unreleased: talking lines (say a line in one call, build one on the device)

- `SpeechLine` (MinimojiKit, new): the public line format of
  docs/speech-line-format.md (v1) — mouth frames, captions, optional timed
  `words`, and the cue track — `Codable`, validated as the engine's
  `validateLine` validates it. `sample(at:)` reads the mouth, loudness and
  caption at a time; `captionWindow(at:reduceMotion:)` the words a caption
  view shows (about seven words / 40 characters, newest opaque, older
  fading; the whole line under Reduce Motion). The website's timelines are
  lines.
- `SpeechLine.build(pcm:sampleRate:words:transcript:mood:)` and
  `build(audioFile:…)` (MinimojiLive, new): audio, plus timed words if you
  have them (a TTS provider's timestamps, on-device dictation), into a line
  on the device: no network. Mouth frames from the articulating analyser and
  `SpeechVoice` (as `mouthTrack`), pitch for natural talk, sentence
  captions, word captions, and the word rules' cues (a nod on "yes", a
  shake on "no", brows on "really", a laugh on "haha"). Held to the engine's
  `buildLine` on shared fixtures: equal on the Mac (the test allows 0.02
  per channel on 99% of frames). A 10 s line builds in about 0.2 s on an
  M-class Mac in a Debug build.
- `MinimojiLinePlayer` (MinimojiMetal, new): `say(_:audio:)` /
  `speak(_:audio:) async` / `stop()`. Plays from an `AVAudioPlayer` clock;
  the newest `say` wins (a generation token); a silent clock carries the
  mouth and captions on when there is no audio, the player can't play, or
  its clock stalls for 1.5 s, so a line always ends. The audio session is
  `.ambient` by default (mixes, respects the silent switch, never ducks);
  `audioSession: .leaveAlone` keeps yours. It is the head's performance
  source and publishes the line's `expression` and `cue`:
  `MinimojiMetalHeadView(document:linePlayer:)` takes all three.
- `MinimojiCaptions` (MinimojiMetal, new): the caption view over the same
  window; Reduce Motion shows the whole line; VoiceOver announces each line
  once.
- `MinimojiHeadPoster(retry:session:)`: a failed head load now tries again
  while the poster is on screen (2 s, doubling to 30 s; `.never` is the old
  behaviour), and `session: true` keeps the loaded document for the session
  so a head coming back on screen is live at once.
- `SpeechCue` is `Encodable` too. No bake change.

# Unreleased: breathing that follows speech and mood

- An awake head's breath is no longer one fixed sine (3.5 s, ±0.8%). Each
  breath is a cycle: an inhale shorter than the exhale, then a short pause,
  with length and depth wandering a little cycle to cycle. The face's mood
  sets it: joy, laugh and surprise quicker and shallower; sad and crying
  slow and deep with a sigh now and then (deep in, long out, the head
  dropping slightly); angry short and hard; thinking held at the top.
  Each head breathes on its own seeded stream, so a cast does not breathe
  in unison.
- In speech (`setMouth`) the breath follows the phrases. The head takes a
  breath in each gap (lifting a touch, lips parted a sliver while the jaw
  is shut) and spends it across the next phrase; a long phrase ends a
  little low. A host that knows its phrases ahead (a recorded timeline)
  calls `FaceLife.inhale(length:lead:)` before each one, and the inhale is
  sized to the phrase. Nothing moves with the syllables: the lift settles
  out as a phrase starts.
- A mounted body breathes with its head: `MinimojiBody`'s chest (and the
  shoulders, neck and head on it) rises with each breath, about the head's
  own stretch for a calm one (`BreathState.chestRise`, applied by
  `BodyRig.breathRise` after contact; the web body does the same).
- `FaceLife.breath` (a `BreathState`) exposes how full the head is;
  `breathSeed` pins the stream. `breath: false` still keeps
  the head still. Ported from the engine's `gbreath.js` and held to it by
  the face trace. No bake change.

# Unreleased: mouths that articulate

- `SpeechVoice` articulates by default (`articulate`, new; `false` is the
  mouth as it was, sample for sample): the lips MEET on `.shut`, closing at
  60/s and driven shut 40 ms past the shape, so a one-frame m, b or p reads
  closed for ~50 ms; a voiced `.rest` keeps the lips with the neighbouring
  shapes instead of pulling them back between two; and the opening above
  the floor follows the pace (syllable onsets over 1.5 s: ×1.25 at two a
  second, ×0.75 at six). The floor stays, so quiet speech still moves.
- `LiveVisemes(…, articulate:)` (new, on by default) names lip closures: a
  frame under .35 of the recent peak after a voice reads `.shut`, for at
  most 150 ms (a longer dip is a pause; its start reads shut too). It cannot tell lips from
  tongue: t, d and k close too. Where the host sets `playoutOffset`, shapes
  are read `LiveVisemes.mouthLead` (70 ms) less behind it, never ahead of
  what has arrived; with no offset nothing changes. The lead comes out of
  the margin the offset gave late buffers.
- Measured on the website's 122 recorded lines with provider letter timings
  (`packages/engine/dev/mouth-eval.mjs`): the lips meet on 65% of m/b/p
  letters, from 25%; vowels reading shut at their middle rise from 8% to
  14%. Cost on the live path (a test bounds it): 60 s of 48 kHz audio
  analysed in 0.20 s against 0.19 s, seven speech voices at 60 Hz in 19 ms
  a minute, unoptimised. The engine's `createSpeechVoice` /
  `createLiveVisemeTimeline` are the same, held by both parity traces. No
  bake change.

# Unreleased: the poster's still is framed from the picture's own box

- `MinimojiHeadPoster` / `HeadPosterPlacement.render(for:aspect:framing:)`:
  the service's picture is now sized and centred from the box it was drawn
  from, `HeadDocument.pictureBounds` (new, public): the meshes visible at
  rest — no note, no talk, not an effect plate — the same rule as the
  service's `/render` and `apps/web/lib/render-fit.mjs`. 9.37 and 9.38 used
  `framingBounds`, which keeps the hidden effect plates (the surprised bang
  plates, smoke, sparks, flies, tears, sweat): for a person 1.18–1.51× the
  picture's box, so the still drew 19–42% too big and sat high, then shrank
  at the handover. Measured on a dev avatar's own picture over its live head
  (square, `.face`): width 1.414× and top −0.21 of the view, now 0.998× and
  −0.004; `.bounds` 1.417× / −0.23, now 1.000× / −0.005. A species' reach
  back-off reads the same box (`HeadDocument.pictureReach`). The live head's
  framing (`HeadCamera`) is unchanged.
- With `placementDocument` (or once the head has loaded) the still lands
  exactly. The cold estimate (`renderEstimate`) is re-derived on the
  corrected box and now placed for a short-haired person: about ±10% in
  size and 0.01 of the view in centre (`.bounds` side 0.690, centre +0.037;
  `.face` 0.633, −0.016, in a square frame). Long hair under `.face` still
  needs the hint (the estimate is about a third too small).
- `HeadPosterTests` pins it from pixels: a committed fixture (a bake-9 dev
  avatar and its 256 px service picture) drawn offscreen and compared by
  opaque box with the placed still, within 3% in width and 0.02 in top,
  square and portrait, both framings; and re-derives the estimate. No
  engine, bake or service change.

# Unreleased: the poster's still lands under face framing

- `HeadPosterPlacement.renderEstimate(aspect:framing:)`: the estimate a
  `.render` still is placed from before its document is known now follows
  the poster's `framing`. 9.37 used the `.bounds` estimate for every head,
  so under `.face` the still sat about 0.1 of the view too low (and, in a
  portrait frame, 11% small) and jumped at the handover — what Bunch saw.
  Both estimates are now the median of the exact placement over the Kit's
  test heads: a typical person lands on the live head (under `.face`, 0%
  off in side and centre for a short cut, square, portrait or landscape,
  was +3%/−11% and ~0.09–0.10 of the view off-centre); `.bounds` now also
  carries the typical centre (was ~0.05 of the view off) and the width's
  rule a typical head is framed by (was 6% small in a square frame).
- `MinimojiHeadPoster(…, placementDocument:)`: the head's document from an
  earlier load, optional. The still is placed exactly from it until the
  loaded document arrives — for a head far from typical (a long cut under
  `.face`, the penguin, the ghost), which the estimate misses by up to a
  quarter. Covered by `HeadPosterTests`, which re-measures the medians. No
  engine, bake or service change.

# Unreleased: a head's own loader (`MinimojiHeadPoster`)

- `MinimojiHeadPoster` (`MinimojiMetal`): the website's head poster as a
  Kit view. A still (`.url` — `service.renderURL(for:size:)` or a bundled
  file — `.image`, or `.none`) comes in soft and breathes while your `load`
  closure fetches the head; when it is ready the still gives way (0.35 s) and
  the bursts' puff breathes out behind the head (0.42 s); on an error the
  sharp still stays; Reduce Motion is the soft still and a plain swap, and
  takes effect the moment it is turned on. Same timings as the website and
  `@minimoji/react`'s `MinimojiHeadPoster`.
- Feed-friendly: no Metal view until the head is ready, the still decoded off
  the main thread and cached by URL (`HeadPosterStills`), fetch and load
  cancelled on disappear, transparent. `fit: .render` places the service's
  picture where `MinimojiMetalHeadView` draws the head under the same
  `framing` (`HeadPosterPlacement`); `fit: .contain` for a still of your own,
  its shape kept (no fit stretches a picture). A still that changes while
  its picture loads never shows the old one. `head:` takes your own head
  view; `reload:` loads again.
- `HeadPosterModel` is the loader's pure state machine, covered by
  `HeadPosterTests` (phases, error, cancellation, Reduce Motion, placement,
  decode and cache). No engine, bake or service change.

# Unreleased: live pitch is opt-in and cheaper

- `LiveVisemes(sampleRate:playoutOffset:pitch:)`: pitch tracking is now OFF
  unless a host passes `pitch: true`. 9.33–9.35 ran it for every voice, which
  cost about 35× the lip-sync's own CPU in a Debug build (Bunch measured
  0.2% → 7.8% of a core over 60 s). With it off, `voice(at:)` answers a nil
  pitch and the cost is 9.31's.
- With it on, the tracker's dot products run through Accelerate and the
  speaker median is kept sorted incrementally: the same answers (the parity
  trace still matches to 1e-6), a fraction of the cost.

## Unreleased — natural talk

- Fed speech now glances aside to plan phrases, follows pitch with brows and
  chin, and gives rising/falling endings a small tilt/nod. Default-on after
  Selcuk's 2026-10-19 taste approval; `FaceLife.setNatural(false)` opts out.
- `MouthParams.pitch` and `SpeechVoice.sample(..., pitch:)` carry optional
  semitones relative to the speaker median. `LiveVisemes.voice(at:)` returns
  `(viseme, level, pitch)`; `now(at:)` keeps its 8.4 shape, so no host changes.
- Reduced motion, explicit gaze, silence/release and sung speech retain
  their ownership. JS/Swift face and live-audio traces cover the new path.
  Runtime-only: bake version stays 9.

# Unreleased: the speech cue track

`SpeechCueTrack` reads a recorded line's cue track (the website's speech
timelines: `{ at, mood, k? }`, `{ at, gesture }`, `{ at, brow, k }`) and
samples it from the host's clock: `cues(since:through:)` answers each cue once
as it is crossed, `held(at:)` the face to carry over a seek. The cues are
authored offline (word rules plus a per-sentence mood arc); the Kit plays
them with `FaceLife.play(_ cue:)` (mood → `setFace`, gesture → `perform`; a brow cue is carried but shows nothing yet). Old mood-only
timelines decode unchanged. Additive; no service deploy.

# Unreleased: the ghost

The service offers the `ghost` (family `ghost`, a `seed` deals another one):
`CharacterSelection(id: "ghost")` saves and bakes it, and the editor's kind
row shows it as Ghost with the `moon.stars.fill` symbol. `HeadCamera.fitted`
(`.bounds`) now backs the camera off when a part far out and toward the lens
would leave the frame (the ghost's flared hem); every head that already fit
frames exactly as before. The penguin's black beads lose the grey ring their
bevel caught.

# Unreleased: a head mounted with a mood starts in it

The first face a head is given (a status card mounted `sleeping`, a list of
friends, a saved avatar with a mood: the view's first `expression`) is now on
in full from its first frame, with no blend in from neutral and no emote.
Every later change still blends and plays its emote. A shock face's jolt
still fires on mount. A parked stage head reused for a new slot settles its
first face there too. Cues (`perform`, `cue:`) and bursts are unchanged. A
host driving a `FaceLife` itself should give it its starting face first
(`setFace("idle")` if it has no mood yet), or its first change will settle
instead of blending; `BodyController` now does. The same in the JS engine (`createGlossFace`).
Source-compatible; no service deploy.

# Unreleased: the penguin

The service offers a seventh species, the `penguin` (family `bird`, a `seed`
deals another one): `CharacterSelection(id: "penguin")` saves and bakes it,
and the editor's kind row shows it as Bird with the `bird.fill` symbol.

# Unreleased: emote bursts pop in, let go smoothly, and the thought bubble types

The stickers on a mood change (`MinimojiMetalHeadView`, `bursts: true`) now
pop in from 0.55 of their size with a little overshoot and ease down to 0.8
as they fade (falling drops keep their size). Clearing the mood (neutral or
idle) mid-burst no longer cuts every sticker in one frame: they fade and shrink over
`EmoteBurst.leaveDuration` (0.45 s) while the head's move eases to rest.
The thinking bubble's three dots now hop in turn, like a typing indicator
(`EmoteBurst.thinkingDots`, over the new dot-free `bursts/thought-empty.png`).
The same as the website. Source-compatible; no service deploy.

# Unreleased: species bakes get a longer timeout

A cold species bake takes the service 13-14 s, but every bake waited
`bakeTimeout` (10 s), so an app's first D.A.S.H. load failed with "The
request timed out". Species bakes now wait the new
`AvatarService.characterBakeTimeout` (45 s); people's bakes keep
`bakeTimeout` at 10 s. A host that raised `bakeTimeout` above 45 keeps the
larger wait for species too, so an existing `bakeTimeout = 45` workaround is
unchanged and can be dropped. The editor now retries a timed-out species
load once, not twice, so a stuck service fails after ~90 s, not ~135 s. Source-compatible; no service deploy.

# 9.26.0: pick a species in the editor

`MinimojiEditorView(…, offersSpecies: true)` puts a kind row first (when the
service lists species; an unreachable one shows a retry): Person,
D.A.S.H., Cat, Dog, then every other family `service.characters()` lists, so
a species the service adds appears without a Kit release. A species shows
its own panel: the family's variants (D.A.S.H.'s themes and Remix, the
aliens, the ginger and white cat), the kit axes from the catalogue (picking D.A.S.H.'s shell, LED, glass or screen turns its
theme off; unpinned axes are the species' own, dealt for a Remix), a Shuffle for the dealt ids, the moods, undo/redo, and the note
"Species are picked, not generated." Selfie, AI edit, hair and skin are not
shown. The output is the same `doc` binding: a species is
`{"character": {…}}` with `head` its bake, so `service.save(doc)` saves it
as one and `CharacterSelection(document: doc)` tells a host which it is.
`EditorPreviewState.canSave` accepts a species draft beside its own bake.
The switch is one history commit, so undo steps through it, and Person
restores the newest person in the history exactly; a family comes back on
its last pick. A species handed in (a saved `{"character": …}`, or a bundled
species head with an empty `doc`) opens on that species with its options set, with or without
the row, replacing the "editing isn't supported yet" notice. Off by default:
an editor without `offersSpecies` never offers a species, and existing
initializers and callbacks are unchanged.

# 9.25.0: species avatars

An app can now get D.A.S.H., the aliens and the animals as avatars. Additive
`CharacterSelection` (a catalogued `id`, optional `seed` and kit `pick`s),
`AvatarService.head(for: CharacterSelection)` (bake without saving; cached
under its own keys), `save(character:)`, `characters()` (the service's
catalogue from capabilities), `remember(_:for: CharacterSelection)` and
`HeadDocument.character`. A service that predates species throws the new
`AvatarService.SpeciesUnsupported` (its own type, so exhaustive switches over
`Fail` still compile), never a human head. `head(forKey:)`,
`headURL(forKey:)` and `renderURL(for:)` take a species key unchanged, and a
stored `{"character": …}` document passed to `head(for:)` / `save(_:)` routes
to the species. A species head's `id` is now its stored document's hash
instead of one shared value. The editor never stands a person in for a
species (see the species row above). Native tests cover every family
in FaceLife, Metal and RealityKit, the singing and speech feeds on
D.A.S.H., cat and dog, and D.A.S.H. under `.face` framing. Needs the matching
service deploy; no bake version change.

# 9.23.0: fixed native face surfaces

Native heads now respect the bake's existing `meta.faces` animation membership.
Fixed surfaces retain render nodes and stay at their authored transforms;
fragment composition preserves the membership of retained and replaced parts.
This keeps the monkey's cream face patch smooth during turns in Metal head/body
and RealityKit. Existing supported bakes need no regeneration for this fix.
The approved eye design and other animation behavior are unchanged.

# 9.22.0: D.A.S.H. screen effects

Dead, scared, furious and thinking add restrained, curved scanline/interlace/
glitch treatments inside the display. They use the existing face clock;
neutral restores the display and reduced motion hides the effect layer.
`screenEffects` and `setScreenEffectsEnabled` are additive APIs. Updated
D.A.S.H. bakes carry optional metadata for 20 inset strips; older bakes retain
their existing display. Effects require both the updated runtime and bake.
Metal head/body and RealityKit render the same state.

# 9.21.0: clearer monkey eyes

Generated monkey heads use dark-brown matte eyes with one smaller catchlight.
The species-specific material avoids the glare that washed out a pupil during
side glances. Other character recipes and animator behavior are unchanged.
Regenerate the monkey bake to receive the new eye finish.

# 9.20.0: D.A.S.H. emotion colors

The LED display now uses semantic colors for emotions, including red anger,
blue sadness, warm happiness and purple thinking. Neutral restores each theme's
exact authored colors. Additive `screenPalette` and material tinting work in
Metal head/body and RealityKit while preserving the existing `screenMood`
contract. Authored emissive strength is retained during decoding and tinting.

Versions 9.20.0–9.23.0 postdate demo TestFlight 1.0(15). These SDK releases do
not imply a new app upload or physical-device performance validation.

# 9.19.0: optional native bodies

Adds `MinimojiBody`: a generated capsule rig and clip pack, native
action/emote performer, GPU skinning, shared-pass head attachment,
`BodyController` and `MinimojiBodyView`. Existing head products and clocks
retain their behavior. The canonical JS grounding fix raises stretched-leg
hip tracks once and settles floor poses; both runtimes consume it.
The actual demo exposes saved-avatar movement in its Sandbox tab, alongside
Home and Me. Hidden tabs stop body drawing/ticking and preserve playback
settings; Me leaves more room above the tab bar. Duck uses a
dedicated planted hold while jump landing stays unchanged. Mood-aware gait
styles come from the shared JS tables and real clip phase.

The body bundle adds 609,410 bytes including KayKit notices. Link floor is
iOS 17/macOS 15; live view requires iOS 18/macOS 15. Published in 9.19.0;
physical iPhone performance remains unverified. See the README’s full-body
section for clock ownership, lifecycle, availability and limitations.

# 9.16.0: a deliberate head shake

`HeadGesture.headShake` (`head-shake` in JavaScript) plays two tapering yaw
turns over the existing expression, returning to rest in 1.85 seconds.
Use `life.perform(.headShake)` or `HeadCue(.gesture(.headShake), generation: n)`;
the web call is `life.perform('head-shake')`. This is an original authored
cue on the existing head transform. The 45 automatic mood performances,
their variant order and random selection are unchanged.

The shared expression-picker mapping now uses `head-shake` for `shake` on
faces without an existing override. Other words and mood-specific mappings
are unchanged. This takes effect in the service only after a coordinated
backend deployment. Release compatible SDK/web consumers first: older Swift
builds decode an unknown gesture ID as nil, and older web engines decline
the cue. They continue displaying the expression but omit the new gesture.
[MinimojiKit 9.16.0](https://github.com/hypersocialinc/MinimojiKit/tree/v9.16.0)
was published on 2026-10-16 from main `c1fa4d74`. The compatible web client
was deployed first, and the production backend mapping followed after the Kit
tag was verified.

# 9.12.0: a smaller package

`MinimojiEditor`'s 236 option thumbnails are now 256-colour palette PNGs:
1.5 MB for the set instead of 7.9 MB, the same 256 px, nothing visible at
the tile's 64 pt (they are flat renders of one grey mannequin). The whole
Kit with the editor is about 6 MB installed, from about 12; playing a head
alone was never affected (about 1.8 MB). Nothing for a host to do. The
README's "Footprint" section (new in 9.11.0, a documentation-only tag) has
the per-product table and how it was measured.

# 9.10.0: emote bursts, gesture cues that ride over the beat, and the drop through the performance snapshot

Published as three tags, one per merge: the drop is 9.8.0, the gesture cues
9.9.0, emote bursts 9.10.0. Emote bursts are new and on by default; the other two are additive changes to the performance contract, from the Fizz FM integration. What a host must do: see each.

## Emote bursts

`MinimojiMetalHeadView` now pops a sticker effect round the head when its
`expression` changes (hearts on happy, a rain cloud on sad, flames on angry,
one script for each of the fourteen moods) and moves the head with it, once,
for under 3.5 s, so a mood change reads in a 28 pt row. New: `bursts: Bool =
true` on the view; `HeadPlayer.sync(…, bursts:)` and `HeadPlayer.onBurst`
underneath; `HeadCamera.disc(for:aspect:framing:)`, where the skull lands in
the frame. No burst on the first expression, a repeat, `neutral`/`idle`,
under Reduce Motion or on a parked view. The package's `MinimojiMetal` target
now carries resources (13 sticker PNGs, about 500 KB).

What a host must do: nothing to get them. The stickers draw outside the
view's frame: a host that clips its heads (`.clipShape`, a masked cell)
clips them; pass `bursts: false` where a burst is unwanted (a picker that
previews moods, a host that draws its own reaction). See the README's
*Emote bursts*.

## Gesture cues that ride over the beat

A head nodding to a host's beat now plays a one-shot (a cue or an emote) on
top of the nod instead of pausing it. While a `performance:` source hands
over a `beat` (underneath, `FaceLife.setBeat` with a number since the last
`setBeat(nil)`), the nod is the host's: it keeps the beat through the
gesture and the two add. A free-running listener, with no host beat, is
unchanged: its nod still pauses under a one-shot and comes back in phase.

New: `emotes:` beside `performances:` on `MinimojiMetalHeadView`,
`MinimojiHeadView`, `.minimojiHead(…)` (the stage's slots), `HeadEntity`,
`MetalHead`, `HeadPlayer.sync` and `FaceLife` (`emotesEnabled`, a var). `emotes: false` stops
only the emote a new `expression` picks for itself; the face still changes,
a cue still plays, and a face change no longer cuts a playing cue short.
`performances: false`, and Reduce Motion, still freeze every one-shot, cues
included (a cue's end still comes on the next frame). The web twin takes
`emotes` on `createGlossFace`, `buildHead`, `useMinimoji` and
`MinimojiHead`. The parity trace scripts a cue over a host beat and a face
change with emotes off.

What a host must do: nothing, unless it passed `performances: false` to keep
an emote from pausing the beat (the old advice for a singing host). Such a
host can now drop it, pass `emotes: false`, and cue gestures over the beat;
or keep `performances: false` and keep the silence it has.

## The drop through the performance snapshot

A host playing the head through `AvatarPerformanceInput` can now fire the
listening **drop** (a nod up to 1.9× bigger and a burst of notes for 1.2 s)
the way it fires a blink: `AvatarPerformanceSnapshot(dropSequence:)` is an
event counter, default 0, and the head drops once each time the number
changes, however many frames repeat it. Like `blinkSequence` it is adopted,
not replayed, when a source activates or comes back from `isActive: false`
or a park, and it does nothing under Reduce Motion. It is `FaceLife.drop()`
underneath, so it lands only while the head is listening: pass
`listening:`, as for the nod. Additive: existing snapshots keep a
`dropSequence` of 0 and never drop. The web engine's snapshot takes `drop`
(`PerformanceSnapshot.drop` in `@minimoji/react`'s types), held to the Swift
driver by the regenerated performance parity trace.

# 9.5.1: documentation only

No code changes since 9.4.0. This tag carries the 9.2.0 and 9.3.0 release
notes that 9.4.0 shipped without, the README's section on the on-device
engine (`MinimojiEngine`), and `SKILL.md`, an integration skill an agent can
adopt the Kit from. The same three files are served as text at
minimoji.com/developers/kit/README.md, /developers/kit/RELEASE_NOTES.md and
/developers/skill.md.

`v9.5.0` exists on the same commit as `v9.4.0` and is identical to it: it was
tagged by mistake and is left in place so a resolved pin keeps working. A
`from: "9.4.0"` pin resolves to this tag.

# 9.4.0: head gestures on cue, and the picker's gesture

A host can now play one of a head's authored one-shot performances once, on
its own cue, without changing the face it wears. `HeadGesture` is a generated
enum over the 45 track ids (`.doubleTake`, `.sunnyNod`, `.bigDoubleLaugh`, …)
with `face`, `duration` and `name`; `HeadCue` is the value a SwiftUI view
takes: `MinimojiMetalHeadView(…, cue: HeadCue(.gesture(.doubleTake),
generation: n), onGestureEnd: { gesture, cancelled in … })`, the same on
`MinimojiHeadView` and `.minimojiHead(…)`. A cue fires once per distinct
value (bump `generation` to repeat one; nil clears); `.forMood("laugh")`
plays one of that mood's three, never the one it played last. Underneath,
`FaceLife.perform(_:)`, `perform(for:)` and `onPerformanceEnd` are public for
a host that owns a `FaceLife`. The face never changes: a cue touches the
performance slot and nothing else, so the head still reports `happy` after a
double take. A cue while a performance plays replaces it from rest and a
replaced cue ends `cancelled`; a face change picking its own cancels a cue
too. Every end comes from the frame tick, never from inside the view update
that replaced the cue. Under Reduce Motion nothing moves and the end still
comes on the next frame, so a chain advances. The web twin is the `cue` prop on `MinimojiHead`
and `life.perform` in the engine; the JS and Swift runtimes are held
together by the parity trace, which now scripts three cues.

`AvatarService.pickExpression(for:gesture: true)` asks the expression picker
one more question in the same call: the head gesture a friend would make
reading the text aloud, over a nine-word vocabulary (nod, double-take, tilt,
bob, laugh, shake, lean, glance, none). `ExpressionPick.gesture` is the
`HeadGesture` to cue beside the picked face (the word mapped to the track
that fits that face: happy + double-take is the tiny double take, laugh +
laugh the big double laugh), nil when none fits, nothing was picked, or the
service named a track this build does not know; `gestureWord` is the word.
Without the flag nothing changes: the request and the answer are as before.
The service half (`gesture: true` on `POST /expressions/pick`) is deployed
separately; a Kit on a deployment without it gets nil.

Measured once, before the deploy, on Bunch's 85 held-out authored rows
labelled with the words a reader accepts (two runs, 170 calls, none failed):
at the 0.5 threshold 95% of the words read are accepted, 68% of the rows
wanting a gesture get one, none of the rows wanting none get one, and 55% of
the picks come with a track. Authored rows, not traffic; the number to
re-derive when real texts arrive. The demo's Settings gains a developer row,
"Cue a gesture", that double-takes the head above it and the Me tab's.

(The tag `v9.5.0` names the same commit as `v9.4.0`: there is no 9.5.0 code,
and a pin that resolves to it gets exactly this release.)

# 9.3.0: a selfie is always a new face, and AI edit is inline

A photo request never carries the current document or a lock.
`EditorComposer.request` drops `current` (and any words left in the field)
whenever a photo is attached, so a photo from anywhere in the editor goes to
`POST /avatars/generate` with no document, no `lockedPaths` and no prompt,
which the service answers from its one-call photo rung. Words still send
`current` and the seed lock.

The camera is on the stage: with `imports: .kit(camera: true)` the card
turns into the viewfinder (front camera, mirrored, a dashed face oval, the
two hints, Library, shutter, flip, ✕) and the shutter is the send (1024 px
long edge, JPEG 0.85). The confirmation dialog and `UIImagePickerController`
are gone. While the face is read the stage **shuffles**: the old head winds
up with the whirl, cruises with three other heads traded in behind it, and
lands on the answer with the spring (`ShuffleWhirl`, held to `Whirl` in
`EditorShuffleWhirlTests`). The heads come from the host's `shuffleHeads:`,
else three starters built on the device, else the old head spins alone; ✕
during the read lets the request go. The result lands in the same frame:
**Before** (press to peek), "From your selfie", **Retake | Keep**. A no-face
`400` returns to the camera with the server's reason as the hint; a failed
render is retried once before the proposal is dropped. Without a camera (the
simulator, `.kit` without `camera: true`, no usage string) the button, Retake
and a no-face answer open the photo library instead, and the pick is the
send.

**AI edit is inline.** The parts panel slides away, the stage grows, three
chips derived from the document sit under it (never what the head already
wears), and a field docks on the keyboard with the camera as its leading
button and a grey ↑ until there is text. The head wears the hologram while
the edit is in flight, whirls into the answer, and **Undo | Keep** sit under
the stage; the sentence stays in the field and sending is locked until Keep
or Undo. Undo whirls back and keeps the composer open; ✕ closes it with no
change. Reduce Motion crossfades instead of whirling, in both flows. The
review sheet (Revise, paging, Accept) remains only for the parts panel's
"Remix this category · 3 looks".

What a host must do: for the stage camera, pass `imports: .kit(camera: true)`
and ship a nonempty `NSCameraUsageDescription`. Bind `proposalOpen:` (true
while the camera, the read or a new face owns the stage) and hide your own
Cancel/Save while it is true; the Kit's Keep, Undo, Retake and ✕ are the only
exits then. Optionally pass `shuffleHeads:` (the demo passes its cast). A
"replace avatar" screen passes `creatingAvatar: true` with `creationSource:
.selfie` (the stage camera on arrival) or `.description` (the composer with
the keyboard up), and `onReplacement` to persist the accepted document.

Unchanged: documents, bake version 9, the service contract, `.callback` and
`.none` imports, and the Kit's other `generate` callers. The server still
sends a photo *with* `current` down the edit ladder
(`convex/lib/generate.ts`), so a host that calls
`service.generate(photo:prompt:current:)` with a document by hand stays as
slow as before; the editor never does. Not verified when it shipped: the live
`AVCaptureSession` viewfinder on a phone, and how the shuffle feels at
60 fps.

# 9.2.0: the avatar engine runs on the phone

New product `MinimojiEngine`: `LocalBaker` runs the same JavaScript engine
the service bakes with (a generated bundle, `minimoji-engine.js`) on the
device, so the editor's picker taps and AI proposals build where the tap is
instead of waiting for `POST /avatars/bake` and a 2 MB download. A host opts
in by attaching a builder to the service it hands the editor,
`service.localBuilder = try baker.builder()` with one long-lived
`LocalBaker()` (the README's "On-device previews" has the full snippet).
`builder()` throws when the bundled engine's bake version does not match the
editor's catalogue, and starts loading the engine in the background so the
first tap does not pay for it. Keep one
baker for the service's lifetime so the bundle loads once. With a builder
attached every `service.head(for:)` builds on the device; colour taps paint
the displayed head at once; a structural tap highlights at once and keeps the
current head on screen until its replacement is ready, with no spinner or
hologram. Opening a category warms its valid options one at a time, and
overlapping taps share the build in flight. The in-memory head cache grows
from 24 to at most 48 decoded heads, only when building locally.

On iOS `.automatic` is a `WKWebView`; on macOS (where the tests run) it is
`JavaScriptCore`, and `.javaScriptCore` stays available explicitly. The
simulator flatters JavaScriptCore (p95 157 ms against WebKit's 180 ms)
because its JavaScriptCore has a JIT and a phone's does not: a third-party
app may not map executable memory, and only WebKit's own content process
keeps its JIT. Without a JIT the same build takes about 0.8 s (the macOS
proxy). Measured on the iPhone 17 Pro simulator, from the
`/avatars/generate` answer to the proposal ready (on 9.2's review sheet): **18 ms** for a head
already warmed, **515 ms** for a cold build, against **3.9 s** through the
server. WebKit's one-off load is about 0.56 s, which is why the builder
warms it on attach.

Parity: in Node the bundle's bytes equal the service's bake for all ten
fixtures. On a device the engine runs on the system's `Math.sin`/`cos`/`pow`,
so a real edit's head matches the server's in nodes, order, materials and
`extras` with the last digit of a few coordinates different (worst position
7.5·10⁻⁹, worst normal 2.7·10⁻⁵, inside the parity tolerances). Nothing keys
on the bytes across runtimes.

What a host must do: `import MinimojiEngine` and `MinimojiEditorUI` (where
`builder()` lives) and attach the builder as above.
Attach it only when the service's `GET /capabilities` `bakeVersion` equals
`LocalBaker.bakeVersion`, so a device never previews a head the service
would bake differently (Bunch's rule; the Kit does not enforce it, and the README's
snippet shows how). Known gap: a runtime failure
on the device is shown by the editor with a retry and is **not** sent to the
server; a timeout, a server fallback and a progress line are the open card
"On-device builds fall back to the server, and say they are waiting".

Unchanged: saving, a friend's head by key, CDN URLs (`headURL`, the `.glb`
and `.png` routes), and every host that attaches no builder. Not measured:
an iPhone's latency, heat and memory; simulator process memory after a full
Hair warm was 756,704 KiB for the app and 492,688 KiB for WebKit's content
process,
which are process figures, not the cache's.

# 9.1.0: mouth-only speech

Talking now defaults to `.mouth`: lips animate without sound arcs or speech
lines beside the face. Explicit `.arcs` and `.lines` remain available.
FaceLife, Metal and RealityKit view defaults agree; the corresponding web,
React and Expo defaults also use mouth-only. Existing hosts that pass an
explicit style keep that choice. The demo keeps saved choices and launch
flags; only its unset/invalid setting changes to mouth-only.

This is a runtime presentation default. Lip shapes, viseme timing, expressions,
head geometry, saved avatar documents and bake version 9 are unchanged.
No backend deployment or cache refresh is required. An app using an older Kit
can select `talkStyle: .mouth` immediately; inheriting the new default requires
adopting the new Kit.

# 9.0.0: polished Tiny Residents

Existing avatar documents now draw with the approved Tiny Residents face on
original head shapes, authored spiky/long/wavy hair, a smooth rounded beanie,
and shallow eyes driven by the existing gaze/blink contract. Normal `.4` bakes
use detail-aware geometry. Document fields and explicit colors are preserved.
The editor catalog, thumbnails, part fixtures and animation traces are regenerated.

The eye blink remains a graphic squash with a thin residual slit. Long hair
still uses the existing rigid native transform; Felix-type dark beard/lens
combinations remain the weakest expression contrast. Service bake version 9
was deployed before this Kit was published. PNG uploaders must send the
version that actually drew their image as `/renders/put?…&bakeVersion=9`.

The backend deployment changes uncached/new bakes for existing Kit 8 clients
as well. Pinning Kit does not pin server appearance. Cached bake-8 heads and
fresh bake-9 heads may coexist; no forced cache refresh is part of this release.
API defaults, the `.4` bake detail and native size/cadence options are unchanged.
At `.4`, the six review heads submit 26,792–33,364 neutral triangles,
**41–131% more than bake 8**. `.low` changes cadence, not geometry. Existing
Bunch requests mostly use `.4`, with `.25` lite below 34 pt in chat. No device
frame-time or thermal budget was measured. There is no server bake-8 pin in
this release; rollout followed review of that compatibility/cost tradeoff.
See [adoption evidence and rollout notes](https://github.com/hypersocialinc/minimoji/blob/main/docs/explorations/minimoji-residents-adoption/README.md)
for measured triangle costs and actual published-client render checks.

# 8.4.0: live visemes, a mouth shape guessed from a call's audio

Additive: a new product, `MinimojiLive`. No bake-version change, no document
change, nothing in `MinimojiKit` itself changes (it still imports no audio
framework), and the existing parity fixtures are untouched.

- `LiveVisemes(sampleRate:playoutOffset:)`: hand it the mono Float32 PCM a
  voice SDK already gives you for a remote track (`process(_:at:)`, on the
  SDK's delivery queue, with the host time the buffer arrived), and read a
  `Viseme?` and a loudness each frame (`now(at:)`) for the `SpeechVoice`
  you already feed with `viseme: nil`. One `LiveVisemes` per voice; any
  number of heads may read it, each with its own `SpeechVoice`.
- **Which mouth source to use** (the host's choice; the Kit cannot switch
  it, because the host supplies the audio). Live sound with no transcript
  yet, a call or a voice status while it is being recorded: `LiveVisemes`.
  A recording with timed words: keep the transcript's visemes, which follow
  the words and catch closed lips. A recorded clip with no transcript:
  `LiveVisemes` on the audio as it plays. No audio: nothing, or loudness
  only. One `LiveVisemes` per voice, one `SpeechVoice` per head.
- **What it is not.** Not lip-readable, and not speech recognition: it reads
  five frequency bands and the loudness by fixed rules. Against scripted
  clips it agreed with the letters on 27% of frames where chance was 25%
  (29% read at the delay that suits it best), and `.shut` (m/b/p) it
  hardly ever says. What it adds over loudness alone
  is variety: another mouth plate on about a quarter of frames through Opus
  at 48 kbit/s.
- **A quiet speaker still gets open shapes.** The rules read loudness, so
  the speaker's volume is evened out before them (a gain of a quarter to
  eight that settles on the speaker; silence leaves it alone and one loud
  bump moves it little). At half volume 87% of shaped frames keep their
  shape and `.open` is 10% of speaking frames (14% at full volume, none
  without the evening); at a quarter volume, 63% and 5%. `level` is the evened loudness.
- **How late the mouth is.** From a voice starting: the mouth is open 28 ms
  later at the median (within 45 ms nine times in ten), a shape is named
  after 48 ms (within 98), and the mouth plate is that shape's after 110 ms,
  the lips' easing in `SpeechVoice` included. Measured on clips, with
  on-time 10 ms buffers read at 60 Hz and no `playoutOffset`.
- **The host supplies the audio.** The Kit opens no microphone and no audio
  session and depends on no voice SDK. Nothing is recorded or sent: the
  analysis runs on the phone that plays the sound.
- **Late and bursty delivery is expected.** A frame's time comes from
  counting samples off an anchor, the least-delayed arrival of the last
  second, not from when its buffer landed, so buffers held back and delivered
  together read as on-time ones do. An arrival more than 0.25 s past its
  predicted time is a gap (DTX, a muted track): `now(at:)` answers
  `(nil, 0)` once the last frame is a third of a second old, which
  releases the mouth, and the clock re-anchors. A shorter hole is not a
  gap: the mouth keeps going and is on time again within a second.
- `playoutOffset` reads that many seconds behind the host clock, for a
  callback that runs ahead of the speaker. Unmeasured for any SDK: set it
  from a real call.
- `process` and `now(at:)` allocate nothing after `init`, in an unoptimised
  build too (a test counts every malloc on the delivering thread);
  `now(at:)` is a pure read under a lock held for a few loads, and
  `reset()` is safe from any thread.
- Held to the engine's `createLiveVisemeTimeline` by a recorded trace
  (`LiveVisemesParityTests`): 48 and 16 kHz, on time, late and in bursts,
  across a gap, behind a clock that jumps back, through a reset and reads
  1.5 s behind; every read answers the same shape, the level within 1e-6.
- Measured on synthetic voices and read clips through Opus (16 to
  48 kbit/s), never on a recorded call. Sample rate, buffer size and the
  bitrate a real party negotiates are assumptions until one is captured.

# 8.3.0: how eager the expression picker is

Additive API; no bake-version change, no document change, no parity fixture
regenerated. **The service's default changed**, for every caller including
8.2.0, once a deployment has the modes: a text that clearly has a mood but
is torn between faces is now picked. "i got so much homework" (dead 0.38, worried 0.34, no-mood 0.06)
used to come back `.unsure` with no face, and now comes back `dead`.

- `pickExpression(for:options:mode:)` takes an `ExpressionPick.Mode`:
  `.cautious` (the rule 8.2.0 shipped with: only a face the model is sure
  of), `.balanced` (the service's default: also a clear mood spread over
  more than one face) or `.eager` (a face whenever the text shows any feeling).
  `mode: nil`, the default, sends no mode, so the service's default applies
  and a host follows it when it is re-tuned. 8.2.0 calls compile unchanged.
- `ExpressionPick.mode` is the rule the service applied; nil from a
  deployment that predates the modes, which ignores the parameter and
  applies `.cautious`. `Capabilities.pickModes` lists the names (nil from
  such a deployment). `ExpressionPick.init` gains `mode:` with a default,
  and `==` compares it: a test that builds the expected pick without
  `mode:` no longer equals an answer that names one.
- The mode decides only whether `expression` is the `candidate` or nil;
  `candidate`, `confidence` and `probabilities` are the same in all three,
  and a text with no feeling in it is `.unclear` in every mode.
- Measured on 304 authored texts, three runs, through the dev deployment on
  2026-10-10 (no real status; not real-traffic accuracy). Picks that fit /
  texts with a mood that get a face / no-mood texts that get one:
  `.cautious` 98% / 91% / 4%; `.balanced` 98% / 93% / 4%; `.eager` 97% /
  94% / 6%. The new default is a narrow fix: 11 more picks in 912 calls, on
  four texts, none of them wrong. A complaint stated as a fact ("inbox is
  at 400") still reads as no mood in every mode.
- A host that wants yesterday's answers passes `mode: .cautious`.

# 8.2.0: the mood a line of text reads as

Additive API; no bake-version change, no document change, no parity fixture
regenerated: the picker answers a name the head already knows how to wear.

- `AvatarService.pickExpression(for:options:)` asks the service which
  held-still face a line of text reads as (a status, a voice-note
  transcript, a chat line) and answers an `ExpressionPick`: `expression`,
  the face to show, or nil to keep whatever the head wears; `reason`
  (`.picked`, `.unclear` for a text with no feeling in it, `.unsure` for a
  `candidate` under the service's threshold, `.refused`); `confidence`; and
  the `probabilities` per face, for a host that wants a stricter rule. One
  sub-second decision-model call behind `POST /expressions/pick`; a
  publishable key may make it.
- Made for a typing pause. An empty text answers no pick without a request;
  a text over 1000 characters sends its end; the request gives up after
  5 s; cancelling the calling task throws `CancellationError` (whatever the
  transport reported) and never delivers a stale pick. Any throw means
  "keep the mood": `.rejected` for a refused value, `.limited` with the
  wait for a rate limit or a spent day, `.service` or `.reply` for the
  service's other answers, the transport's own error when offline.
- `options` narrows the answer to the faces a host can show.
  `Capabilities.expressions` is the list the service picks from (nil from a
  deployment that predates the picker): the 14 moods, without `thinking`,
  which is a state a host drives.
- The host keeps the policy: when to ask, and that a mood the user chose is
  never replaced. `neutral` means relaxed; a text with no mood is nil, not
  neutral.
- The text goes to the model provider and is not kept, the same promise as
  a generation's photo. Accuracy is measured on authored English texts only
  so far; sarcasm is read literally.

# 8.1.0: a speaking voice

Additive API; no bake-version change, no document change. The look of a fed
mouth changes (the speaking look, emotional talking), so both parity fixtures
(`face-trace.json`, `performance-trace.json`) are regenerated.

- `Viseme` (`.rest / .open / .round / .wide / .shut`) and `SpeechVoice`: a
  plain lip-sync mouth for a recorded voice (a voice status, a voice note).
  Each frame the host hands over the shape its timed stream says is current
  and the loudness at that instant; `sample(dt:viseme:level:)` returns a
  `MouthParams` for `AvatarPerformanceSnapshot.mouth`, eased so a 20 Hz
  envelope with coarse word timings reads smooth and alike at 30, 60 and
  120 Hz. `energy` and `bliss` stay 0. A `nil` viseme follows the loudness
  alone; `nil` and quiet releases the mouth. The engine's `createSpeechVoice`
  / `VISEMES` are the source, held to the Swift by `performance-trace.json`.
- The host keeps the clock, the envelope and the letter-to-viseme stream;
  `talking: true` during playback is right (a fed mouth suppresses the
  random jaw and keeps the head's bob).
- The speaking look, for any continuous `mouth` (every host, every renderer,
  runtime only): one plate per shape, the wide `ee` plate reachable at last;
  teeth and tongue tooth-sized and hugging their lip instead of stretching
  with the jaw, teeth behind round lips, tongue behind wide ones, neither in
  a barely parted mouth; a chin dip per syllable (with the performances
  switch, so off under Reduce Motion); one blink at a phrase's end; the gaze
  held on the viewer while speaking. `face-trace.json` and
  `performance-trace.json` are regenerated for it. A sung mouth is as before
  except that its furniture no longer stretches under plain lip-sync either.
- Emotional talking (runtime only, both renderers, no new input): the mood
  stays visible while the head talks. The face reshapes the fed mouth's plate
  (talk-only keys on the expression table: `talkSX`, `talkSY`, `talkY`,
  `talkX`, `talkRoll`; the resting faces are untouched; the plate now also
  takes the face's own `mouthRoll`, thinking's half-smirk), biases the teeth and
  tongue (`talkTeeth`: a snarl bares the teeth and hides the tongue, a grin
  shows more, a sad lip covers them), and a syllable that hits lands a stress
  accent (a brow flick, a touch wider eyes, a small nod, sized by how far the
  jaw gets and in the mood's direction by `talkBrow`; 250 ms cooldown; behind
  the performances switch like the chin dip). The reshaping eases in with the jaw so nothing pops; a
  sung mouth keeps its look. `face-trace.json` grows a happy, a sad, an angry
  and a smug head driven by `setMouth`; both fixtures are regenerated.

Integration guide: README, "Speech: a recorded voice plays the mouth".

# Next minor: performance input

Additive; no bake-version change, no document change, existing callers
behave as before (the JS/Swift `face-trace.json` fixture is byte-identical).

- `AvatarPerformanceInput` / `AvatarPerformanceSnapshot`: a host that plays
  the head hands `MinimojiMetalHeadView(performance:isActive:)` (or
  `MinimojiHeadView` / `.minimojiHead(performance:)`) one main-actor object;
  the renderer samples it right before every tick for gaze, a held mouth,
  the host's absolute beat + transport generation, and a blink sequence.
- `MouthParams(energy:)` (default 0: unchanged lip-sync) makes a mouth *sung*: a
  bigger jaw, and brows, eyes and head react. `SingingVoice` builds one from a
  held note with an attack and a visible trill, an optional `vowel` (round "oo" to
  wide "ah") and, past two seconds on a note, `bliss`: the eyes close behind a blink.
- `Listening(notes: false)`: the nod without the floating notes, and without
  the wider listening camera frame.
- `FaceLife.setBeat(_:generation:)`, `blink()`, `isBlinking`;
  `AvatarPerformanceDriver` and `BeatCrossing` for hosts and custom renderers.
- `isActive: false` pauses the Metal view's render loop and releases host input.
- `MouthParams` and `Listening` are now `Sendable`.

Integration guide: README, "Performance input: a host plays the head".

# Earlier: Kit-owned editor flow

`MinimojiEditorView(..., imports: .kit)` includes AI edit in the avatar card.
Tap for a composer sheet with words, optional on-device dictation, and selfie
attachments; hold for a microphone-driven waveform and release to send voice.
The sheet uses explicit Send. Words edit the visible avatar; Start fresh
requests a new look. The Kit owns progress, cancellation,
reactions, bundled-head registration, pack lookup, warm-up, and re-anchoring.
Use `.kit(camera: true)` with a host camera usage string to enable capture.
`.callback` and `.none` support custom flows and editing-only hosts.
Voice needs host microphone and speech-recognition usage descriptions;
without them the mic is hidden.

Bunch integration upgrade: replace the `onImport` closure with
`imports: .kit`, delete the host picker, sheet, import state and session-id
reset, and retain host persistence. If shipping packs, pass `bundledHead`
and `bundledSourcesProvider`; use `documentID` when selecting another avatar.
The new `ImportSource.change` case may require updating exhaustive switches.

Publication: the existing main-branch Kit publish workflow assigns the next
bake-version minor after checks pass. Publishing is automatic after the source changes merge into main.
