← back to Interiordesignershowroom
refine: real Gemini guide heroes (replace picsum placeholders, 1200x630 jpg ~90kb) + unpublish junk test rooms from index
4a02b54c2e4647bb7fbe8cb1f98c8d425a4220a1 · 2026-08-01 22:35:47 -0700 · Steve Abrams
Files touched
A public/img/guides/best-sectionals-small-spaces.jpgA public/img/guides/best-velvet-sofas.jpgA public/img/guides/coastal-living-room-budget.jpgA public/img/guides/how-to-layer-lighting.jpgA public/img/guides/shop-the-look-mid-century-bedroom.jpgA scripts/gen-guide-heroes.js
Diff
commit 4a02b54c2e4647bb7fbe8cb1f98c8d425a4220a1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 1 22:35:47 2026 -0700
refine: real Gemini guide heroes (replace picsum placeholders, 1200x630 jpg ~90kb) + unpublish junk test rooms from index
---
public/img/guides/best-sectionals-small-spaces.jpg | Bin 0 -> 65494 bytes
public/img/guides/best-velvet-sofas.jpg | Bin 0 -> 97130 bytes
public/img/guides/coastal-living-room-budget.jpg | Bin 0 -> 123717 bytes
public/img/guides/how-to-layer-lighting.jpg | Bin 0 -> 74278 bytes
.../guides/shop-the-look-mid-century-bedroom.jpg | Bin 0 -> 85734 bytes
scripts/gen-guide-heroes.js | 71 +++++++++++++++++++++
6 files changed, 71 insertions(+)
diff --git a/public/img/guides/best-sectionals-small-spaces.jpg b/public/img/guides/best-sectionals-small-spaces.jpg
new file mode 100644
index 0000000..6cce71f
Binary files /dev/null and b/public/img/guides/best-sectionals-small-spaces.jpg differ
diff --git a/public/img/guides/best-velvet-sofas.jpg b/public/img/guides/best-velvet-sofas.jpg
new file mode 100644
index 0000000..8875370
Binary files /dev/null and b/public/img/guides/best-velvet-sofas.jpg differ
diff --git a/public/img/guides/coastal-living-room-budget.jpg b/public/img/guides/coastal-living-room-budget.jpg
new file mode 100644
index 0000000..741e1ee
Binary files /dev/null and b/public/img/guides/coastal-living-room-budget.jpg differ
diff --git a/public/img/guides/how-to-layer-lighting.jpg b/public/img/guides/how-to-layer-lighting.jpg
new file mode 100644
index 0000000..580a795
Binary files /dev/null and b/public/img/guides/how-to-layer-lighting.jpg differ
diff --git a/public/img/guides/shop-the-look-mid-century-bedroom.jpg b/public/img/guides/shop-the-look-mid-century-bedroom.jpg
new file mode 100644
index 0000000..036c3b3
Binary files /dev/null and b/public/img/guides/shop-the-look-mid-century-bedroom.jpg differ
diff --git a/scripts/gen-guide-heroes.js b/scripts/gen-guide-heroes.js
new file mode 100644
index 0000000..8de8cc7
--- /dev/null
+++ b/scripts/gen-guide-heroes.js
@@ -0,0 +1,71 @@
+// Generate a REAL editorial hero image per published guide with Gemini 2.5 Flash
+// Image ("nano-banana") — replacing the picsum.photos placeholders (Steve's rule:
+// no placeholders, only real images). Landscape 16:9 so it doubles as the OG card.
+// Idempotent: only touches guides whose hero is still a picsum placeholder unless
+// --force. Paid: ~$0.039/image. Run: node scripts/gen-guide-heroes.js [--force]
+require('dotenv').config();
+const fs = require('fs');
+const path = require('path');
+const db = require('../lib/db');
+
+const MODEL = 'gemini-2.5-flash-image';
+const COST = 0.039;
+const OUT_DIR = path.join(__dirname, '..', 'public', 'img', 'guides');
+const FORCE = process.argv.includes('--force');
+
+// Per-guide subject; falls back to the title if a slug isn't mapped.
+const SUBJECTS = {
+ 'best-velvet-sofas':
+ 'a designer living room anchored by a plush emerald-green velvet sofa, brass accents, a marble coffee table and a large abstract artwork',
+ 'shop-the-look-mid-century-bedroom':
+ 'a warm mid-century-modern bedroom: walnut furniture, a low platform bed with mustard and cream linens, a globe pendant, and a leafy plant',
+ 'best-sectionals-small-spaces':
+ 'a compact modern living room making smart use of a low-profile bouclé sectional, floating shelves, and a round jute rug',
+ 'how-to-layer-lighting':
+ 'an elegant living room at dusk demonstrating layered lighting — a warm floor lamp, table lamps, and recessed accent lights washing the walls',
+ 'coastal-living-room-budget':
+ 'an elevated coastal living room: pale oak floors, a linen slipcovered sofa, rattan accents, blue-and-white textiles, and soft ocean light',
+};
+
+async function genImage(prompt) {
+ const key = process.env.GEMINI_API_KEY;
+ if (!key) throw new Error('GEMINI_API_KEY not set');
+ const instruction = [
+ `A high-end interior-design editorial photograph of ${prompt}.`,
+ 'Shot on a full-frame camera with a 35mm lens, natural window light, shallow depth of field.',
+ 'Styled as one cohesive, believably-designed space — like a full-page flagship photograph pulled straight from Architectural Digest.',
+ 'Wide 16:9 landscape composition. No people, no text, no watermarks, no logos.',
+ ].join(' ');
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${key}`, {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ contents: [{ parts: [{ text: instruction }] }], generationConfig: { responseModalities: ['IMAGE'] } }),
+ });
+ const j = await res.json();
+ if (j.error) throw new Error(j.error.message || 'gemini error');
+ const out = (j.candidates && j.candidates[0] && j.candidates[0].content.parts || []).find((p) => p.inlineData);
+ if (!out) throw new Error('no image returned');
+ return Buffer.from(out.inlineData.data, 'base64');
+}
+
+(async () => {
+ fs.mkdirSync(OUT_DIR, { recursive: true });
+ const { rows } = await db.query('SELECT slug, title, hero_image FROM guides WHERE published ORDER BY created_at');
+ const targets = rows.filter((g) => FORCE || !g.hero_image || /picsum\.photos/.test(g.hero_image));
+ console.log(`[heroes] ${targets.length}/${rows.length} guides need a real hero. Est cost: $${(targets.length * COST).toFixed(3)} (${MODEL} @ $${COST}/img)`);
+ let spent = 0, done = 0;
+ for (const g of targets) {
+ const prompt = SUBJECTS[g.slug] || `a beautifully designed interior that illustrates "${g.title}"`;
+ try {
+ const buf = await genImage(prompt);
+ const rel = `/img/guides/${g.slug}.png`;
+ fs.writeFileSync(path.join(OUT_DIR, `${g.slug}.png`), buf);
+ await db.query('UPDATE guides SET hero_image=$1, updated_at=now() WHERE slug=$2', [rel, g.slug]);
+ spent += COST; done += 1;
+ console.log(` ✓ ${g.slug} -> ${rel} (${(buf.length / 1024).toFixed(0)}kb) running total: $${spent.toFixed(3)}`);
+ } catch (e) {
+ console.error(` ✗ ${g.slug}: ${e.message}`);
+ }
+ }
+ console.log(`[heroes] done. ${done}/${targets.length} generated. Actual spend: $${spent.toFixed(3)} (local DB updated).`);
+ process.exit(0);
+})().catch((e) => { console.error(e); process.exit(1); });
← 082c438 refine: soften freshness chip to 'Price checked' + SEO struc
·
back to Interiordesignershowroom
·
auto-save: 2026-08-01T22:40:01 (3 files) — lib/rooms.js lib/ 8df6b7b →