Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01As1S7fpDqEDnZxcGbSSoUW
felt-animation
Owns Felt's animation and
behavior decision-making: reacting to conversation, ambient idling/
wandering, and lipsync. Runs entirely locally on the desktop - no cloud, no
GPU dependency. Also owns the felt-overlay (Godot) process itself and the
local TCP relay that drives it, a role that used to belong to felt-tray.
Full design background: ../docs/plans/gemini-task-felt-animation.md and
../docs/plans/gemini-task-felt-natural-animation.md (the follow-up pass
that added blink/expression layers, crossfade blend timing, and anti-repeat
clip selection).
Running
cp .env.example .env # fill in the real paths
uv run felt-animation
Exposes two endpoints on FELT_ANIMATION_PORT (default 8421) for
felt-tray to call:
POST /request_started- fired the moment a request goes to felt-core; starts a thinking reaction.POST /reply_ready(multipart:text,animation_tag,audio) - fired once felt-core's reply arrives; runs lipsync and the reply reaction, waiting for whatever's currently playing to finish first (seestate.py).
Requirements
- felt-core reachable at
FELT_CORE_URL(optional) for reaction classification (POST /v1/reaction-tag) when felt-core doesn't already supply ananimation_tag; without it the fallback tag is used. rhubarb.exe(vendored underrhubarb/, trimmed to its runtime files - see.gitignorefor what's deliberately excluded from the release zip).- A built
felt-overlayexport (FELT_OVERLAY_EXE) - seefelt-character-assets/felt-godot-overlay's own docs for the export command. Since WP-22 felt-animation itself keeps every overlay window click-through (clickthrough.py:WS_EX_LAYERED|WS_EX_TRANSPARENTon each "Engine" window of the overlay's PID, re-checked every 0.5 s) - no helper script next to the exe any more. The overlay has one window per monitor;geometry.pymaps virtual-desktop pixels to her world x across all of them (come here, shoo, roaming, the chest's home on DISPLAY2).
Outfits (WP-27)
outfits.json lists every VRM (id, res://characters/..., pool daily / themed / special). outfits.py picks what she wears: one daily-pool pick per day (from 05:00, never yesterday's), Oct 31 halloween, Dec 23-26 xmas/reindeer alternating, pyjama 01:00-05:00 or when sleepy, rare ambient swaps (85% daily / 15% themed), and a 1% bikini on a sunny weather answer. Scheduled changes use the walk-off ("proper"); ambient/bikini use a spin ("quick"). The overlay is sent {"outfit": {"id", "vrm", "fx"}} and answers outfit_applied / outfit_failed; the current outfit is re-sent on every overlay (re)connect. The WP-33 tee applies only while the default outfit is worn.
Config (.env): FELT_OUTFITS_PATH, FELT_OUTFIT_AMBIENT_CHANCE (per ambient tick, default 0.0015 = about 1-2 a day), FELT_OUTFIT_BIKINI_CHANCE (default 0.01). Debug: GET /outfit, POST /outfit {"id": "maid", "proper": false}.
Known content gaps
felt-overlay only plays a tag if it has clips loaded for it - there's no
fallback/broadening. As of 2026-09-21 the animation library has no
thinking, gesture/talking, idle/sitting, or transition/sit_to_stand
clips yet, so felt-animation degrades gracefully (falls back to idle,
logs a clear warning) until Mario adds them from Mixamo.
Current state (2026-09-21)
- Reliability fixes: the "thinking" reaction loop had a real race
condition (
state.py'sbegin_thinking()) where the stop flag could be cleared by a concurrent task before the loop even started, causing it to spin forever - fixed by clearing it synchronously before the loop task is created, not inside it. (The formerollama_health.pyOllama watchdog was removed in WP-36 D'' - reaction classification now goes through felt-core.) - Natural animation pass: crossfade blend duration is now tag-aware
(
TAG_BLEND_TIMESinfelt-character-assets/felt-godot-overlay/main.gd), idle posture (idlevsidle/sitting) is tracked and respected on auto-return instead of hardcoded to plainidle, and a procedural blink/expression layer runs on top of whatever clip is playing. - Lipsync was silently broken for most of a session before being fixed
2026-09-21 - the real Felt VRM's mesh only has blend shapes named
Fcl_MTH_A/I/U/E/O;aa/ih/ou/ee/oh(what Rhubarb and this service's cue mapping use) are only VRM-spec preset aliases for those, never real matchable blend shape names. The cache and the weight- application code used different key styles and could never agree, so nothing was ever actually applied. Fixed on the felt-overlay side with a canonical-key alias table (MOUTH_SHAPE_ALIASESin main.gd) - seefelt-character-assets/README.mdfor the full writeup, since the root cause and fix both live there, not in this service. - felt-animation and the rest of the Felt project now live at
E:\Felt(junctioned back intoF:\workspace\Projectsfor convenience) instead of loose inside the general workspace - see the top-levelREADME.mdatE:\Felt.