paradramorama/tools/cardgen/install_picks.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

96 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""Install chosen card art onto monsterrobot.games.
Usage: install_picks.py "cratewars=vector stylus9=neon ..." [--dry]
For each pick it:
1. trims the paper border z-image sometimes paints around 'paint' variants,
2. writes covers/<name>.jpg at the tile's native 896x1152,
3. queues a matching risograph <name>_riso.jpg for the page's hover flip,
4. rsyncs both to the lander (host path — the nginx mount is read-only).
"""
import json, os, subprocess, sys, tempfile
S = os.path.dirname(os.path.abspath(__file__))
CARDS = f'{S}/cards'
RISO = f'{S}/riso'
STAGE = f'{S}/install'
VPS = 'humanjing@100.71.119.27'
LANDER = '/home/humanjing/monsterrobot.games/covers'
PROFILES = {p['id']: p for p in json.load(open(f'{S}/profiles.json'))}
RISO_STYLE = ("risograph screenprint poster, three flat spot colours only, visible halftone "
"dot texture, slight misregistration, bold graphic shapes, flat matte inks")
READS = ("single bold hero subject filling the frame, strong clean silhouette, high contrast, "
"poster composition, reads clearly at thumbnail size")
NOTEXT = ("completely wordless, no text, no letters, no words, no logo, no watermark, "
"no signature, blank unlabelled sleeves")
def sh(*a):
r = subprocess.run(a, capture_output=True)
if r.returncode:
sys.exit(f'FAILED: {" ".join(a)}\n{r.stderr.decode()[:400]}')
def to_cover(src, dst, trim):
"""896x1152 jpeg, optionally trimming a painted paper border first."""
with tempfile.TemporaryDirectory() as td:
cur = src
if trim:
w = int(subprocess.run(['sips', '-g', 'pixelWidth', src], capture_output=True)
.stdout.decode().split(':')[-1])
h = int(subprocess.run(['sips', '-g', 'pixelHeight', src], capture_output=True)
.stdout.decode().split(':')[-1])
cur = os.path.join(td, 'c.png')
sh('sips', '-c', str(int(h * 0.955)), str(int(w * 0.955)), src, '--out', cur)
sh('sips', '-s', 'format', 'jpeg', '-s', 'formatOptions', '82',
'-z', '1152', '896', cur, '--out', dst)
def main():
picks = dict(p.split('=') for p in sys.argv[1].split() if '=' in p)
dry = '--dry' in sys.argv
os.makedirs(STAGE, exist_ok=True)
os.makedirs(RISO, exist_ok=True)
# 1. main covers from the picked variants
riso_spec = []
for gid, variant in picks.items():
src = f'{CARDS}/{gid}__{variant}.png'
if not os.path.exists(src):
print(f' ! missing {src}'); continue
name = PROFILES[gid].get('current_cover', gid)
if name == '__none__':
name = gid
to_cover(src, f'{STAGE}/{name}.jpg', trim=(variant == 'paint'))
print(f'{name}.jpg ({variant})')
p = PROFILES[gid]
riso_spec.append({
'out': f'{RISO}/{name}_riso.png', 'operator': 'flux_local',
'params': {'prompt': f"{RISO_STYLE}. {p['hero_subject']}. {READS}. {NOTEXT}",
'model': 'flux2-klein-4b', 'steps': 4,
'width': 896, 'height': 1152, 'seed': 7}})
# 2. the hover b-sides
json.dump(riso_spec, open(f'{RISO}/spec.json', 'w'), indent=1)
if not dry:
sh(sys.executable, f'{os.path.expanduser("~")}/Documents/paradramorama/tools/mbgen.py',
f'{RISO}/spec.json')
for item in riso_spec:
base = os.path.basename(item['out'])[:-4]
if os.path.exists(item['out']):
to_cover(item['out'], f'{STAGE}/{base}.jpg', trim=False)
print(f'{base}.jpg')
# 3. ship
if dry:
print('\n[dry] staged only, nothing uploaded')
return
sh('rsync', '-az', f'{STAGE}/', f'{VPS}:{LANDER}/')
print(f'\n✓ uploaded {len(os.listdir(STAGE))} files to {LANDER}')
if __name__ == '__main__':
main()