[object Object]

← 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

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 →