← back to Wallco Ai
generate_room_mockups: add --all-published, --kind-filter, --category-filter, --regen-old, --dry-run
d43deaf0d053a7c645467b043dd153c104cc074c · 2026-05-28 13:53:29 -0700 · Steve Abrams
Implements the CLI surface that task-41 audit recommended (yolo-loop output
41-mural-mockup-coverage-2026-05-28.md). Required to run the mural-mode
regen across the 815-row published cohort without listing IDs by hand.
Flags:
- --all-published iterate every is_published=TRUE row (refuses to run
without --kind-filter or --category-filter — guard
against accidentally re-rendering the full catalog)
- --kind-filter csv restrict to kind IN (csv). CAUTION: 201 published rows
have kind='mural_panel' but a tile-y category
(face-skull-damask, designer-zoo-calm, stripe); they
should NOT be force-rendered in mural-mode.
- --category-filter csv restrict to category LIKE 'X%' OR 'Y%' etc. — safer
scope for the mural batch (matches by category prefix
so colorway sub-cats are included).
- --regen-old re-render existing mockups whose mtime is before
MURAL_MODE_CUTOFF_EPOCH (default 2026-05-28 13:00 PT)
— kills stale tile-mode mockups.
- --dry-run print cohort + cost estimate + sample IDs, run NO
Gemini calls. Mandatory preview before paid batches.
Bug fixes:
- SQL used non-existent columns 'title' and 'saturation' on all_designs
(they only live on the spoon_all_designs view + the snapshot). Switched
to ai_title; sort by id. Memory feedback_psql_execsync_nan_trap noted
that psql -At -q exits 0 on errors, so the bad SQL would have silently
returned 0 rows. Verified by direct psql call.
Dry-run scope previews (this commit):
- --category-filter monterey-mural,cactus-11ft-mural,cactus-pine-scenic,tree-mural
→ 626 designs · $8.76 @ $0.014/call (RECOMMENDED safe scope)
- --kind-filter mural,mural_panel
→ 815 designs · $11.41 (includes 201 misclassified colorway rows)
NO actual batch run. Awaits Steve's approval on (a) scope choice and
(b) authorization of the $8.76 spend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M scripts/generate_room_mockups.py
Diff
commit d43deaf0d053a7c645467b043dd153c104cc074c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 28 13:53:29 2026 -0700
generate_room_mockups: add --all-published, --kind-filter, --category-filter, --regen-old, --dry-run
Implements the CLI surface that task-41 audit recommended (yolo-loop output
41-mural-mockup-coverage-2026-05-28.md). Required to run the mural-mode
regen across the 815-row published cohort without listing IDs by hand.
Flags:
- --all-published iterate every is_published=TRUE row (refuses to run
without --kind-filter or --category-filter — guard
against accidentally re-rendering the full catalog)
- --kind-filter csv restrict to kind IN (csv). CAUTION: 201 published rows
have kind='mural_panel' but a tile-y category
(face-skull-damask, designer-zoo-calm, stripe); they
should NOT be force-rendered in mural-mode.
- --category-filter csv restrict to category LIKE 'X%' OR 'Y%' etc. — safer
scope for the mural batch (matches by category prefix
so colorway sub-cats are included).
- --regen-old re-render existing mockups whose mtime is before
MURAL_MODE_CUTOFF_EPOCH (default 2026-05-28 13:00 PT)
— kills stale tile-mode mockups.
- --dry-run print cohort + cost estimate + sample IDs, run NO
Gemini calls. Mandatory preview before paid batches.
Bug fixes:
- SQL used non-existent columns 'title' and 'saturation' on all_designs
(they only live on the spoon_all_designs view + the snapshot). Switched
to ai_title; sort by id. Memory feedback_psql_execsync_nan_trap noted
that psql -At -q exits 0 on errors, so the bad SQL would have silently
returned 0 rows. Verified by direct psql call.
Dry-run scope previews (this commit):
- --category-filter monterey-mural,cactus-11ft-mural,cactus-pine-scenic,tree-mural
→ 626 designs · $8.76 @ $0.014/call (RECOMMENDED safe scope)
- --kind-filter mural,mural_panel
→ 815 designs · $11.41 (includes 201 misclassified colorway rows)
NO actual batch run. Awaits Steve's approval on (a) scope choice and
(b) authorization of the $8.76 spend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
scripts/generate_room_mockups.py | 87 ++++++++++++++++++++++++++++++++++++++--
1 file changed, 84 insertions(+), 3 deletions(-)
diff --git a/scripts/generate_room_mockups.py b/scripts/generate_room_mockups.py
index 572fd39..5bf5b94 100644
--- a/scripts/generate_room_mockups.py
+++ b/scripts/generate_room_mockups.py
@@ -86,7 +86,13 @@ def generate(design_id, image_path, room_type='living_room', kind=None):
return None
# ── CLI flags ──────────────────────────────────────────────────────────
-# Usage: generate_room_mockups.py [--top N] [--rooms living_room,bedroom]
+# Usage:
+# generate_room_mockups.py [--top N] [--rooms living_room,bedroom]
+# generate_room_mockups.py --id 37893 --rooms living_room
+# generate_room_mockups.py --all-published --kind-filter mural,mural_panel \
+# --rooms living_room --regen-old
+# generate_room_mockups.py --all-published --category-filter monterey-mural,cactus-11ft-mural \
+# --rooms living_room # category-precise mural-mode batch
import argparse
ap = argparse.ArgumentParser()
ap.add_argument('--top', type=int, default=5)
@@ -94,8 +100,29 @@ ap.add_argument('--id', type=int, default=None,
help='single design id (e.g. a curator HOT pick) — bypasses the snapshot top-N')
ap.add_argument('--rooms', default='living_room',
help='comma-separated room types (living_room, bedroom, dining_room, office)')
+# 2026-05-28 — batch flags added per yolo-loop tasks 40/41 plan; required to run the
+# mural-mode regen across the published cohort without listing 815 ids by hand.
+ap.add_argument('--all-published', action='store_true',
+ help='iterate every is_published=TRUE row (bypasses --top); requires --kind-filter or --category-filter')
+ap.add_argument('--kind-filter', default=None,
+ help='comma-separated kind values to restrict to (e.g. mural,mural_panel). '
+ 'CAUTION: 201 published rows have kind=mural_panel but a tile-y category '
+ '(face-skull-damask, designer-zoo-calm, stripe); --category-filter is safer for mural-mode.')
+ap.add_argument('--category-filter', default=None,
+ help='comma-separated category prefixes (e.g. monterey-mural,cactus-11ft-mural,cactus-pine-scenic,tree-mural). '
+ 'Match is LIKE category||\"%%\" so colorway sub-cats are included.')
+ap.add_argument('--regen-old', action='store_true',
+ help='re-render existing mockups whose mtime is before MURAL_MODE_CUTOFF_EPOCH '
+ '(default 1780023600 = 2026-05-28 13:00 PT) — i.e. stale tile-mode renders.')
+ap.add_argument('--dry-run', action='store_true',
+ help='print the cohort + cost estimate + sample IDs, run NO Gemini calls. '
+ 'Preview before authorizing a paid batch.')
args = ap.parse_args()
ROOM_TYPES = [r.strip() for r in args.rooms.split(',') if r.strip()]
+KIND_FILTER = set(k.strip() for k in (args.kind_filter or '').split(',') if k.strip())
+CAT_FILTER = [c.strip() for c in (args.category_filter or '').split(',') if c.strip()]
+import os as _os
+REGEN_CUTOFF_EPOCH = int(_os.environ.get('MURAL_MODE_CUTOFF_EPOCH', '1780023600')) # 2026-05-28 13:00 PT
if args.id:
# Single-design mode (curator HOT pick) — read straight from the DB, since a
@@ -103,6 +130,32 @@ if args.id:
title = psql(f"SELECT COALESCE(title,'design') FROM all_designs WHERE id={args.id};") or 'design'
top = [{'id': args.id, 'title': title, 'saturation': 0}]
print(f'Generating {ROOM_TYPES} mockup for HOT pick #{args.id}')
+elif args.all_published:
+ # Batch-mode: pull EVERY published row from PG (not the snapshot — published-only,
+ # always fresh). Required for the mural-mode regen across the 815-row cohort.
+ if not KIND_FILTER and not CAT_FILTER:
+ raise SystemExit('--all-published requires at least one of --kind-filter / --category-filter (refusing to render the entire catalog).')
+ where = ['is_published=TRUE']
+ if KIND_FILTER:
+ kinds_sql = ','.join("'" + k.replace("'","''") + "'" for k in KIND_FILTER)
+ where.append(f'kind IN ({kinds_sql})')
+ if CAT_FILTER:
+ cat_clauses = " OR ".join(f"category LIKE '{c.replace(chr(39),chr(39)+chr(39))}%%'" for c in CAT_FILTER)
+ where.append(f'({cat_clauses})')
+ # Use ai_title (the real title column on all_designs; bare 'title' does not exist
+ # there — only on the spoon_all_designs view and the data/designs.json snapshot).
+ # No saturation column on all_designs either; sort by id for deterministic order.
+ sql = (f"SELECT id, COALESCE(ai_title,'design') "
+ f"FROM all_designs WHERE {' AND '.join(where)} ORDER BY id;")
+ rows = psql(sql).splitlines()
+ top = []
+ for r in rows:
+ parts = r.split('|', 1)
+ if parts and parts[0]:
+ try: top.append({'id': int(parts[0]), 'title': parts[1] if len(parts) > 1 else 'design', 'saturation': 0})
+ except ValueError: pass
+ print(f'Generating {ROOM_TYPES} mockups for {len(top)} published designs '
+ f'(kind={sorted(KIND_FILTER) or "*"}, cat-prefix={CAT_FILTER or "*"}, regen-old={args.regen_old})')
else:
# Pull top-N saturation designs from snapshot
snap = json.loads(Path(ROOT / 'data' / 'designs.json').read_text())
@@ -113,6 +166,22 @@ else:
subprocess.run(['psql','dw_unified','-q','-c',
'ALTER TABLE all_designs ADD COLUMN IF NOT EXISTS room_mockups JSONB;'], check=True)
+# --dry-run: print the planned batch + cost estimate and exit BEFORE any spend.
+if args.dry_run:
+ PER_CALL_USD = 0.014 # current Gemini room-render cost; cost-tracker pricing.json
+ n_designs = len(top)
+ n_rooms = len(ROOM_TYPES)
+ n_calls = n_designs * n_rooms
+ est_cost = n_calls * PER_CALL_USD
+ print(f'\nDRY RUN — would render {n_designs} designs × {n_rooms} room(s) = {n_calls} calls')
+ print(f' rooms : {ROOM_TYPES}')
+ print(f' regen-old : {args.regen_old}')
+ print(f' kind-filter : {sorted(KIND_FILTER) if KIND_FILTER else "*"}')
+ print(f' cat-filter : {CAT_FILTER if CAT_FILTER else "*"}')
+ print(f' cost @ ${PER_CALL_USD}/call : ${est_cost:.2f}')
+ print(f' sample IDs : {[d["id"] for d in top[:8]]}{"..." if n_designs > 8 else ""}')
+ raise SystemExit(0)
+
results = []
for d in top:
design_id = d['id']
@@ -134,8 +203,20 @@ for d in top:
for room in ROOM_TYPES:
if room in have and Path(have[room]).exists():
- print(f'#{design_id} {room} ✓ already exists, skipping')
- continue
+ # --regen-old: treat pre-cutoff mockups as stale (tile-mode era, need
+ # mural-mode rebuild). Default cutoff = 2026-05-28 13:00 PT.
+ if args.regen_old:
+ try:
+ mtime = Path(have[room]).stat().st_mtime
+ except OSError:
+ mtime = 0
+ if mtime >= REGEN_CUTOFF_EPOCH:
+ print(f'#{design_id} {room} ✓ NEW (post-cutoff), skipping')
+ continue
+ print(f'#{design_id} {room} regen — file is pre-cutoff (mtime={int(mtime)})')
+ else:
+ print(f'#{design_id} {room} ✓ already exists, skipping')
+ continue
mode_tag = ' [MURAL mode]' if (kind or '').lower() in ('mural', 'mural_panel') else ''
print(f'#{design_id} {d["title"]} → generating {room}{mode_tag}…')
t0 = time.time()
← 87fa2ac wallco.ai: finish Scenic+Studio sub-brand strip — kill orpha
·
back to Wallco Ai
·
add botanical-wonders substitute generator + curator (12 des 71a12b2 →