[object Object]

← back to Interiordesignershowroom

TK-10402: backfill-room-scenes.js — regenerate scene_image for reconciled rooms via nano-banana (reuses lib/scene + lib/hotspots, idempotent, per-image cost)

cb0b958a11d8a4962a2e865da86db0f7296828d4 · 2026-08-10 11:18:10 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit cb0b958a11d8a4962a2e865da86db0f7296828d4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 10 11:18:10 2026 -0700

    TK-10402: backfill-room-scenes.js — regenerate scene_image for reconciled rooms via nano-banana (reuses lib/scene + lib/hotspots, idempotent, per-image cost)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/backfill-room-scenes.js | 65 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 65 insertions(+)

diff --git a/scripts/backfill-room-scenes.js b/scripts/backfill-room-scenes.js
new file mode 100644
index 0000000..8990b20
--- /dev/null
+++ b/scripts/backfill-room-scenes.js
@@ -0,0 +1,65 @@
+// TK-10402 (Option B) — backfill scene_image for EXISTING rooms whose scene was
+// NULLed by the TK-10390 reconcile. Unlike scripts/gen-room-setting.js (which
+// CREATES new rooms), this regenerates the hero scene for rooms that already exist,
+// reusing each room's OWN products as image references, then writes the scene +
+// hotspots back to that room. Gemini 2.5 Flash Image ("nano-banana"), ~$0.039/scene
+// + ~$0.001/vision-locate. Idempotent: only rooms with a NULL scene_image unless --force.
+//
+//   node scripts/backfill-room-scenes.js --limit 1     # CANARY: one scene, show cost
+//   node scripts/backfill-room-scenes.js               # all NULL-scene rooms
+//   node scripts/backfill-room-scenes.js --force       # regenerate every room
+//
+// Paid + writes DB. Whichever DATABASE_URL is active is the one updated (run on prod
+// to fill the reconciled prod rows; run local to fill local). Per-image + running
+// cost printed every step. Failures are per-room (logged, skipped) — never aborts the batch.
+require('dotenv').config();
+const path = require('path');
+const db = require('../lib/db');
+const scene = require('../lib/scene');
+const hotspots = require('../lib/hotspots');
+
+const FORCE = process.argv.includes('--force');
+const li = process.argv.indexOf('--limit');
+const LIMIT = li !== -1 ? parseInt(process.argv[li + 1], 10) : null;
+
+(async () => {
+  const where = FORCE ? '' : 'WHERE scene_image IS NULL';
+  const lim = LIMIT ? `LIMIT ${LIMIT}` : '';
+  const { rows: targets } = await db.query(
+    `SELECT slug, title, room_type, style, product_ids FROM rooms ${where} ORDER BY created_at ${lim}`);
+
+  const est = (targets.length * (scene.COST_PER_IMAGE + (hotspots.COST_PER_CALL || 0))).toFixed(2);
+  console.log(`[scenes] ${targets.length} room(s) need a scene. Est cost ~$${est} (nano-banana $${scene.COST_PER_IMAGE}/img + vision-locate).`);
+
+  let spent = 0, done = 0, skipped = 0;
+  for (const r of targets) {
+    try {
+      const ids = r.product_ids || [];
+      if (!ids.length) { console.log(`  · ${r.slug}: no products — skipped`); skipped++; continue; }
+      const { rows: products } = await db.query(
+        `SELECT id, title, image_url, price, sale_price, advertiser, brand
+           FROM products WHERE id = ANY($1) AND NOT suppressed AND image_url IS NOT NULL`, [ids]);
+      if (!products.length) { console.log(`  · ${r.slug}: no usable product images — skipped`); skipped++; continue; }
+
+      // 1) render the scene (paid) using this room's own pieces as references
+      const out = await scene.generateScene({ style: r.style, room_type: r.room_type, products });
+      let cost = out.cost || 0;
+
+      // 2) vision-locate the pieces so the room stays shoppable (paid)
+      const imgPath = path.join(__dirname, '..', 'public', out.url);
+      const loc = await hotspots.locateProducts(imgPath, products);
+      cost += loc.cost || 0;
+
+      // 3) write scene + hotspots back to THIS room
+      await db.query('UPDATE rooms SET scene_image=$1, hotspots=$2, updated_at=now() WHERE slug=$3',
+        [out.url, JSON.stringify(loc.hotspots || []), r.slug]);
+
+      spent += cost; done++;
+      console.log(`  ✓ ${r.slug} -> ${out.url}  (${loc.hotspots.length}/${products.length} spots, $${cost.toFixed(3)})  running: $${spent.toFixed(3)}`);
+    } catch (e) {
+      console.error(`  ✗ ${r.slug}: ${e.message}`);
+    }
+  }
+  console.log(`[scenes] done. ${done} generated, ${skipped} skipped. Actual spend: $${spent.toFixed(3)}.`);
+  process.exit(0);
+})().catch((e) => { console.error('[backfill-room-scenes]', e.message); process.exit(1); });

← 666bd0c chore: bump v0.4.4 (session close) — guide-image fallback (3  ·  back to Interiordesignershowroom  ·  auto-data-snapshot: 2026-08-10T11:29:10 (6 data files) — .pl a89299b →