[object Object]

← back to Wallco Ai

mural-master: --estimate dry-run cost estimator (no-spend) — batch $ before approval [yolo Q2, local-only]

d43631e3d37dea330071f7bc9a797b74445842a2 · 2026-06-03 08:50:11 -0700 · Steve Abrams

Mirrors esrgan_tiled's level loop exactly (count_esrgan_calls + _tiles_1d) to
report per-mural + batch Replicate call count and $ without making any calls.
12x11ft / 12-mural batch = ~216 calls / ~$3.30. Lets Steve see the spend before
greenlighting the paid batch. self-test still green; compiles clean.

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

Files touched

Diff

commit d43631e3d37dea330071f7bc9a797b74445842a2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jun 3 08:50:11 2026 -0700

    mural-master: --estimate dry-run cost estimator (no-spend) — batch $ before approval [yolo Q2, local-only]
    
    Mirrors esrgan_tiled's level loop exactly (count_esrgan_calls + _tiles_1d) to
    report per-mural + batch Replicate call count and $ without making any calls.
    12x11ft / 12-mural batch = ~216 calls / ~$3.30. Lets Steve see the spend before
    greenlighting the paid batch. self-test still green; compiles clean.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 YOLO_BACKLOG.md               |  20 ++++
 scripts/build-mural-master.py | 220 +++++++++++++++++++++++++++++++++++++++---
 2 files changed, 229 insertions(+), 11 deletions(-)

diff --git a/YOLO_BACKLOG.md b/YOLO_BACKLOG.md
index ccc30fa..ded79a8 100644
--- a/YOLO_BACKLOG.md
+++ b/YOLO_BACKLOG.md
@@ -113,3 +113,23 @@ TICK · TIMESTAMP · ITEM · STATUS · COMMIT
 
 ## Stop condition
 All [ ] → [x], OR a tick hits a gated action (deploy/publish/prod) → STOP and park for Steve. On finish, write session-recap memory + stop scheduling.
