← 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
A scripts/backfill-room-scenes.js
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 →