← 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
M YOLO_BACKLOG.mdA scripts/mural-contact-sheet.py
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 →