PROJECT.md: the art pipeline, corrected — and the brute stays parked

A day went into sprite consistency and most of it was spent solving the problem
in the wrong place. Written down so the next session starts from the answer
instead of rediscovering it.

The boundary everything else follows:
  the generator, GIVEN REFERENCE IMAGES  ->  identity, pose, composition
  the pipeline, DETERMINISTICALLY        ->  exact colour
  the gates                              ->  layout accidents only

With the proof, because it is the part that is hard to believe until measured:
the brute's serve sheet came back with his build, his garments and his DARK
TONES pixel-identical to the reference and his big pale shirt two shades
brighter. The model was looking straight at the character. Every call
re-renders and re-rendering re-lights. No prompt fixes that.

And the rule underneath all of it — A REFERENCE BEATS A DESCRIPTION, at every
level. The racket zombie's sheet was made from the character as an IMAGE plus
the poses as IMAGES and landed first time; the brute's three sheets each
re-derived the character from the same paragraph and produced three different
brutes. Corollary worth having: every corrective sentence in a prompt is
compensating for a reference you did not supply. When a prompt grows a third
"do NOT" clause, add an image instead.

Also written down: correct-then-check, calibrate-before-you-gate (with the
numbers every threshold was calibrated on), the five gates and what each one
actually measures, six traps that each shipped something silently before they
were caught, and three things still open — including that `changed` passed a
headless brute that still had its head, because it knows something moved and
not what.

The brute is PARKED: out of ZFAMS, 31 frames on disk, nine batches in the JSON,
one line away from live. Its healthy eleven are good; its wound stages are not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-12 14:54:09 +02:00
1 parent 759488dd44
commit e674677f4e
2 files changed
+122

No files matched your search

