| |
| """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/<slug>.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] |
| |
| 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) |
| |
| 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 |
| |
| 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() |
|
|