[object Object]

← 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

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 →