← 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 →