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>
96 lines
3.8 KiB
Python
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()
|