Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions .claude/skills/pixellab-cli-animation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,16 +130,30 @@ nothing else to offer and nothing to refuse against.
pixellab-cli character animate <character_id> --action/-a --template --direction/-d --frames
--start-pose --end-pose --enhance
--drop-first-frame --terse --any-pose
--name --seed
--name --seed --again
```

**Every direction is a separate job and a separate charge**, at one generation per frame
per direction. It defaults to south alone. Frames times directions is the bill.

Three things are refused before anything is sent, and each has an escape for the
**Name every direction in one call where you can.** PixelLab starts a new animation for
each call and will not add a direction to one that exists — `animation_group_id` is
refused on the way in, and repeating the name or the template makes a second animation
rather than extending the first. So `-d south -d east -d north` is one animation over
three directions, and three calls are three animations of one motion.

That split is real on the account and not here: `character show` reads the groups of one
motion as one animation and says how many PixelLab holds, `inspect` gathers the runs the
same way, and an atlas built from that animation is built from every direction of it. So
coming back later to animate the directions you skipped is fine and is what `--again`
guards: a direction the motion already has is refused before anything is sent, because a
second take of one it holds is a copy nobody asked for. `--again` buys it anyway.

Four things are refused before anything is sent, and each has an escape for the
person who means it: a pose belonging to another character, a pose the record says was
made for a different motion (`--any-pose`), and an action that is a label rather than a
motion (`--terse`). None of them fires on something the record has not seen.
made for a different motion (`--any-pose`), an action that is a label rather than a
motion (`--terse`), and a direction this motion already holds (`--again`). None of them
fires on something the record has not seen.

`--start-pose` takes a state's id or a file and is the frame the motion starts on.
Without it the animation starts from the neutral rotation and has to invent the pose and
Expand Down
1 change: 1 addition & 0 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ findings.
- **object** — a PixelLab-managed prop with a stable `object_id`, generated in one direction or eight. Avoid: item sprite
- **rotation** — one directional view of a character or object. Avoid: facing direction
- **animation** — an ordered frame sequence belonging to one character or object and one direction. Avoid: anim, clip
- **animation group** — the directions PixelLab holds together as one animation of a character or object, identified by an `animation_group_id` that `GET /v2/characters/{character_id}` reports. One per call: a motion animated over two calls has two, and only an object's routes accept the id to extend one. Avoid: animation set
- **spritesheet** — one uniform-grid image holding a character's rotations and animation frames, plus the layout JSON that says which cell is which. Avoid: sprite atlas
- **tileset** — a set of seamlessly connecting terrain tiles generated in one PixelLab call. Avoid: terrain set
- **concept image** — a non-pixel-art image generated on fal, used as a reference for a PixelLab route or delivered as box art. Avoid: concept art
Expand Down
3 changes: 3 additions & 0 deletions docs/notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,6 @@ over this file answers with the example above as well as with the notes. -->
- n-0050 2026-09-20 #gotcha @docs/wiki/pages/character-consistency.md @src/pixellab_cli/skill/pixellab-cli-animation/SKILL.md — a state animated as itself carries one character already in the pose; naming the state with custom_start_frame instead makes the call hold a neutral character and another's frame, for the same price
- n-0051 2026-09-20 #gotcha @src/pixellab_cli/commands/character.py — the enrichment action must not name a direction: -d picks which rotation to read the frame from, and the text it returns is the -a of every direction's animation call, so a south in the wording describes the north animation as walking south
- n-0052 2026-09-20 #gotcha @src/pixellab_cli/harness.py — pointing Claude Code at a skill's SKILL.md gets a file read rather than the skill loaded, so the managed block names the skill and carries no path
- n-0053 2026-09-20 #gotcha @src/pixellab_cli/commands/character.py — characters/animations names a v3 animation custom- plus the action's first thirty characters, truncated mid-word, which is how a motion is matched to one the character already has
- n-0054 2026-09-20 #gotcha @src/pixellab_cli/commands/character.py — animation_group_id is extra_forbidden on characters/animations and only the site's unprefixed animate-with-text-v3/character/background takes it, which refuses the API key
- n-0055 2026-09-20 #ceiling @src/pixellab_cli/subject.py — a motion is matched to one the character already has by the name the call would carry, so a bare action on one call and --name on the next are two motions and neither the refusal nor the join reporting fires
31 changes: 31 additions & 0 deletions docs/wiki/pages/animation-frames.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,37 @@ alone, which is also the only thing that makes an eight-frame test call cheap.
eight frames that is two and a half to five times the price for one direction, which is
why v3 is both PixelLab's recommendation and this tool's default.

## One animation, or several

The directions of one motion are an **animation group**, and PixelLab makes a new one
for every call. `animation_group_id` is returned by `GET /v2/characters/{character_id}`
and accepted on the way in only by the object routes — `AnimateObjectRequest` documents
it as *"pass the animation_group_id of an existing animation on this object to add more
directions to it"*. `POST /v2/characters/animations` refuses it outright:

```json
{"type": "extra_forbidden", "loc": ["body", "animation_group_id"],
"msg": "Extra inputs are not permitted"}
```

Nor is there an implicit join to fall back on. Repeating `animation_name`, or the same
`template_animation_id`, makes a second group rather than extending the first — an
account checked for this held `walk`, `jump` and `run` twice over on one character, and
a `spell` over six directions beside a `spell 2` holding the two added later.

So one group over eight directions comes from **one call naming eight directions**, and
a posed animation can never be one at all: `custom_start_frame` is a single frame per
call and the pose differs per direction ([[pixellab-asset-routing]]).

`pixellab-cli` closes the split on this side rather than pretending it is closed.
`character animate` refuses a direction the motion already holds before spending
anything, `character show` reads the groups of one motion as one animation and says how
many PixelLab holds, and the subject record gathers the runs the same way, so the atlas
built from that animation carries every direction of it. PixelLab's own web app extends
a group over `POST /animate-with-text-v3/character/background`, unprefixed and taking a
singular `direction`; it answers `403 {"detail": "Invalid token"}` to an API key, which
is why nothing here calls it.

## Mirroring is not a route

There is no mirror or flip operation anywhere in REST v2. `mirror` does not occur in the
Expand Down
10 changes: 10 additions & 0 deletions specs/asset-workspace/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,13 @@ together, a table of prices copied from a pricing page can never be corrected.
Both injected. A run id derived from `datetime.now()` makes a test that asserts a
directory name unwritable, and this is a feature whose entire behaviour is about what
gets written where.

## An animation made over two calls is one animation

PixelLab starts a new animation for every call, so a character animated south today and
east tomorrow holds two animations of one motion — the reasoning is in
`specs/characters-and-animation/design.md`. The record does not repeat that split: runs
of one character are gathered by the motion they animate, which is the name the run
carried or the action it was made from, and the gathered animation holds every
direction they covered and every file they wrote. An atlas built from one animation is
then built from every direction of it, which is what `export atlas` is given.
7 changes: 4 additions & 3 deletions specs/asset-workspace/requirements.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
autonomy: auto
ci: wait
branch: feat/the-subject-manifest
delivery: merged
pr: 60
branch: feat/one-animation-many-directions
delivery: in-review
pr: 78
---

# Asset workspace — requirements
Expand Down Expand Up @@ -68,6 +68,7 @@ A file nobody can account for is worth less than the same file with a manifest.
- **R7.2** The asset workspace shall derive the subject's manifest from the run manifests every time it writes it, so that it cannot disagree with what was paid for.
- **R7.3** Where a call is made from an identifier an earlier call produced, the asset workspace shall record that identifier beside the request, because a request carrying a frame does not say which pose the frame came from.
- **R7.4** When asked to describe a subject, the asset workspace shall report it without calling a provider, and shall report the subjects it holds where none is named.
- **R7.5** (ADDED) The asset workspace shall gather the animation runs of one character made for one motion into a single animation holding every direction they cover and every file they wrote.

## Out of scope

Expand Down
5 changes: 5 additions & 0 deletions specs/asset-workspace/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,8 @@
- [x] 6.5 (Unit) Give each run of a kind its own version directory, numbering from the first free one and leaving loose files alone — R1.5, R1.10, R2.1
_Depends 6.1_
_Reason a second attempt at the same asset became a file called -2, which is a name rather than a history_

## 7 · One animation over many directions
- [x] 7.1 (Unit) Gather one character's animation runs for a motion into one — R7.5
_Reason the provider starts a new animation per call_

47 changes: 47 additions & 0 deletions specs/characters-and-animation/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,50 @@ The matcher underneath is word overlap between the state's `--edit` text and the
stores a character, not an idle. That is a real ceiling: a state described in words the
motion does not reuse reads as unrelated. It fails towards the refusal rather than away
from it, and `--any-pose` is the way past.

## One animation, many directions — and why the provider will not extend one

A character animation is one group with a direction each, and PixelLab returns its
`animation_group_id` from `GET /characters/{character_id}`. Extending that group is
what `AnimateObjectRequest` documents for objects — *"pass the animation_group_id of an
existing animation on this object to add more directions to it"* — and what
`CreateCharacterAnimationRequest` does not offer. Sent anyway, the route answers:

```json
{"type": "extra_forbidden", "loc": ["body", "animation_group_id"],
"msg": "Extra inputs are not permitted"}
```

The body is `extra="forbid"`, confirmed by sending an invented field alongside it and
reading the same error back. Nor does the provider join by name or by template: the
account this was checked against holds `walk`, `jump` and `run` twice each on one
character as separate groups, and a `spell` over six directions beside a `spell 2` over
the remaining two. **Every POST to `/characters/animations` starts a new group.** One
group over eight directions comes from one call naming eight directions, which
`character animate -d south -d east …` already sends.

PixelLab's own site does extend a group, over an endpoint that is not in the v2 schema
and not reachable with an API key:
`POST https://api.pixellab.ai/animate-with-text-v3/character/background`, unprefixed,
taking `direction` in the singular beside `animation_group_id`. It answers
`403 {"detail":"Invalid token"}` to the bearer token this tool holds, and
`https://api.pixellab.ai/openapi.json` is `404`. Reaching for it would mean a second
credential, taken out of a browser session, for an endpoint nobody documented and
nothing obliges PixelLab to keep. Not adopted, and this paragraph is why.