+11
View File
@@ -197,6 +197,17 @@ he ran, where the ball died. That triple diagnosed every defect in the contact c
fine (`check: 'same'`) can be rejected for not out-detailing a redraw; `'loose'` is the right bar fine (`check: 'same'`) can be rejected for not out-detailing a redraw; `'loose'` is the right bar
for that job, and resetting the placeholder to the coarse original resets the bar. for that job, and resetting the placeholder to the coarse original resets the bar.
## The art pipeline
Everything learned generating this mode's sprites is in **PROJECT.md, "THE ART PIPELINE, corrected"**
— read it before touching `assets/_regen.py`, `_fine.py` or `_master.py`. The one-line version: the
generator does identity and pose given REFERENCE IMAGES, the pipeline does exact colour with
arithmetic, and the gates only catch layout accidents. A day was spent discovering that the middle
one is not a prompting problem.
The match opponent's own art is complete — 33 frames, three wound stages, the serve and the fall —
and `assets/_master.py` turns that pose set into the master sheets a new opponent is restyled from.
## Do not ## Do not
- Re-add regicide to the match (killing the body with a smash) — removed deliberately; it made every - Re-add regicide to the match (killing the body with a smash) — removed deliberately; it made every
+111
View File
@@ -1641,6 +1641,117 @@ Known generator quirks, all still true:
- **Colour-match any regenerated frame** of an existing pair before shipping: fit a per-channel - **Colour-match any regenerated frame** of an existing pair before shipping: fit a per-channel
linear map onto the original's mean and sd, then verify with a mean-difference number. linear map onto the original's mean and sd, then verify with a mean-difference number.
## THE ART PIPELINE, corrected — what a day of chasing the generator taught (2026-08-12)
A whole day went into sprite consistency and most of it was spent solving the problem in the wrong
place. Read this before touching `assets/_regen.py`, `_fine.py` or `_master.py`; it will save the
same day.
### The boundary that everything else follows
| what it is good at | who does it |
|---|---|
| identity, pose, composition | the generator, **given reference images** |
| exact colour values | the pipeline, **deterministically** |
| layout accidents — clipped limbs, grids, merged frames, wrong frame count | the gates |
Six gates were built in one day trying to make a generator do arithmetic. It cannot. The proof is
unambiguous: the brute's serve sheet came back with his build, his garments and his DARK TONES
pixel-identical to the reference — `(56,56,32)` in both — and his big pale shirt two shades
brighter, `(200,200,184)` against `(176,176,152)`. The model was looking straight at the character.
Every call re-renders, and re-rendering re-lights. **No prompt fixes that; `match_to_anchor()` does,
in one pass, retroactively, on art already on disk.**
### A REFERENCE BEATS A DESCRIPTION. Every time, at every level.
This is the single highest-value rule here, and every failure of the day was a violation of it:
- The racket zombie's play sheet was generated from `[z_c1, o_idle1, o_run1, o_swing1]` — the
character as an IMAGE, the poses as IMAGES — and landed first time.
- The brute's three sheets each restyled from the master with the character in **words**, so each
one re-derived "pale grey-green work shirt" from the same paragraph and the game got three
different brutes.
- Wound stages only started accumulating damage when stage 2 referenced stage 1's SHEET instead of
being told in prose that the arm was still missing. The arm kept coming back because the reference
picture had two arms in it, and a picture beats a paragraph — including when the picture is wrong.
Corollary: every corrective sentence in a prompt is compensating for a reference you did not supply.
When a prompt grows a third "do NOT" clause, add an image instead.
### CORRECT, THEN CHECK
`match_to_anchor()` runs before the palette gate, so the gate is a safety net for what arithmetic
could not fix rather than a tripwire for what it can. It pairs each of a frame's dominant colours
with its nearest anchor colour, fits one gain+offset per channel, and applies it to every pixel.
Pairs further apart than `NEW_COLOUR` are dropped from the FIT — those are colours the anchor
genuinely has not got, i.e. blood on a fresh wound — but they still RECEIVE the transform, so a
wound stays the right red relative to the body it is on.
### CALIBRATE BEFORE YOU GATE
The discipline that was missing all day. Every threshold in `_regen.py` was picked by measuring
frames known to be right against drifts that were visibly wrong, and rejecting the formulations that
overlapped. Three formulations were tried for the palette gate; two overlapped and were thrown away.
Do not add a gate without printing that table first.
The gates, and the numbers they were calibrated on:
- **stride** — is a walk cycle ONE DRAWING WITH THE LEGS MOVED? Height ratio, upper-body delta, leg
delta on a feet-aligned overlay. Good pairs: brute lurch `1.00/0.000/0.638`, shambler
`1.00/0.070/0.520`. Failures sat at upper 0.47-0.85 (two unrelated illustrations) or legs 0.09
(the same picture twice). Opt-in per batch, because a four-frame RUN legitimately swings the arms.
- **palette** — are the big colour areas the anchor's colours? TOP SIX by area, proportions ignored
so a pose change is invisible, and blood is never in the top six so a fresh wound cannot look like
drift. Right frames 0.0-8.5, visible drifts 12.0-15.9, threshold 10. **This one number replaced
four hand-picked features** (`kit`'s shirt redness, shorts brightness, shorts hue, near-white
fraction), every one of which a different character broke — the brute's shirt has no red in it at
all, two of the four move with POSE, and none of them ever measured SKIN.
- **changed** — did the wound actually happen? A copy scores 0.02-0.05, an amputation 0.12-0.38, a
decapitation 0.27-0.75. Measured whole-body OR above-the-hip, whichever moved more, because an arm
is a body-relative amount of silhouette: 12-38% of a lean man, 9% of a brute.
- **no grids** — asked of each cut PIECE, not of the sheet. A body is vertically continuous, so a
full-width empty band inside one piece means the cut caught two of them.
- plus the older ones: frame count, magenta background, nothing touching a canvas edge, inner-edge
density.
### Traps, each of which shipped something silently before it was caught
- **A rescue that turns a loud failure into a quiet one is worse than no rescue.** `_fine.py` gained
a valley-cut fallback so a sheet whose figures touch is not thrown away — and it happily sliced a
2x2 GRID into four frames of a torso above someone else's boots. It installed cleanly and read in
the log exactly like a success.
- **A failed stage must block the stages drawn from it.** The brute's headless sheet installed off an
arm-off stage that never landed: three perfect frames of a two-armed corpse. Running was worse than
failing, because it SUCCEEDED. `_regen.py` now skips a batch whose `ref_only` is in a failed
batch's outputs.
- **A placeholder is a stand-in for art that does not exist yet, and nothing else.** Re-running
`--variant` to rewrite a spec re-seeded EVERY placeholder from the master and silently replaced a
finished brute's serve and fall sheets with copies of the lean man they were posed from. Three
passes were then spent regenerating sheets that already existed. `variant()` never overwrites now.
- **The detail bar RATCHETS against the file being replaced**, so a chained stage measured against a
copy of the master has to out-detail art two steps better than the stage it follows. Chained
placeholders are seeded at RUN time, from whatever the reference has become by then.
- **`_fine.py` keyed every magenta pixel, not just the background** — and the generator paints racket
strings in the backdrop colour more often than not, so sheet after sheet installed a body holding
an empty hoop. Background is the region connected to the border; magenta left INSIDE a body is
repainted pale grey. (`Image.fromarray` images are READ-ONLY and PIL's `floodfill` swallows the
write error, so the first version of that fix was a silent no-op.)
- **Put the background rule FIRST in the prompt.** Six rejections in eleven rolls were the generator
painting the zombies on a tennis court, because the magenta instruction sat behind three hundred
words of "keep everything exactly as the reference".
### Still open
- **`changed` does not know WHAT changed.** It passed a "headless" brute that still had its head,
because something moved. It measures that a frame differs from its reference, not that it differs
in the right way, and no cheap measure of "the head is gone" has been found.
- **A variant is reliable for its play sheet and a gamble for its wound stages.** The brute's healthy
eleven are good; its arm-off fall frames are standing figures of the wrong build and its headless
row is not headless. `--variant <key>` and `_master.py`'s master sheets are the right shape; the
wound half needs the reference discipline applied harder than prose currently applies it.
- The brute is PARKED, not deleted: `config.ZFAMS` is one entry away from making it live, its 31
frames are on disk, and its nine batches are in `_regen_batches.json`.
## Session log: Oliver's playtest of the barrel weapons (2026-08-03, same day) ## Session log: Oliver's playtest of the barrel weapons (2026-08-03, same day)
Everything below is a fix to what shipped that morning. All four notes were real defects, which is Everything below is a fix to what shipped that morning. All four notes were real defects, which is