Skip to content

fix(kaori): stop voice on leave + FS360 head/shoulder work + stylish variant - #7

Merged
wbaxterh merged 8 commits into
v2-rebuildfrom
fix/kaori-companion-voice-animation
Jul 14, 2026
Merged

fix(kaori): stop voice on leave + FS360 head/shoulder work + stylish variant#7
wbaxterh merged 8 commits into
v2-rebuildfrom
fix/kaori-companion-voice-animation

Conversation

@wbaxterh

Copy link
Copy Markdown
Owner

Overview

Fixes for the Kaori 3D companion: the voice no longer keeps talking after you leave the stage, plus FS360 animation work — head/shoulder detail on the existing trick, a synced stance/board finish, and a new "stylish" FS360 variant.

Voice — stop on leave ✅ (high confidence)

The voice kept talking after navigating away/back because expo-router keeps the stage screen mounted, so useKithVoice's unmount cleanup never ran. Added stop() to the hook (interrupt playback + tell the backend to stop generating) and fire it on the focused → blurred edge (also stops the mic if listening). Fires exactly once per blur.

FS360 — existing "clean" version (kept, only enhanced)

  • Head over the shoulder: new headRoll channel + stronger headLead through the air, so the spin isn't flat.
  • Shoulders lead, hips follow: stronger, decaying coil through the air — the shoulders come around first to spot the landing and the hips catch up.
  • Ends looking downhill, not straight at the camera (settle head turn; the idle riding stance gazes downhill too).

Stance / board finish sync

The board no longer lingers as a faint ghost after she's stood up — faster blend-out + a higher board visibility cutoff so the stance return and the board fade finish together.

New "stylish" FS360 (frontside-360-stylish) 🚧 first pass

  • Lifts the back leg as she spins (board angles tail-up), then ~¾ through pushes the back leg down and lifts the front leg (board angles the other way), landing tail-first then nose.
  • Added an asymmetric leg-lift capability + a board-tilt channel to the pose/board system (riderFundamentals + TrickBoard).
  • The demo auto-loop now alternates the clean and stylish FS360, so both show while she explains.

Technical notes

  • New RiderPose fields: headRoll, backLegLift, frontLegLift, boardTilt (all default 0; the existing mini-demos inherit the defaults).
  • TrickBoard now applies rotation.z = boardTilt.
  • resetRiderBones clears neck roll/pitch so a trick can't leave the head tilted in idle.

Testing

  • tsc --noEmit 0 errors · biome check . 0 errors.
  • This is animation — it must be tuned on a physical device. The stylish variant especially is a first pass: the board-tilt axis/sign, the leg-lift amounts, the head-roll direction, and the "downhill" look direction all need a look on-device and will likely need a nudge. The voice fix and the head/shoulder/coil changes are lower-risk.

How to see it

Ask Kaori to show a 360 (e.g. "show me a frontside 360"). She does the clean version first, then the auto-loop alternates clean ↔ stylish while she talks.

wbaxterh and others added 8 commits July 12, 2026 07:31
…variant

Voice:
- Silence Kaori the moment the stage loses focus. expo-router keeps the
  screen mounted, so the useKithVoice unmount cleanup never fired and her
  voice kept talking after you navigated away/back. Added a stop() to the
  hook (interrupt playback + tell the backend to stop) and fire it on the
  focused→blurred edge.

FS360 (existing "clean" version — kept, only enhanced):
- Head cranks over the leading shoulder through the air (new headRoll +
  stronger headLead), so the spin isn't flat.
- Shoulders lead the rotation to spot the landing and the hips catch up
  (stronger, decaying coil through the air).
- She ends looking DOWNHILL, not straight at the camera (settle headLead;
  the idle stance also gazes downhill now).

Stance/board sync:
- The board no longer lingers after she's stood up — faster blend-out and a
  higher visibility cutoff so the stance return and the board fade finish
  together.

New "stylish" FS360 (frontside-360-stylish) — FIRST PASS, needs on-device
tuning of amounts + the board-tilt axis/sign:
- Lifts the back leg as she spins (board angles tail-up), then ~3/4 through
  pushes the back leg down and lifts the front leg (board angles the other
  way), landing tail-first then nose.
