Automate the redraw, and run the first eight batches through it
assets/_regen.py plus a batch file: build a reference sheet from the sprites in the repo, ask for a HIGH-resolution redraw, slice with _fine.py, colour-match each frame onto the one it replaces, install. The point of the whole thing is the style block — the old prompts specified coarse art and got it. It refuses to install a bad sheet. Wrong frame count, any figure touching a canvas edge (that is what clipped z_f1 in the first place), or a redraw that is not measurably finer than what it replaces, and the batch is left alone with a reason printed. Re-running a batch is the fix; three of the first eleven needed it. The bar is 1.25x inner-edge density rather than 1.5x, because a sheet that already has blood on it starts high and a genuinely finer wounded body still scores a smaller gain. Done so far: both shamblers and their wound skins, the brute's walk and its two sets of wound stages, the steward and his wound skins, the turned CPU. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No files matched your search
@@ -0,0 +1,126 @@
|
||||
# _regen.py — redraw a batch of sprites at real pixel-art resolution, end to end.
|
||||
#
|
||||
# python3 assets/_regen.py shambler_a # one batch
|
||||
# python3 assets/_regen.py --list # what batches exist
|
||||
# python3 assets/_regen.py --all # everything not already done
|
||||
#
|
||||
# Per batch it: builds a reference sheet from the sprites in the repo, asks the generator to redraw
|
||||
# them at HIGH art resolution (the style block below is the whole point — the previous prompts asked
|
||||
# for "big chunky pixel blocks", which is a specification for coarse art), slices with _fine.py,
|
||||
# colour-matches each frame onto what it replaces, and installs — but only if the sheet passes its
|
||||
# checks: right number of frames, nothing touching a canvas edge (that is what clipped z_f1), and a
|
||||
# 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 numpy as np
|
||||
from PIL import Image
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SCRATCH = os.environ.get('SPLAT_SCRATCH', '/tmp/splatter-regen')
|
||||
GEN = os.path.expanduser('~/.claude/skills/generate-image/scripts/generate_image.py')
|
||||
PY = os.environ.get('SPLAT_PY', sys.executable)
|
||||
|
||||
STYLE = (
|
||||
"Redraw them as HIGH-RESOLUTION pixel art. The art pixels must be SMALL: build each figure out "
|
||||
"of roughly 90 to 110 pixels of height instead of the reference's 40, so there is room for real "
|
||||
"detail, and spend it — a face with teeth, sunken eyes and a jawline; individual fingers; "
|
||||
"clothing with visible seams, hems, folds and frayed torn edges; laced shoes; four or five "
|
||||
"distinct shades per material with fine hatched shading where the light falls away. Keep every "
|
||||
"pose, every body proportion, every height, every facing and every wound EXACTLY as in the "
|
||||
"reference — only the drawing resolution and the amount of fine detail change. Keep the same "
|
||||
"colours. Hard black outline around each figure. The background must be ONE FLAT solid magenta "
|
||||
"#FF00FF across the entire canvas — no scenery, no gradient, no vignette. Keep at least 200 "
|
||||
"pixels of empty magenta between figures and at least 150 pixels of empty magenta at every edge "
|
||||
"of the canvas: no figure or limb may touch or run off an edge. No shadow, no ground line, no text."
|
||||
)
|
||||
|
||||
def batches():
|
||||
with open(os.path.join(HERE, '_regen_batches.json')) as f:
|
||||
return json.load(f)
|
||||
|
||||
# ---------------------------------------------------------------- helpers ----
|
||||
def ref_sheet(names, out, scale=2.0, gap=220, pad=150):
|
||||
ims = [Image.open(os.path.join(HERE, n + '.png')).convert('RGBA') for n in names]
|
||||
ims = [i.resize((max(1, int(i.width * scale)), max(1, int(i.height * scale))), Image.NEAREST) for i in ims]
|
||||
w = sum(i.width for i in ims) + gap * (len(ims) - 1) + pad * 2
|
||||
h = max(i.height for i in ims) + pad * 2
|
||||
s = Image.new('RGBA', (w, h), (255, 0, 255, 255))
|
||||
x = pad
|
||||
for i in ims:
|
||||
s.paste(i, (x, h - pad - i.height), i); x += i.width + gap
|
||||
s.save(out)
|
||||
return out
|
||||
|
||||
def density(path):
|
||||
"""Inner-edge density: how much detail lives INSIDE the silhouette. Coarse art ~0.15, the
|
||||
redrawn runner ~0.45. This is the number that matched Oliver's eye."""
|
||||
a = np.array(Image.open(path).convert('RGBA'))
|
||||
solid = a[..., 3] > 128
|
||||
d = np.abs(np.diff(a[..., :3].astype(int), axis=1)).sum(2)
|
||||
return round(float(((d > 40) & solid[:, :-1]).sum() / max(1, solid.sum())), 3)
|
||||
|
||||
def touches_edge(sheet):
|
||||
a = np.array(Image.open(sheet).convert('RGB')).astype(int)
|
||||
bg = a[0, 0]
|
||||
fg = (((a - bg) ** 2).sum(2) > 3600) & ~(np.minimum(a[..., 0], a[..., 2]) > a[..., 1] + 40)
|
||||
return bool(fg[0].any() or fg[-1].any() or fg[:, 0].any() or fg[:, -1].any())
|
||||
|
||||
def colour_match(src, dst_name):
|
||||
"""Fit the new frame onto the palette statistics of the sprite it replaces, so a redrawn body
|
||||
does not change colour halfway through a walk cycle."""
|
||||
op = lambda x: x[..., 3] > 200
|
||||
old = np.array(Image.open(os.path.join(HERE, dst_name + '.png')).convert('RGBA'), dtype=float)
|
||||
new = np.array(Image.open(src).convert('RGBA'), dtype=float)
|
||||
po, pn = old[op(old)][:, :3], new[op(new)][:, :3]
|
||||
m = op(new)
|
||||
px = (new[m][:, :3] - pn.mean(0)) / np.maximum(pn.std(0), 1e-6) * po.std(0) + po.mean(0)
|
||||
new[m] = np.concatenate([np.clip(px, 0, 255), new[m][:, 3:]], axis=1)
|
||||
Image.fromarray(new.astype('uint8'), 'RGBA').save(src)
|
||||
|
||||
# ------------------------------------------------------------------- run ----
|
||||
def run(name, spec, dry=False):
|
||||
os.makedirs(SCRATCH, exist_ok=True)
|
||||
sprites = spec['sprites']
|
||||
ref = ref_sheet(sprites, f'{SCRATCH}/{name}_ref.png', spec.get('scale', 2.0))
|
||||
sheet = f'{SCRATCH}/{name}_hi.png'
|
||||
prompt = f"{spec['subject']} {STYLE}"
|
||||
if dry:
|
||||
print(f'[{name}] would generate {len(sprites)} frames: {", ".join(sprites)}'); return True
|
||||
print(f'[{name}] generating {len(sprites)} frames...')
|
||||
r = subprocess.run([PY, GEN, prompt, '--input', ref, '--output', sheet],
|
||||
capture_output=True, text=True, cwd=os.path.dirname(HERE))
|
||||
if not os.path.exists(sheet):
|
||||
print(f'[{name}] FAILED: generator returned no image\n{r.stdout[-400:]}'); return False
|
||||
if touches_edge(sheet):
|
||||
print(f'[{name}] REJECTED: a figure runs off the canvas edge — re-run'); return False
|
||||
outs = [f'{SCRATCH}/{name}_{i}.png' for i in range(len(sprites))]
|
||||
cut = ['--cut', spec['cut']] if spec.get('cut') else []
|
||||
r = subprocess.run([PY, os.path.join(HERE, '_fine.py'), sheet, *outs,
|
||||
'--anchor', str(spec.get('anchor', 0)), '--h', str(spec.get('h', 224)), *cut],
|
||||
capture_output=True, text=True)
|
||||
print(r.stdout.strip() or r.stderr.strip())
|
||||
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
|
||||
before = [density(os.path.join(HERE, s + '.png')) for s in sprites]
|
||||
after = [density(o) for o in outs]
|
||||
# 1.25x, not 1.5x: a sheet that already has blood on it starts with a high edge density, so a
|
||||
# genuinely finer redraw of a wounded body scores a smaller gain than a clean one.
|
||||
if sum(after) / len(after) < sum(before) / len(before) * 1.25:
|
||||
print(f'[{name}] REJECTED: not finer than what it replaces {before} -> {after}'); return False
|
||||
for o, s in zip(outs, sprites):
|
||||
colour_match(o, s)
|
||||
Image.open(o).save(os.path.join(HERE, s + '.png'))
|
||||
print(f'[{name}] installed. detail {before} -> {after}')
|
||||
return True
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = sys.argv[1:]
|
||||
B = batches()
|
||||
if not args or '--list' in args:
|
||||
for k, v in B.items(): print(f'{k:16s} {len(v["sprites"]):2d} frames {", ".join(v["sprites"])}')
|
||||
sys.exit()
|
||||
dry = '--dry' in args
|
||||
names = list(B) if '--all' in args else [a for a in args if not a.startswith('--')]
|
||||
bad = [n for n in names if not run(n, B[n], dry)]
|
||||
print('\nFAILED BATCHES (re-run):', ' '.join(bad) if bad else 'none')
|
||||
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"shambler_a": {
|
||||
"sprites": ["z_a1", "z_a2", "z_a3", "z_a4"],
|
||||
"subject": "Four frames of the same walking zombie in a grey shirt and dark trousers, a four-frame shamble cycle, drawn as coarse low-resolution pixel art with big blocky pixels and almost no internal detail.",
|
||||
"anchor": 0
|
||||
},
|
||||
"shambler_b": {
|
||||
"sprites": ["z_b1", "z_b2", "z_b3", "z_b4"],
|
||||
"subject": "Four frames of the same walking zombie in a brown coat over a shirt, a four-frame shamble cycle, drawn as coarse low-resolution pixel art with big blocky pixels and almost no internal detail.",
|
||||
"anchor": 0
|
||||
},
|
||||
"shambler_wounds": {
|
||||
"sprites": ["z_aw", "z_awb", "z_bw", "z_bwb"],
|
||||
"subject": "Four frames of two WOUNDED walking zombies, drawn as coarse low-resolution pixel art. The first two are the same grey-shirted zombie whose right arm ends in a ragged bloody stump, in two stages of one stride. The last two are the same brown-coated zombie with an open red gash across his skull, in two stages of one stride. Keep each wound exactly where it is.",
|
||||
"anchor": 0
|
||||
},
|
||||
"brute_walk": {
|
||||
"sprites": ["z_g1", "z_g2", "z_g3", "z_g4"],
|
||||
"subject": "Four frames of the same enormous fat zombie brute in a stained grey-green shirt and brown trousers, dragging a rusty pole in one hand, a four-frame heavy lumber, drawn as coarse low-resolution pixel art.",
|
||||
"anchor": 0
|
||||
},
|
||||
"brute_wounds": {
|
||||
"sprites": ["z_gw", "z_gw2", "z_gw3", "z_gw4"],
|
||||
"subject": "Four damage stages of the same enormous fat zombie brute, drawn as coarse low-resolution pixel art: (1) one arm torn off at the shoulder leaving a ragged stump, still dragging his pole; (2) both arms gone; (3) chest and belly torn wide open with ribs showing; (4) headless, ribs out, still standing. Keep each stage's injuries exactly as they are.",
|
||||
"anchor": 0
|
||||
},
|
||||
"brute_wounds_b": {
|
||||
"sprites": ["z_gwb", "z_gw2b", "z_gw3b", "z_gw4b"],
|
||||
"subject": "Four damage stages of the same enormous fat zombie brute, mid-stride on the opposite foot, drawn as coarse low-resolution pixel art: (1) one arm torn off at the shoulder, dragging his pole; (2) both arms gone; (3) chest and belly torn open with ribs showing; (4) headless, ribs out. Keep each stage's injuries exactly as they are.",
|
||||
"anchor": 0
|
||||
},
|
||||
"steward": {
|
||||
"sprites": ["z_d1", "z_d2", "z_d3", "z_d4"],
|
||||
"subject": "Four frames of the same big heavy zombie stadium steward in a high-visibility ORANGE safety vest over a dark shirt, dark trousers and heavy boots, a four-frame walk cycle, drawn as coarse low-resolution pixel art. Keep the vest's reflective stripes.",
|
||||
"anchor": 0
|
||||
},
|
||||
"steward_wounds": {
|
||||
"sprites": ["z_dw", "z_dwb"],
|
||||
"subject": "Two frames of the same badly wounded zombie stadium steward in a torn high-visibility ORANGE vest — one arm mangled, a deep bloody gash across the chest, head lolling — walking, two stages of one stride, drawn as coarse low-resolution pixel art.",
|
||||
"anchor": 0
|
||||
},
|
||||
"cpu_walk": {
|
||||
"sprites": ["z_c1", "z_c2", "z_c3", "z_c4"],
|
||||
"subject": "Four frames of the same zombie tennis player facing the viewer — yellow hair, red headband, red shirt, grey shorts, a wooden racket hanging from one hand — a four-frame shuffle towards the camera, drawn as coarse low-resolution pixel art.",
|
||||
"anchor": 0
|
||||
},
|
||||
"cpu_wounds": {
|
||||
"sprites": ["z_cw1", "z_cw1b", "z_cw2", "z_cw2b"],
|
||||
"subject": "Four frames of the same wounded zombie tennis player in a red shirt holding a wooden racket, drawn as coarse low-resolution pixel art. The first two are him wounded and bleeding, in two stages of one stride. The last two are the same character HEADLESS, a ragged red stump where the neck was, still holding the racket, in two stages of one stride.",
|
||||
"anchor": 0
|
||||
},
|
||||
"shield": {
|
||||
"sprites": ["z_s1", "z_s2", "z_s3", "z_s4", "z_sdown"],
|
||||
"subject": "Five frames of the same zombie spectator in a grey-brown coat, drawn as coarse low-resolution pixel art. In the first four he holds a wooden folding chair up in front of himself like a shield, shuffling forward in a four-frame walk. In the fifth the chair has been knocked DOWN out of his hands and he is staggering with his arms up, exactly as shown.",
|
||||
"anchor": 0
|
||||
},
|
||||
"player_stance": {
|
||||
"sprites": ["p_idle1", "p_idle2", "p_bounce", "p_bounce2"],
|
||||
"subject": "Four frames of the same tennis player seen from BEHIND in a white shirt, blue shorts and a white headband, holding a racket, drawn as coarse low-resolution pixel art: two waiting stances and two frames of him bouncing a ball before serving.",
|
||||
"anchor": 0
|
||||
},
|
||||
"player_serve": {
|
||||
"sprites": ["p_toss", "p_smash", "p_swing1", "p_swing2"],
|
||||
"subject": "Four frames of the same tennis player in a white shirt, blue shorts and a white headband, drawn as coarse low-resolution pixel art: tossing the ball up for a serve; the smash at full stretch with the racket overhead; and two frames of a forehand swing.",
|
||||
"anchor": 0
|
||||
},
|
||||
"player_run": {
|
||||
"sprites": ["p_run1", "p_run2", "p_run3", "p_run4"],
|
||||
"subject": "Four frames of the same tennis player seen from BEHIND in a white shirt and blue shorts, running away from the camera, a four-frame run cycle with no racket, drawn as coarse low-resolution pixel art.",
|
||||
"anchor": 0
|
||||
},
|
||||
"player_death": {
|
||||
"sprites": ["p_die1", "p_die2", "p_die3", "p_die4", "p_die5"],
|
||||
"subject": "Five frames of the same tennis player in a white shirt and blue shorts collapsing, each one lower than the last, drawn as coarse low-resolution pixel art: arched backwards with both arms flung up; dropped to his knees; pitching forward onto one hand; face down and flat with both arms out in a dark pool; and a low flattened heap of torn cloth and dark red with the stained headband on top. Each figure must stay clearly shorter than the one before it.",
|
||||
"anchor": 0
|
||||
},
|
||||
"opponent_stance": {
|
||||
"sprites": ["o_idle1", "o_idle2", "o_bounce", "o_bounce2"],
|
||||
"subject": "Four frames of the same tennis opponent in a RED shirt and dark shorts, seen from the front across the net, holding a racket, drawn as coarse low-resolution pixel art: two waiting stances and two frames of him bouncing a ball before serving.",
|
||||
"anchor": 0
|
||||
},
|
||||
"opponent_play": {
|
||||
"sprites": ["o_toss", "o_smash", "o_swing1", "o_swing2"],
|
||||
"subject": "Four frames of the same tennis opponent in a RED shirt and dark shorts, drawn as coarse low-resolution pixel art: tossing the ball up for a serve; the smash at full stretch with the racket overhead; and two frames of a forehand swing.",
|
||||
"anchor": 0
|
||||
},
|
||||
"opponent_run": {
|
||||
"sprites": ["o_run1", "o_run2"],
|
||||
"subject": "Two frames of the same tennis opponent in a RED shirt and dark shorts running across the court towards the camera, two stages of one stride, drawn as coarse low-resolution pixel art.",
|
||||
"anchor": 0
|
||||
},
|
||||
"ump_boy": {
|
||||
"sprites": ["x_ump", "x_boyrun1", "x_boyrun2"],
|
||||
"subject": "Three frames drawn as coarse low-resolution pixel art: a tennis UMPIRE in a white sun hat, blazer and tie, seated upright with a clipboard; and two frames of a young BALL BOY in a white polo shirt and shorts sprinting, two stages of one stride. Keep the umpire seated and the ball boy running.",
|
||||
"anchor": 0
|
||||
},
|
||||
"boy_carry": {
|
||||
"sprites": ["x_boycarry1", "x_boycarry2", "x_boycarry3", "x_boycarry4"],
|
||||
"subject": "Four frames of the same young ball boy in a white polo shirt and shorts walking with a mop shouldered, a four-frame walk cycle, drawn as coarse low-resolution pixel art.",
|
||||
"anchor": 0
|
||||
},
|
||||
"boy_mop": {
|
||||
"sprites": ["x_boymop1", "x_boymop2", "x_boymop3", "x_boymop4"],
|
||||
"subject": "Four frames of the same young ball boy in a white polo shirt and shorts pushing a mop across the ground, a four-frame mopping cycle, bent over the mop handle, drawn as coarse low-resolution pixel art.",
|
||||
"anchor": 0
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 28 KiB |