Brooklyn Nicholson 8c48be90ac feat(pets): generate a custom pet from a prompt (desktop)
Add an in-app pet generator: describe a creature, get four cheap
base-look drafts, pick one, then hatch the full six-state animated atlas
and preview every frame before adopting.

- backend: agent/pet/generate/ (deterministic atlas assembly ported from
  the hatch-pet skill, prompt builders, provider wrapper, orchestration)
- store.register_local_pet so generated pets install + adopt without a
  manifest entry
- gateway pet.generate (draft variants) + pet.hatch (build preview, not
  active); adopt via existing pet.select, discard via pet.remove
- OpenAI provider: reference-image edit path + opt-in transparent
  background; imagegen retries without the flag on models that reject it
- transparency hardened with a chroma-key cutout pass on base drafts
- desktop: Cmd+K "Generate a pet" page (draft grid, retry, animated
  post-hatch preview, adopt/start-over), egg icon, i18n
2026-06-16 23:11:24 -05:00

75 lines
3.4 KiB
Python

"""Prompt builders for pet generation.
Two prompt shapes: a *base* prompt (prompt-only, produces the canonical look the
user picks between) and per-*state* *row* prompts (grounded on the chosen base,
produce one horizontal strip of N poses). Prompts stay concise and
sprite-production oriented; the identity lock and "one transparent row" framing
matter more than flowery description.
Hermes drives six states (see :data:`agent.pet.generate.atlas.ROW_SPECS`); these
mirror that set rather than the petdex/Codex nine.
"""
from __future__ import annotations
# What each Hermes state should depict (kept short — these go straight into the
# row prompt). Phrased to avoid the common sprite-gen failure modes (detached
# effects, motion lines, shadows).
STATE_ACTIONS: dict[str, str] = {
"idle": "a calm idle loop: subtle breathing, a tiny blink or gentle bob, no big gestures",
"wave": "a friendly greeting: raising a paw/hand/limb to wave, clear up-and-down gesture",
"run": "focused active work: leaning in, concentrating, busy 'thinking/processing' energy (NOT foot-running)",
"failed": "a sad or deflated reaction: slumped, dejected, small frown — readable but not noisy",
"review": "careful inspection: a focused lean, head tilt, studying something intently",
"jump": "a happy celebration jump: anticipation, lift off the ground, peak, and land",
}
_STYLE_HINTS: dict[str, str] = {
"auto": "",
"pixel": " Render in clean pixel-art style.",
"plush": " Render as a soft plush toy.",
"clay": " Render as a claymation / soft 3D clay figure.",
"sticker": " Render as a glossy die-cut sticker.",
"flat-vector": " Render in flat vector mascot style.",
"3d-toy": " Render as a glossy 3D toy.",
"painterly": " Render in a soft painterly style.",
}
_BACKGROUND = (
"Center one full-body character on a fully transparent background. "
"No text, no labels, no shadow, no ground line, no scenery, no frame, no border."
)
def style_hint(style: str | None) -> str:
return _STYLE_HINTS.get((style or "auto").strip().lower(), "")
def build_base_prompt(concept: str, *, style: str | None = "auto") -> str:
"""The base look: a single, clean, centered full-body mascot."""
concept = (concept or "a cute friendly mascot creature").strip()
return (
f"A cute, characterful mascot pet: {concept}. "
"Compact, whole-body silhouette that reads clearly at small size, "
"appealing face, simple consistent palette. "
f"{_BACKGROUND}{style_hint(style)}"
)
def build_row_prompt(state: str, frame_count: int, concept: str, *, style: str | None = "auto") -> str:
"""A row strip: *frame_count* poses of the SAME character, left→right.
The attached base image is the identity source of truth; the prompt locks
species, palette, face, and props to it.
"""
action = STATE_ACTIONS.get(state, "a simple idle pose")
concept = (concept or "the mascot").strip()
return (
f"Using the attached reference image as the exact same character "
f"(same species, face, colors, markings, proportions, and props), "
f"draw a single horizontal strip of {frame_count} animation frames showing {action}. "
f"The {frame_count} poses must be evenly spaced left to right, each fully separated "
"(not overlapping), same size and baseline, forming a smooth loop. "
f"Keep the character identical across all frames. {_BACKGROUND}{style_hint(style)}"
)