← back to Interiordesignershowroom
lib/roomgen.js
85 lines
// Core room-generation pipeline, extracted from scripts/gen-room-setting.js so
// both the standalone room cron AND the guide generator (scripts/gen-guide.js)
// build rooms the same way: pick a coherent room (room-type + style + a diverse
// set of pieces) -> render an AD-style scene (Gemini image) -> vision-locate each
// piece so the room is shoppable (Gemini Flash) -> save it PUBLIC.
const path = require('path');
const rooms = require('./rooms');
const scene = require('./scene');
const hotspots = require('./hotspots');
const ROOM_TYPES = ['living-room', 'bedroom', 'dining', 'office'];
const STYLES = ['modern', 'mid-century', 'coastal', 'traditional', 'scandinavian', 'boho', 'industrial', 'farmhouse'];
const pick = (a) => a[Math.floor(Math.random() * a.length)];
const cap = (s) => (s || '').replace(/\b\w/g, (c) => c.toUpperCase()).replace(/-/g, ' ');
// Choose a diverse, coordinated set of pieces: at most one per "category" bucket so
// a room reads like a designed space (a sofa + a table + lighting + a rug), not 5 sofas.
const BUCKETS = [
/sofa|sectional|loveseat|settee/i, /bed\b|headboard/i, /desk/i,
/coffee table|side table|end table|console|dining table|\btable\b/i,
/chair|stool|bench/i, /lamp|light|sconce|pendant|chandelier/i,
/rug/i, /art|print|mirror|wall/i, /shelf|bookcase|cabinet|dresser|credenza|sideboard|buffet|console|hutch/i,
/vase|planter|decor|throw|pillow|cushion/i,
];
function diverseSet(pool, n = 6) {
const used = new Set(), out = [];
for (const p of pool) {
const b = BUCKETS.findIndex((rx) => rx.test(p.title));
const key = b === -1 ? `x${out.length}` : b;
if (used.has(key)) continue;
used.add(key); out.push(p);
if (out.length >= n) break;
}
// top up if we couldn't fill from distinct buckets
for (const p of pool) { if (out.length >= n) break; if (!out.includes(p)) out.push(p); }
return out;
}
// loosen filters until we land a populated, coherent pool (style+room -> room -> any)
async function candidatePool(wantRoom, wantStyle) {
const room = wantRoom || pick(ROOM_TYPES);
const style = wantStyle || pick(STYLES);
for (const attempt of [{ room, style }, { room }, {}]) {
const rows = await rooms.searchProducts({ ...attempt, limit: 40 });
if (rows.length >= 4) {
rows.sort(() => Math.random() - 0.5); // light shuffle for variety across runs
return { room, style: attempt.style || rows[0].style || null, pool: rows };
}
}
return { room, style: null, pool: [] };
}
// Generate + save one public shoppable room. Returns everything the caller needs
// (incl. run cost) or throws when inventory can't fill a room.
async function generateRoom({ room, style, log = () => {} } = {}) {
const t0 = Date.now();
const picked = await candidatePool(room, style);
if (picked.pool.length < 4) throw new Error('not enough inventory to build a room');
const products = diverseSet(picked.pool, 6);
const title = `${cap(picked.style) ? cap(picked.style) + ' ' : ''}${cap(picked.room)}`;
log(`[room-gen] "${title}" — ${products.length} pieces from ${new Set(products.map((p) => p.advertiser)).size} advertiser(s). Est cost ~$0.040`);
// 1) render the scene (paid Gemini image)
const out = await scene.generateScene({ style: picked.style, room_type: picked.room, products });
let cost = out.cost || 0;
log(` ✓ scene ${out.url} ($${cost.toFixed(3)}, ${out.refs} refs)`);
// 2) vision-locate the pieces so the room is shoppable (paid Gemini Flash)
const imgPath = path.join(__dirname, '..', 'public', out.url);
const loc = await hotspots.locateProducts(imgPath, products);
cost += loc.cost || 0;
log(` ✓ hotspots ${loc.hotspots.length}/${products.length} located${loc.error ? ' (err: ' + loc.error + ')' : ''} ($${(loc.cost || 0).toFixed(3)})`);
// 3) save the room PUBLIC + shoppable
const slug = await rooms.createRoom({
title, room_type: picked.room, style: picked.style,
product_ids: products.map((p) => p.id),
scene_image: out.url, hotspots: loc.hotspots, created_by: 'auto',
});
log(`[room-gen] saved /room/${slug} | run cost $${cost.toFixed(3)} | ${((Date.now() - t0) / 1000).toFixed(1)}s`);
return { slug, title, room: picked.room, style: picked.style, products, scene_image: out.url, hotspots: loc.hotspots, cost };
}
module.exports = { generateRoom, ROOM_TYPES, STYLES, cap };