ONE palette gate replaces four feature-picked ones

Oliver, on the brute's fall frames: "now the fall zombie has a different skin
tone.. man this is all so tiresome." Both halves were right. The skin IS twenty
points lighter across that sheet, and the tiresome part is mine: I had been
adding one hand-picked gate per drift I discovered — redness, then hue, then
`changed`, then no-grids — which is a loop with no end, because naming features
is not the same as comparing pictures.

`kit` names four: shirt redness, shorts brightness, shorts hue, near-white
fraction. Every character breaks a different one. The brute's shirt has no red
in it at all, so its reference redness was 0 and blood scored 70-100 against a
tolerance meant for a garment. A body face down shows a fraction of the shirt it
shows standing, so two of the four move with POSE. And nothing in that list ever
measured SKIN, which is exactly how a brute's flesh drifted across a whole sheet
with six gates green.

The palette gate asks the only question that generalises: are the big colour
areas of this frame the same colours as the anchor's? Top SIX by area — the
garments and the skin. Proportions ignored, so a pose change is invisible; blood
is never in the top six, so a fresh wound cannot look like drift.

CALIBRATED BEFORE IT WAS WIRED IN, which is the discipline I skipped all day.
Frames known to be right score 0.0-8.5; the drifts we could actually see score
12.0-15.9. The threshold is 10, in the gap. Three formulations were measured and
the two that overlapped were thrown away.

And every sheet of a variant is now anchored to that variant's FIRST sheet, not
to the stage above it. Chaining let a legal hop compound into a drift nobody
authorised — the shorts walking -9 to -19 down the wound ladder was that, and so
was this.

Its first verdict on the existing brute: br_play is the character, and every
other sheet sits 15 to 48 away from it. It would have caught all of this on the
first roll.

Also in: the GRID guard from the same session — the valley-cut rescue would slice
a 2x2 sheet into four frames of a torso above someone else's boots, turning a
loud failure into a silent one. Asked per cut PIECE, because a body is vertically
continuous and a piece with a gap in it caught two of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-12 13:45:41 +02:00
1 parent df454c5d92
commit 0e92c72457
33 files changed
+312 -19

No files matched your search

