diff --git a/HANDOFF-MATCH.md b/HANDOFF-MATCH.md index 04112cb..79e3bcf 100644 --- a/HANDOFF-MATCH.md +++ b/HANDOFF-MATCH.md @@ -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 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 - Re-add regicide to the match (killing the body with a smash) — removed deliberately; it made every diff --git a/PROJECT.md b/PROJECT.md index 24e2b45..e4fbc2a 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -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 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 ` 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) Everything below is a fix to what shipped that morning. All four notes were real defects, which is