[object Object]

← back to Wallco Ai

mural-contact-sheet.py: visual panel review aid for any built master [yolo T3, local-only]

e5dc16382d82495c73464da642085ad78a1085f0 · 2026-06-03 09:23:24 -0700 · Steve Abrams

Lays out a master's vertical panels per width (24/36/54") side-by-side, downscaled,
with seam lines + drop labels → <master_dir>/contact-sheet.png. Lets Steve eyeball
the slicing + bleed without opening a multi-GB TIF. Pure local PIL — no spend, no
DB, no network. Validated on the 10557 proof master (24"=2 drops, 36"=2, 54"=1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit e5dc16382d82495c73464da642085ad78a1085f0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jun 3 09:23:24 2026 -0700

    mural-contact-sheet.py: visual panel review aid for any built master [yolo T3, local-only]
    
    Lays out a master's vertical panels per width (24/36/54") side-by-side, downscaled,
    with seam lines + drop labels → <master_dir>/contact-sheet.png. Lets Steve eyeball
    the slicing + bleed without opening a multi-GB TIF. Pure local PIL — no spend, no
    DB, no network. Validated on the 10557 proof master (24"=2 drops, 36"=2, 54"=1).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 YOLO_BACKLOG.md                |  1 +
 scripts/mural-contact-sheet.py | 93 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 94 insertions(+)

diff --git a/YOLO_BACKLOG.md b/YOLO_BACKLOG.md
index 2317045..32a17b9 100644
--- a/YOLO_BACKLOG.md
+++ b/YOLO_BACKLOG.md
@@ -135,3 +135,4 @@ $ per mural + the low-res demotion is customer-facing; both await Steve's go.
 - (ticks appended below)
 - T1 · 2026-06-03 ~08:1x · Q2 build-mural-master --estimate (no-spend cost est) · done · d43631e · CNCP win-2026-06-03-wallco-ai-9
 - T2 · 2026-06-03 ~08:2x · Q3 curate-monterey-batch.py → 12 hue-spread published Monterey → data/mural-batch-monterey.json · done (read-only DB) · CNCP pending
+- T3 · 2026-06-03 ~09:2x · mural-contact-sheet.py (panel review aid, local PIL, no spend) · done · validated on 10557 (24"/36"/54" drops) · CNCP pending
diff --git a/scripts/mural-contact-sheet.py b/scripts/mural-contact-sheet.py
new file mode 100644
index 0000000..eddb20f
--- /dev/null
+++ b/scripts/mural-contact-sheet.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+"""
+mural-contact-sheet.py — review aid for a built mural master. Lays out the
+vertical panels for each panel-width side-by-side (downscaled) with seam lines
++ width labels, so you can eyeball the slicing and bleed without opening a
+multi-GB TIF. NO spend, NO DB, NO network — pure local PIL.
+
+Usage:
+  python3 scripts/mural-contact-sheet.py <design_id>        # → <master_dir>/contact-sheet.png
+  python3 scripts/mural-contact-sheet.py <design_id> --h 1400
+"""
+import argparse
+import json
+import os
+import sys
+from pathlib import Path
+
+from PIL import Image, ImageDraw, ImageFont
+Image.MAX_IMAGE_PIXELS = None
+
+ROOT = Path(__file__).resolve().parents[1]
+_HENRY = Path("/Volumes/Henry/wallco-ai-archive/mural-masters")
+MURAL_DIR = Path(os.environ.get("WALLCO_MURAL_DIR")
+                 or (_HENRY if _HENRY.parent.is_dir() else ROOT / "data" / "mural-masters"))
+
+
+def load_panels(pdir):
+    man = json.loads((pdir / "manifest.json").read_text())
+    return man
+
+
+def build_sheet(design_id, panel_h=1400):
+    mdir = MURAL_DIR / str(design_id)
+    if not mdir.is_dir():
+        print(f"no master dir for {design_id} at {mdir}", file=sys.stderr); return 2
+    pdirs = sorted([d for d in mdir.iterdir() if d.is_dir() and d.name.startswith("panels_")])
+    if not pdirs:
+        print(f"no panel dirs under {mdir}", file=sys.stderr); return 3
+
+    gap, pad, label_h = 14, 24, 34
+    rows = []
+    for pdir in pdirs:
+        man = load_panels(pdir)
+        # scale every panel to panel_h tall
+        thumbs = []
+        for m in man:
+            im = Image.open(pdir / m["file"]).convert("RGB")
+            s = panel_h / im.size[1]
+            tw = max(1, round(im.size[0] * s))
+            thumbs.append((im.resize((tw, panel_h), Image.LANCZOS), m))
+        row_w = sum(t.size[0] for t, _ in thumbs) + gap * (len(thumbs) - 1)
+        rows.append((pdir.name, thumbs, row_w))
+
+    sheet_w = max(r[2] for r in rows) + pad * 2
+    sheet_h = pad + sum(panel_h + label_h + gap for _ in rows) + pad
+    sheet = Image.new("RGB", (sheet_w, sheet_h), (250, 249, 246))
+    d = ImageDraw.Draw(sheet)
+    try:
+        font = ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial.ttf", 22)
+    except Exception:
+        font = ImageFont.load_default()
+
+    y = pad
+    for name, thumbs, row_w in rows:
+        width_in = name.replace("panels_", "").replace("in", "")
+        d.text((pad, y), f'{width_in}" panels  ·  {len(thumbs)} drops', fill=(40, 40, 40), font=font)
+        y += label_h
+        x = pad
+        for i, (t, m) in enumerate(thumbs):
+            sheet.paste(t, (x, y))
+            d.rectangle([x, y, x + t.size[0] - 1, y + panel_h - 1], outline=(180, 60, 60), width=2)
+            d.text((x + 6, y + 6), f'#{m["index"]}  {m["width_in"]:g}"', fill=(255, 255, 255), font=font)
+            x += t.size[0] + gap
+        y += panel_h + gap
+
+    out = mdir / "contact-sheet.png"
+    sheet.save(out)
+    print(f"contact sheet → {out}  ({sheet.size[0]}x{sheet.size[1]})")
+    for name, thumbs, _ in rows:
+        print(f"  {name}: {len(thumbs)} panels {[round(t.size[0]) for t,_ in thumbs]}px(thumb)")
+    return 0
+
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument("design_id", type=int)
+    ap.add_argument("--h", type=int, default=1400, help="thumbnail panel height px")
+    a = ap.parse_args()
+    sys.exit(build_sheet(a.design_id, a.h))
+
+
+if __name__ == "__main__":
+    main()

← fe7ac02 global search: live typeahead overlay consuming /api/search  ·  back to Wallco Ai  ·  loop tick2: UNSTABLE flag benign, designer-scenic 363 root-c 6e65e8c →