+81 -9
View File
@@ -12,7 +12,7 @@
# real gain in inner-edge density (the number that tracks "is the drawing actually finer").
#
# Failures leave the sprites alone and print why, so a batch can just be re-run.
import json, os, subprocess, sys
import json, os, shutil, subprocess, sys
import numpy as np
from PIL import Image
@@ -147,6 +147,13 @@ def kit(path):
# NOTE the fourth tolerance: kit() returns four numbers and kit_off zips them against this, so a
# three-long tuple would silently skip the hue check rather than fail — the same shape of no-op the
# read-only floodfill in _fine.py was. Every caller passing its own `palette_tol` needs four too.
# A measure only means something if the garment it measures EXISTS. The brute wears a pale grey-green
# work shirt with no red in it at all, so the reference's redness is 0 and every roll with a spot of
# blood on it scored 70-100 — twenty-six points of tolerance against a number that was never
# measuring a shirt. Below this floor `redness` is reading BLOOD, which is supposed to change between
# wound stages, so the axis is skipped rather than fought. The tolerances in a batch were calibrated
# on a red-shirted zombie; a variant inherits them and must not inherit their assumptions.
RED_FLOOR = 40
def kit_off(outs, ref_name, tol=(12, 10, 1.5, 4)):
"""Is this roll wearing the right kit? Generative rolls drift the palette EVERY TIME and nothing
else here catches it: colour_match only runs for `check: finer`, and it is a global mean/sd fit
@@ -154,9 +161,10 @@ def kit_off(outs, ref_name, tol=(12, 10, 1.5, 4)):
headband, the toss shipped twice in pale grey shorts, and a swing pair shipped in brown shoes.
Compared against a canonical frame that is NOT part of the batch (o_idle1 for the opponent)."""
ref = kit(os.path.join(HERE, ref_name + '.png'))
skip = [ref[0] < RED_FLOOR, False, False, False] # no red garment -> redness means nothing
for o in outs:
got = kit(o)
if any(abs(g - r) > t for g, r, t in zip(got, ref, tol)):
if any(abs(g - r) > t for i, (g, r, t) in enumerate(zip(got, ref, tol)) if not skip[i]):
return (f'kit drifted vs {ref_name}: redness/shorts/bright {got} vs {ref} '
f'(tolerance {tol})')
return None
@@ -208,11 +216,18 @@ def changed_off(outs, refs, want):
for o, r in zip(outs, refs):
rp = os.path.join(HERE, r + '.png')
if not os.path.exists(rp): continue
_, _, ma, mb, _ = _masks(o, rp)
d = _d(ma, mb)
if d < want:
_, _, ma, mb, h = _masks(o, rp)
# WHOLE BODY *OR* UPPER BODY, whichever moved more. An arm is a body-relative amount of
# silhouette: on the lean club pro it is 12-38% of him, on a brute twice his width it is 9%,
# so a threshold calibrated on one rejects a perfectly good amputation on the other. Above
# the hip is where an arm lives, and there the same removal reads roughly twice as loud —
# the stride gate splits at the same line for the same reason.
hip = int(h * STRIDE_HIP)
d, up = _d(ma, mb), _d(ma[:hip], mb[:hip])
if max(d, up) < want:
return (f'{os.path.basename(o)} is {r} redrawn — only {d:.3f} of the silhouette differs '
f'(want >= {want}). The thing that was supposed to change did not change.')
f'({up:.3f} above the hip, want >= {want}). '
f'The thing that was supposed to change did not change.')
return None
def stride_off(outs, pairs):
@@ -227,6 +242,41 @@ def stride_off(outs, pairs):
f'want ~1.00 / <={STRIDE_TOP} / >={STRIDE_LEG})')
return None
# ---- THE PALETTE GATE, which replaces four hand-picked ones. `kit` names features — shirt redness,
# shorts brightness, shorts hue, near-white fraction — and every character breaks a different one:
# the brute's shirt has no red in it, a body face down shows a fraction of the shirt it shows
# standing, and NOTHING in that list ever measured SKIN, which is how a brute's flesh got twenty
# points lighter across its fall sheet without a single gate noticing.
#
# This asks the only question that generalises: are the big colour areas of this frame the same
# colours as the anchor's? Top SIX by area — the garments and the skin. Proportions are ignored, so a
# pose change is invisible; blood is never in the top six, so a fresh wound cannot look like drift.
# Calibrated on frames known to be right (0.0-8.5) against the drifts we could see (12.0-15.9), so
# the default sits in the gap. One number, no per-character tuning.
def dom_colours(path, n=6, q=8, floor=0.005):
a = np.array(Image.open(path).convert('RGBA')).astype(int)
px = a[..., :3][a[..., 3] > 128]
if not len(px): return np.zeros((0, 3)), np.zeros(0)
u, c = np.unique((px // q) * q, axis=0, return_counts=True)
k = c / c.sum() >= floor
u, c = u[k], c[k] / c[k].sum()
o = np.argsort(-c)[:n]
return u[o], c[o] / c[o].sum()
def palette_off(outs, anchor, want):
ap = os.path.join(HERE, anchor + '.png')
if not os.path.exists(ap): return None
ua, _ = dom_colours(ap)
if not len(ua): return None
for o in outs:
uo, wo = dom_colours(o)
if not len(uo): continue
d = float((np.sqrt(((uo[:, None, :] - ua[None, :, :]) ** 2).sum(2)).min(1) * wo).sum())
if d > want:
return (f'{os.path.basename(o)} is not painted in {anchor}\'s colours — its main areas sit '
f'{d:.1f} away (want <= {want}). Something the size of a garment or the skin changed.')
return None
def not_magenta(sheet):
"""The generator sometimes ignores the magenta and paints its own background. That was written
off as harmless — the key uses the corner colour — right up until it returned a GREEN sheet and
@@ -300,6 +350,15 @@ def run(name, spec, dry=False):
if r.returncode or not all(os.path.exists(o) for o in outs):
print(f'[{name}] REJECTED: slicing failed (frames merged?) — re-run or add "cut"'); return False
if lead: outs = outs[1:] # the scale reference is not installed
# A CHAINED BATCH'S BASELINE IS ITS REFERENCE, MEASURED NOW. Seeding every placeholder up front
# meant a wound stage was scored against a copy of the MASTER — art two steps better than the
# stage it actually follows — and three brute batches were rejected for not out-detailing a
# sheet they were never drawn from.
refs0 = spec.get('ref_only') or []
if len(refs0) == len(sprites):
for r, t in zip(refs0, sprites):
tp, rp = (os.path.join(HERE, n + '.png') for n in (t, r))
if not os.path.exists(tp) and os.path.exists(rp): shutil.copy(rp, tp)
before = [density(os.path.join(HERE, s + '.png')) for s in sprites]
after = [density(o) for o in outs]
# Two jobs, two bars. A coarse->fine REDRAW has to be measurably finer (1.25x, not 1.5x: a sheet
@@ -315,7 +374,11 @@ def run(name, spec, dry=False):
bar = {'finer': 1.25, 'same': 0.85, 'loose': 0.7, 'any': 0.0}[spec.get('check', 'finer')]
if sum(after) / len(after) < sum(before) / len(before) * bar:
print(f'[{name}] REJECTED: not finer than what it replaces {before} -> {after}'); return False
if spec.get('palette_ref'):
# the palette gate supersedes the feature-picked kit gate wherever a batch declares an anchor
if spec.get('palette_anchor'):
bad = palette_off(outs, spec['palette_anchor'], spec.get('palette_max', 10))
if bad: print(f'[{name}] REJECTED: {bad}'); return False
elif spec.get('palette_ref'):
bad = kit_off(outs, spec['palette_ref'], tuple(spec.get('palette_tol', (12, 10, 1.5))))
if bad: print(f'[{name}] REJECTED: {bad}'); return False
if spec.get('stride'):
@@ -363,11 +426,20 @@ if __name__ == '__main__':
for a in args: # frames merged, or a limb off the canvas
if a.startswith('--tries='): tries = int(a.split('=')[1])
names = list(B) if '--all' in args else [a for a in args if not a.startswith('--')]
bad = []
bad, stale = [], set()
for n in names:
# A CHAINED BATCH IS DRAWN FROM THE STAGE ABOVE IT, so if that stage failed its reference is
# still a placeholder and whatever comes back is drawn from the wrong body. The brute's
# headless sheet installed cleanly off an arm-off stage that never landed — three perfect
# frames of a two-armed corpse. Skipping is the only honest answer; running is worse than
# failing, because it SUCCEEDS.
blocked = [r for r in (B[n].get('ref_only') or []) if r in stale]
if blocked:
print(f'[{n}] SKIPPED: drawn from {", ".join(blocked)}, which did not land')
bad.append(n); stale.update(B[n]['sprites']); continue
for attempt in range(1, tries + 1):
if run(n, B[n], dry): break
print(f'[{n}] attempt {attempt}/{tries} rejected')
else:
bad.append(n)
bad.append(n); stale.update(B[n]['sprites'])
print('\nFAILED BATCHES (re-run):', ' '.join(bad) if bad else 'none')