The bottom-of-screen agent avatar (Hemp Henry) can now be grabbed and dragged around. While held it plays a new 'lifted' dangle clip and follows the pointer; on release it drops under gravity, keeps any throw momentum, and bounces off the walls and floor with damping before settling and resuming its wander. A quick press with no drag still counts as a tap (random emote). - AgentAvatar.tsx: pointer-capture drag + a gravity/bounce fall integrator; the wrapper now spans the full content area so it can be lifted, staying pointer-events-none except on the sprite itself. - New 'lifted' Hemp Henry clip (nanobanana + brain bgremove): pose added to gen-avatar-sprites.sh + avatar_pipeline.py (fps 4), strip + manifest entry installed. - install_sheets now merges into the published manifest instead of replacing it, so generating a single clip no longer clobbers the rest. - visibility.ts: new 'Pick up avatar' switch (avatarDrag) so the grab/drop behavior can be turned off. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
9.8 KiB
Bash
Executable File
141 lines
9.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gen-avatar-sprites.sh — generate the Hemp Henry avatar sprite sheets.
|
|
#
|
|
# Per clip: nanobanana renders a 2x2 grid (4 frames of the same character, from
|
|
# the reference image), then avatar_pipeline.py splits it, strips each frame's
|
|
# background via the brain `bgremove` model (EVOX2), aligns the frames and
|
|
# writes an archival 4x1 strip (512px cells) into the workdir. `install` then
|
|
# downscales the strips to 256px cells into frontend/public/avatars/hemp-henry/
|
|
# and writes the matching manifest.json.
|
|
#
|
|
# Usage:
|
|
# gen-avatar-sprites.sh [clip ...] # generate listed clips (default: all missing)
|
|
# gen-avatar-sprites.sh --force clip # regenerate even if the grid exists
|
|
# gen-avatar-sprites.sh install # downscale sheets + manifest into public/
|
|
#
|
|
# Needs on PATH: nanobanana (GEMINI_API_KEY via its env file), uv.
|
|
# BRAIN_API_KEY comes from the repo root .env.
|
|
|
|
set -euo pipefail
|
|
|
|
SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
REPO_ROOT="$(cd "$SVC_DIR/../.." && pwd)"
|
|
WORK="${AVATAR_WORKDIR:-$SVC_DIR/.ai/avatar-work}"
|
|
PUB="$SVC_DIR/frontend/public/avatars/hemp-henry"
|
|
REF="${AVATAR_REF:-$PUB/reference.jpg}"
|
|
MODEL="${AVATAR_MODEL:-gemini-3.1-flash-image}"
|
|
CELL_ARCHIVE=512
|
|
CELL_INSTALL=256
|
|
|
|
# BRAIN_API_KEY / BRAIN_URL for the pipeline. The .env is dotenv, not shell
|
|
# (raw passwords with shell metachars) — pull just the keys we need.
|
|
if [ -f "$REPO_ROOT/.env" ]; then
|
|
for key in BRAIN_API_KEY BRAIN_URL; do
|
|
val="$(grep -E "^${key}=" "$REPO_ROOT/.env" | tail -1 | cut -d= -f2- || true)"
|
|
if [ -n "$val" ]; then export "$key=$val"; fi
|
|
done
|
|
fi
|
|
|
|
mkdir -p "$WORK/grids"
|
|
VENV="$WORK/venv"
|
|
if [ ! -x "$VENV/bin/python" ]; then
|
|
uv venv -q "$VENV"
|
|
uv pip install -q --python "$VENV/bin/python" Pillow requests
|
|
fi
|
|
PY="$VENV/bin/python"
|
|
|
|
# --- clip -> the 4 cell poses -------------------------------------------------
|
|
declare -A POSES=(
|
|
[idle]="Cell 1 (top-left): standing relaxed, heavy-lidded chilled-out eyes, slight slouch. Cell 2 (top-right): same relaxed stance, eyes closed mid-blink. Cell 3 (bottom-left): same stance, one ear twitching up slightly. Cell 4 (bottom-right): back to the relaxed stand, tiny content smile."
|
|
[walk]="A walk cycle, three-quarter view facing slightly right, mid-stride in every cell. Cell 1 (top-left): right foot forward planted, left arm swung forward. Cell 2 (top-right): legs passing under him, bobbing up. Cell 3 (bottom-left): left foot forward planted, right arm swung forward. Cell 4 (bottom-right): legs passing again, ears bouncing."
|
|
[read]="He is reading an open book held in both paws. Cell 1 (top-left): book open, eyes scanning the page. Cell 2 (top-right): turning a page with one paw. Cell 3 (bottom-left): holding the book closer, peering at it intently. Cell 4 (bottom-right): lowering the book a little, thoughtful look."
|
|
[write]="He is writing with a big yellow pencil on a small notepad. Cell 1 (top-left): pencil raised, about to write. Cell 2 (top-right): scribbling on the pad, focused. Cell 3 (bottom-left): still scribbling, tongue sticking out in concentration. Cell 4 (bottom-right): holding the pad up, admiring his work."
|
|
[bash]="He is typing on a small dark laptop that sits on a stool in front of him. Cell 1 (top-left): paws hovering over the keyboard. Cell 2 (top-right): typing fast, leaning in. Cell 3 (bottom-left): hammering the keys, ears back, focused. Cell 4 (bottom-right): leaning back, one paw on the keyboard, satisfied."
|
|
# NB: rocket-launch / throwing phrasings get PROHIBITED_CONTENT from Gemini
|
|
# with this reference image — the flag-hoisting metaphor passes.
|
|
[deploy]="He hoists a small green flag up a tiny flagpole beside him, celebrating a job going live. Cell 1 (top-left): gripping the flagpole rope, looking up. Cell 2 (top-right): pulling the rope down, the little green flag halfway up. Cell 3 (bottom-left): the flag reaching the top of the pole, ears perked. Cell 4 (bottom-right): saluting the raised flag with a proud grin."
|
|
[commit]="He is stamping a document with a big rubber stamp. Cell 1 (top-left): holding a sheet of paper and a rubber stamp, stamp raised. Cell 2 (top-right): slamming the stamp down onto the paper. Cell 3 (bottom-left): lifting the stamp, a bold green check mark now on the paper. Cell 4 (bottom-right): holding the stamped paper up, thumbs up."
|
|
[notify]="He is ringing a golden hand bell. Cell 1 (top-left): holding the bell up, about to ring. Cell 2 (top-right): swinging the bell to the left, small motion lines. Cell 3 (bottom-left): swinging the bell to the right, small motion lines. Cell 4 (bottom-right): holding the bell still, pleased smile."
|
|
[happy]="A joyful little bounce. Cell 1 (top-left): standing, big warm smile, eyes half-closed contentedly. Cell 2 (top-right): crouching down slightly, winding up, grinning. Cell 3 (bottom-left): mid-air hop, arms raised, ears flopping up, delighted. Cell 4 (bottom-right): just landed, relaxed smile, ears settling."
|
|
[investigating]="He is inspecting things with a large magnifying glass. Cell 1 (top-left): holding the magnifying glass up to his eye, looking left. Cell 2 (top-right): peering straight through the glass at the viewer, one eye comically enlarged by the lens. Cell 3 (bottom-left): sweeping the glass to the right, leaning. Cell 4 (bottom-right): lowering the glass, scratching his head, puzzled."
|
|
[celebrate]="A victory jump with confetti. Cell 1 (top-left): crouched, fists clenched, winding up with an excited grin. Cell 2 (top-right): leaping upward, arms flung out, a few confetti pieces appearing. Cell 3 (bottom-left): peak of the jump, star pose, colorful confetti bursting around him. Cell 4 (bottom-right): landed with both arms up in triumph, confetti falling."
|
|
[thinking]="He is pondering. Cell 1 (top-left): paw on chin, looking up, three small grey thought-dots floating above his head. Cell 2 (top-right): eyes closed, tapping his chin, the dots still floating. Cell 3 (bottom-left): frowning slightly in concentration, ears drooped. Cell 4 (bottom-right): looking up again, one ear raised, still pondering with the dots above."
|
|
[oops]="An embarrassed facepalm. Cell 1 (top-left): eyes wide, startled, a single sweat drop by his head. Cell 2 (top-right): wincing, raising a paw toward his face. Cell 3 (bottom-left): full facepalm, ears drooping flat. Cell 4 (bottom-right): peeking out sheepishly from behind his paw."
|
|
# Held up in the air by an invisible hand — feet dangling off the ground,
|
|
# surprised then wobbling. Plays while the user drag-lifts the avatar.
|
|
[lifted]="He has just been picked up off the ground and dangles in mid-air, both feet hanging down with nothing beneath them, arms out for balance. Cell 1 (top-left): yanked upward, eyes wide with surprise, arms flung out, legs kicking. Cell 2 (top-right): dangling and swaying gently to the left, ears flopping, a nervous little smile. Cell 3 (bottom-left): dangling and swaying to the right, legs bicycling in the air. Cell 4 (bottom-right): hanging relaxed now, arms limp, feet dangling, a resigned amused look."
|
|
)
|
|
ALL_CLIPS=(idle walk read write bash deploy commit notify happy investigating celebrate thinking oops lifted)
|
|
|
|
prompt_for() {
|
|
local clip="$1"
|
|
echo "A 2x2 sprite sheet: exactly 4 animation frames of the exact same character from the reference image (keep his rendering style, fur colors, face, proportions and outfit identical in every frame). Animation clip: '$clip'. ${POSES[$clip]} Full body visible in every cell, the character drawn at the same size in each cell, centered in each cell, plain flat solid white background, no grid lines, no borders, no text, no ground shadows."
|
|
}
|
|
|
|
install_sheets() {
|
|
mkdir -p "$PUB"
|
|
"$PY" - "$WORK" "$PUB" "$CELL_INSTALL" <<'EOF'
|
|
import json, sys
|
|
from pathlib import Path
|
|
from PIL import Image
|
|
|
|
work, pub, cell = Path(sys.argv[1]), Path(sys.argv[2]), int(sys.argv[3])
|
|
manifest = json.loads((work / "manifest.json").read_text())
|
|
# Merge into the already-published manifest so a partial run (one clip) only
|
|
# adds/updates its own entries instead of clobbering the rest of the set.
|
|
pub_manifest = pub / "manifest.json"
|
|
out = json.loads(pub_manifest.read_text()) if pub_manifest.exists() else {}
|
|
installed = 0
|
|
for clip, meta in sorted(manifest.items()):
|
|
src = work / "sheets" / f"{clip}.png"
|
|
if not src.exists():
|
|
print(f"skip {clip}: no sheet", file=sys.stderr)
|
|
continue
|
|
img = Image.open(src)
|
|
n = meta["frames"]
|
|
img = img.resize((cell * n, cell), Image.LANCZOS)
|
|
img.save(pub / f"{clip}.png", optimize=True)
|
|
out[clip] = {**meta, "cell": cell, "src": f"{clip}.png"}
|
|
installed += 1
|
|
pub_manifest.write_text(json.dumps(dict(sorted(out.items())), indent=2, sort_keys=True) + "\n")
|
|
print(f"installed {installed} sheets -> {pub}")
|
|
EOF
|
|
}
|
|
|
|
if [ "${1:-}" = "install" ]; then
|
|
install_sheets
|
|
exit 0
|
|
fi
|
|
|
|
FORCE=0
|
|
if [ "${1:-}" = "--force" ]; then FORCE=1; shift; fi
|
|
CLIPS=("${@:-}")
|
|
if [ -z "${CLIPS[0]:-}" ]; then CLIPS=("${ALL_CLIPS[@]}"); fi
|
|
|
|
[ -f "$REF" ] || { echo "reference image missing: $REF" >&2; exit 1; }
|
|
|
|
FAILED=()
|
|
for clip in "${CLIPS[@]}"; do
|
|
[ -n "${POSES[$clip]:-}" ] || { echo "unknown clip: $clip" >&2; exit 1; }
|
|
grid="$WORK/grids/$clip.png"
|
|
if [ ! -f "$grid" ] || [ "$FORCE" = 1 ]; then
|
|
echo "== $clip: generating grid ($MODEL)"
|
|
# One flaky generation (e.g. an empty Gemini response) must not kill the batch.
|
|
if ! nanobanana -m "$MODEL" -o "$grid" "$(prompt_for "$clip")" "$REF"; then
|
|
echo "!! $clip: generation failed, skipping" >&2
|
|
FAILED+=("$clip"); rm -f "$grid"; continue
|
|
fi
|
|
else
|
|
echo "== $clip: grid exists, skipping generation"
|
|
fi
|
|
if ! "$PY" "$SVC_DIR/scripts/avatar_pipeline.py" "$grid" "$clip" "$WORK" --cell "$CELL_ARCHIVE"; then
|
|
echo "!! $clip: pipeline failed, skipping" >&2
|
|
FAILED+=("$clip")
|
|
fi
|
|
done
|
|
if [ "${#FAILED[@]}" -gt 0 ]; then
|
|
echo "failed clips (rerun to retry): ${FAILED[*]}" >&2
|
|
fi
|
|
|
|
echo "done. run '$0 install' to publish into $PUB"
|