Cover tooling + deploy: flux base art, Pillow title stamp + riso duotone, MBW/MBH dims
Live at https://monsterrobot.games/games/nomeansnoquest/ (botchat bind mount); card on the landing arc grid. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0976611f36
commit
c43fa2fa18
BIN
pics/cover-blank.png
Normal file
BIN
pics/cover-blank.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
94
tools/make_cover.py
Normal file
94
tools/make_cover.py
Normal file
@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Finish the landing-page cover pair from the flux base image.
|
||||
|
||||
Stamps the title (so the band name is spelled by us, not by a diffusion
|
||||
model) and derives the risograph hover variant (two-ink duotone, offset
|
||||
plates, paper grain). python3 tools/make_cover.py OUT_DIR
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
|
||||
from PIL import Image, ImageDraw, ImageEnhance, ImageFont, ImageOps
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
BASE = os.path.join(HERE, "..", "pics", "cover-blank.png")
|
||||
|
||||
FONTS = [
|
||||
"/System/Library/Fonts/Supplemental/Impact.ttf",
|
||||
"/System/Library/Fonts/Supplemental/Arial Black.ttf",
|
||||
"/System/Library/Fonts/Helvetica.ttc",
|
||||
]
|
||||
|
||||
|
||||
def font(size):
|
||||
for f in FONTS:
|
||||
if os.path.exists(f):
|
||||
try:
|
||||
return ImageFont.truetype(f, size)
|
||||
except OSError:
|
||||
continue
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def stamp_title(img):
|
||||
d = ImageDraw.Draw(img)
|
||||
w = img.width
|
||||
|
||||
def line(text, y, size, fill):
|
||||
f = font(size)
|
||||
tw = d.textlength(text, font=f)
|
||||
x = (w - tw) / 2
|
||||
# fat xerox outline: many offset passes
|
||||
for dx in range(-4, 5, 2):
|
||||
for dy in range(-4, 5, 2):
|
||||
d.text((x + dx, y + dy), text, font=f, fill=(12, 10, 8))
|
||||
d.text((x, y), text, font=f, fill=fill)
|
||||
|
||||
line("NOMEANSNO", 26, 118, (238, 232, 220))
|
||||
line("QUEST", 148, 96, (208, 40, 32))
|
||||
return img
|
||||
|
||||
|
||||
def riso(img):
|
||||
paper = Image.new("RGB", img.size, (240, 233, 216))
|
||||
g = ImageOps.autocontrast(img.convert("L"))
|
||||
# ink plates from tone bands
|
||||
dark = g.point(lambda v: 255 if v < 90 else 0)
|
||||
mid = g.point(lambda v: 255 if 90 <= v < 175 else 0)
|
||||
blue = Image.new("RGB", img.size, (36, 62, 168))
|
||||
red = Image.new("RGB", img.size, (222, 60, 44))
|
||||
out = paper.copy()
|
||||
# misregistered plates, like a real duplicator on a deadline
|
||||
out.paste(red, (3, 2), mid)
|
||||
out.paste(blue, (0, 0), dark)
|
||||
# grain
|
||||
rng = random.Random(41)
|
||||
px = out.load()
|
||||
for _ in range(img.width * img.height // 14):
|
||||
x = rng.randrange(img.width)
|
||||
y = rng.randrange(img.height)
|
||||
r, gr, b = px[x, y]
|
||||
n = rng.randint(-18, 18)
|
||||
px[x, y] = (max(0, min(255, r + n)), max(0, min(255, gr + n)), max(0, min(255, b + n)))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
out_dir = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "..", "covers")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
base = Image.open(BASE).convert("RGB")
|
||||
base = ImageEnhance.Contrast(base).enhance(1.08)
|
||||
titled = stamp_title(base.copy())
|
||||
|
||||
cover = ImageOps.fit(titled, (311, 400), Image.LANCZOS)
|
||||
cover.save(os.path.join(out_dir, "nomeansnoquest.jpg"), quality=88)
|
||||
|
||||
riso_img = ImageOps.fit(riso(stamp_title(base.copy())), (311, 400), Image.LANCZOS)
|
||||
riso_img.save(os.path.join(out_dir, "nomeansnoquest_riso.jpg"), quality=88)
|
||||
print("covers written:", out_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -56,7 +56,9 @@ def api(path, data=None, raw=False):
|
||||
|
||||
def main():
|
||||
name, prompt = sys.argv[1], sys.argv[2]
|
||||
job = api("/api/jobs", {"operator": "flux_local", "params": {"prompt": prompt, "width": 1024, "height": 608}})
|
||||
w = int(os.environ.get("MBW", "1024"))
|
||||
h = int(os.environ.get("MBH", "608"))
|
||||
job = api("/api/jobs", {"operator": "flux_local", "params": {"prompt": prompt, "width": w, "height": h}})
|
||||
jid = job.get("id") or job.get("job_id")
|
||||
print(f"submitted flux_local job {jid}")
|
||||
for _ in range(120):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user