[object Object]

← back to Wallco Ai

build-tif: Real-ESRGAN mural upscale stage (DTD verdict A) — large-format scenic murals

8dad76a49e61ccc6a89f85ead172ab591e16bf90 · 2026-06-03 08:13:33 -0700 · Steve Abrams

Mural-kind rows (mural/mural_panel) are now upscaled toward the requested print
size at build time: chained Real-ESRGAN x4 via Replicate (same model+version as
the /api/design/:id/download route), then an exact LANCZOS resize, with a pure-
LANCZOS fallback when no Replicate token. Lets a 1024px scenic mural fulfill a
20ft order instead of capping at ~7" of true 150-DPI print.

Patterns are untouched — they tile infinitely so the native repeat IS the
master (compute_max_print still returns the 240x132" infinite-tile cap for
seamless_tile/pattern_repeat/best_seller_seed). SVG skipped for murals (scenic
art isn't vector-eligible; embedding a 36k raster would be pathological). Bulk
path untouched (tiles-only).

Flags: --no-mural-upscale, --mural-print-ft N (default 12ft, cap 20ft/36000px).
Env: WALLCO_MURAL_UPSCALE=0, WALLCO_MURAL_PRINT_FT, WALLCO_ESRGAN_PASSES.
Free-disk headroom guard before upscaling. Offline-validated: resize math,
aspect preservation, native short-circuit, max-print recompute.

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

Files touched

Diff

commit 8dad76a49e61ccc6a89f85ead172ab591e16bf90
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jun 3 08:13:33 2026 -0700

    build-tif: Real-ESRGAN mural upscale stage (DTD verdict A) — large-format scenic murals
    
    Mural-kind rows (mural/mural_panel) are now upscaled toward the requested print
    size at build time: chained Real-ESRGAN x4 via Replicate (same model+version as
    the /api/design/:id/download route), then an exact LANCZOS resize, with a pure-
    LANCZOS fallback when no Replicate token. Lets a 1024px scenic mural fulfill a
    20ft order instead of capping at ~7" of true 150-DPI print.
    
    Patterns are untouched — they tile infinitely so the native repeat IS the
    master (compute_max_print still returns the 240x132" infinite-tile cap for
    seamless_tile/pattern_repeat/best_seller_seed). SVG skipped for murals (scenic
    art isn't vector-eligible; embedding a 36k raster would be pathological). Bulk
    path untouched (tiles-only).
    
    Flags: --no-mural-upscale, --mural-print-ft N (default 12ft, cap 20ft/36000px).
    Env: WALLCO_MURAL_UPSCALE=0, WALLCO_MURAL_PRINT_FT, WALLCO_ESRGAN_PASSES.
    Free-disk headroom guard before upscaling. Offline-validated: resize math,
    aspect preservation, native short-circuit, max-print recompute.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/build-tif.py | 171 ++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 161 insertions(+), 10 deletions(-)

diff --git a/scripts/build-tif.py b/scripts/build-tif.py
index 69a08a9..c3c063e 100755
--- a/scripts/build-tif.py
+++ b/scripts/build-tif.py
@@ -17,12 +17,22 @@ For seamless tiles, the source PNG IS the master (one repeat). The TIF is
 saved at the source's native resolution + 150 DPI metadata. tif_max_print_w_in
 is set to 240 (20 ft) since the printer tiles infinitely.
 
-For murals, the TIF is saved at native resolution; max_print_w_in is computed
-from source_w_px / 150 DPI. No upscaling on this side.
+For murals (non-tiling single images), the source is upscaled toward the
+requested print size at build time — chained Real-ESRGAN x4 via Replicate
+(same model as the digital-download route), then an exact LANCZOS resize, with
+a pure-LANCZOS fallback when no Replicate token is configured. A 1024px mural
+otherwise caps at ~7" of true 150-DPI print; the upscale stage lets us fulfill
+large-format scenic-mural orders. Patterns are NOT upscaled — they tile
+infinitely so the native repeat IS the 150-DPI master. (DTD verdict 2026-06-03,
+Option A: wallco's flat screen-print art reconstructs cleanly under ESRGAN and
+murals are viewed at 4-8 ft where 100-150 DPI is standard; premium/soft results
+route to native panelized re-gen — the mural_panel path — as the Option-B
+upgrade.) Disable per-build with --no-mural-upscale; size with --mural-print-ft.
 """
 import argparse
 import io
 import os
+import re
 import sys
 import time
 import base64
@@ -180,22 +190,133 @@ def write_svg(img, out_path, src_path, vector_attempted):
     return out_path.stat().st_size, False
 
 
+# ── Real-ESRGAN mural upscaler ──────────────────────────────────────────────
+# Patterns tile infinitely (native repeat = master). Murals are single,
+# non-repeating images: at 1024px a mural caps at ~7" of true 150-DPI print, so
+# to fulfill large-format orders we upscale the source toward the target print
+# size. Mechanism mirrors the digital-download route (server.js
+# /api/design/:id/download): Replicate nightmareai/real-esrgan x4, chained, then
+# an exact LANCZOS resize. Pure-LANCZOS fallback (honest — no invented detail)
+# when no Replicate token is set.
+REPLICATE_ESRGAN_VERSION = (
+    "f121d640bd286e1fdc67f9799164c1d5be36ff74576ee11c803ae5b665dd46aa")
+MURAL_KINDS = ("mural", "mural_panel")        # non-tiling, single-image designs
+TILE_KINDS = ("seamless_tile", "pattern_repeat", "best_seller_seed")
+MURAL_DEFAULT_PRINT_FT = float(os.environ.get("WALLCO_MURAL_PRINT_FT", "12"))
+MURAL_MAX_LONG_PX = 36000                      # hard cap (20 ft @ 150 DPI)
+ESRGAN_MAX_PASSES = int(os.environ.get("WALLCO_ESRGAN_PASSES", "2"))
+
+
+def _replicate_token():
+    t = (os.environ.get("REPLICATE_API_TOKEN") or "").strip()
+    if t:
+        return t
+    envf = Path.home() / "Projects/animate-museum-posts/.env"
+    if envf.exists():
+        m = re.search(r"^REPLICATE_API_TOKEN=(\S+)", envf.read_text(), re.M)
+        if m:
+            return m.group(1)
+    return ""
+
+
+def _esrgan_x4(img, token):
+    """One Real-ESRGAN x4 pass via Replicate. Returns a PIL RGB image or raises."""
+    import urllib.request
+    import json as _json
+    buf = io.BytesIO()
+    img.save(buf, format="PNG")
+    b64 = "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
+    req = urllib.request.Request(
+        "https://api.replicate.com/v1/predictions",
+        data=_json.dumps({
+            "version": REPLICATE_ESRGAN_VERSION,
+            "input": {"image": b64, "scale": 4, "face_enhance": False},
+        }).encode(),
+        headers={"Authorization": "Bearer " + token,
+                 "Content-Type": "application/json"})
+    pred = _json.loads(urllib.request.urlopen(req, timeout=60).read())
+    pid = pred.get("id")
+    if not pid:
+        raise RuntimeError("esrgan start failed: " + str(pred)[:200])
+    t0 = time.time()
+    while time.time() - t0 < 600:
+        time.sleep(4)
+        poll = urllib.request.Request(
+            "https://api.replicate.com/v1/predictions/" + pid,
+            headers={"Authorization": "Bearer " + token})
+        pr = _json.loads(urllib.request.urlopen(poll, timeout=60).read())
+        st = pr.get("status")
+        if st == "succeeded":
+            out = pr.get("output")
+            url = out[0] if isinstance(out, list) else out
+            data = urllib.request.urlopen(url, timeout=180).read()
+            return Image.open(io.BytesIO(data)).convert("RGB")
+        if st in ("failed", "canceled"):
+            raise RuntimeError("esrgan " + st + ": " + str(pr.get("error") or "")[:200])
+    raise RuntimeError("esrgan timeout")
+
+
+def upscale_mural(img, target_long_px):
+    """
+    Upscale a mural source toward target_long_px on its long edge: chained
+    Real-ESRGAN x4 (≤ ESRGAN_MAX_PASSES) until ≥ target, then an exact LANCZOS
+    resize to the target. Pure-LANCZOS fallback when Replicate is unavailable.
+    Returns (upscaled_img, method_str).
+    """
+    img = img.convert("RGB")
+    if max(img.size) >= target_long_px:
+        return img, "native (already ≥ target)"
+    token = _replicate_token()
+    method_parts = []
+    cur = img
+    if token:
+        passes = 0
+        while max(cur.size) < target_long_px and passes < ESRGAN_MAX_PASSES:
+            try:
+                cur = _esrgan_x4(cur, token)
+                passes += 1
+                method_parts.append("esrgan-x4")
+                print(f"    esrgan pass {passes} → {cur.size[0]}×{cur.size[1]} px",
+                      flush=True)
+            except Exception as e:
+                print(f"    esrgan pass {passes+1} failed ({e}); "
+                      f"finishing with LANCZOS", file=sys.stderr)
+                break
+    else:
+        print("    no REPLICATE_API_TOKEN → pure-LANCZOS upscale "
+              "(interpolated, no ESRGAN-reconstructed detail)", file=sys.stderr)
+    cw, ch = cur.size
+    scale = target_long_px / max(cw, ch)
+    if abs(scale - 1.0) > 1e-3:
+        cur = cur.resize((round(cw * scale), round(ch * scale)), Image.LANCZOS)
+        method_parts.append(f"lanczos→{max(cur.size)}px")
+    return cur, (" + ".join(method_parts) if method_parts else "lanczos")
+
+
 def compute_max_print(kind, w_px, h_px):
     """Max printable size at 150 DPI."""
-    if kind in ("seamless_tile", "pattern_repeat", "best_seller_seed"):
+    if kind in TILE_KINDS:
         # Tiles infinitely — printer can fill any wall.
         return MURAL_MAX_W_FT * 12, MURAL_MAX_H_FT * 12  # 240" × 132"
-    # Mural: bound by source resolution / DPI.
+    # Mural: bound by (post-upscale) source resolution / DPI.
     return round(w_px / DPI, 2), round(h_px / DPI, 2)
 
 
-def build_one(design_id, force=False, check_only=False, force_svg=None):
+def build_one(design_id, force=False, check_only=False, force_svg=None,
+              mural_upscale=None, mural_print_ft=None):
     # force_svg: write an SVG even when the design isn't crisp/vector-eligible
     # (raster-embed fallback). Steve's rule: every SKU gets an SVG file. Default
     # pulls from WALLCO_FORCE_SVG so the generator hook can opt every new pattern
     # in without changing the signature for unrelated callers.
     if force_svg is None:
         force_svg = os.environ.get("WALLCO_FORCE_SVG") == "1"
+    # mural_upscale: for mural-kind rows, upscale toward the print size at build
+    # time (default ON; disable per-build with --no-mural-upscale or
+    # WALLCO_MURAL_UPSCALE=0). mural_print_ft sizes the target long edge.
+    if mural_upscale is None:
+        mural_upscale = os.environ.get("WALLCO_MURAL_UPSCALE", "1") != "0"
+    if mural_print_ft is None:
+        mural_print_ft = MURAL_DEFAULT_PRINT_FT
     if free_gb() < MIN_FREE_GB:
         print(f"REFUSE: only {free_gb():.1f} GB free (need ≥ {MIN_FREE_GB}).",
               file=sys.stderr)
@@ -235,9 +356,29 @@ def build_one(design_id, force=False, check_only=False, force_svg=None):
     t0 = time.time()
     img = Image.open(src)
     w_px, h_px = img.size
-    max_w_in, max_h_in = compute_max_print(d.get("kind"), w_px, h_px)
-
-    print(f"design {design_id} ({d.get('kind')}) → {w_px}×{h_px} px → "
+    kind = d.get("kind")
+
+    # Mural upscale stage — single non-repeating images only. Patterns tile, so
+    # their native repeat is already the 150-DPI master and are never upscaled.
+    if kind in MURAL_KINDS and mural_upscale:
+        target_long = min(MURAL_MAX_LONG_PX, round(mural_print_ft * 12 * DPI))
+        if max(w_px, h_px) < target_long:
+            # Rough headroom: final RGB buffer + an in-flight working copy.
+            need_gb = (target_long * target_long * 3 * 2) / (1024**3)
+            if free_gb() < max(MIN_FREE_GB, need_gb):
+                print(f"REFUSE mural-upscale: need ~{need_gb:.1f} GB headroom, "
+                      f"only {free_gb():.1f} GB free on {TIF_DIR}", file=sys.stderr)
+                conn.close()
+                return 2
+            print(f"  mural upscale → target {target_long}px long edge "
+                  f"(~{mural_print_ft:.0f} ft @ {DPI} DPI)…")
+            img, mural_method = upscale_mural(img, target_long)
+            w_px, h_px = img.size
+            print(f"  ✓ upscaled to {w_px}×{h_px} px via {mural_method}")
+
+    max_w_in, max_h_in = compute_max_print(kind, w_px, h_px)
+
+    print(f"design {design_id} ({kind}) → {w_px}×{h_px} px → "
           f"max print {max_w_in}\"×{max_h_in}\" at {DPI} DPI")
 
     tif_bytes = write_tif(img, tif_path)
@@ -250,10 +391,12 @@ def build_one(design_id, force=False, check_only=False, force_svg=None):
 
     svg_bytes = None
     svg_is_vector = False
-    if eligible or force_svg:
+    if (eligible or force_svg) and kind not in MURAL_KINDS:
         # Attempt real vectorization only when the design is crisp enough to be
         # worth it; otherwise (force_svg on a fuzzy design) go straight to the
         # raster-embed SVG so every SKU still gets a scalable .svg file.
+        # Murals are skipped: scenic art isn't vector-eligible and an SVG
+        # embedding the upscaled (up to 36k px) raster would be pathological.
         svg_bytes, svg_is_vector = write_svg(img, svg_path, src, vector_attempted=eligible)
         print(f"  ✓ SVG  {svg_path}  {svg_bytes:,} bytes "
               f"({'real vector' if svg_is_vector else 'embedded raster — install vtracer for real vectors'})")
@@ -402,6 +545,12 @@ def main():
     p.add_argument("--check", action="store_true")
     p.add_argument("--force-svg", dest="force_svg", action="store_true",
                    help="Always emit an SVG (raster-embed fallback) even for fuzzy/non-crisp designs")
+    p.add_argument("--no-mural-upscale", dest="mural_upscale", action="store_false",
+                   default=None,
+                   help="Skip the Real-ESRGAN mural upscale (mural-kind rows are upscaled by default)")
+    p.add_argument("--mural-print-ft", dest="mural_print_ft", type=float, default=None,
+                   help=f"Target mural print width in feet (default {MURAL_DEFAULT_PRINT_FT:.0f}, "
+                        f"capped at {MURAL_MAX_LONG_PX//DPI//12} ft / {MURAL_MAX_LONG_PX}px @ {DPI} DPI)")
     p.add_argument("--bulk-eligible-only", action="store_true",
                    help="Scan all published designs; write TIF+SVG only for those ≤8 dominant colors")
     p.add_argument("--rebuild", action="store_true",
@@ -413,7 +562,9 @@ def main():
     if args.design_id is None:
         p.error("design_id is required (or pass --bulk-eligible-only)")
     sys.exit(build_one(args.design_id, force=args.force, check_only=args.check,
-                       force_svg=(args.force_svg or None)))
+                       force_svg=(args.force_svg or None),
+                       mural_upscale=args.mural_upscale,
+                       mural_print_ft=args.mural_print_ft))
 
 
 if __name__ == "__main__":

← 2ef1803 LA Toile: stage+publish 30 Nano Banana toiles to live (peer-  ·  back to Wallco Ai  ·  detectors: handle Nano Banana C2PA JPEG (b64 magic /9j/, pat 46bc416 →