So the seam moves to this side of the wire. R2.39 spends nothing to prevent the waste:
the character payload `_require_character` already fetches names every animation and
every direction it covers, so a direction asked for twice is a refusal that names the
animation holding it, with `--again` for the case where a second take is the point.
Matching a motion to a group is by the name the call would carry — `--name` when given,
otherwise `custom-` and the first thirty characters of the action, which is how the
provider derives `animation_type` for a v3 animation, or the template id for a
template.

R4.5, and the record requirement added to `specs/asset-workspace/` beside it, then read
several groups as one animation:
`character show` folds the account's groups by that name and says how many the provider
holds behind it, and the subject record folds the runs the same way, so an animation
built over two calls reaches `inspect` — and the atlas built from it — as one animation
over every direction it covers. The grouping the provider refuses is real everywhere a
game engine can see it.
10 changes: 7 additions & 3 deletions specs/characters-and-animation/requirements.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
autonomy: auto
ci: wait
branch: feat/room-for-the-motion
delivery: merged
pr: 68
branch: feat/one-animation-many-directions
delivery: in-review
pr: 78
---

# Characters and animation — requirements
Expand Down Expand Up @@ -82,6 +82,9 @@ different endpoint family and no skeleton.
- **R2.37** (ADDED) If no start pose is given and the subject's record holds no pose of that character made for the motion being animated, then the character commands shall refuse the call and say that the state the motion starts from is made first, before spending anything, unless the caller asks to animate from the character's rest frame.
- **R2.38** (ADDED) The character commands shall offer a check that reports whether a motion and a character's poses go together, and shall complete it without calling a provider.

