File size: 3,675 Bytes
e2f6f15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#!/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/<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]
    # 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()