[object Object]

← back to Designer Wallcoverings

Reusable local room-render pipeline (any DW catalog)

16b100211f231d57c6503e409a89caaf47e0999e · 2026-07-07 08:28:11 -0700 · Steve

Generalize the Carl Robinson bake-off into a parameterized, catalog-agnostic tool:
- gen_manifest.sh: build sku|color|material|swatch manifest from any catalog's local_image_path
- batch_kontext.sh / batch_sdxl.py: env-parameterized (CATALOG/VIEW/OUTDIR/BASE_ROOM/WALL_MASK)
- render_book.sh: one-shot orchestrator (ENGINES="kontext sdxl")
- README: setup + run + judge + finalize + gated-Shopify note

Replaces paid Gemini room API (~$0.014/room) with $0 local for all future books.

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

Files touched

Diff

commit 16b100211f231d57c6503e409a89caaf47e0999e
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 7 08:28:11 2026 -0700

    Reusable local room-render pipeline (any DW catalog)
    
    Generalize the Carl Robinson bake-off into a parameterized, catalog-agnostic tool:
    - gen_manifest.sh: build sku|color|material|swatch manifest from any catalog's local_image_path
    - batch_kontext.sh / batch_sdxl.py: env-parameterized (CATALOG/VIEW/OUTDIR/BASE_ROOM/WALL_MASK)
    - render_book.sh: one-shot orchestrator (ENGINES="kontext sdxl")
    - README: setup + run + judge + finalize + gated-Shopify note
    
    Replaces paid Gemini room API (~$0.014/room) with $0 local for all future books.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .../room-engines-generic/README.md                 | 42 ++++++++++++++++++
 .../room-engines-generic/batch_kontext.sh          | 32 ++++++++++++++
 .../room-engines-generic/batch_sdxl.py             | 51 ++++++++++++++++++++++
 .../room-engines-generic/gen_manifest.sh           | 21 +++++++++
 .../room-engines-generic/render_book.sh            | 26 +++++++++++
 5 files changed, 172 insertions(+)