- Adds asymmetric leg lift + a board-tilt channel to the pose/board system.
- The demo auto-loop now ALTERNATES the clean and stylish FS360 so both show.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stylish FS360 drove the board tilt on its own timeline, independent of
the leg lifts, so the deck drifted away from the feet. Instead, derive the
board's world transform from the actual raw foot bones each frame in
KaoriStage (after vrm.update): aim the board's long axis down the line
between the two feet and keep the deck facing up, so the bindings stay under
the soles and the board angle FOLLOWS the legs — lift the back leg, the tail
rises. The stylish pose now only moves the legs (board tilt removed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… stays

Head: Kaori now looks DOWNHILL toward the board nose (front foot) at the
start and end of the trick instead of square-across / off to the side —
DOWNHILL_LOOK 0.32 -> 0.6 plus a small DOWNHILL_CHIN, wired into both the
on-board idle stance and the FS360 settle so start and end match.

Smoothness: the FS360 head channel was hard if-branches with constant
values that SNAPPED (0.6->0 at trick start, 0->0.65 at air, 0.65->0.5 at
the spot boundary). Replaced with one continuous curve driven off the
smooth spin progress — no snaps from beginning to end. Final fade now holds
the downhill riding stance instead of snapping to a neutral REST pose.

Board: hold the stance/board up while ANY action is still running, not just
while the session is live. A trick that outlasts the spoken reply now
FINISHES with the board under her instead of spinning on after the board
faded (Kaori doing a boardless 360 while the reply tail played).

Arms: ship the coil-locked arm swing (pump/wrap/elbow-flex that loads on the
wind-up and whips with the shoulders) so they stop dangling at her sides.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three fixes + one new trick, from a multi-agent investigation:

1. Begin/end GLITCH: the demo drove torso/head/arms at full strength even at
   zero stance (only legs eased in), so the head snapped ~34° stepping on/off
   the board. Now the WHOLE demo pose eases in/out by the stance weight — the
   idle↔demo handoff is a smooth blend both ways.

2. Board VANISHES before the last move: two independent causes.
   (a) Board presence was welded to the exact `session` flag, which Kith flips
   false mid-reply on a ~500ms audio-drain grace during between-sentence pauses
   — dropping the board in the sub-second gap before the last 360. Added a
   holdT grace-hold (BOARD_HOLD_SECONDS 1.2) so the board stays pinned across
   any gap and only retracts a beat after she's truly done; startAction
   re-anchors it so a late cue keeps it continuously visible.
   (b) The stylish leg-lift could tilt the foot-to-foot vector near-vertical,
   collapsing the feet-lock basis → boardLocked=false → the fallback flung the
   deck to the world origin. Now the basis rebuilds off world-forward and the
   fallback reuses the last-good transform, so the board can't fly off.

3. BACKSIDE 360 (new): frontside + backside now share one spin360PoseAt(dir,
   spotStart) core (frontside stays byte-identical: dir 1, spot 0.72). Backside
   = dir -1, totalSpin -2π (CW from above), spots later (0.55, blind first
   half). A new pose `dir` channel flips the body wind, head look and arm wrap
   in the appliers while coil stays frontside-signed so the arm load→whip TIMING
   is preserved (the naive coil-flip plays the arms backwards — caught in
   verify). "backside 360 / bs 360 / back 3" selects it; plain "360" stays
   frontside; the auto-loop never drags a backside session to frontside.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…side)

The trick was only set when the user's text had BOTH a demo verb
(show/demo/do) AND named the trick; if the demo instead fired from Kaori's
reply ("watch this") with requestedTrick===null, the trick stayed at its
default frontside-360 — so a backside request rendered a frontside spin
(which is why backside "looked exactly the same as frontside").

Split trick IDENTIFICATION (detectTrickId, no verb gate — also runs on the
reply) from demo INTENT, broaden the intent verbs, and ALWAYS resolve the
trick: user's ask → what the reply names → current. Never fall through to a
stale/default frontside. Dev log prints the chosen trick to confirm on-device.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Head: was LEADING the spin (headLead = dir*0.68) so the gaze swept blindly
all the way around and stared backwards. Replaced with a spotting model — the
body sweeps a full dir*2π via rootYaw and the neck (its child) counter-rotates
to hold her gaze on the landing (DOWNHILL_LOOK − dir*2π*spin), capped at the
neck limit (~75°); she holds forward as long as anatomically possible, whips
FAST through the apex (a quick glance, not a slow blind sweep), and re-fixates
forward — measured from the end of the revolution — landing looking downhill.
Continuous end to end; seamless at spin 0/1. Knobs: NECK_CAP, HEAD_HOLD_FRAC,
HEAD_WHIP_END.

