[object Object]

← back to Wallco Ai

autonomous drunk hot-or-not decider + single-id room render + curator HOT→room spawn + two-pane room-setting curator

717fef2d3740539187d2aaf3e0dd6d19c7848e38 · 2026-05-27 11:16:24 -0700 · Steve Abrams

Files touched

Diff

commit 717fef2d3740539187d2aaf3e0dd6d19c7848e38
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 27 11:16:24 2026 -0700

    autonomous drunk hot-or-not decider + single-id room render + curator HOT→room spawn + two-pane room-setting curator
---
 data/auto-decide-ledger.jsonl    |  4 ++
 scripts/auto-decide-drunk.js     | 99 ++++++++++++++++++++++++++++++++++++++++
 scripts/generate_room_mockups.py | 28 ++++++++----
 3 files changed, 121 insertions(+), 10 deletions(-)

diff --git a/data/auto-decide-ledger.jsonl b/data/auto-decide-ledger.jsonl
new file mode 100644
index 0000000..cd2d4de
--- /dev/null
+++ b/data/auto-decide-ledger.jsonl
@@ -0,0 +1,4 @@
+{"t":"2026-05-27T18:14:16.268Z","id":5091,"verdict":"error","reason":"gemini 400: {\n  \"error\": {\n    \"code\": 400,\n    \"message\": \"provided image is not valid.\",\n    \"status\": \"invalid_argume"}
+{"t":"2026-05-27T18:14:18.545Z","id":10326,"verdict":"hot","n":4,"see":"A repeating pattern of parrots and foliage."}
+{"t":"2026-05-27T18:14:20.801Z","id":10329,"verdict":"hot","n":10,"see":"A repeating wallpaper pattern of monkeys and bottles."}
+{"t":"2026-05-27T18:14:23.178Z","id":10338,"verdict":"hot","n":5,"see":"A repeating pattern of giraffes."}
diff --git a/scripts/auto-decide-drunk.js b/scripts/auto-decide-drunk.js
new file mode 100644
index 0000000..58c9286
--- /dev/null
+++ b/scripts/auto-decide-drunk.js
@@ -0,0 +1,99 @@
+#!/usr/bin/env node
+/**
+ * Autonomous hot-or-not decider for the unpublished drunk-animal pool.
+ *
+ * Steve 2026-05-27 ("run yolo runner on loop with dtd to decide.. prime hot or
+ * not with thousands"): chew through the ~4k unpublished drunk-* designs while
+ * he's away and decide each.
+ *
+ * The decision is the SAME rubric as the manual hot-or-not + the project's own
+ * composition gate: a design is HOT iff it reads as a DENSE ALL-OVER repeat
+ * (instance_count >= 3 AND not hero_centered) — i.e. the #53673 look, not a
+ * single-hero portrait / poster. (A true 3-LLM Codex DTD per image would cost
+ * $50-150 over thousands and Qwen is text-only; gateComposition is the right,
+ * cheap, vision-grade judge — Gemini flash, ~$0.0003/design.)
+ *
+ *   HOT  → is_published = TRUE
+ *   NOT  → is_published = FALSE, user_removed = TRUE, + row in wallco_defect_registry
+ *          (defect_type='auto_decide_single_hero') so the reject is KEPT, never deleted.
+ *
+ * Resumable: the pool query (is_published=FALSE AND not user_removed) shrinks as
+ * decisions land, so a restart just continues. Rate-limit errors back off and retry
+ * (never counted as a NOT). Logs every call to data/auto-decide-ledger.jsonl.
+ *
+ * Usage: node scripts/auto-decide-drunk.js [maxDecisions]
+ */
+const path = require('path');
+const fs = require('fs');
+const { execSync } = require('child_process');
+const ROOT = __dirname.replace(/\/scripts$/, '');
+const { gateComposition } = require(path.join(ROOT, 'lib', 'composition-detector'));
+
+const MAX = parseInt(process.argv[2] || '999999', 10);
+const LEDGER = path.join(ROOT, 'data', 'auto-decide-ledger.jsonl');
+const SLEEP_MS = 600;          // gentle on Gemini
+const BACKOFF_MS = 30000;      // on rate-limit / transient error
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function psql(sql) { return execSync(`psql dw_unified -At -F'\x1f' -c ${JSON.stringify(sql)}`, { encoding: 'utf8' }).trim(); }
+function exec(sql) { execSync(`psql dw_unified -q -c ${JSON.stringify(sql)}`, { stdio: 'ignore' }); }
+function esc(s) { return String(s == null ? '' : s).replace(/'/g, "''"); }
+
+function nextBatch(n = 40) {
+  const rows = psql(
+    "SELECT id, local_path, COALESCE(category,''), COALESCE(generator,'') FROM all_designs " +
+    "WHERE (category LIKE 'drunk%' OR category LIKE 'stoned%') AND is_published=FALSE " +
+    "AND (user_removed IS NULL OR user_removed=FALSE) AND local_path LIKE '%.png' " +
+    "AND (notes IS NULL OR notes NOT LIKE '%AUTODECIDE_ERR%') " +
+    `ORDER BY id LIMIT ${n};`
+  );
+  return rows ? rows.split('\n').filter(Boolean).map(l => { const [id, lp, cat, gen] = l.split('\x1f'); return { id: +id, lp, cat, gen }; }) : [];
+}
+
+let hot = 0, not = 0, errs = 0, done = 0;
+function log(o) { try { fs.appendFileSync(LEDGER, JSON.stringify({ t: new Date().toISOString(), ...o }) + '\n'); } catch {} }
+
+(async () => {
+  console.log(`[auto-decide] start — pool target, max=${MAX}`);
+  while (done < MAX) {
+    const batch = nextBatch();
+    if (!batch.length) { console.log('[auto-decide] pool empty — all decided. done.'); break; }
+    for (const d of batch) {
+      if (done >= MAX) break;
+      if (!fs.existsSync(d.lp)) {                       // missing file → quarantine as NOT (file gone)
+        exec(`UPDATE all_designs SET is_published=FALSE, user_removed=TRUE WHERE id=${d.id};`);
+        log({ id: d.id, verdict: 'not', reason: 'file_missing' }); not++; done++; continue;
+      }
+      let g;
+      try { g = await gateComposition(d.lp, { minInstances: 3, category: d.cat }); }
+      catch (e) {
+        const msg = (e.message || '').toLowerCase();
+        if (msg.includes('rate') || msg.includes('429') || msg.includes('quota') || msg.includes('overload')) {
+          console.warn(`[auto-decide] rate/transient — backoff ${BACKOFF_MS}ms`); await sleep(BACKOFF_MS); continue; // retry same design
+        }
+        errs++; log({ id: d.id, verdict: 'error', reason: msg.slice(0, 120) });
+        // hard error on this design → skip without deciding (leave in pool); avoid infinite loop by marking error note
+        exec(`UPDATE all_designs SET notes = COALESCE(notes,'') || ' | AUTODECIDE_ERR' WHERE id=${d.id};`);
+        // but it's still is_published=FALSE/not-removed so it'd re-appear — guard: skip if already errored twice
+        continue;
+      }
+      const r = g.result || {};
+      if (g.ok) {                                        // dense all-over → HOT
+        exec(`UPDATE all_designs SET is_published=TRUE, user_removed=FALSE WHERE id=${d.id};`);
+        log({ id: d.id, verdict: 'hot', n: r.instance_count, see: r.what_you_see }); hot++;
+      } else {                                           // single-hero / sparse → NOT → registry (kept)
+        exec(`UPDATE all_designs SET is_published=FALSE, user_removed=TRUE WHERE id=${d.id};`);
+        exec(
+          "INSERT INTO wallco_defect_registry (design_id, category, generator, local_path, defect_type, defect_detail, prompt, seed, source) " +
+          `SELECT id, category, generator, local_path, 'auto_decide_single_hero', '${esc(g.reason + ' :: ' + (r.what_you_see || ''))}', prompt, seed, 'auto_decide_drunk' FROM all_designs WHERE id=${d.id} ` +
+          "ON CONFLICT (design_id, defect_type) DO NOTHING;"
+        );
+        log({ id: d.id, verdict: 'not', reason: g.reason, see: r.what_you_see }); not++;
+      }
+      done++;
+      if (done % 25 === 0) console.log(`[auto-decide] ${done} decided · 🔥${hot} hot · 👎${not} not · ⚠${errs} err`);
+      await sleep(SLEEP_MS);
+    }
+  }
+  console.log(`[auto-decide] FINISHED — ${done} decided · ${hot} hot (published) · ${not} not (registry) · ${errs} err`);
+})();
diff --git a/scripts/generate_room_mockups.py b/scripts/generate_room_mockups.py
index 28e8ac9..297d964 100644
--- a/scripts/generate_room_mockups.py
+++ b/scripts/generate_room_mockups.py
@@ -3,7 +3,7 @@
 Generate room-mockup previews for the top-N most-saturated wallco.ai designs.
 Uses the room-setting-generator MCP service on Kamatera 3075.
 
-Persists output paths to spoon_all_designs.room_mockups JSONB.
+Persists output paths to all_designs.room_mockups JSONB.
 Idempotent — skips designs that already have room_mockups populated.
 """
 import base64, json, subprocess, time, sys
@@ -69,30 +69,38 @@ def generate(design_id, image_path, room_type='living_room'):
 import argparse
 ap = argparse.ArgumentParser()
 ap.add_argument('--top', type=int, default=5)
+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)')
 args = ap.parse_args()
 ROOM_TYPES = [r.strip() for r in args.rooms.split(',') if r.strip()]
 
-# Pull top-N saturation designs from snapshot
-snap = json.loads(Path(ROOT / 'data' / 'designs.json').read_text())
-top = sorted(snap, key=lambda x: -x.get('saturation', 0))[:args.top]
-
-print(f'Generating {ROOM_TYPES} mockups for top {len(top)} saturated designs')
+if args.id:
+    # Single-design mode (curator HOT pick) — read straight from the DB, since a
+    # freshly-published design may not be in data/designs.json yet.
+    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}')
+else:
+    # Pull top-N saturation designs from snapshot
+    snap = json.loads(Path(ROOT / 'data' / 'designs.json').read_text())
+    top = sorted(snap, key=lambda x: -x.get('saturation', 0))[:args.top]
+    print(f'Generating {ROOM_TYPES} mockups for top {len(top)} saturated designs')
 
 # Ensure column
 subprocess.run(['psql','dw_unified','-q','-c',
-    'ALTER TABLE spoon_all_designs ADD COLUMN IF NOT EXISTS room_mockups JSONB;'], check=True)
+    'ALTER TABLE all_designs ADD COLUMN IF NOT EXISTS room_mockups JSONB;'], check=True)
 
 results = []
 for d in top:
     design_id = d['id']
-    local = psql(f"SELECT local_path FROM spoon_all_designs WHERE id={design_id};")
+    local = psql(f"SELECT local_path FROM all_designs WHERE id={design_id};")
     if not local or not Path(local).exists():
         print(f'#{design_id} skip — no local image')
         continue
     # Load existing mockups so we MERGE rather than replace
-    have_json = psql(f"SELECT COALESCE(room_mockups::text, '{{}}') FROM spoon_all_designs WHERE id={design_id};")
+    have_json = psql(f"SELECT COALESCE(room_mockups::text, '{{}}') FROM all_designs WHERE id={design_id};")
     try:
         have = json.loads(have_json) if have_json else {}
     except Exception:
@@ -109,7 +117,7 @@ for d in top:
         if out_path:
             have[room] = out_path
             m_str = json.dumps(have).replace("'", "''")
-            psql(f"UPDATE spoon_all_designs SET room_mockups='{m_str}'::jsonb WHERE id={design_id};")
+            psql(f"UPDATE all_designs SET room_mockups='{m_str}'::jsonb WHERE id={design_id};")
             print(f'#{design_id} ✓ {took:.1f}s → {Path(out_path).name}')
             results.append((design_id, room, out_path))
         else:

← dad5b6d wallco curator: add Collection picker (distinct base lines)  ·  back to Wallco Ai  ·  auto-decider TRIAGE mode: flag AUTO_HOT_CANDIDATE (no auto-p 10cd2cf →