diff --git a/scripts/wallquest-refresh/room-engines-generic/README.md b/scripts/wallquest-refresh/room-engines-generic/README.md
new file mode 100644
index 00000000..7b2d346c
--- /dev/null
+++ b/scripts/wallquest-refresh/room-engines-generic/README.md
@@ -0,0 +1,42 @@
+# Local room-setting rendering — reusable, $0
+
+Generate product room mockups **locally on the M3 Ultra** for any DW/WallQuest catalog,
+replacing the paid Gemini room API (~$0.014/room). See memory `local-room-render-pipeline`.
+
+## Engines (bake-off winner = FLUX Kontext)
+- **FLUX Kontext** (`batch_kontext.sh`) — mflux/MLX, non-gated `akx/FLUX.1-Kontext-dev-mflux-4bit`.
+  Photoreal, full-wall, no seams. ~5 min/room. Texture is prompt-described (generic weave). **Default.**
+- **SDXL + IP-Adapter** (`batch_sdxl.py`) — diffusers/MPS. Injects the EXACT swatch (ViT-H CLIP-Vision).
+  ~37 s/room but coverage is patchy — use only when exact-texture fidelity is required.
+- **PIL** — free/instant baseline, flat + seams (reference only).
+
+## One-time setup
+```sh
+python3.12 -m venv /tmp/flux-venv && /tmp/flux-venv/bin/pip install mflux         # FLUX (MLX)
+python3.12 -m venv /tmp/sdxl-venv && /tmp/sdxl-venv/bin/pip install torch torchvision diffusers transformers accelerate safetensors
+```
+(system python 3.14 is too new for torch/mflux — use 3.12). First run downloads weights (throttled on
+unauthenticated HF; one-time). Ensure launchd `com.steve.keepawake` is alive for multi-hour runs.
+
+## Run a book
+```sh
+CATALOG=daisy_bennett_catalog \
+VIEW=/path/to/<book>-viewer \                 # dir containing swatch img/<sku>.jpg
+OUTDIR=/path/to/<book>-viewer/rooms-local \   # {sku}-kontext.png / {sku}-sdxl.png
+BASE_ROOM=.../bakeoff-assets/empty-room.png \
+WALL_MASK=.../bakeoff-assets/wall-mask.png \  # only needed for sdxl
+ENGINES="kontext sdxl" \
+zsh render_book.sh
+```
+Manifest is auto-built from the catalog's `local_image_path`. Batches are idempotent + resumable.
+
+## Judge + finalize (local, $0)
+- `judge_rooms.py` — qwen2.5vl (Ollama) COMPARATIVE judge → per-SKU winner (`judgments.json`).
+  Comparative ranking is essential; absolute scoring rates everything ~9.5.
+- `compare-rooms.html` (via `build_comparison.py`) — swatch│Gemini│Kontext│SDXL + 🏆 auto-pick + manual pick.
+- `apply_winners.py` — writes chosen room into `room_setting_images` + `room_variants` +
+  `chosen_room_engine` (manual `picks.json` > auto-judge > kontext > gemini). Local DB only.
+
+## Then (GATED)
+Pushing the chosen room image to the live Shopify products is a customer-facing write — Steve runs it
+via `!`. Never push live from the agent.
diff --git a/scripts/wallquest-refresh/room-engines-generic/batch_kontext.sh b/scripts/wallquest-refresh/room-engines-generic/batch_kontext.sh
new file mode 100644
index 00000000..32539f32
--- /dev/null
+++ b/scripts/wallquest-refresh/room-engines-generic/batch_kontext.sh
@@ -0,0 +1,32 @@
+#!/bin/zsh
+# Generic FLUX Kontext room batch (mflux, MLX). $0 local. Idempotent + resumable.
+# Env: MANIFEST (sku|color|material|swatch), VIEW (dir holding swatch rel paths),
+#      ROOM (empty-room base png), OUTDIR (writes {sku}-kontext.png), MFLUX_BIN,
+#      STEPS(24) GUIDANCE(2.5). Renders a plausible material texture from the prompt.
+set -u
+: ${MANIFEST:?}; : ${VIEW:?}; : ${ROOM:?}; : ${OUTDIR:?}
+MFLUX_BIN=${MFLUX_BIN:-/tmp/cr-room-bakeoff/flux-venv/bin/mflux-generate-kontext}
+STEPS=${STEPS:-24}; GUIDANCE=${GUIDANCE:-2.5}
+mkdir -p "$OUTDIR"
+tex() { case "$1" in
+  Grasscloth) echo "hand-woven grasscloth with horizontal natural plant fibers";;
+  Paperweave) echo "woven paperweave with a fine crosshatch texture";;
+  Raffia) echo "natural raffia palm-fiber weave";; Netting) echo "open natural netting weave";;
+  Bamboo) echo "vertical bamboo-reed texture";; Sisal) echo "coarse sisal woven fiber";;
+  Cork) echo "natural cork with fine speckled grain";; Fiber) echo "natural plant-fiber weave";;
+  Jute) echo "coarse jute burlap weave";; Mica) echo "mica-flake mineral shimmer";;
+  Nonwoven) echo "raw-silk textured nonwoven";; Silk) echo "fine silk weave with soft sheen";;
+  "Wood Veneer") echo "thin natural wood-veneer grain";; *) echo "natural woven $1 texture";; esac; }
+n=0; done=0; skip=0; total=$(grep -c . "$MANIFEST")
+while IFS='|' read -r sku color mat img; do
+  [ -z "$sku" ] && continue; n=$((n+1))
+  t="$OUTDIR/${sku}-kontext.png"
+  [ -f "$t" ] && { skip=$((skip+1)); echo "[$n/$total] skip $sku"; continue; }
+  p="Cover the large blank accent wall with natural ${mat} wallcovering in a soft ${color} off-white tone, $(tex "$mat"), subtle organic color variation. Keep the sofa, wood floor, side table, lamp, soft daylight and composition exactly the same. Photorealistic interior design photograph."
+  echo "$p" > /tmp/_gkp.txt
+  echo "[$n/$total] kontext $sku ($mat/$color)"
+  "$MFLUX_BIN" -m akx/FLUX.1-Kontext-dev-mflux-4bit --base-model dev --steps "$STEPS" \
+    --guidance "$GUIDANCE" --image-path "$ROOM" --prompt-file /tmp/_gkp.txt --output "$t" \
+    >> "$OUTDIR/../batch_kontext.log" 2>&1 && done=$((done+1)) || echo "  FAIL $sku"
+done < "$MANIFEST"
+echo "KONTEXT DONE: rendered=$done skipped=$skip of $total"
diff --git a/scripts/wallquest-refresh/room-engines-generic/batch_sdxl.py b/scripts/wallquest-refresh/room-engines-generic/batch_sdxl.py
new file mode 100644
index 00000000..868cda84
--- /dev/null
+++ b/scripts/wallquest-refresh/room-engines-generic/batch_sdxl.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+# Generic SDXL-inpaint + IP-Adapter room batch. Injects each SKU's OWN swatch (exact
+# texture) into the masked wall. $0 local (torch MPS). Idempotent + resumable.
+# Env: MANIFEST, VIEW, OUTDIR, BASE_ROOM (empty room png), WALL_MASK (png),
+#      IP_SCALE(1.0), STEPS(30). Loads the pipeline ONCE, loops the manifest.
+import os, torch
+from PIL import Image
+from diffusers import StableDiffusionXLInpaintPipeline
+from transformers import CLIPVisionModelWithProjection
+
+MAN = os.environ['MANIFEST']; VIEW = os.environ['VIEW']; OUT = os.environ['OUTDIR']
+BASE = os.environ['BASE_ROOM']; MASK = os.environ['WALL_MASK']
+IP_SCALE = float(os.environ.get('IP_SCALE', '1.0')); STEPS = int(os.environ.get('STEPS', '30'))
+os.makedirs(OUT, exist_ok=True)
+DEV = 'mps' if torch.backends.mps.is_available() else 'cpu'
+DT = torch.float16 if DEV == 'mps' else torch.float32
+
+room = Image.open(BASE).convert('RGB').resize((1024, 1024))
+mask = Image.open(MASK).convert('L').resize((1024, 1024))
+TEX = {'Grasscloth':'hand-woven grasscloth, horizontal fibers','Paperweave':'woven paperweave crosshatch',
+       'Raffia':'raffia palm-fiber weave','Netting':'open netting weave','Bamboo':'vertical bamboo reed',
+       'Sisal':'coarse sisal fiber','Cork':'cork speckled grain','Fiber':'natural plant fiber',
+       'Jute':'coarse jute burlap','Mica':'mica mineral shimmer','Nonwoven':'raw silk nonwoven',
+       'Silk':'fine silk weave','Wood Veneer':'wood veneer grain'}
+
+print('loading ViT-H encoder + SDXL inpaint + IP-Adapter…', flush=True)
+enc = CLIPVisionModelWithProjection.from_pretrained('h94/IP-Adapter', subfolder='models/image_encoder', torch_dtype=DT).to(DEV)
+pipe = StableDiffusionXLInpaintPipeline.from_pretrained(
+    'diffusers/stable-diffusion-xl-1.0-inpainting-0.1', torch_dtype=DT, variant='fp16', image_encoder=enc).to(DEV)
+pipe.load_ip_adapter('h94/IP-Adapter', subfolder='sdxl_models', weight_name='ip-adapter_sdxl_vit-h.safetensors')
+pipe.set_ip_adapter_scale(IP_SCALE); pipe.set_progress_bar_config(disable=True)
+
+rows = [l.rstrip('\n').split('|') for l in open(MAN) if l.strip()]
+total = len(rows); done = skip = fail = 0
+for i, (sku, color, mat, img) in enumerate(rows, 1):
+    t = f'{OUT}/{sku}-sdxl.png'
+    if os.path.exists(t): skip += 1; print(f'[{i}/{total}] skip {sku}', flush=True); continue
+    sw = f'{VIEW}/{img}'
+    if not os.path.exists(sw): fail += 1; print(f'[{i}/{total}] MISS swatch {sku}', flush=True); continue
+    prompt = (f'natural {mat} wallcovering in a soft {color} off-white tone on the wall, '
+              f'{TEX.get(mat, mat.lower()+" texture")}, organic texture, realistic lighting, photorealistic')
+    neg = 'framed, poster, border, blurry, seams, tiling artifacts, furniture changed, blank wall'
+    g = torch.Generator(device='cpu').manual_seed(7)
+    try:
+        out = pipe(prompt=prompt, negative_prompt=neg, image=room, mask_image=mask,
+                   ip_adapter_image=Image.open(sw).convert('RGB'), strength=0.98, guidance_scale=6.0,
+                   num_inference_steps=STEPS, generator=g).images[0]
+        out.save(t); done += 1; print(f'[{i}/{total}] ok {sku}', flush=True)
+    except Exception as e:
+        fail += 1; print(f'[{i}/{total}] FAIL {sku}: {str(e)[:100]}', flush=True)
+print(f'SDXL DONE: rendered={done} skipped={skip} failed={fail} of {total}', flush=True)
diff --git a/scripts/wallquest-refresh/room-engines-generic/gen_manifest.sh b/scripts/wallquest-refresh/room-engines-generic/gen_manifest.sh
new file mode 100644
index 00000000..25fe8d44
--- /dev/null
+++ b/scripts/wallquest-refresh/room-engines-generic/gen_manifest.sh
@@ -0,0 +1,21 @@
+#!/bin/zsh
+# Generic manifest generator for local room rendering. Emits sku|color|material|swatch-rel-path
+# for ANY DW catalog table that has a swatch path in local_image_path (jsonb ["img/..."]).
+# Env:
+#   CATALOG   catalog table (required, e.g. carl_robinson_catalog)
+#   SKU_COL   default dw_sku
+#   COLOR_COL default color_name
+#   MAT_COL   default material
+#   IMG_COL   default local_image_path (jsonb array; element 0 used)
+#   MANIFEST  output path (default /tmp/<catalog>-manifest.tsv)
+#   DATABASE_URL  default postgresql://dw_admin@127.0.0.1:5432/dw_unified
+set -u
+: ${CATALOG:?set CATALOG=<table>}
+SKU_COL=${SKU_COL:-dw_sku}; COLOR_COL=${COLOR_COL:-color_name}; MAT_COL=${MAT_COL:-material}
+IMG_COL=${IMG_COL:-local_image_path}
+CONN=${DATABASE_URL:-postgresql://dw_admin@127.0.0.1:5432/dw_unified}
+MANIFEST=${MANIFEST:-/tmp/${CATALOG}-manifest.tsv}
+psql "$CONN" -tAc "SELECT ${SKU_COL}, COALESCE(${COLOR_COL},''), COALESCE(${MAT_COL},''), \
+  COALESCE((${IMG_COL}::jsonb)->>0,'') FROM ${CATALOG} \
+  WHERE (${IMG_COL}::jsonb)->>0 IS NOT NULL ORDER BY ${SKU_COL}" > "$MANIFEST"
+echo "manifest -> $MANIFEST ($(grep -c . "$MANIFEST") rows)"
diff --git a/scripts/wallquest-refresh/room-engines-generic/render_book.sh b/scripts/wallquest-refresh/room-engines-generic/render_book.sh
new file mode 100644
index 00000000..0cd8c1cf
--- /dev/null
+++ b/scripts/wallquest-refresh/room-engines-generic/render_book.sh
@@ -0,0 +1,26 @@
+#!/bin/zsh
+# One-shot: render local room settings for an entire catalog with FLUX Kontext (+ optional
+# SDXL), then judge + finalize. $0 local. See README.md.
+# Required env: CATALOG, VIEW (dir with swatch img/), OUTDIR (rooms-local), BASE_ROOM, WALL_MASK
+# Optional: ENGINES="kontext sdxl" (default "kontext"), MANIFEST
+set -u
+: ${CATALOG:?}; : ${VIEW:?}; : ${OUTDIR:?}; : ${BASE_ROOM:?}
+ENGINES=${ENGINES:-kontext}
+export MANIFEST=${MANIFEST:-/tmp/${CATALOG}-manifest.tsv}
+HERE=${0:A:h}
+echo "== manifest =="; CATALOG="$CATALOG" MANIFEST="$MANIFEST" zsh "$HERE/gen_manifest.sh"
+mkdir -p "$OUTDIR"
+if [[ "$ENGINES" == *kontext* ]]; then
+  echo "== kontext batch (bg) =="
+  MANIFEST="$MANIFEST" VIEW="$VIEW" ROOM="$BASE_ROOM" OUTDIR="$OUTDIR" \
+    nohup zsh "$HERE/batch_kontext.sh" > "$OUTDIR/../render_kontext.out" 2>&1 &
+  echo "  kontext PID $!"
+fi
+if [[ "$ENGINES" == *sdxl* ]]; then
+  : ${WALL_MASK:?sdxl needs WALL_MASK}
+  echo "== sdxl batch (bg) =="
+  MANIFEST="$MANIFEST" VIEW="$VIEW" OUTDIR="$OUTDIR" BASE_ROOM="$BASE_ROOM" WALL_MASK="$WALL_MASK" \
+    nohup python3 "$HERE/batch_sdxl.py" > "$OUTDIR/../render_sdxl.out" 2>&1 &
+  echo "  sdxl PID $!"
+fi
+echo "launched. watch: ls $OUTDIR | wc -l ; then judge + apply (see README)."

← 8df04ff8 Local VLM auto-judge + finalize for room bake-off  ·  back to Designer Wallcoverings  ·  Multi-book local-room queue: prep swatches + sequential rend e194baa0 →