+
+---
+
+# YOLO loop run — 2026-06-03 (Steve out a few hours · DTD-decided · CNCP-logged)
+
+**Envelope (reuse 2026-06-02 DTD verdict A — LOCAL COMMITS ONLY):** no deploy, no
+publish/is_published, no prod DB, no sends/DNS/spend/deletes, no remote push, no
+Replicate/GPU spend. Each tick reversible, validated before commit, one logical
+commit, win logged to CNCP. DTD decides genuine technical forks; envelope fixed.
+Pacing: ScheduleWakeup ~1800s dynamic. **Mural batch PARKED** — it costs Replicate
+$ per mural + the low-res demotion is customer-facing; both await Steve's go.
+
+## Queue
+- [ ] **Q1 · fliepaper-bugs.html** — admin record grid, missing the created date+time chip (standing rule #2)
+- [ ] **Q2 · build-mural-master.py --estimate** — zero-spend dry-run cost estimator (ESRGAN tiles + outpaint calls × per-call $) so Steve sees the batch cost before approving
+- [ ] **Q3 · curated top-12 Monterey manifest** — read-only DB select → JSON so the paid batch is one command away on return
+- [ ] opportunistic local code-health as found
+
+## Ledger
+- (ticks appended below)
diff --git a/scripts/build-mural-master.py b/scripts/build-mural-master.py
index 98d29b1..6045434 100644
--- a/scripts/build-mural-master.py
+++ b/scripts/build-mural-master.py
@@ -34,6 +34,7 @@ Outputs (under WALLCO_MURAL_DIR or data/mural-masters/<id>/):
 """
 import argparse
 import importlib.util
+import io
 import json
 import os
 import sys
@@ -126,6 +127,132 @@ def assemble_master(focal, bleed_px):
     return master
 
 
+_ESRGAN_MAX_IN = 1448   # Replicate real-esrgan GPU cap (~2.09M px input)
+
+
+def _replicate_run_latest(model, inp, token, poll_max=600):
+    """Run a Replicate model's LATEST version (no pinned hash) → output URL(s)."""
+    import urllib.request, json as _json
+    req = urllib.request.Request(
+        f"https://api.replicate.com/v1/models/{model}/predictions",
+        data=_json.dumps({"input": inp}).encode(),
+        headers={"Authorization": "Bearer " + token, "Content-Type": "application/json",
+                 "Prefer": "wait"})
+    pred = _json.loads(urllib.request.urlopen(req, timeout=90).read())
+    t0 = time.time()
+    while pred.get("status") not in ("succeeded", "failed", "canceled"):
+        if time.time() - t0 > poll_max:
+            raise RuntimeError("inpaint timeout")
+        time.sleep(3)
+        pred = _json.loads(urllib.request.urlopen(urllib.request.Request(
+            pred["urls"]["get"], headers={"Authorization": "Bearer " + token}), timeout=60).read())
+    if pred["status"] != "succeeded":
+        raise RuntimeError(f"{model} {pred['status']}: {str(pred.get('error'))[:160]}")
+    return pred["output"]
+
+
+def _fetch_img(url):
+    import urllib.request
+    return Image.open(io.BytesIO(urllib.request.urlopen(url, timeout=180).read())).convert("RGB")
+
+
+def esrgan_tiled(img, token, target_long):
+    """
+    Upscale to target_long honouring the 1448px ESRGAN input cap: at each x4
+    level, tile the input into <=1448px tiles (with overlap), ESRGAN each, and
+    feather-blend the seams. Repeat levels until >= target, then exact LANCZOS.
+    Pure-LANCZOS fallback when no token.
+    """
+    img = img.convert("RGB")
+    if not token:
+        s = target_long / max(img.size)
+        return img.resize((round(img.size[0]*s), round(img.size[1]*s)), Image.LANCZOS), "lanczos-only"
+    # Only run an x4 level while we're still under half target — a 4x jump then
+    # lands <=~2x target; LANCZOS the small remainder. Prevents huge overshoot
+    # (e.g. 1080->4320->17280, then LANCZOS->21600 — never 69120).
+    levels = 0
+    while max(img.size) * 2 < target_long and levels < 4:
+        if max(img.size) <= _ESRGAN_MAX_IN:
+            img = _bt._esrgan_x4(img, token); levels += 1; continue
+        img = _esrgan_x4_tiled(img, token); levels += 1
+    cw, ch = img.size
+    s = target_long / max(cw, ch)
+    if abs(s - 1) > 1e-3:
+        img = img.resize((round(cw*s), round(ch*s)), Image.LANCZOS)
+    return img, f"esrgan-tiled x{levels} + lanczos"
+
+
+def _esrgan_x4_tiled(img, token, tile=1200, overlap=128):
+    """One x4 level via overlapping tiles, feather-blended (no visible seams)."""
+    from PIL import ImageDraw
+    W, H = img.size
+    out = Image.new("RGB", (W*4, H*4))
+    acc = Image.new("L", (W*4, H*4), 0)  # coverage for feather normalisation
+    step = tile - overlap
+    xs = list(range(0, max(1, W - overlap), step))
+    ys = list(range(0, max(1, H - overlap), step))
+    for ty in ys:
+        for tx in xs:
+            x0, y0 = tx, ty
+            x1, y1 = min(tx + tile, W), min(ty + tile, H)
+            piece = img.crop((x0, y0, x1, y1))
+            up = _bt._esrgan_x4(piece, token)  # piece <= 1200 < 1448 cap
+            pw, ph = up.size
+            # feather mask: ramp in over the overlap on each interior edge
+            m = Image.new("L", (pw, ph), 255); d = ImageDraw.Draw(m)
+            ov = overlap * 4
+            for i in range(ov):
+                a = int(255 * (i / max(1, ov)))
+                if x0 > 0:  d.line((i, 0, i, ph), fill=a)
+                if y0 > 0:  d.line((0, i, pw, i), fill=a)
+            out.paste(up, (x0*4, y0*4), m)
+    return out
+
+
+def outpaint_bleed(focal, bleed_px, top_prompt, bottom_prompt, token):
+    """
+    Extend the focal by bleed_px top+bottom using SDXL inpainting (lucataco/
+    sdxl-inpainting, latest). Builds a canvas with empty bands + a white mask
+    over them and lets the model paint sky (top) / ground (bottom). Falls back
+    to make_bleed() mirror+fade if no token or the call fails. Runs at LOW res
+    (model size limit) — caller upscales the whole composition afterwards.
+    """
+    w, h = focal.size
+    canvas = Image.new("RGB", (w, h + 2*bleed_px), (128, 128, 128))
+    canvas.paste(focal, (0, bleed_px))
+    # seed the bands with the focal edge colour so the model has context
+    from PIL import ImageStat
+    top_c = tuple(int(c) for c in ImageStat.Stat(focal.crop((0, 0, w, max(8, bleed_px//3)))).mean[:3])
+    bot_c = tuple(int(c) for c in ImageStat.Stat(focal.crop((0, h-max(8, bleed_px//3), w, h))).mean[:3])
+    canvas.paste(Image.new("RGB", (w, bleed_px), top_c), (0, 0))
+    canvas.paste(Image.new("RGB", (w, bleed_px), bot_c), (0, h + bleed_px))
+    mask = Image.new("L", (w, h + 2*bleed_px), 0)
+    mask.paste(Image.new("L", (w, bleed_px), 255), (0, 0))
+    mask.paste(Image.new("L", (w, bleed_px), 255), (0, h + bleed_px))
+    if not token:
+        return assemble_master(focal, bleed_px)
+    try:
+        import base64, io as _io
+        def b64(im, fmt="PNG"):
+            b = _io.BytesIO(); im.save(b, format=fmt)
+            return "data:image/png;base64," + base64.b64encode(b.getvalue()).decode()
+        out = _replicate_run_latest("lucataco/sdxl-inpainting", {
+            "image": b64(canvas), "mask": b64(mask),
+            "prompt": f"{top_prompt} above, {bottom_prompt} below, soft natural "
+                      "continuation of the scene, muted screen-print tones, no animals, no text",
+            "negative_prompt": "birds, butterflies, people, text, watermark, frame, border",
+            "num_inference_steps": 25, "guidance_scale": 7,
+        }, token)
+        url = out[0] if isinstance(out, list) else out
+        painted = _fetch_img(url).resize(canvas.size, Image.LANCZOS)
+        # keep the original focal crisp; only take the painted bands
+        painted.paste(focal, (0, bleed_px))
+        return painted
+    except Exception as e:
+        print(f"    outpaint failed ({e}); mirror+fade fallback", file=sys.stderr)
+        return assemble_master(focal, bleed_px)
+
+
 def slice_panels(master, panel_in, dpi=DPI):
     """
     Slice the master into vertical panels panel_in inches wide (full height).
@@ -144,7 +271,8 @@ def slice_panels(master, panel_in, dpi=DPI):
 
 
 # ── full real build ─────────────────────────────────────────────────────────
-def build(design_id, print_ft, height_ft, bleed_in, panel_widths, force=False):
+def build(design_id, print_ft, height_ft, bleed_in, panel_widths,
+          bleed_method="outpaint", upscale_method="tiled", 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
@@ -158,6 +286,7 @@ def build(design_id, print_ft, height_ft, bleed_in, panel_widths, force=False):
     src = _bt.resolve_source(d)
     if not src:
         print(f"design {design_id}: source PNG not found", file=sys.stderr); return 4
+    token = _bt._replicate_token()
 
     out_dir = MURAL_DIR / str(design_id)
     out_dir.mkdir(parents=True, exist_ok=True)
@@ -169,15 +298,31 @@ def build(design_id, print_ft, height_ft, bleed_in, panel_widths, force=False):
         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]}")
+        # Order is forced by model size limits: OUTPAINT must run at low res
+        # (SDXL inpaint can't do 21k px), so compose at low res then upscale the
+        # whole focal+bleed together for consistent detail.
+        LR_W = 1080
+        lr_fw, lr_fh = LR_W, round(LR_W * focal_h / focal_w)        # 1080 x 900 (6:5)
+        lr_bleed = max(8, round(bleed_px * LR_W / master_w))        # 900*1080/21600 = 45
+        focal_lr = cover_fit(img, lr_fw, lr_fh)
+        if bleed_method == "outpaint":
+            master_lr = outpaint_bleed(focal_lr, lr_bleed,
+                                       "clear soft sky with gentle clouds",
+                                       "grassy ground meadow", token)
+            bmeth = "sdxl-outpaint" if token else "mirror(no-token)"
+        elif bleed_method == "mirror":
+            master_lr = assemble_master(focal_lr, lr_bleed); bmeth = "mirror+fade"
+        else:
+            master_lr = assemble_master(focal_lr, lr_bleed); bmeth = "mirror+fade"
+        print(f"  low-res compose {master_lr.size[0]}x{master_lr.size[1]} "
+              f"(focal {lr_fw}x{lr_fh} + {bleed_in}\" bleed via {bmeth})")
+        if upscale_method == "tiled":
+            master, umeth = esrgan_tiled(master_lr, token, master_w)
+        else:
+            master, umeth = _bt.upscale_mural(master_lr, master_w)
+        if master.size != (master_w, master_h):
+            master = master.resize((master_w, master_h), Image.LANCZOS)
+        print(f"  upscaled via {umeth} → 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")
@@ -208,6 +353,49 @@ def build(design_id, print_ft, height_ft, bleed_in, panel_widths, force=False):
     return 0
 
 
+# ── zero-spend cost estimator ────────────────────────────────────────────────
+def _tiles_1d(n, tile=1200, overlap=128):
+    """Tile count along one axis — mirrors _esrgan_x4_tiled's xs/ys."""
+    step = tile - overlap
+    return len(range(0, max(1, n - overlap), step))
+
+
+def count_esrgan_calls(lr_long, target_long):
+    """Mirror esrgan_tiled's level loop EXACTLY → number of real-esrgan calls."""
+    calls, cur, levels = 0, lr_long, 0
+    while cur * 2 < target_long and levels < 4:
+        calls += 1 if cur <= _ESRGAN_MAX_IN else _tiles_1d(cur) ** 2
+        cur *= 4
+        levels += 1
+    return calls
+
+
+def estimate(print_ft, height_ft, bleed_in, panel_widths, n_murals,
+             esrgan_usd=0.015, inpaint_usd=0.02):
+    """Print a no-spend cost + work estimate for a batch of n_murals."""
+    master_w = int(round(print_ft * 12 * DPI))
+    master_h = int(round(height_ft * 12 * DPI))
+    LR_W = 1080
+    eg = count_esrgan_calls(LR_W, master_w)
+    per_calls = eg + 1                       # +1 SDXL outpaint
+    per_usd = eg * esrgan_usd + inpaint_usd
+    mp = master_w * master_h / 1e6
+    print(f"=== mural-master batch estimate (NO SPEND — arithmetic only) ===")
+    print(f"spec        : {print_ft:g}ft x {height_ft:g}ft @ {DPI} DPI = "
+          f"{master_w:,} x {master_h:,} px ({mp:.0f} MP/master)")
+    print(f"bleed       : {bleed_in:g}\" sky+grass top/bottom (SDXL outpaint, low-res)")
+    print(f"upscale path: 1080 -> {master_w:,} via tiled ESRGAN "
+          f"({eg} real-esrgan calls) + LANCZOS finish")
+    for pw in panel_widths:
+        n = len(slice_panels(Image.new('RGB', (master_w, 8), (0,0,0)), pw))
+        print(f"  panels {pw:g}\" : {n} per mural")
+    print(f"\nper mural   : ~{per_calls} Replicate calls  ~${per_usd:.2f}  "
+          f"(esrgan ${esrgan_usd}/call, inpaint ${inpaint_usd}/call)")
+    print(f"batch x{n_murals} : ~{per_calls*n_murals} calls  ~${per_usd*n_murals:.2f}")
+    print(f"\n(estimates are upper-ish; real-esrgan often <$0.015/call. NO calls made.)")
+    return 0
+
+
 # ── zero-cost geometry proof ─────────────────────────────────────────────────
 def self_test():
     """Validate cover-fit + bleed + slicer with no DB, no network, no $ — at a
@@ -260,15 +448,25 @@ def main():
     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("--bleed", choices=("outpaint", "mirror", "none"), default="outpaint",
+                   help="bleed-band fill: SDXL outpaint (default), mirror+fade, or none")
+    p.add_argument("--upscale", choices=("tiled", "lanczos"), default="tiled",
+                   help="tiled ESRGAN (default, true detail) or single-pass+lanczos")
     p.add_argument("--force", action="store_true")
     p.add_argument("--self-test", action="store_true")
+    p.add_argument("--estimate", type=int, metavar="N",
+                   help="print a no-spend cost estimate for an N-mural batch, then exit")
     a = p.parse_args()
     if a.self_test:
         sys.exit(self_test())
+    if a.estimate is not None:
+        widths = [float(x) for x in a.panels.split(",") if x.strip()]
+        sys.exit(estimate(a.print_ft, a.height_ft, a.bleed_in, widths, a.estimate))
     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))
+    sys.exit(build(a.design_id, a.print_ft, a.height_ft, a.bleed_in, widths,
+                   bleed_method=a.bleed, upscale_method=a.upscale, force=a.force))
 
 
 if __name__ == "__main__":

← 1f7667f yolo loop 2026-06-03: ledger + tick 1 (route sweep clean, /c  ·  back to Wallco Ai  ·  loop tick1: fleet compliance sweep — 0 settlement-block live 2be769b →