#!/usr/bin/env python3 """Optimize + publish the matched GariFuturism illustrations into the webapp (EB-021). The lesson illustration refs point at high-res master images (~5 MB each) on the director-works volume — far too heavy to serve, esp. for the offline-first Belize pilot. This resizes each referenced PUBLISHABLE image to web size (max 1000px) and re-encodes to WebP (~100-150 KB) into web/public/illustrations/, then stamps a served `src` (/illustrations/.webp) onto each lesson illustration ref so the UI can render it. Only images already attached to lessons (publishable, non-sacred, fail-closed per EB-020) are published — sacred/SD-PENDING never reach here. python3 scripts/optimize_illustrations.py # dry-run (report) python3 scripts/optimize_illustrations.py --write # optimize + stamp src """ from __future__ import annotations import hashlib, json, re, sys from pathlib import Path from PIL import Image WEB = Path(__file__).resolve().parent.parent LMS = WEB.parent / "lms" MI = Path("/Volumes/AI External/NISAMINA_DIRECTOR_WORKS/03_ILLUSTRATIONS/MASTER_ILLUSTRATIONS") OUT = WEB / "public" / "illustrations" MAX_PX = 1000 WEBP_Q = 72 def slug(rel: str) -> str: s = re.sub(r"\.[a-zA-Z]+$", "", rel).lower() s = re.sub(r"[^a-z0-9]+", "-", s).strip("-")[:72] # append a short hash of the full rel so distinct images never collide h = hashlib.sha1(rel.encode("utf-8")).hexdigest()[:8] return f"{s}-{h}" def iter_lessons(): for ed in sorted(p for p in LMS.iterdir() if p.is_dir() and not p.name.startswith("_")): for sub in sorted(ed.glob("lessons*")): if sub.is_dir(): for fp in sorted(sub.glob("*.json")): yield fp def main(): write = "--write" in sys.argv OUT.mkdir(parents=True, exist_ok=True) # collect unique referenced images refs = {} lessons = [] for fp in iter_lessons(): try: d = json.loads(fp.read_text(encoding="utf-8")) except Exception: continue ims = (d.get("illustrations") or {}).get("images") if ims: lessons.append((fp, d)) for im in ims: refs.setdefault(im["rel"], slug(im["rel"])) print(f"{len(lessons)} illustrated lessons; {len(refs)} unique images to optimize") done = bytes_out = 0 for rel, sg in refs.items(): src = MI / rel dst = OUT / f"{sg}.webp" if not src.exists(): print(" MISSING:", rel); continue if write and not dst.exists(): try: with Image.open(src) as im: im = im.convert("RGB") im.thumbnail((MAX_PX, MAX_PX)) im.save(dst, "WEBP", quality=WEBP_Q, method=6) except Exception as e: print(" ERR", rel, e); continue if dst.exists(): bytes_out += dst.stat().st_size done += 1 # stamp served src onto lesson illustration refs stamped = 0 if write: for fp, d in lessons: ch = False for im in d["illustrations"]["images"]: sp = f"/illustrations/{slug(im['rel'])}.webp" if im.get("src") != sp: im["src"] = sp ch = True if ch: fp.write_text(json.dumps(d, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") stamped += 1 print(f"optimized {done} images ({bytes_out/1e6:.1f} MB total) -> {OUT}" + (f"; stamped src on {stamped} lessons" if write else " [dry-run]")) if __name__ == "__main__": main()