← back to Wallco Ai
mural-master: room-size 12x11ft scenic-master builder + vertical-panel slicer + lowres-demotion report
5bfe5e9ea4433f84021b8518ecaaf0ee10028998 · 2026-06-03 08:33:26 -0700 · Steve Abrams
build-mural-master.py: recreate Monterey scenic murals as ONE continuous
12ft W x 11ft H scene (21,600 x 19,800px @ 150 DPI) — 'the entire design is all
the panels, create room size to fit'. Upscales curated art via build-tif's
Real-ESRGAN chain (reused via importlib), cover-fits the 12x10ft focal (no
stretch), synthesizes 6in forgiving sky/grass bleed bands top+bottom (installer
shift/trim zone), then slices into customer-width vertical panels: 24in->6,
36in->4, 54in->3+remainder. v1 bleed = deterministic mirror+fade (free, no model,
never trips settlement foliage detectors); generative outpaint is the upgrade.
--self-test proves geometry at zero cost; all assertions pass.
demote-lowres-murals.py: REPORT-ONLY scan of mural-kind rows below 12ft-master
hi-res (tif_w_px<21600): 1,652 rows (644 published). Writes a plan JSONL of what
an apply would touch; makes NO DB changes. Applying stays Steve-gated. No DB
creds in the file — reuses build-tif.db() via importlib.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A scripts/build-mural-master.pyA scripts/demote-lowres-murals.py
Diff
commit 5bfe5e9ea4433f84021b8518ecaaf0ee10028998
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jun 3 08:33:26 2026 -0700
mural-master: room-size 12x11ft scenic-master builder + vertical-panel slicer + lowres-demotion report
build-mural-master.py: recreate Monterey scenic murals as ONE continuous
12ft W x 11ft H scene (21,600 x 19,800px @ 150 DPI) — 'the entire design is all
the panels, create room size to fit'. Upscales curated art via build-tif's
Real-ESRGAN chain (reused via importlib), cover-fits the 12x10ft focal (no
stretch), synthesizes 6in forgiving sky/grass bleed bands top+bottom (installer
shift/trim zone), then slices into customer-width vertical panels: 24in->6,
36in->4, 54in->3+remainder. v1 bleed = deterministic mirror+fade (free, no model,
never trips settlement foliage detectors); generative outpaint is the upgrade.
--self-test proves geometry at zero cost; all assertions pass.
demote-lowres-murals.py: REPORT-ONLY scan of mural-kind rows below 12ft-master
hi-res (tif_w_px<21600): 1,652 rows (644 published). Writes a plan JSONL of what
an apply would touch; makes NO DB changes. Applying stays Steve-gated. No DB
creds in the file — reuses build-tif.db() via importlib.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
scripts/build-mural-master.py | 275 ++++++++++++++++++++++++++++++++++++++++
scripts/demote-lowres-murals.py | 101 +++++++++++++++
2 files changed, 376 insertions(+)
diff --git a/scripts/build-mural-master.py b/scripts/build-mural-master.py
new file mode 100644
index 0000000..98d29b1
--- /dev/null
+++ b/scripts/build-mural-master.py
@@ -0,0 +1,275 @@
+#!/usr/bin/env python3
+"""
+build-mural-master.py — room-size scenic-mural master + vertical-panel slicer.
+
+Steve's brief (2026-06-03): recreate the Monterey scenic murals as ONE
+continuous 12ft-wide x 11ft-tall scene ("the entire design is all the panels —
+create room size to fit"). The wall is sliced into vertical PANELS the customer
+picks the width of (24" / 36" / 54"). Assemble the panels side-by-side → the
+complete scene.
+
+Geometry @ 150 DPI:
+ master = 144" x 132" = 21,600 x 19,800 px (12ft W x 11ft H)
+ focal = 144" x 120" = 21,600 x 18,000 px (the protected scene)
+ bleed = top 6" + bottom 6" (900px each) of FORGIVING content (sky/clouds
+ up top, grass/ground at the bottom) so the installer can shift the
+ design up/down and trim WITHIN those bands without losing the scene.
+
+Method (DTD verdict 2026-06-03, Steve's pick): UPSCALE existing curated art via
+the Real-ESRGAN chain in build-tif.py, COVER-fit to the focal box (no stretch —
+scale-to-cover + center-crop), then synthesize the 6" sky/grass bleed bands.
+v1 bleed = deterministic edge-band mirror+fade (free, no model, never trips the
+settlement foliage detectors). Generative outpaint (SDXL inpaint) is the
+documented upgrade — drop it into make_bleed() behind --bleed=generative later.
+
+Usage:
+ python3 scripts/build-mural-master.py <design_id> # full real build (ESRGAN, ~$0.12, multi-GB)
+ python3 scripts/build-mural-master.py <design_id> --panels 24,36 # only these panel widths
+ python3 scripts/build-mural-master.py --self-test # zero-cost geometry proof (no DB/net)
+ python3 scripts/build-mural-master.py <design_id> --print-ft 12 --height-ft 11 --bleed-in 6
+
+Outputs (under WALLCO_MURAL_DIR or data/mural-masters/<id>/):
+ master_12x11ft_150dpi.tif
+ panels_24in/panel_01.tif ... panel_06.tif (+ manifest.json per width)
+"""
+import argparse
+import importlib.util
+import json
+import os
+import sys
+import time
+from pathlib import Path
+
+from PIL import Image
+
+Image.MAX_IMAGE_PIXELS = None # a 21,600 x 19,800 master is ~428 MP — intentional.
+
+ROOT = Path(__file__).resolve().parents[1]
+
+# Reuse the ESRGAN upscaler + TIF writer from build-tif.py (hyphenated filename
+# → load via importlib rather than import). Single source of truth for the
+# Replicate mechanism, DPI, and the LZW-TIF writer.
+_bt_spec = importlib.util.spec_from_file_location(
+ "buildtif", str(ROOT / "scripts" / "build-tif.py"))
+_bt = importlib.util.module_from_spec(_bt_spec)
+_bt_spec.loader.exec_module(_bt)
+
+DPI = _bt.DPI # 150
+
+_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"))
+
+
+# ── geometry ────────────────────────────────────────────────────────────────
+def cover_fit(img, box_w, box_h):
+ """Scale-to-cover box_w x box_h then center-crop. No aspect distortion."""
+ img = img.convert("RGB")
+ w, h = img.size
+ scale = max(box_w / w, box_h / h)
+ nw, nh = max(box_w, round(w * scale)), max(box_h, round(h * scale))
+ img = img.resize((nw, nh), Image.LANCZOS)
+ left, top = (nw - box_w) // 2, (nh - box_h) // 2
+ return img.crop((left, top, left + box_w, top + box_h))
+
+
+def make_bleed(focal, bleed_px, edge):
+ """
+ Synthesize a forgiving bleed band of height bleed_px by extending the
+ focal's edge band. edge='top' → sky/clouds extended upward; edge='bottom'
+ → grass/ground extended downward. Deterministic mirror+fade: sample a band
+ off the focal edge, mirror it, and fade toward the edge's mean tone so the
+ extension reads as "more sky"/"more ground", not a hard seam.
+ """
+ w, _ = focal.size
+ sample_h = min(bleed_px, max(64, bleed_px // 2))
+ if edge == "top":
+ band = focal.crop((0, 0, w, sample_h))
+ else:
+ band = focal.crop((0, focal.size[1] - sample_h, w, focal.size[1]))
+ band = band.transpose(Image.FLIP_TOP_BOTTOM) # mirror so the seam matches
+ # Tile the mirrored band up to bleed_px tall.
+ out = Image.new("RGB", (w, bleed_px))
+ y = 0
+ while y < bleed_px:
+ out.paste(band, (0, y))
+ y += sample_h
+ out = out.crop((0, 0, w, bleed_px))
+ # Fade the far edge toward the band's mean tone for a soft, forgiving end.
+ from PIL import ImageStat
+ mean = tuple(int(c) for c in ImageStat.Stat(band).mean[:3])
+ solid = Image.new("RGB", (w, bleed_px), mean)
+ mask = Image.new("L", (w, bleed_px))
+ px = mask.load()
+ for yy in range(bleed_px):
+ # 0 at the focal-adjacent edge → 255 at the far (trim) edge.
+ t = yy / max(1, bleed_px - 1)
+ a = int(255 * (t ** 1.4))
+ row = a
+ for xx in range(0, w, 8): # coarse fill; mask is low-freq, 8px step is fine
+ for k in range(xx, min(xx + 8, w)):
+ px[k, yy] = row
+ if edge == "top":
+ mask = mask.transpose(Image.FLIP_TOP_BOTTOM) # far edge = the very top
+ return Image.composite(solid, out, mask)
+
+
+def assemble_master(focal, bleed_px):
+ """Stack top-bleed + focal + bottom-bleed into the full master."""
+ w, h = focal.size
+ top = make_bleed(focal, bleed_px, "top")
+ bot = make_bleed(focal, bleed_px, "bottom")
+ master = Image.new("RGB", (w, h + 2 * bleed_px))
+ master.paste(top, (0, 0))
+ master.paste(focal, (0, bleed_px))
+ master.paste(bot, (0, bleed_px + h))
+ return master
+
+
+def slice_panels(master, panel_in, dpi=DPI):
+ """
+ Slice the master into vertical panels panel_in inches wide (full height).
+ Returns a list of (index, x0, x1, width_px, width_in) — the last panel is
+ the remainder when panel_in doesn't divide the wall evenly.
+ """
+ W, H = master.size
+ pw = int(round(panel_in * dpi))
+ panels, x, i = [], 0, 0
+ while x < W:
+ x1 = min(x + pw, W)
+ i += 1
+ panels.append((i, x, x1, x1 - x, round((x1 - x) / dpi, 2)))
+ x = x1
+ return panels
+
+
+# ── full real build ─────────────────────────────────────────────────────────
+def build(design_id, print_ft, height_ft, bleed_in, panel_widths, force=False):
+ master_w = int(round(print_ft * 12 * DPI)) # 21,600
+ master_h = int(round(height_ft * 12 * DPI)) # 19,800
+ bleed_px = int(round(bleed_in * DPI)) # 900
+ focal_w, focal_h = master_w, master_h - 2 * bleed_px # 21,600 x 18,000
+
+ conn = _bt.db()
+ d = _bt.fetch_design(conn, design_id)
+ conn.close()
+ if not d:
+ print(f"design {design_id} not found", file=sys.stderr); return 3
+ src = _bt.resolve_source(d)
+ if not src:
+ print(f"design {design_id}: source PNG not found", file=sys.stderr); return 4
+
+ out_dir = MURAL_DIR / str(design_id)
+ out_dir.mkdir(parents=True, exist_ok=True)
+ master_path = out_dir / f"master_{print_ft:g}x{height_ft:g}ft_{DPI}dpi.tif"
+ if master_path.exists() and not force:
+ print(f"master exists {master_path} — use --force to rebuild");
+ else:
+ t0 = time.time()
+ img = Image.open(src)
+ print(f"design {design_id} ({d.get('kind')}) src {img.size[0]}x{img.size[1]} "
+ f"→ master {master_w}x{master_h}px ({print_ft:g}x{height_ft:g}ft @ {DPI}dpi)")
+ # 1) ESRGAN-upscale toward the focal long edge, then 2) cover-fit focal.
+ target_long = max(focal_w, focal_h)
+ up, method = _bt.upscale_mural(img, target_long)
+ print(f" upscaled via {method} → {up.size[0]}x{up.size[1]}")
+ focal = cover_fit(up, focal_w, focal_h)
+ print(f" cover-fit focal → {focal.size[0]}x{focal.size[1]}")
+ # 3) synth bleed bands → full master
+ master = assemble_master(focal, bleed_px)
+ print(f" + {bleed_in}\" sky/grass bleed → master {master.size[0]}x{master.size[1]}")
+ _bt.write_tif(master, master_path)
+ print(f" ✓ master {master_path} ({master_path.stat().st_size/1e6:.0f} MB) "
+ f"in {time.time()-t0:.0f}s")
+
+ master = Image.open(master_path)
+ summary = {"design_id": design_id, "master": str(master_path),
+ "master_px": list(master.size), "dpi": DPI,
+ "print_ft": print_ft, "height_ft": height_ft, "bleed_in": bleed_in,
+ "panels": {}}
+ for pw_in in panel_widths:
+ pdir = out_dir / f"panels_{pw_in:g}in"
+ pdir.mkdir(exist_ok=True)
+ panels = slice_panels(master, pw_in)
+ man = []
+ for (i, x0, x1, wpx, win) in panels:
+ ppath = pdir / f"panel_{i:02d}.tif"
+ master.crop((x0, 0, x1, master.size[1])).save(
+ ppath, format="TIFF", compression="tiff_lzw", dpi=(DPI, DPI))
+ man.append({"index": i, "file": ppath.name, "x0": x0, "x1": x1,
+ "width_px": wpx, "width_in": win, "height_px": master.size[1]})
+ (pdir / "manifest.json").write_text(json.dumps(man, indent=2))
+ summary["panels"][f"{pw_in:g}in"] = {"count": len(man), "dir": str(pdir),
+ "panel_px": [m["width_px"] for m in man]}
+ print(f" ✓ {pw_in:g}\" → {len(man)} panels "
+ f"({', '.join(str(m['width_px']) for m in man)} px)")
+ (out_dir / "summary.json").write_text(json.dumps(summary, indent=2))
+ print(f"\nDONE → {out_dir}/summary.json")
+ return 0
+
+
+# ── zero-cost geometry proof ─────────────────────────────────────────────────
+def self_test():
+ """Validate cover-fit + bleed + slicer with no DB, no network, no $ — at a
+ 1/10-scale geometry, then assert the real 12x11ft panel math arithmetically."""
+ print("SELF-TEST — geometry only, no ESRGAN/DB/disk-master\n")
+ ok = True
+
+ # 1/10 scale: 2160 x 1980 master, 90px bleed, focal 2160 x 1800.
+ sc = 10
+ mw, mh, bp = 21600 // sc, 19800 // sc, 900 // sc
+ fw, fh = mw, mh - 2 * bp
+ src = Image.new("RGB", (102, 102))
+ for y in range(102): # vertical gradient so cover-fit/bleed are visible
+ for x in range(0, 102, 6):
+ for k in range(x, min(x + 6, 102)):
+ src.putpixel((k, y), (120, 150 + y // 3, 90))
+ focal = cover_fit(src, fw, fh)
+ assert focal.size == (fw, fh), focal.size
+ print(f" cover_fit square 102² → focal {focal.size} (expect {(fw, fh)}) ✓")
+ master = assemble_master(focal, bp)
+ assert master.size == (mw, mh), master.size
+ print(f" assemble focal+2*{bp}px bleed → master {master.size} (expect {(mw, mh)}) ✓")
+
+ # Real 12x11ft @150dpi panel math.
+ REAL_W = 12 * 12 * DPI # 21,600
+ cases = {24: 6, 36: 4, 54: 3}
+ for pw_in, expect_n in cases.items():
+ panels = slice_panels(Image.new("RGB", (REAL_W, 100)), pw_in)
+ widths = [p[3] for p in panels]
+ total = sum(widths)
+ good = len(panels) == expect_n and total == REAL_W
+ ok &= good
+ print(f" slice {pw_in}\" → {len(panels)} panels {widths}px "
+ f"(Σ={total}, expect {expect_n} panels / Σ{REAL_W}) {'✓' if good else '✗'}")
+
+ # Bleed band must be the right size and not throw.
+ tb = make_bleed(focal, bp, "top"); bb = make_bleed(focal, bp, "bottom")
+ assert tb.size == (fw, bp) and bb.size == (fw, bp)
+ print(f" bleed top/bottom bands {tb.size} ✓")
+
+ print("\n" + ("ALL GEOMETRY ASSERTIONS PASS ✓" if ok else "FAIL ✗"))
+ return 0 if ok else 1
+
+
+def main():
+ p = argparse.ArgumentParser()
+ p.add_argument("design_id", type=int, nargs="?")
+ p.add_argument("--print-ft", type=float, default=12.0)
+ p.add_argument("--height-ft", type=float, default=11.0)
+ p.add_argument("--bleed-in", type=float, default=6.0)
+ p.add_argument("--panels", default="24,36,54",
+ help="comma-separated panel widths in inches (default 24,36,54)")
+ p.add_argument("--force", action="store_true")
+ p.add_argument("--self-test", action="store_true")
+ a = p.parse_args()
+ if a.self_test:
+ sys.exit(self_test())
+ if a.design_id is None:
+ p.error("design_id is required (or pass --self-test)")
+ widths = [float(x) for x in a.panels.split(",") if x.strip()]
+ sys.exit(build(a.design_id, a.print_ft, a.height_ft, a.bleed_in, widths, force=a.force))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/demote-lowres-murals.py b/scripts/demote-lowres-murals.py
new file mode 100644
index 0000000..3bf8a5c
--- /dev/null
+++ b/scripts/demote-lowres-murals.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+"""
+demote-lowres-murals.py — REPORT-ONLY scan of mural-kind rows that aren't a
+real hi-res print master, so they can be removed from PHYSICAL print and kept
+as DIGITAL-only sales.
+
+Steve's brief (2026-06-03): "any murals created that were not high res need to
+be removed and made available for digital sales." A mural is physical-print-
+viable only if its image hits 150 DPI across the full 12ft wall (21,600 px
+wide). Everything below that (the whole current catalog tops out at ~4096 px)
+is digital-only until rebuilt via build-mural-master.py.
+
+DEFAULT = report only. It writes a plan JSONL + a summary and touches NOTHING in
+the DB. Applying the demotion (unpublish-from-physical + keep-digital) is a
+customer-facing change on hundreds of live rows → it stays Steve-gated and the
+exact "digital-only" surfacing mechanism is confirmed with Steve before --apply
+is implemented. Per the destructive-defaults rule: soft-flag + JSONL ledger,
+never DELETE.
+
+Usage:
+ python3 scripts/demote-lowres-murals.py # report (all mural kinds)
+ python3 scripts/demote-lowres-murals.py --published # only currently-published rows
+ python3 scripts/demote-lowres-murals.py --min-px 21600 # hi-res threshold (default 21600 = 12ft@150dpi)
+"""
+import argparse
+import importlib.util
+import json
+from pathlib import Path
+
+from psycopg2.extras import RealDictCursor
+
+ROOT = Path(__file__).resolve().parents[1]
+LEDGER_DIR = ROOT / "data"
+MURAL_KINDS = ("mural", "mural_panel")
+
+# Reuse build-tif.py's db() (hyphenated filename → importlib) so this script
+# carries no DB credentials of its own.
+_bt_spec = importlib.util.spec_from_file_location(
+ "buildtif", str(ROOT / "scripts" / "build-tif.py"))
+_bt = importlib.util.module_from_spec(_bt_spec)
+_bt_spec.loader.exec_module(_bt)
+db = _bt.db
+
+
+def main():
+ p = argparse.ArgumentParser()
+ p.add_argument("--min-px", type=int, default=21600,
+ help="hi-res threshold on tif_w_px (default 21600 = 12ft @ 150 DPI)")
+ p.add_argument("--published", action="store_true",
+ help="restrict to currently-published rows")
+ a = p.parse_args()
+
+ conn = db()
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
+ cur.execute(f"""
+ SELECT id, category, kind, is_published,
+ COALESCE(tif_w_px, 0) AS tif_w_px,
+ COALESCE(tif_h_px, 0) AS tif_h_px,
+ digital_file_at
+ FROM all_designs
+ WHERE kind IN %s
+ AND COALESCE(tif_w_px, 0) < %s
+ {"AND is_published" if a.published else ""}
+ ORDER BY is_published DESC, category, id
+ """, (MURAL_KINDS, a.min_px))
+ rows = cur.fetchall()
+ conn.close()
+
+ pub = [r for r in rows if r["is_published"]]
+ by_cat = {}
+ for r in rows:
+ c = str(r["category"] or "?").split(" · ")[0]
+ by_cat[c] = by_cat.get(c, 0) + 1
+
+ print(f"=== demote-lowres-murals (REPORT ONLY) ===")
+ print(f"hi-res threshold: tif_w_px >= {a.min_px} (12ft @ 150 DPI)\n")
+ print(f"mural rows below threshold : {len(rows):,}")
+ print(f" of which PUBLISHED (live): {len(pub):,} ← these leave the physical grid")
+ print(f" unpublished : {len(rows)-len(pub):,}\n")
+ print("top categories affected:")
+ for c, n in sorted(by_cat.items(), key=lambda kv: -kv[1])[:15]:
+ print(f" {n:5,} {c}")
+
+ # Plan JSONL — exactly what an --apply would touch. No DB writes here.
+ plan_path = LEDGER_DIR / "mural-demotion-plan.jsonl"
+ with plan_path.open("w") as f:
+ for r in rows:
+ f.write(json.dumps({
+ "id": r["id"], "category": r["category"], "kind": r["kind"],
+ "is_published": r["is_published"], "tif_w_px": r["tif_w_px"],
+ "action": "unpublish_physical_keep_digital",
+ "reason": f"tif_w_px {r['tif_w_px']} < {a.min_px} (not 12ft-master hi-res)",
+ }) + "\n")
+ print(f"\nplan written → {plan_path} ({len(rows):,} rows)")
+ print("NO DB changes made. Applying is Steve-gated (customer-facing, hundreds of")
+ print("live rows) and needs the digital-only surfacing mechanism confirmed first.")
+ return 0
+
+
+if __name__ == "__main__":
+ main()
← 547c9df dogs/luxe gen: GEMINI_IMAGE_MODEL env override (re-roll on a
·
back to Wallco Ai
·
wallco: flip 137 reviewed misclassified mural_panel->seamles e131ed2 →