← back to Wallco Ai
bh-abstract-leaves: regen all 44 as contemporary abstract via text-only Gemini (no source image → no banana-leaf bleed). Cycles 8 shape families, preserves BH color palettes, backs up old PNGs to ~/.wallco-deleted/<date>/bh-leaves-superseded/
6f6143e2ac604933e6f66fd5669915ef6977742c · 2026-05-13 14:47:44 -0700 · SteveStudio2
Files touched
A scripts/regen-bh-abstract-leaves-contemporary.js
Diff
commit 6f6143e2ac604933e6f66fd5669915ef6977742c
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 13 14:47:44 2026 -0700
bh-abstract-leaves: regen all 44 as contemporary abstract via text-only Gemini (no source image → no banana-leaf bleed). Cycles 8 shape families, preserves BH color palettes, backs up old PNGs to ~/.wallco-deleted/<date>/bh-leaves-superseded/
---
scripts/regen-bh-abstract-leaves-contemporary.js | 275 +++++++++++++++++++++++
1 file changed, 275 insertions(+)
diff --git a/scripts/regen-bh-abstract-leaves-contemporary.js b/scripts/regen-bh-abstract-leaves-contemporary.js
new file mode 100644
index 0000000..c12b880
--- /dev/null
+++ b/scripts/regen-bh-abstract-leaves-contemporary.js
@@ -0,0 +1,275 @@
+#!/usr/bin/env node
+/**
+ * Regenerate every bh-abstract-leaves design as CONTEMPORARY ABSTRACT SHAPES.
+ *
+ * Steve's brief 2026-05-13 — the BH source images bleed banana leaves through
+ * Gemini even with anti-prompts (see project_wallco_banana_purge_20260513).
+ * Fix: stop sending the BH image. Extract ONLY the palette from the existing
+ * row (already encoded in prompt), then send a TEXT-ONLY Gemini call with a
+ * contemporary-abstract-shapes brief. The new designs should "feel like" the
+ * original from a distance (matching color energy) but carry zero botanical
+ * vocabulary up close.
+ *
+ * Workflow per design:
+ * 1. Read row (dominant_hex, palette, prompt) from spoon_all_designs
+ * 2. Extract palette phrase from the existing prompt (between parens after
+ * "palette" / "Single tonal palette pulled from the source:")
+ * 3. Pick a contemporary shape vocabulary (cycled across the 44 to keep
+ * visual variety in the category)
+ * 4. Settlement-gate the prompt (no banana/monstera/palm/tropical/leaf)
+ * 5. Call Gemini 2.5 Flash Image TEXT-ONLY (no inline_data)
+ * 6. Backup old PNG to ~/.wallco-deleted/<date>/bh-leaves-superseded/
+ * 7. Write new PNG to data/generated/
+ * 8. UPDATE row in place — local_path, image_url, prompt, motifs, tags
+ * (drop leaf vocab from motifs/tags), notes (audit trail)
+ *
+ * Concurrency: BOUNDED 4-wide. Gemini 2.5 Flash Image's free tier is ~10 RPM;
+ * 4 parallel keeps headroom for retries.
+ *
+ * Run:
+ * cd ~/Projects/wallco-ai && node scripts/regen-bh-abstract-leaves-contemporary.js
+ * Options:
+ * --dry-run print what it would do, no calls, no DB writes
+ * --limit N cap at N rows (for smoke-testing)
+ * --concurrency N worker count (default 4)
+ */
+'use strict';
+require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+const { execSync } = require('child_process');
+const os = require('os');
+
+const KEY = process.env.GEMINI_API_KEY;
+if (!KEY) { console.error('GEMINI_API_KEY missing'); process.exit(1); }
+
+const args = process.argv.slice(2);
+const DRY_RUN = args.includes('--dry-run');
+const LIMIT = (() => { const i = args.indexOf('--limit'); return i >= 0 ? parseInt(args[i+1], 10) : Infinity; })();
+const CONCURRENCY = (() => { const i = args.indexOf('--concurrency'); return i >= 0 ? Math.max(1, parseInt(args[i+1], 10) || 4) : 4; })();
+
+const OUT_DIR = path.join(__dirname, '..', 'data', 'generated');
+fs.mkdirSync(OUT_DIR, { recursive: true });
+
+const BACKUP_DIR = path.join(os.homedir(), '.wallco-deleted', new Date().toISOString().slice(0,10), 'bh-leaves-superseded');
+fs.mkdirSync(BACKUP_DIR, { recursive: true });
+
+const DB = process.env.WALLCO_DB || 'dw_unified';
+const PSQL = (process.platform === 'linux')
+ ? `sudo -n -u postgres psql ${DB} -At -F'\\x1f' -q`
+ : `psql ${DB} -At -F'\\x1f' -q`;
+function psql(sql) { return execSync(PSQL, { input: sql, encoding: 'utf8' }).trim(); }
+function esc(v) { if (v == null) return 'NULL'; return "'" + String(v).replace(/'/g, "''") + "'"; }
+
+// ── 8 contemporary shape vocabularies — cycle across the 44 rows for variety
+const SHAPE_FAMILIES = [
+ { key: 'soft-circles', phrase: 'soft hand-drawn circles and ovals scattered at varying scales' },
+ { key: 'brush-strokes', phrase: 'oblique sumi-ink brushstrokes and tapered marks at multiple angles' },
+ { key: 'color-blocks', phrase: 'overlapping rounded color blocks and rectangles in a loose modernist arrangement' },
+ { key: 'amoeba-forms', phrase: 'fluid amoeba-shaped organic blobs at varying scales, softly blurred edges' },
+ { key: 'torn-paper', phrase: 'torn-paper collage shapes with rough deckle edges, overlapping in loose composition' },
+ { key: 'ribbon-waves', phrase: 'undulating horizontal ribbon-wave bands at varying thickness' },
+ { key: 'micro-grid', phrase: 'a fine micro-grid of small abstract dot-and-dash marks, modernist gallery aesthetic' },
+ { key: 'arcs-rays', phrase: 'concentric arcs and radiating ray-lines arranged in a soft repeating composition' },
+];
+
+// Extract the palette phrase from the existing leaf-vocab prompt.
+// Several patterns observed across the 44 rows:
+// • "palette (warm cream ground, ...)" ← original
+// • "palette pulled from the source (warm cream ground, ...)" ← Stylized Frond variant
+// • "pulled from the source: warm cream ground, ... ." ← Cubist Blocks variant
+// • "Single tonal palette pulled from the source: warm cream ..." ← misc
+function extractPalette(prompt) {
+ if (!prompt) return null;
+ // Pattern A: any "( ... )" group that comes after "palette" OR "from the source"
+ const mA = prompt.match(/(?:palette|from the source)[^()]{0,60}\(([^)]+)\)/i);
+ if (mA) return mA[1].trim();
+ // Pattern B: "pulled from the source: <palette text>."
+ const mB = prompt.match(/pulled from the source:?\s*([^.]+)\./i);
+ if (mB) return mB[1].trim();
+ // Pattern C: first parenthesized phrase that mentions a color word
+ const COLOR_WORDS = ['ground','ivory','cream','navy','olive','blue','red','green','gold','black','white','champagne','marine','teal','cornflower','umber','moss','ink','bone','orange','yellow'];
+ const allParens = prompt.match(/\(([^)]+)\)/g) || [];
+ for (const p of allParens) {
+ const inner = p.slice(1,-1).toLowerCase();
+ if (COLOR_WORDS.some(w => inner.includes(w))) return p.slice(1,-1).trim();
+ }
+ return null;
+}
+
+// Hard guardrail — palette is the only user-derived input; anti-prompt boilerplate
+// in the template legitimately names these tokens (e.g., "no leaves of any kind").
+// So we ONLY scan the palette string for unsafe vocabulary that would leak through.
+const FORBIDDEN_IN_PALETTE = [
+ 'banana','musa','monstera','frond','palm leaf','palm frond','palmate','tropical',
+ 'leaf','leaves','foliage','botanical','plant','flora','floral','flower',
+ 'vine','vines','branch','tree','trunk','bird','butterfly','insect','grape','fruit',
+];
+function paletteSafe(palette) {
+ const lc = (palette || '').toLowerCase();
+ return !FORBIDDEN_IN_PALETTE.some(tok => new RegExp(`\\b${tok.replace(/\s+/g,'\\s+')}\\b`).test(lc));
+}
+
+function buildPrompt(palette, shapeIdx) {
+ const family = SHAPE_FAMILIES[shapeIdx % SHAPE_FAMILIES.length];
+ // PURE contemporary-abstract brief. ZERO botanical vocabulary.
+ // Palette is the ONLY thing carried from the BH source.
+ const p = [
+ `Contemporary abstract wallpaper repeat — modernist gallery aesthetic.`,
+ `Composition: ${family.phrase}.`,
+ `Color palette only (do NOT depict any subject from the palette description, treat it strictly as a color spec): ${palette}.`,
+ `Absolutely no representational content. No leaves of any kind. No foliage. No botanical motifs. No plants. No florals. No vines. No fronds. No trees. No fruit. No animals. No insects. No avian figures. No human figures. No architectural elements. No text. No watermark. No signature.`,
+ `Pure abstract geometric or organic forms ONLY. Edge-to-edge coverage. Seamless tile, archival quality, square crop.`,
+ ].join(' ');
+ return { prompt: p, family };
+}
+
+async function callGemini(prompt) {
+ const body = {
+ contents: [{ parts: [{ text: prompt }] }],
+ generationConfig: { responseModalities: ['IMAGE'] },
+ };
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=${KEY}`;
+ const r = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+ if (!r.ok) {
+ const txt = (await r.text()).slice(0, 240);
+ throw new Error(`HTTP ${r.status}: ${txt}`);
+ }
+ const j = await r.json();
+
+ // best-effort cost log
+ try {
+ const { logGemini } = require(os.homedir() + '/.claude/skills/cost-tracker/scripts/log-gemini.js');
+ logGemini(j, { app: 'wallco-ai', note: 'bh-abstract-leaves contemporary regen', model: 'gemini-2.5-flash-image' });
+ } catch {}
+
+ const part = j.candidates?.[0]?.content?.parts?.find(p => p.inline_data || p.inlineData);
+ if (!part) throw new Error(`no image in response (${JSON.stringify(j).slice(0,200)})`);
+ const data = part.inline_data?.data || part.inlineData?.data;
+ if (!data) throw new Error('image part had no data');
+ return Buffer.from(data, 'base64');
+}
+
+async function processRow(row, shapeIdx) {
+ const palette = extractPalette(row.prompt);
+ if (!palette) throw new Error(`could not extract palette from prompt for id=${row.id}`);
+
+ if (!paletteSafe(palette)) throw new Error(`palette contains forbidden token (id=${row.id}): ${palette}`);
+
+ if (DRY_RUN) {
+ const dryFam = SHAPE_FAMILIES[shapeIdx % SHAPE_FAMILIES.length];
+ console.log(`[dry] id=${row.id} family=${dryFam.key} palette="${palette.slice(0,60)}..."`);
+ return { ok: true, id: row.id, dry: true };
+ }
+
+ // 1) Generate via TEXT-ONLY Gemini call — retry across shape families on IMAGE_RECITATION/NO_IMAGE
+ let png = null, family = null, lastErr = null;
+ for (let attempt = 0; attempt < SHAPE_FAMILIES.length; attempt++) {
+ const tryIdx = (shapeIdx + attempt) % SHAPE_FAMILIES.length;
+ const built = buildPrompt(palette, tryIdx);
+ try {
+ png = await callGemini(built.prompt);
+ family = built.family;
+ var newPrompt = built.prompt;
+ if (attempt > 0) console.log(`[retry] id=${row.id} succeeded on family=${family.key} (attempt ${attempt+1})`);
+ break;
+ } catch (e) {
+ lastErr = e;
+ if (!/IMAGE_RECITATION|NO_IMAGE|no image in response/i.test(e.message)) throw e; // hard error, don't retry
+ }
+ }
+ if (!png) throw lastErr || new Error('all shape families refused');
+
+ // 2) Backup old file (forensic only — never republish; banana-bleed risk)
+ if (row.local_path && fs.existsSync(row.local_path)) {
+ const bn = `${row.id}_${path.basename(row.local_path)}`;
+ try { fs.copyFileSync(row.local_path, path.join(BACKUP_DIR, bn)); } catch {}
+ }
+
+ // 3) Write new PNG
+ const seed = Math.floor(Math.random() * 1e9);
+ const slug = `bh_abstract_contemporary_${family.key}_${row.id}_${Date.now()}_${seed}.png`;
+ const outAbs = path.join(OUT_DIR, slug);
+ fs.writeFileSync(outAbs, png);
+ const outUrl = `/designs/img/${slug}`;
+
+ // 4) UPDATE row in place — same id, replaced content, fresh motifs/tags
+ // Mark as needing re-review (is_published stays FALSE)
+ // Strip leaf-vocab tags/motifs, add the contemporary tags
+ const newTags = ['bh', 'bh90210', 'beverly-hills', 'contemporary-abstract', family.key];
+ const newMotifs = ['contemporary-abstract', family.key, 'bh90210'];
+ const auditNote = `Regenerated 2026-05-13 from leaf-vocab → contemporary abstract (${family.key}). Palette preserved: ${palette.slice(0,120)}. Old PNG backed up to ${path.join(BACKUP_DIR, `${row.id}_${path.basename(row.local_path || 'unknown')}`)}.`;
+
+ const sql = `
+ UPDATE spoon_all_designs SET
+ local_path = ${esc(outAbs)},
+ image_url = ${esc(outUrl)},
+ prompt = ${esc(newPrompt)},
+ motifs = ARRAY[${newMotifs.map(esc).join(',')}]::text[],
+ tags = ARRAY[${newTags.map(esc).join(',')}]::text[],
+ seed = ${seed},
+ is_published = FALSE,
+ notes = COALESCE(notes,'') || E'\\n[bh-leaves-regen 2026-05-13] ' || ${esc(auditNote)},
+ rated_at = NULL,
+ combined_score = NULL
+ WHERE id = ${row.id};
+ `;
+ psql(sql);
+
+ return { ok: true, id: row.id, family: family.key, file: outAbs };
+}
+
+async function main() {
+ // Pull all live rows that still need regen — rows already-regen'd have
+ // 'contemporary-abstract' in their motifs array, skip those.
+ const ONLY_PENDING = args.includes('--only-pending');
+ const rawSql = `
+ SELECT json_agg(row_to_json(t)) FROM (
+ SELECT id, dominant_hex, prompt, local_path
+ FROM spoon_all_designs
+ WHERE category='bh-abstract-leaves'
+ AND COALESCE(user_removed,false)=false
+ ${ONLY_PENDING ? "AND NOT ('contemporary-abstract' = ANY(motifs))" : ''}
+ ORDER BY id
+ ) t;
+ `;
+ const raw = psql(rawSql);
+ const rows = raw ? JSON.parse(raw) : [];
+ if (!rows.length) { console.log('no rows in bh-abstract-leaves'); return; }
+
+ const targets = rows.slice(0, LIMIT);
+ console.log(`[plan] ${targets.length} of ${rows.length} rows · concurrency=${CONCURRENCY} · dryRun=${DRY_RUN}`);
+
+ let nextIdx = 0;
+ const results = [];
+ async function worker(wid) {
+ while (true) {
+ const myIdx = nextIdx++;
+ if (myIdx >= targets.length) return;
+ const row = targets[myIdx];
+ const t0 = Date.now();
+ try {
+ const r = await processRow(row, myIdx);
+ const dt = ((Date.now()-t0)/1000).toFixed(1);
+ console.log(`[w${wid}] OK id=${row.id} family=${r.family || 'dry'} ${dt}s (${myIdx+1}/${targets.length})`);
+ results.push(r);
+ } catch (e) {
+ console.error(`[w${wid}] FAIL id=${row.id} ${e.message}`);
+ results.push({ ok: false, id: row.id, error: e.message });
+ }
+ }
+ }
+ await Promise.all(Array.from({ length: CONCURRENCY }, (_, i) => worker(i+1)));
+
+ const ok = results.filter(r => r.ok).length;
+ const fail = results.length - ok;
+ console.log(`\n[done] ok=${ok} fail=${fail} of ${targets.length}`);
+ if (fail) process.exit(1);
+}
+
+main().catch(e => { console.error(e); process.exit(1); });
← 27158ce marketplace: designer-powered Wallco marketplace (Patternban
·
back to Wallco Ai
·
purge: hard-delete all 44 bh-abstract-leaves designs 2b84709 →