β back to Wallco Ai
/designs: replace binary 'In a room' chip with a chip row β All / Any / π Living / π Bedroom / π· Dining / πΌ Office. Each chip shows a count + dims to 45% opacity when count=0. Server filter accepts specific room type (room_mockups[] contains the value) OR binary 'any' (length > 0). Breadcrumb reads 'In a bedroom' etc instead of generic 'In-room previews'
441d3810563e62d1760f86a097c0cee6f38fde9a Β· 2026-05-28 17:50:07 -0700 Β· Steve Abrams
Files touched
A scripts/gen-blank-wall-base.pyA scripts/inpaint-blank-wall-base.pyM scripts/pil-room-composite.pyM server.js
Diff
commit 441d3810563e62d1760f86a097c0cee6f38fde9a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 28 17:50:07 2026 -0700
/designs: replace binary 'In a room' chip with a chip row β All / Any / π Living / π Bedroom / π· Dining / πΌ Office. Each chip shows a count + dims to 45% opacity when count=0. Server filter accepts specific room type (room_mockups[] contains the value) OR binary 'any' (length > 0). Breadcrumb reads 'In a bedroom' etc instead of generic 'In-room previews'
---
scripts/gen-blank-wall-base.py | 99 ++++++++++++++++++++++++++++++++++++++
scripts/inpaint-blank-wall-base.py | 75 +++++++++++++++++++++++++++++
scripts/pil-room-composite.py | 4 +-
server.js | 61 ++++++++++++++++++-----
4 files changed, 224 insertions(+), 15 deletions(-)
diff --git a/scripts/gen-blank-wall-base.py b/scripts/gen-blank-wall-base.py
new file mode 100644
index 0000000..2658772
--- /dev/null
+++ b/scripts/gen-blank-wall-base.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+"""
+gen-blank-wall-base.py β one-time generation of a blank-wall room photo.
+
+Targets Mac1 ComfyUI (http://100.94.103.98:8188) with SDXL base. Outputs a
+1024Β² living room with a CLEAN cream/beige wall that has NO existing
+wallpaper pattern β which makes it a usable template for PIL composite room
+mockups.
+
+Saves to data/rooms/_template_blank_living_room.png. Re-run anytime to
+regenerate (e.g., to try a different room photo).
+"""
+import json, time, urllib.request, urllib.parse, urllib.error
+from pathlib import Path
+
+COMFY = 'http://100.94.103.98:8188'
+ROOT = Path(__file__).resolve().parents[1]
+OUT = ROOT / 'data' / 'rooms' / '_template_blank_living_room.png'
+OUT.parent.mkdir(parents=True, exist_ok=True)
+
+PROMPT = (
+ "photograph of an empty modern living room interior, "
+ "EYE-LEVEL straight-on camera angle looking directly at the back wall, "
+ "huge flat blank cream-colored wall fills the upper two thirds of the frame, "
+ "low gray sofa centered in the lower third with its back to the camera, "
+ "wooden floor visible at the bottom, "
+ "soft natural daylight, "
+ "no pattern no wallpaper no art no posters no shelves on the wall, "
+ "the wall is the main subject, "
+ "architectural digest photography, photoreal, 8k, sharp"
+)
+NEGATIVE = (
+ "wallpaper, pattern, texture on wall, art on wall, posters, shelves, "
+ "high angle, top-down view, aerial view, "
+ "reflective floor, mirror, glass, "
+ "people, text, low quality, blurry, painting, illustration, "
+ "cartoon, drawing, watermark, plant in foreground"
+)
+
+# Minimal SDXL text-to-image ComfyUI workflow.
+WORKFLOW = {
+ "3": {"inputs": {"seed": 8137, "steps": 28, "cfg": 7.0,
+ "sampler_name": "euler", "scheduler": "normal", "denoise": 1.0,
+ "model": ["4", 0], "positive": ["6", 0], "negative": ["7", 0],
+ "latent_image": ["5", 0]},
+ "class_type": "KSampler"},
+ "4": {"inputs": {"ckpt_name": "sd_xl_base_1.0.safetensors"},
+ "class_type": "CheckpointLoaderSimple"},
+ "5": {"inputs": {"width": 1024, "height": 1024, "batch_size": 1},
+ "class_type": "EmptyLatentImage"},
+ "6": {"inputs": {"text": PROMPT, "clip": ["4", 1]}, "class_type": "CLIPTextEncode"},
+ "7": {"inputs": {"text": NEGATIVE, "clip": ["4", 1]}, "class_type": "CLIPTextEncode"},
+ "8": {"inputs": {"samples": ["3", 0], "vae": ["4", 2]}, "class_type": "VAEDecode"},
+ "9": {"inputs": {"filename_prefix": "blank_wall_base", "images": ["8", 0]},
+ "class_type": "SaveImage"},
+}
+
+def post(path, body):
+ req = urllib.request.Request(f'{COMFY}{path}', data=json.dumps(body).encode(),
+ headers={'Content-Type': 'application/json'},
+ method='POST')
+ with urllib.request.urlopen(req, timeout=20) as r:
+ return json.loads(r.read())
+
+def get(path):
+ with urllib.request.urlopen(f'{COMFY}{path}', timeout=20) as r:
+ return r.read()
+
+print(f'[base-gen] submitting blank-wall workflow β {COMFY}')
+resp = post('/prompt', {'prompt': WORKFLOW, 'client_id': 'wallco-base-gen'})
+prompt_id = resp.get('prompt_id')
+print(f' prompt_id={prompt_id}')
+
+# Poll history for completion
+t0 = time.time()
+img_filename = None
+while time.time() - t0 < 180:
+ time.sleep(2)
+ try:
+ hist = json.loads(get(f'/history/{prompt_id}').decode())
+ if prompt_id in hist:
+ outs = hist[prompt_id].get('outputs', {}).get('9', {}).get('images', [])
+ if outs:
+ img_filename = outs[0]['filename']
+ subfolder = outs[0].get('subfolder', '')
+ fold_type = outs[0].get('type', 'output')
+ break
+ except urllib.error.HTTPError:
+ pass
+ print(f' β¦polling ({int(time.time()-t0)}s)')
+
+if not img_filename:
+ print('[fail] timed out waiting for ComfyUI'); exit(1)
+
+print(f' done Β· file={img_filename} Β· {int(time.time()-t0)}s')
+qs = urllib.parse.urlencode({'filename': img_filename, 'subfolder': subfolder, 'type': fold_type})
+data = get(f'/view?{qs}')
+OUT.write_bytes(data)
+print(f'[saved] {OUT} ({len(data)//1024} KB)')
diff --git a/scripts/inpaint-blank-wall-base.py b/scripts/inpaint-blank-wall-base.py
new file mode 100644
index 0000000..1b7e7f5
--- /dev/null
+++ b/scripts/inpaint-blank-wall-base.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+"""
+inpaint-blank-wall-base.py β make a blank-wall living room base template
+by cv2-inpainting the wallpaper off an existing room mockup.
+
+Takes one of the 800 known-good upstream-rendered room PNGs that has a
+clearly-defined wall region, masks the wall area, and uses cv2.inpaint() to
+content-aware-fill it with the surrounding ceiling/floor/sofa colors. Result
+is a real photoreal interior composition with a clean wall ready for PIL
+pasting.
+
+Output: data/rooms/_template_blank_living_room.png
+
+The WALL_QUAD coords are tuned for design_53872's living room (1024Β², the
+same shot we've been using for hero). The inpaint radius is generous (12px)
+so the result reads as a smooth wall rather than a patchy blend.
+"""
+import sys
+from pathlib import Path
+import numpy as np
+import cv2
+
+ROOT = Path(__file__).resolve().parents[1]
+SRC = ROOT / 'data' / 'rooms' / 'design_53872_living_room.png'
+OUT = ROOT / 'data' / 'rooms' / '_template_blank_living_room.png'
+
+# Wall region on the 53872 source (1024x1024). Covers the full chevron
+# wallpapered area: upper wall + sides behind TV + section below TV that
+# fades into the console. The TV+console sit IN FRONT of this region in
+# the original β they'll re-appear via the underlying pixels (we composite
+# them back in pil-room-composite.py via the dark-feature mask).
+WALL_QUAD = np.array([
+ [130, 85],
+ [890, 85],
+ [870, 700],
+ [150, 700],
+], dtype=np.int32)
+
+# BGR cream color (matches a typical interior cream/eggshell wall).
+WALL_FILL = (210, 220, 232) # BGR β #E8DCD2
+
+def main():
+ img = cv2.imread(str(SRC))
+ if img is None:
+ print(f'[fail] cannot read {SRC}'); sys.exit(1)
+ h, w = img.shape[:2]
+
+ # Build the wall mask
+ mask = np.zeros((h, w), dtype=np.uint8)
+ cv2.fillPoly(mask, [WALL_QUAD], 255)
+
+ # cv2.inpaint sampled too much from the dark TV+side neighbors β muddy
+ # wall. Skip inpaint; just paint the wall quad with a uniform cream
+ # color. PIL composite will paste the design pattern onto this cream
+ # rectangle, no surrounding-wallpaper bleed.
+ out = img.copy()
+ cv2.fillPoly(out, [WALL_QUAD], WALL_FILL)
+
+ # Lightly smooth the edge so the cream-to-room transition isn't a hard
+ # cut. 5px feather is enough; the composite will write over the inside
+ # anyway.
+ smooth_mask = cv2.GaussianBlur(mask, (11, 11), 0).astype(np.float32) / 255.0
+ smooth_mask = smooth_mask[..., None]
+ out = (out.astype(np.float32) * smooth_mask + img.astype(np.float32) * (1 - smooth_mask)).astype(np.uint8)
+
+ cv2.imwrite(str(OUT), out, [cv2.IMWRITE_PNG_COMPRESSION, 7])
+ print(f'[saved] {OUT} size={out.shape}')
+
+ # Also save the mask so we can re-use it for the PIL composite step.
+ mask_out = ROOT / 'data' / 'rooms' / '_template_wall_mask.png'
+ cv2.imwrite(str(mask_out), mask)
+ print(f'[mask] {mask_out}')
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/pil-room-composite.py b/scripts/pil-room-composite.py
index c1d0b69..a690b8e 100644
--- a/scripts/pil-room-composite.py
+++ b/scripts/pil-room-composite.py
@@ -25,10 +25,10 @@ ROOT = Path(__file__).resolve().parents[1]
ROOMS_DIR = ROOT / 'data' / 'rooms'
DESIGNS_JSON = ROOT / 'data' / 'designs.json'
-BASE_TEMPLATE = ROOMS_DIR / 'design_53872_living_room.png' # 1024x1024 reference
+BASE_TEMPLATE = ROOMS_DIR / '_template_blank_living_room.png' # cv2-prepared blank wall
# Approx wall quad (visual inspection of the 53872 living room). The mock is
# centered, walls visible. Order: TL, TR, BR, BL in pixel coords on 1024x1024.
-WALL_QUAD = [(150, 95), (875, 95), (855, 470), (170, 470)]
+WALL_QUAD = [(150, 95), (875, 95), (855, 690), (170, 690)]
# Optional: a brightness multiplier to make the pasted pattern feel like wall
# light (the upstream renderer adds soft shadows + lighting falloff; we just
# darken slightly so it doesn't look pasted).
diff --git a/server.js b/server.js
index 90b7968..34ef814 100644
--- a/server.js
+++ b/server.js
@@ -5820,7 +5820,18 @@ app.get('/designs', (req, res) => {
if (cat) filtered = filtered.filter(d => d.category === cat);
if (motifQ) filtered = filtered.filter(d => (d.motifs || []).includes(motifQ));
if (hueQ) filtered = filtered.filter(d => hueBucketOf(d.dominant_hex) === hueQ);
- if (roomQ) filtered = filtered.filter(d => (d.room_mockups || []).length > 0);
+ // Room filter β supports binary ('1'/'any'/'all' = any mockup) AND specific
+ // room types ('living_room', 'bedroom', 'dining_room', 'office'). The
+ // customer-facing chip row at /designs lets buyers narrow to "show me
+ // designs that have a [bedroom] mockup so I can see it in MY context".
+ if (roomQ) {
+ const SPECIFIC = ['living_room','bedroom','dining_room','office'];
+ if (SPECIFIC.includes(roomQ)) {
+ filtered = filtered.filter(d => (d.room_mockups || []).includes(roomQ));
+ } else {
+ filtered = filtered.filter(d => (d.room_mockups || []).length > 0);
+ }
+ }
// Search-alias expansion β typing "bh" / "beverly hills" / "bh90210"
// still resolves the LA collection (and vice-versa). Group-of-synonyms:
// any token in the same row matches any other token in that row.
@@ -6060,7 +6071,8 @@ app.get('/designs', (req, res) => {
hueQ && `${hueQ.charAt(0).toUpperCase() + hueQ.slice(1)} designs`,
motifQ && `${motifQ.charAt(0).toUpperCase() + motifQ.slice(1)} motif`,
cat && `${cat.charAt(0).toUpperCase() + cat.slice(1)}`,
- roomQ && 'In-room previews'
+ roomQ && (roomQ === '1' || roomQ === 'any' || roomQ === 'all' ? 'In-room previews'
+ : `In a ${roomQ.replace('_',' ')}`)
].filter(Boolean);
const _titleHead = _titleCrumbs.length
? `${_titleCrumbs[0]} Β· ${total} design${total===1?'':'s'} β wallco.ai`
@@ -6320,22 +6332,45 @@ ${(req.query.source === 'all') ? `
${topMotifs.map(([m,n]) => `<a href="/designs?${cat?'cat='+encodeURIComponent(cat)+'&':''}motif=${encodeURIComponent(m)}" class="motif-chip${motifQ===m?' is-on':''}" style="font:11px var(--sans);padding:5px 12px;border:1px solid ${motifQ===m?'var(--accent)':'var(--line)'};background:${motifQ===m?'var(--accent)':'transparent'};color:${motifQ===m?'var(--bg)':'var(--ink-soft)'};border-radius:999px;text-decoration:none">${m}</a>`).join('')}
</div>
- <!-- Room-preview filter chip (toggles ?room=1 β shows only designs that have a residential mockup) -->
+ <!-- Room-preview filter chips. "Any room" = binary (any mockup) β keeps
+ the original behavior. The 4 specific chips narrow to designs with
+ that exact room type, so a buyer can browse "what would this look
+ like in MY bedroom?" without sifting through living-room-only sets. -->
${(() => {
- const roomCnt = DESIGNS.filter(d => (d.room_mockups || []).length > 0).length;
- const on = !!roomQ;
- const href = `/designs${[
+ const SPECIFIC_ROOMS = [
+ ['living_room','π Living'], ['bedroom','π Bedroom'],
+ ['dining_room','π· Dining'], ['office','πΌ Office'],
+ ];
+ // Count designs per room type (computed once per request, not per chip)
+ const counts = { __any: DESIGNS.filter(d => (d.room_mockups || []).length > 0).length };
+ for (const [k] of SPECIFIC_ROOMS) {
+ counts[k] = DESIGNS.filter(d => (d.room_mockups || []).includes(k)).length;
+ }
+ const baseQS = [
cat ? `cat=${encodeURIComponent(cat)}` : '',
motifQ? `motif=${encodeURIComponent(motifQ)}`: '',
hueQ ? `hue=${encodeURIComponent(hueQ)}` : '',
- !on ? 'room=1' : ''
- ].filter(Boolean).join('&').replace(/^/, on ? '' : '?')}`;
+ ].filter(Boolean);
+ const chipHref = (rv) => {
+ const qs = baseQS.concat(rv ? [`room=${rv}`] : []).join('&');
+ return `/designs${qs ? '?' + qs : ''}`;
+ };
+ const chip = (label, rv, count, active, title) => {
+ const isOn = active;
+ const dim = count === 0;
+ return `<a href="${chipHref(rv)}" class="motif-chip${isOn?' is-on':''}" title="${title}"
+ style="font:11px var(--sans);padding:5px 12px;border:1px solid ${isOn?'var(--accent)':'var(--line)'};background:${isOn?'var(--accent)':'transparent'};color:${isOn?'var(--bg)':'var(--ink-soft)'};border-radius:999px;text-decoration:none;${dim?'opacity:.45;':''}">
+ ${label}${count != null ? ` <span style="opacity:.7;font-variant-numeric:tabular-nums">${count}</span>` : ''}
+ </a>`;
+ };
+ const noRoomFilter = !roomQ;
return `<div class="motif-chips" style="display:flex;flex-wrap:wrap;gap:6px;margin:0 0 6px;align-items:center">
- <span style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);margin-right:6px">Preview</span>
- <a href="${href}" class="motif-chip${on?' is-on':''}" title="Show only designs with a room mockup preview"
- style="font:11px var(--sans);padding:5px 12px;border:1px solid ${on?'var(--accent)':'var(--line)'};background:${on?'var(--accent)':'transparent'};color:${on?'var(--bg)':'var(--ink-soft)'};border-radius:999px;text-decoration:none">
- In a room
- </a>
+ <span style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);margin-right:6px">See in room</span>
+ ${chip('All', '', null, noRoomFilter, 'Show every design (no room filter)')}
+ ${chip('Any room', '1', counts.__any, roomQ === '1' || roomQ === 'any' || roomQ === 'all', 'Show only designs that have at least one room preview')}
+ ${SPECIFIC_ROOMS.map(([k, lbl]) =>
+ chip(lbl, k, counts[k], roomQ === k, `Show only designs with a ${k.replace('_',' ')} mockup`)
+ ).join('')}
<button type="button" id="fav-only-toggle" class="motif-chip" aria-pressed="false"
style="font:11px var(--sans);padding:5px 12px;border:1px solid var(--line);background:transparent;color:var(--ink-soft);border-radius:999px;cursor:pointer">
β‘ Favorites
β 06db4d7 feat(pdp): V3 Sticky Hero + Tabs theme β /design/:id/tabs +
Β·
back to Wallco Ai
Β·
feat(pdp): V4-V10 theme variants β full 10-variant Theme sub 34281f4 β