Arms: past the pop the coil decayed to ~0, so windup/whip died and the arms
hung dead at the sides for the whole rotation. Added a continuous air driver
(spinSwing = sin(π·spin): zero at both ends, peak mid-spin) that draws the arms
in, wraps the hands around the torso, folds the elbows, and reaches the lead
arm forward through the air, then opens for the landing. Coil-driven frontside
wind-up load/throw is byte-identical; the pose.dir wrap mirror is preserved;
still eased from rest by w. uz clamped so the arms never rotate past T overhead.

Designed + adversarially verified via workflow (world-yaw traced per spin
point both directions; continuity, neck-limit, frontside regression checked).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Flips rotate around a HORIZONTAL axis pivoting at the center of mass — a new
axis vs the 360s (Y-yaw at the feet). Added a separate `pitch` pose channel
(parallel to `spin`) so spins and flips never fight: a flip keeps spin=0 (no
yaw, no coil Y-wind, no arm Y-wrap) and drives pitch; a spin keeps pitch=0.

- riderFundamentals: RiderPose gains `pitch` (0 in REST_POSE / spin360PoseAt).
- trickAnimations: TrickTimeline gains optional signed `totalFlip`; flipPoseAt
  (deep vertical load → pop → tight knees-to-chest tuck → absorb) with a
  pitch-based head SPOT that reads the two apart (wildcat blind/late re-spot,
  tamedog spots early); wildcat totalFlip=-2π (backflip over the tail),
  tamedog=+2π (frontflip over the nose). driveDemo outputs rootPitch (gated by
  stanceEase, zeroed on retract).
- KaoriStage: whole-body transform is now a yaw*pitch quaternion pivoted at the
  CoM/hip (position = arcCoM − q·comLocal) so she flips around her hip on the
  jump arc, not her feet. Pure 360 (rootPitch=0) collapses byte-identical to the
  old rotation.y=rootYaw / position.y=rootY. Board-through-inversion fix:
  lockBoardToFeet now derives board-up from her body quaternion (rootQuat),
  published each frame, so the deck ROLLS with her and stays soles-down through
  the somersault instead of lying world-flat under an upside-down rider.
- [botId].tsx: "wildcat/backflip" → wildcat, "tamedog/frontflip" → tamedog
  (checked before the 360 branch); flip words cue a full run.

Designed + adversarially verified via workflow (CoM-pivot math, quaternion
order, board-inversion, backward-compat all checked). On-device knobs:
COM_LOCAL_Y (pivot height), the totalFlip signs, and _flipPitchAxis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e straps in

New SnowWorld component crossfades a full alpine scene IN as Kaori steps onto
her board and OUT as she steps off — driven by the SAME stance weight that
fades the board in, so "strapping in" transforms the whole stage in lockstep.

- Procedural + vertex-colored, PNG-free, no drei (honors the expo-gl rules):
  a gradient sky (inverted vertex-colored icosahedron, unlit/BackSide), a lit
  snow ground disc, a ring of 14 low-poly mountains (slate→white caps), 700
  THREE.Points falling snow animated in useFrame, FogExp2 for depth, and cool
  alpine key/fill lights. All opacities/fog/background/lights lerp off one 0→1
  weight (demo.current.stance) — zero React re-renders, alloc-free per frame,
  and nearly free in studio mode (snow animation gated on weight > 0.02).
- scene.fog would fog everything, so per the material rules Kaori's unlit
  MeshBasicMaterial, the brand ring, and the TrickBoard deck/base now set
  fog:false — only the intentional scene geometry (ground, mountains) recedes.
- The static <color> background seeds frame 0; SnowWorld owns it per frame,
  lerping studio #0b0e17 → snow #bcd3ea.

Designed + adversarially verified via workflow (expo-gl feasibility, fog-on-
Kaori, single background owner, perf all checked). On-device knobs: SNOW_COUNT,
fog density, mountain count, the studio↔snow color endpoints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wbaxterh
wbaxterh merged commit 217866f into v2-rebuild Jul 14, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant