2026-09-21 17:50:30 +01:00

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 (see state.py).

Requirements

  • felt-core reachable at FELT_CORE_URL (optional) for reaction classification (POST /v1/reaction-tag) when felt-core doesn't already supply an animation_tag; without it the fallback tag is used.
  • rhubarb.exe (vendored under rhubarb/, trimmed to its runtime files - see .gitignore for what's deliberately excluded from the release zip).
  • A built felt-overlay export (FELT_OVERLAY_EXE) - see felt-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_TRANSPARENT on 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.py maps 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's begin_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 former ollama_health.py Ollama 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_TIMES in felt-character-assets/felt-godot-overlay/main.gd), idle posture (idle vs idle/sitting) is tracked and respected on auto-return instead of hardcoded to plain idle, 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_ALIASES in main.gd) - see felt-character-assets/README.md for 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 into F:\workspace\Projects for convenience) instead of loose inside the general workspace - see the top-level README.md at E:\Felt.
S
Description
Felt's behaviour engine: reacts to conversation, idles and wanders, drives lip-sync for the Godot avatar overlay (Python)
Readme
168 MiB
0 Stars 1 Watchers 0 Forks
Languages
Python 100%