- **R2.39** (ADDED) If a direction asked for is already animated on that character for the motion being animated, then the character commands shall refuse the call, name the animation and the directions it already holds, and say that the provider starts a new animation rather than adding a direction to one, before spending anything, unless the caller asks for it to be generated again.
- **R2.40** (ADDED) Where a direction is animated for a motion the character already has, the character commands shall report that the frames join that motion in the workspace and not in the animation the provider holds.


## R3 · Rotating

Expand All @@ -93,6 +96,7 @@ different endpoint family and no skeleton.
- **R4.2** The character commands shall show one character's rotations, animations and settings.
- **R4.3** When asked for a spritesheet, the character commands shall download the sheet and its layout file and write both to the workspace.
- **R4.4** Where a command names an asset that does not exist, the character commands shall say so rather than reporting an empty result.
- **R4.5** (ADDED) When showing a character, the character commands shall present the animations it holds for one motion as a single animation over every direction they cover, and shall say how many the provider holds for it.

## Out of scope

Expand Down
15 changes: 15 additions & 0 deletions specs/characters-and-animation/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,18 @@
R2.27
_Depends 6.3_
_Reason the interpolation tool was asked for after delivery_

## 7 · One animation over many directions

- [x] 7.1 (Unit) Read the directions each animation covers from the character — R2.39
_Reason the provider refuses animation_group_id for characters_
- [x] 7.2 (Unit) Refuse a direction this motion already has, unless asked again — R2.39,
R2.40
_Depends 7.1_
_Reason the provider refuses animation_group_id for characters_
- [x] 7.3 (Unit) Show the animations of one motion as one, over every direction — R4.5
_Depends 7.1_
_Reason the provider refuses animation_group_id for characters_
- [x] 7.4 (Unit) Teach the skill that one call carries every direction — R2.40
_Depends 7.2_
_Reason the provider refuses animation_group_id for characters_
Loading
Loading