paradramorama/tools/cardgen/gen_cards.py
m3ultra f3773919e5 Card-art generation pipeline for monsterrobot.games
Four directions per tile (neon box art / vector side art / amiga comic /
80s airbrush) generated on the farm, plus the picker page and the installer
that ships chosen art and its risograph hover b-side.

Recipe rules, all learned the hard way against the existing covers:
tiles render 172-240px so silhouette and colour are all that read; the page
draws every title in HTML so art carries no lettering (the old covers baked
in garbled type); busy interiors smuggle text back via album sleeves and go
muddy small, so it is one hero subject per card.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 12:05:04 +10:00

63 lines
2.8 KiB
Python

#!/usr/bin/env python3
"""Build the 4-variant card-art spec for monsterrobot.games from profiles.json.
Recipe rules, all learned from the bake-off against the existing covers:
* Tiles render 172-240px wide -> silhouette and colour are all that read.
* The page draws every title in HTML, so art carries NO lettering. The old
covers baked in garbled type ("INFTE! COPFRIICGN") - that is the slop tell.
* Busy interiors smuggle text back in via album sleeves/posters and go muddy
at thumbnail size. Single hero subject, always.
"""
import json, os, sys
OUT = sys.argv[2] if len(sys.argv) > 2 else '/private/tmp/claude-501/-Users-m3ultra-Documents/097e3817-807d-439d-b5f5-95a6ffd4a1f9/scratchpad/cards'
READS = ("single bold hero subject filling the frame, strong clean silhouette, "
"dramatic rim light, high contrast, saturated limited palette, "
"deep dark background, poster composition, reads clearly at thumbnail size")
NOTEXT = ("completely wordless, no text, no letters, no words, no title, no logo, "
"no signage, no watermark, no signature, blank unlabelled sleeves and "
"plain unmarked surfaces")
VARIANTS = [
# key model steps style
('neon', 'flux2-klein-4b', 4,
"1987 British home-computer game box art, airbrushed gouache, glossy "
"highlights, heroic low angle, screaming neon cyan and magenta on pure black"),
('vector', 'flux2-klein-4b', 4,
"1980s arcade cabinet side art, bold flat vector shapes, hard edges, thick "
"black outlines, blazing neon on black, geometric and graphic, no gradients"),
('comic', 'flux2-klein-4b', 4,
"early-1990s Amiga game cover painting, chunky saturated brushwork, bold cel "
"shading, comic-book energy, primary colours, dark vignette"),
('paint', 'z-image-turbo', 8,
"classic 1980s airbrush poster painting, silhouetted figures against a glowing "
"backlit haze, lush deep colour, cinematic, painted on black"),
]
def main():
profiles = json.load(open(sys.argv[1]))
spec = []
for p in profiles:
subj = p['hero_subject']
pal = p.get('palette') or ''
for key, model, steps, style in VARIANTS:
params = {
'prompt': f"{style}. {subj}. {pal + '. ' if pal else ''}{READS}. {NOTEXT}",
'model': model, 'steps': steps,
'width': 896, 'height': 1152, 'seed': 7,
}
if model == 'z-image-turbo':
params['quantize'] = '4'
spec.append({'out': f"{OUT}/{p['id']}__{key}.png",
'operator': 'flux_local', 'params': params})
os.makedirs(OUT, exist_ok=True)
path = OUT + '/spec.json'
json.dump(spec, open(path, 'w'), indent=1)
print(f'{len(spec)} jobs ({len(profiles)} games x {len(VARIANTS)}) -> {path}')
if __name__ == '__main__':
main()