From e674677f4e0b5090a6cf9fb6693b56d237ffbc76 Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Wed, 12 Aug 2026 14:54:09 +0200 Subject: [PATCH] =?UTF-8?q?PROJECT.md:=20the=20art=20pipeline,=20corrected?= =?UTF-8?q?=20=E2=80=94=20and=20the=20brute=20stays=20parked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- HANDOFF-MATCH.md | 11 +++++ PROJECT.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) 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