[object Object]

← 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

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 β†’