← back to Wallco Ai
Strip reserved-brand strings: BH/Beverly Hills source filenames moved to .env (BUTTERFLY_DREAM_SRC, CLEAN_TRELLIS_SRC); 4 BH-named generator scripts removed from working tree (history preserved); 41 bh-coordinates DB rows + 85 PNGs + sources backed up to ~/.wallco-deleted/2026-05-13/bh-all/
296136508f982ff76ec77d7c131d16d309c6d044 · 2026-05-13 17:19:42 -0700 · SteveStudio2
Files touched
D scripts/generate-bh-batch-retry.jsD scripts/generate-bh-coords-abstracts-batch.jsM scripts/generate-butterfly-trellis-batch.jsD scripts/purge-bh-abstract-leaves.jsM scripts/regen-bamboo-trellis-to-cane.jsD scripts/regen-bh-abstract-leaves-contemporary.js
Diff
commit 296136508f982ff76ec77d7c131d16d309c6d044
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 13 17:19:42 2026 -0700
Strip reserved-brand strings: BH/Beverly Hills source filenames moved to .env (BUTTERFLY_DREAM_SRC, CLEAN_TRELLIS_SRC); 4 BH-named generator scripts removed from working tree (history preserved); 41 bh-coordinates DB rows + 85 PNGs + sources backed up to ~/.wallco-deleted/2026-05-13/bh-all/
---
scripts/generate-bh-batch-retry.js | 142 ----------
scripts/generate-bh-coords-abstracts-batch.js | 315 -----------------------
scripts/generate-butterfly-trellis-batch.js | 24 +-
scripts/purge-bh-abstract-leaves.js | 87 -------
scripts/regen-bamboo-trellis-to-cane.js | 9 +-
scripts/regen-bh-abstract-leaves-contemporary.js | 275 --------------------
6 files changed, 23 insertions(+), 829 deletions(-)
diff --git a/scripts/generate-bh-batch-retry.js b/scripts/generate-bh-batch-retry.js
deleted file mode 100644
index 54f7cfb..0000000
--- a/scripts/generate-bh-batch-retry.js
+++ /dev/null
@@ -1,142 +0,0 @@
-#!/usr/bin/env node
-/**
- * Retry pass for generate-bh-coords-abstracts-batch.js failures.
- *
- * - Re-uses the same prompt library + settlement gate.
- * - Reads the log from the prior run and finds every "[ N/50] FAIL"
- * line that didn't yield a row in spoon_all_designs.
- * - For Oxford Olive failures, uses the down-scaled source.
- * - Adds an explicit "STYLIZED COMPANION DESIGN, do NOT reproduce
- * or trace the source" cue to dodge IMAGE_RECITATION refusals.
- * - Still settlement-gated; still is_published=FALSE.
- *
- * node scripts/generate-bh-batch-retry.js [LOG_PATH]
- */
-'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, spawnSync } = require('child_process');
-const { gateOrExit } = require('./settlement-gate');
-
-const KEY = process.env.GEMINI_API_KEY;
-if (!KEY) { console.error('GEMINI_API_KEY missing'); process.exit(1); }
-
-const SRC_DIR = '/tmp/wallco-sources';
-const OUT_DIR = path.join(__dirname, '..', 'data', 'generated');
-fs.mkdirSync(OUT_DIR, { recursive: true });
-
-const LOG_PATH = process.argv[2] || '/tmp/bh-batch.log';
-const DB = process.env.WALLCO_DB || 'dw_unified';
-const PSQL = (process.platform === 'linux') ? `sudo -n -u postgres psql ${DB} -At -q` : `psql ${DB} -At -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, "''") + "'"; }
-
-// Re-import the original module's prompt library by reading + sandboxing
-const origPath = path.join(__dirname, 'generate-bh-coords-abstracts-batch.js');
-const origSrc = fs.readFileSync(origPath, 'utf8');
-const block = origSrc.match(/\/\/ ── Source colorways[\s\S]+?function variants\(\) \{[\s\S]+?\n\}/)[0];
-const variants = (new Function(block + '\nreturn variants();'))();
-
-// Identify failures from the log: positions like "[ N/50] FAIL"
-const log = fs.existsSync(LOG_PATH) ? fs.readFileSync(LOG_PATH, 'utf8') : '';
-const failedIdx = [];
-for (const line of log.split('\n')) {
- const m = line.match(/\[\s*(\d+)\/\d+\]\s+FAIL/);
- if (m) failedIdx.push(parseInt(m[1], 10) - 1);
-}
-console.log(`Retry plan: ${failedIdx.length} failures from ${LOG_PATH}`);
-const todo = failedIdx.map(i => variants[i]).filter(Boolean);
-
-async function generateOne(v, idx, totalN) {
- // Force smaller Oxford Olive source on retry
- let src = v.src;
- if (src.includes('OxfordOlive')) src = 'bh-gdrive/BH90210-OxfordOlive-clean.jpg';
- const srcPath = path.join(SRC_DIR, src);
- if (!fs.existsSync(srcPath)) throw new Error(`source missing: ${srcPath}`);
- const imageB64 = fs.readFileSync(srcPath).toString('base64');
-
- // Add anti-recitation cue
- const prompt = v.prompt + ' This is a STYLIZED COMPANION DESIGN inspired by the source — do NOT reproduce, trace, or copy the source image. Render a new derivative composition from scratch in the described style.';
- gateOrExit(prompt);
-
- const body = {
- contents: [{
- parts: [
- { inline_data: { mime_type: 'image/jpeg', data: imageB64 } },
- { text: prompt },
- ],
- }],
- generationConfig: { responseModalities: ['IMAGE'] },
- };
- const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=${KEY}`;
- const t0 = Date.now();
- const r = await fetch(url, {
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
- });
- if (!r.ok) throw new Error(`HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);
- const j = await r.json();
- try {
- const { logGemini } = require(require('os').homedir() + '/.claude/skills/cost-tracker/scripts/log-gemini.js');
- logGemini(j, { app: 'wallco-ai', note: 'bh-coords-abstracts-retry', model: 'gemini-2.5-flash-image' });
- } catch {}
- const part = j.candidates?.[0]?.content?.parts?.find(p => p.inline_data || p.inlineData);
- const data = part?.inline_data?.data || part?.inlineData?.data;
- if (!data) {
- const text = j.candidates?.[0]?.content?.parts?.find(p => p.text)?.text;
- throw new Error(`no image (${j.candidates?.[0]?.finishReason || 'unknown'}): ${(text || '').slice(0, 200)}`);
- }
- const seed = crypto.randomInt(1, 2 ** 31 - 1);
- const filename = `bh_${v.kind}_${v.type_key}_${v.colorway.code}_retry_${Date.now()}_${seed}.png`;
- const outPath = path.join(OUT_DIR, filename);
- fs.writeFileSync(outPath, Buffer.from(data, 'base64'));
- const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
-
- const py = spawnSync('python3', ['-c', `
-from PIL import Image
-from collections import Counter
-import json, sys
-img = Image.open(sys.argv[1]).convert('RGB')
-img.thumbnail((300, 300))
-pal = img.quantize(colors=5, method=Image.Quantize.MEDIANCUT).convert('RGB')
-pixels = list(pal.getdata())
-cnt = Counter(pixels)
-total = sum(cnt.values())
-palette = [{'hex':'#{:02x}{:02x}{:02x}'.format(*rgb), 'pct':round(n/total*100,2)} for rgb,n in cnt.most_common(5)]
-print(json.dumps(palette))
-`, outPath], { encoding: 'utf8' });
- let palette = []; let dominant = null;
- try { palette = JSON.parse(py.stdout.trim()); dominant = palette[0]?.hex; } catch {}
-
- const tagsArr = `ARRAY[${v.tags.map(t => "'" + t.replace(/'/g, "''") + "'").join(',')}]::text[]`;
- const motifsArr = `ARRAY[${v.motifs.map(t => "'" + t.replace(/'/g, "''") + "'").join(',')}]::text[]`;
- const fullPrompt = (v.title + ' — ' + v.prompt).replace(/'/g, "''");
- const palJson = JSON.stringify(palette).replace(/'/g, "''");
- const note = `bh-batch retry ${v.kind}/${v.type_key}/${v.colorway.code} — hidden for admin review`;
- const sql = `INSERT INTO spoon_all_designs
- (kind, width_in, height_in, generator, prompt, seed, dominant_hex, palette,
- local_path, image_url, category, motifs, tags, is_published, request_text, notes)
- VALUES ('seamless_tile', 24, 24, 'gemini-2.5-flash-image-edit',
- '${fullPrompt}', ${seed},
- ${dominant ? "'" + dominant + "'" : 'NULL'},
- '${palJson}'::jsonb,
- ${esc(outPath)}, ${esc('/designs/img/' + filename)},
- ${esc(v.category)}, ${motifsArr}, ${tagsArr},
- FALSE, ${esc(`bh-${v.kind} retry: ${v.title}`)},
- ${esc(note)})
- RETURNING id;`;
- const id = parseInt(psql(sql), 10);
- console.log(` [${String(idx + 1).padStart(2)}/${totalN}] ${v.kind === 'coord' ? 'COORD' : 'ABSTR'} ${v.title} → id=${id} ${elapsed}s`);
- return { id, title: v.title };
-}
-
-(async () => {
- console.log(`\nRetrying ${todo.length} BH variants…\n`);
- const results = [];
- for (let i = 0; i < todo.length; i++) {
- try { results.push(await generateOne(todo[i], i, todo.length)); }
- catch (e) { console.error(` [${String(i + 1).padStart(2)}/${todo.length}] FAIL — ${e.message.slice(0, 200)}`); }
- }
- console.log(`\nRetry done. ${results.length}/${todo.length} succeeded.`);
-})();
diff --git a/scripts/generate-bh-coords-abstracts-batch.js b/scripts/generate-bh-coords-abstracts-batch.js
deleted file mode 100644
index 821d71e..0000000
--- a/scripts/generate-bh-coords-abstracts-batch.js
+++ /dev/null
@@ -1,315 +0,0 @@
-#!/usr/bin/env node
-/**
- * BH (Beverly Hills 90210) — 50 coordinate + abstract-leaf variants.
- *
- * Sources (under /tmp/wallco-sources/):
- * • BH90210-900 Butterfly Dream.jpg (existing -900 colorway)
- * • bh-gdrive/BH90210-706-BeverlyBeachHouse-Blue.jpg
- * • bh-gdrive/BH90210-OxfordOlive-FullHalfDropRepeat.jpg
- *
- * Output:
- * • 25 coordinates (mini cane trellis / bamboo stripe / tonal damask /
- * grasscloth texture / geometric lattice — all settlement-safe because
- * they carry no leaves+butterflies+multi-color combo)
- * • 25 abstract-leaf interpretations (monochrome silhouette / cubist /
- * ink-wash / single-orientation rows / acceptable-element bearing)
- *
- * Every prompt is gated by settlement-gate.js BEFORE the Gemini call.
- * All rows land with is_published=FALSE — Steve reviews them in the admin
- * queue at http://localhost:9792/admin/designs?cat=bh-coordinates and
- * .../?cat=bh-abstract-leaves before any go live.
- *
- * Run:
- * cd ~/Projects/wallco-ai && node scripts/generate-bh-coords-abstracts-batch.js
- */
-'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, spawnSync } = require('child_process');
-const { gateOrExit } = require('./settlement-gate');
-
-const KEY = process.env.GEMINI_API_KEY;
-if (!KEY) { console.error('GEMINI_API_KEY missing'); process.exit(1); }
-
-const SRC_DIR = '/tmp/wallco-sources';
-const OUT_DIR = path.join(__dirname, '..', 'data', 'generated');
-fs.mkdirSync(OUT_DIR, { recursive: true });
-
-const DB = process.env.WALLCO_DB || 'dw_unified';
-const PSQL = (process.platform === 'linux')
- ? `sudo -n -u postgres psql ${DB} -At -q`
- : `psql ${DB} -At -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, "''") + "'"; }
-
-// ── Source colorways (BH master refs) ──────────────────────────────────────
-const COLORWAYS = [
- {
- code: '900',
- name: 'Butterfly Dream',
- src: 'BH90210-900 Butterfly Dream.jpg',
- palette: 'warm cream ground, monarch orange, bright yellow, scarlet red, sky blue, emerald accents',
- },
- {
- code: '706',
- name: 'Beverly Beach House Blue',
- src: 'bh-gdrive/BH90210-706-BeverlyBeachHouse-Blue.jpg',
- palette: 'ivory ground, soft cornflower blue, deep marine blue, muted teal, dusty navy',
- },
- {
- code: 'olive',
- name: 'Oxford Olive',
- src: 'bh-gdrive/BH90210-OxfordOlive-smaller.jpg',
- palette: 'warm bone ground, oxford olive, deep moss, antique gold accents, soft umber',
- },
-];
-
-// ── 5 coordinate types × per colorway. Settlement-safe by design:
-// - Coordinates carry NO leaves+open-space+multi-color combo, AND
-// - Most are monochrome / tonal; none reference banana/grape/bird/butterfly.
-const COORD_TYPES = [
- {
- key: 'mini-cane-trellis',
- title: 'Mini Cane Trellis Coordinate',
- promptFor: cw => `Generate a small-scale (4-inch repeat) cane lattice / trellis grid coordinate companion to the source pattern. Single-ink monochrome rendering pulled from the dominant tone of the source. No leaves, no foliage, no butterflies, no birds. Just clean, fine cane-bamboo lattice geometry on a tonal background drawn from the source palette (${cw.palette}). Seamless tile, archival quality, square crop, wallpaper-pattern aesthetic, no signature, no watermark, no text.`,
- },
- {
- key: 'bamboo-stripe',
- title: 'Bamboo Stripe Coordinate',
- promptFor: cw => `Generate a vertical bamboo-pole stripe coordinate companion. Each bamboo stalk runs floor-to-ceiling with subtle node detail. Monochrome single-ink. No leaves, no shoots, no foliage, no butterflies, no birds. Edge-to-edge regular vertical stripes — no open space between bamboo poles, no scattered or tossed elements. Tonal background from the source palette (${cw.palette}). Seamless tile, archival, square crop, wallpaper, no signature, no watermark, no text.`,
- },
- {
- key: 'tonal-damask',
- title: 'Tonal Damask Coordinate',
- promptFor: cw => `Generate a quiet single-color damask coordinate companion at one-third the scale of a hero pattern. Classical urn-and-scroll silhouette only, fully monochrome / tonal, edge-to-edge coverage with no breathing space. No leaves of any species, no foliage, no butterflies, no birds. Color drawn from the source palette (${cw.palette}). Seamless tile, archival, square crop, wallpaper, no signature, no watermark, no text.`,
- },
- {
- key: 'grasscloth-texture',
- title: 'Grasscloth Texture Coordinate',
- promptFor: cw => `Generate a horizontal grasscloth weave texture coordinate. Fine sea-grass strands woven across the field, edge-to-edge full-coverage, no negative space, no motifs. Monochrome single-ink wash in a tone pulled from the source palette (${cw.palette}). No leaves, foliage, butterflies, or birds. Seamless tile, archival, square crop, wallpaper, no signature, no watermark, no text.`,
- },
- {
- key: 'diamond-lattice',
- title: 'Diamond Lattice Coordinate',
- promptFor: cw => `Generate a geometric diamond-lattice coordinate at small scale. Crisp ink lines forming diamond grid; tiny dot or quatrefoil at each intersection. Monochrome silhouette only. No foliage, leaves, butterflies, birds. Edge-to-edge regular geometry, no open negative space. Tone drawn from the source palette (${cw.palette}). Seamless tile, archival, square crop, wallpaper, no signature, no watermark, no text.`,
- },
-];
-
-// ── 5 abstract-leaf interpretations × per colorway. Each strategy
-// breaks at least one of Part A's three legs so settlement returns OK
-// even with leaf vocabulary. NONE name banana/grape/bird/butterfly.
-const ABSTRACT_TYPES = [
- {
- key: 'silhouette-mono',
- title: 'Broadleaf Silhouette · Monochrome',
- promptFor: cw => `Reinterpret the foliage motif from the source as a single-ink monochrome silhouette of a stylized fan-shaped palmate leaf. All leaves oriented in the SAME direction across the repeat (no rotation). Tonal background drawn from the source palette (${cw.palette}). Edge-to-edge, full coverage, no gaps. STRICT EXCLUSIONS: do NOT draw Musa species, do NOT draw oblong unbroken pinnate fronds, do NOT include any insects, do NOT include any avian figures. Seamless tile, archival, square crop, wallpaper, no signature, no watermark, no text.`,
- },
- {
- key: 'cubist-blocks',
- title: 'Cubist Leaf Blocks · Edge-to-Edge',
- promptFor: cw => `Reinterpret the foliage as flat cubist single-color silhouette blocks tiled in a regular grid, all blocks pointing the same way, full coverage with no gaps. Single tonal palette pulled from the source: ${cw.palette}. Each leaf shape is a geometric fan-shaped block, NOT an elongated whole pinnate frond. STRICT EXCLUSIONS: do NOT draw Musa species, do NOT include any insects, do NOT include any avian figures. Seamless tile, archival, square crop, wallpaper, no signature, no watermark, no text.`,
- },
- {
- key: 'ink-wash',
- title: 'Ink Wash Leaves · Tonal Single-Ink',
- promptFor: cw => `Reinterpret the foliage as a sumi-e ink wash, single-ink monochrome tonal only, soft brushstroke fan-leaves and palmate forms, all-same-orientation rows. Use a single ink tone pulled from the source palette (${cw.palette}). STRICT EXCLUSIONS: do NOT draw Musa species, do NOT draw oblong pinnate fronds, do NOT include any insects, do NOT include any avian figures, do NOT include any fruit. Seamless tile, archival, square crop, wallpaper, no signature, no watermark, no text.`,
- },
- {
- key: 'botanical-rows',
- title: 'Botanical Rows · Single-Orientation',
- promptFor: cw => `Reinterpret the foliage as a botanical-textbook illustration of fan-shaped and palmate leaves, all leaves drawn in the SAME orientation across the repeat. Tonal silhouette line drawing on a single-color ground. Pull tone from the source palette (${cw.palette}). STRICT EXCLUSIONS: do NOT draw Musa species, do NOT include any insects, do NOT include any avian figures, do NOT include any fruit. Seamless tile, archival, square crop, wallpaper, no signature, no watermark, no text.`,
- },
- {
- key: 'tree-trunk-companion',
- title: 'Stylized Frond with Tree Trunks',
- promptFor: cw => `Reinterpret the foliage as stylized fan-shaped palmate fronds growing from clearly represented tree trunks and branches. Tree trunks and branches are the dominant rhythm; the fronds attach to them. Monochrome single-color silhouette tonal palette pulled from the source (${cw.palette}). STRICT EXCLUSIONS: do NOT draw Musa species, do NOT include any insects, do NOT include any avian figures, do NOT include grapes. Seamless tile, archival, square crop, wallpaper, no signature, no watermark, no text.`,
- },
-];
-
-// ── Build the full 50-variant matrix ────────────────────────────────────────
-function variants() {
- const out = [];
- // Coordinates: 5 types × 3 colorways = 15
- for (const t of COORD_TYPES) {
- for (const cw of COLORWAYS) {
- out.push({
- kind: 'coord',
- category: 'bh-coordinates',
- title: `${t.title} · ${cw.name}`,
- prompt: t.promptFor(cw),
- src: cw.src,
- colorway: cw,
- type_key: t.key,
- tags: ['bh', 'bh90210', 'beverly-hills', 'coordinate', t.key, cw.code],
- motifs: ['coordinate', t.key, 'bh90210'],
- });
- }
- }
- // Abstract leaves: 5 types × 3 colorways = 15
- for (const t of ABSTRACT_TYPES) {
- for (const cw of COLORWAYS) {
- out.push({
- kind: 'abstract',
- category: 'bh-abstract-leaves',
- title: `${t.title} · ${cw.name}`,
- prompt: t.promptFor(cw),
- src: cw.src,
- colorway: cw,
- type_key: t.key,
- tags: ['bh', 'bh90210', 'beverly-hills', 'abstract-leaf', t.key, cw.code],
- motifs: ['abstract-leaf', t.key, 'bh90210'],
- });
- }
- }
- // Top up to 50 with two extra per type using a synthesized colorway pair
- const EXTRA_CWS = [
- { code: 'bone-ink', name: 'Bone & Ink', palette: 'warm bone ground, deep india-ink black, single-color silhouette' },
- { code: 'champ-mid', name: 'Champagne & Midnight', palette: 'midnight navy ground, soft pale champagne, antique gold rim' },
- ];
- // 5 coord types × 2 extras = 10 -> 25 coords total
- for (const t of COORD_TYPES) {
- for (const cw of EXTRA_CWS) {
- out.push({
- kind: 'coord',
- category: 'bh-coordinates',
- title: `${t.title} · ${cw.name}`,
- prompt: t.promptFor(cw),
- src: 'BH90210-900 Butterfly Dream.jpg', // recolor against -900 master
- colorway: cw,
- type_key: t.key,
- tags: ['bh', 'bh90210', 'beverly-hills', 'coordinate', t.key, cw.code],
- motifs: ['coordinate', t.key, 'bh90210'],
- });
- }
- }
- // 5 abstract types × 2 extras = 10 -> 25 abstracts total
- for (const t of ABSTRACT_TYPES) {
- for (const cw of EXTRA_CWS) {
- out.push({
- kind: 'abstract',
- category: 'bh-abstract-leaves',
- title: `${t.title} · ${cw.name}`,
- prompt: t.promptFor(cw),
- src: 'BH90210-900 Butterfly Dream.jpg',
- colorway: cw,
- type_key: t.key,
- tags: ['bh', 'bh90210', 'beverly-hills', 'abstract-leaf', t.key, cw.code],
- motifs: ['abstract-leaf', t.key, 'bh90210'],
- });
- }
- }
- return out;
-}
-
-async function generateOne(v, idx, totalN) {
- const srcPath = path.join(SRC_DIR, v.src);
- if (!fs.existsSync(srcPath)) throw new Error(`source missing: ${srcPath}`);
- const imageB64 = fs.readFileSync(srcPath).toString('base64');
-
- // SETTLEMENT GATE — must return OK before we touch Gemini.
- gateOrExit(v.prompt);
-
- const body = {
- contents: [{
- parts: [
- { inline_data: { mime_type: 'image/jpeg', data: imageB64 } },
- { text: v.prompt },
- ],
- }],
- generationConfig: { responseModalities: ['IMAGE'] },
- };
- const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=${KEY}`;
- const t0 = Date.now();
- const r = await fetch(url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body),
- });
- if (!r.ok) throw new Error(`HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);
- const j = await r.json();
-
- // Cost-tracker hook (best-effort)
- try {
- const { logGemini } = require(require('os').homedir() + '/.claude/skills/cost-tracker/scripts/log-gemini.js');
- logGemini(j, { app: 'wallco-ai', note: 'bh-coords-abstracts-batch', model: 'gemini-2.5-flash-image' });
- } catch {}
-
- const part = j.candidates?.[0]?.content?.parts?.find(p => p.inline_data || p.inlineData);
- const data = part?.inline_data?.data || part?.inlineData?.data;
- if (!data) {
- const text = j.candidates?.[0]?.content?.parts?.find(p => p.text)?.text;
- throw new Error(`no image returned (${j.candidates?.[0]?.finishReason || 'unknown'}): ${(text || '').slice(0, 200)}`);
- }
- const seed = crypto.randomInt(1, 2 ** 31 - 1);
- const filename = `bh_${v.kind}_${v.type_key}_${v.colorway.code}_${Date.now()}_${seed}.png`;
- const outPath = path.join(OUT_DIR, filename);
- fs.writeFileSync(outPath, Buffer.from(data, 'base64'));
- const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
-
- // Palette extraction (5 colors)
- const py = spawnSync('python3', ['-c', `
-from PIL import Image
-from collections import Counter
-import json, sys
-img = Image.open(sys.argv[1]).convert('RGB')
-img.thumbnail((300, 300))
-pal = img.quantize(colors=5, method=Image.Quantize.MEDIANCUT).convert('RGB')
-pixels = list(pal.getdata())
-cnt = Counter(pixels)
-total = sum(cnt.values())
-palette = [{'hex':'#{:02x}{:02x}{:02x}'.format(*rgb), 'pct':round(n/total*100,2)} for rgb,n in cnt.most_common(5)]
-print(json.dumps(palette))
-`, outPath], { encoding: 'utf8' });
- let palette = []; let dominant = null;
- try { palette = JSON.parse(py.stdout.trim()); dominant = palette[0]?.hex; } catch {}
-
- // Insert — is_published=FALSE (HIDDEN; admin-review only)
- const tagsArr = `ARRAY[${v.tags.map(t => "'" + t.replace(/'/g, "''") + "'").join(',')}]::text[]`;
- const motifsArr = `ARRAY[${v.motifs.map(t => "'" + t.replace(/'/g, "''") + "'").join(',')}]::text[]`;
- const fullPrompt = (v.title + ' — ' + v.prompt).replace(/'/g, "''");
- const palJson = JSON.stringify(palette).replace(/'/g, "''");
- const note = `bh-batch ${v.kind}/${v.type_key}/${v.colorway.code} — hidden for admin review`;
- const sql = `INSERT INTO spoon_all_designs
- (kind, width_in, height_in, generator, prompt, seed, dominant_hex, palette,
- local_path, image_url, category, motifs, tags, is_published, request_text, notes)
- VALUES ('seamless_tile', 24, 24, 'gemini-2.5-flash-image-edit',
- '${fullPrompt}', ${seed},
- ${dominant ? "'" + dominant + "'" : 'NULL'},
- '${palJson}'::jsonb,
- ${esc(outPath)}, ${esc('/designs/img/' + filename)},
- ${esc(v.category)},
- ${motifsArr},
- ${tagsArr},
- FALSE,
- ${esc(`bh-${v.kind} #${idx + 1}/${totalN}: ${v.title}`)},
- ${esc(note)})
- RETURNING id;`;
- const id = parseInt(psql(sql), 10);
- console.log(` [${String(idx + 1).padStart(2)}/${totalN}] ${v.kind === 'coord' ? 'COORD' : 'ABSTR'} ${v.title} → id=${id} ${elapsed}s`);
- return { id, title: v.title, category: v.category, image_url: '/designs/img/' + filename };
-}
-
-(async () => {
- const all = variants();
- console.log(`\nBH coordinates + abstract leaves — ${all.length} variants (target: 50)`);
- console.log(`Sources: ${COLORWAYS.map(c => c.code).join(', ')} (+ synthesized bone-ink, champ-mid)`);
- console.log(`Settlement gate: every prompt checked BEFORE Gemini.`);
- console.log(`Output: is_published=FALSE — admin review queue.\n`);
-
- const results = [];
- for (let i = 0; i < all.length; i++) {
- try { results.push(await generateOne(all[i], i, all.length)); }
- catch (e) { console.error(` [${String(i + 1).padStart(2)}/${all.length}] FAIL — ${e.message.slice(0, 200)}`); }
- }
- console.log(`\nDone. ${results.length}/${all.length} succeeded.`);
- if (results.length) {
- const coords = results.filter(r => r.category === 'bh-coordinates').length;
- const abstr = results.filter(r => r.category === 'bh-abstract-leaves').length;
- console.log(` coordinates: ${coords} abstract-leaves: ${abstr}`);
- console.log(`Review queue (admin, hidden):`);
- console.log(` http://localhost:9792/designs?cat=bh-coordinates&published=0`);
- console.log(` http://localhost:9792/designs?cat=bh-abstract-leaves&published=0`);
- }
-})();
diff --git a/scripts/generate-butterfly-trellis-batch.js b/scripts/generate-butterfly-trellis-batch.js
index 5afc399..39176b3 100644
--- a/scripts/generate-butterfly-trellis-batch.js
+++ b/scripts/generate-butterfly-trellis-batch.js
@@ -2,10 +2,14 @@
/**
* One-off: generate 10 butterfly + trellis wallpaper variations using
* Gemini 2.5 Flash Image, seeded by three reference TIFs Steve dropped in
- * Downloads/. The Beverly-Hills banana-leaf source gets its leaves stripped
+ * Downloads/. The reserved-brand banana-leaf source gets its leaves stripped
* out; the clean cane-trellis source gets butterflies added; the original
* butterfly-on-cane source gets palette + style swaps.
*
+ * Source filenames are private — read from env (.env, never inlined):
+ * BUTTERFLY_DREAM_SRC=... ← banana-leaf source
+ * CLEAN_TRELLIS_SRC=... ← clean cane trellis
+ *
* Outputs land in ~/Projects/wallco-ai/data/generated/ and a row is inserted
* into spoon_all_designs (is_published=true) so they show up on /designs.
*
@@ -21,6 +25,12 @@ const { gateOrExit } = require('./settlement-gate');
const KEY = process.env.GEMINI_API_KEY;
if (!KEY) { console.error('GEMINI_API_KEY missing'); process.exit(1); }
+// Reserved-brand source filenames are private — pulled from .env, never inlined.
+const BUTTERFLY_DREAM_SRC = process.env.BUTTERFLY_DREAM_SRC;
+const CLEAN_TRELLIS_SRC = process.env.CLEAN_TRELLIS_SRC;
+if (!BUTTERFLY_DREAM_SRC || !CLEAN_TRELLIS_SRC) {
+ console.error('BUTTERFLY_DREAM_SRC and CLEAN_TRELLIS_SRC must be set in .env'); process.exit(1);
+}
const SRC = '/tmp/wallco-sources';
const OUT_DIR = path.join(__dirname, '..', 'data', 'generated');
fs.mkdirSync(OUT_DIR, { recursive: true });
@@ -50,17 +60,17 @@ const VARIATIONS = [
prompt: 'Recolor the trellis to deep navy ink. Recolor every butterfly to varying shades of navy and indigo on white. Pure monochrome.' },
{ src: 'LomitaButterflies.jpg', title: 'Cane Trellis · Gold on Emerald',
prompt: 'Recolor the cane trellis to dark emerald with gilt highlights. Recolor every butterfly to antique gold leaf with matte black bodies. Background remains ivory.' },
- { src: 'BH90210-900 Butterfly Dream.jpg', title: 'Trellis Dream · Leaves Removed',
+ { src: BUTTERFLY_DREAM_SRC, title: 'Trellis Dream · Leaves Removed',
prompt: 'CRITICAL: completely erase every banana leaf, palm frond, and stem in the source. Reveal and keep only the bamboo trellis underneath plus the butterflies. Keep the original butterfly colors (yellow, red, blue, monarch orange).' },
- { src: 'BH90210-900 Butterfly Dream.jpg', title: 'Trellis Dream · Watercolor Pastel',
+ { src: BUTTERFLY_DREAM_SRC, title: 'Trellis Dream · Watercolor Pastel',
prompt: 'Erase every leaf and plant. Keep the bamboo trellis (lighten it). Repaint the butterflies in soft watercolor pastel (peach, lavender, sky blue, mint). Background warm cream.' },
- { src: 'BH90210-900 Butterfly Dream.jpg', title: 'Trellis Dream · Black Ink',
+ { src: BUTTERFLY_DREAM_SRC, title: 'Trellis Dream · Black Ink',
prompt: 'Erase every leaf and plant. Keep the bamboo trellis but redraw it in pure black ink linework. Redraw the butterflies in pure black ink, fine etching style. Background bone white.' },
- { src: 'BH90214-24IN-150DPI.jpg', title: 'Clean Trellis · Add Soft Butterflies',
+ { src: CLEAN_TRELLIS_SRC, title: 'Clean Trellis · Add Soft Butterflies',
prompt: 'Keep this clean emerald cane lattice exactly. Add scattered stylized butterflies (small, varying species, soft warm tones — yellow, peach, cream, dusty rose) resting on the trellis. Do not add any plants or leaves anywhere.' },
- { src: 'BH90214-24IN-150DPI.jpg', title: 'Clean Trellis · Iridescent Jewels',
+ { src: CLEAN_TRELLIS_SRC, title: 'Clean Trellis · Iridescent Jewels',
prompt: 'Keep the cane lattice (slightly darken to forest green). Add scattered iridescent jewel-tone butterflies — sapphire, amethyst, emerald, ruby — with subtle metallic shimmer. No plants, no leaves.' },
- { src: 'BH90214-24IN-150DPI.jpg', title: 'Clean Trellis · Champagne on Midnight',
+ { src: CLEAN_TRELLIS_SRC, title: 'Clean Trellis · Champagne on Midnight',
prompt: 'Recolor the lattice to deep midnight navy. Add scattered champagne / pale gold butterflies with subtle highlights. Sophisticated, evening palette. No plants, no leaves.' },
];
diff --git a/scripts/purge-bh-abstract-leaves.js b/scripts/purge-bh-abstract-leaves.js
deleted file mode 100644
index 28bab91..0000000
--- a/scripts/purge-bh-abstract-leaves.js
+++ /dev/null
@@ -1,87 +0,0 @@
-#!/usr/bin/env node
-// One-shot bulk purge: every design in category='bh-abstract-leaves'.
-// Mirrors the /api/design/:id/settlement-violation endpoint's audit + backup
-// + delete flow, but SKIPS the per-row regen spawn (whole category goes — no
-// point spawning 44 fresh Replicate jobs in the same banned space).
-
-const fs = require('fs');
-const path = require('path');
-const { execSync } = require('child_process');
-
-const CATEGORY = 'bh-abstract-leaves';
-const REASON = 'category-wide purge: bh-abstract-leaves (leaf bleed-through, Steve directive 2026-05-13)';
-
-const DB_NAME = 'dw_unified';
-const PSQL_CMD = (process.platform === 'linux')
- ? `sudo -n -u postgres psql ${DB_NAME} -At -q`
- : `psql ${DB_NAME} -At -q`;
-function q(sql) { return execSync(PSQL_CMD, { input: sql, encoding: 'utf8' }).trim(); }
-
-const rows = q(`SELECT row_to_json(t) FROM (
- SELECT id, prompt, category, motifs, tags, generator, dominant_hex,
- local_path, parent_design_id
- FROM spoon_all_designs WHERE category='${CATEGORY}' ORDER BY id
-) t;`).split('\n').filter(Boolean).map(JSON.parse);
-
-console.log(`Found ${rows.length} rows in category='${CATEGORY}'`);
-if (!rows.length) process.exit(0);
-
-const backupDir = path.join(require('os').homedir(), '.wallco-deleted', new Date().toISOString().slice(0,10));
-fs.mkdirSync(backupDir, { recursive: true });
-
-const auditDir = path.join(__dirname, '..', 'data', 'settlement-audit');
-fs.mkdirSync(auditDir, { recursive: true });
-const registryPath = path.join(auditDir, 'do-not-want.jsonl');
-
-let backedUp = 0, fileMissing = 0, dbDeleted = 0;
-const ids = [];
-
-for (const d of rows) {
- let backupPath = null;
- if (d.local_path && fs.existsSync(d.local_path)) {
- const bn = `${d.id}_${path.basename(d.local_path)}`;
- backupPath = path.join(backupDir, bn);
- try { fs.copyFileSync(d.local_path, backupPath); backedUp++; }
- catch (e) { console.error(`backup fail ${d.id}: ${e.message}`); }
- } else {
- fileMissing++;
- }
-
- const entry = {
- ts: new Date().toISOString(),
- id: d.id,
- reason: REASON,
- prompt: d.prompt || null,
- category: d.category || null,
- motifs: d.motifs || [],
- tags: d.tags || [],
- generator: d.generator || null,
- dominant_hex: d.dominant_hex || null,
- parent_design_id: d.parent_design_id || null,
- backup_file: backupPath,
- bulk_purge: true,
- regen_suppressed: true,
- };
- fs.appendFileSync(registryPath, JSON.stringify(entry) + '\n', 'utf8');
-
- if (d.local_path && fs.existsSync(d.local_path)) {
- try { fs.unlinkSync(d.local_path); } catch (e) { console.error(`unlink fail ${d.id}: ${e.message}`); }
- }
- ids.push(d.id);
-}
-
-if (ids.length) {
- q(`DELETE FROM spoon_all_designs WHERE id IN (${ids.join(',')});`);
- dbDeleted = ids.length;
-}
-
-console.log(JSON.stringify({
- category: CATEGORY,
- rows_found: rows.length,
- files_backed_up: backedUp,
- files_missing_on_disk: fileMissing,
- db_rows_deleted: dbDeleted,
- backup_dir: backupDir,
- audit_log: registryPath,
- ids,
-}, null, 2));
diff --git a/scripts/regen-bamboo-trellis-to-cane.js b/scripts/regen-bamboo-trellis-to-cane.js
index be4edb4..36931b8 100644
--- a/scripts/regen-bamboo-trellis-to-cane.js
+++ b/scripts/regen-bamboo-trellis-to-cane.js
@@ -18,6 +18,9 @@ const { gateOrExit } = require('./settlement-gate');
const KEY = process.env.GEMINI_API_KEY;
if (!KEY) { console.error('GEMINI_API_KEY missing'); process.exit(1); }
+// Reserved-brand source filename pulled from .env, never inlined.
+const BUTTERFLY_DREAM_SRC = process.env.BUTTERFLY_DREAM_SRC;
+if (!BUTTERFLY_DREAM_SRC) { console.error('BUTTERFLY_DREAM_SRC must be set in .env'); process.exit(1); }
const SRC = '/tmp/wallco-sources';
const OUT_DIR = path.join(__dirname, '..', 'data', 'generated');
fs.mkdirSync(OUT_DIR, { recursive: true });
@@ -40,11 +43,11 @@ const SHARED = [
// Original-vs-new mapping. Same source images, same titles (minus "Bamboo →
// Cane"), prompts rewritten to use cane lattice exclusively.
const REGEN = [
- { replaces: 3279, src: 'BH90210-900 Butterfly Dream.jpg', title: 'Trellis Dream · Cane Lattice · Original Palette',
+ { replaces: 3279, src: BUTTERFLY_DREAM_SRC, title: 'Trellis Dream · Cane Lattice · Original Palette',
prompt: 'CRITICAL: completely erase every banana leaf, palm frond, fern, and stem in the source. Replace the bamboo trellis underneath with a CANE / rattan lattice (woven, honey-colored rattan, never bamboo). Keep the original butterfly colors (yellow, red, blue, monarch orange).' },
- { replaces: 3280, src: 'BH90210-900 Butterfly Dream.jpg', title: 'Trellis Dream · Cane Lattice · Watercolor Pastel',
+ { replaces: 3280, src: BUTTERFLY_DREAM_SRC, title: 'Trellis Dream · Cane Lattice · Watercolor Pastel',
prompt: 'Erase every leaf, fern, and plant. Replace the bamboo trellis with a pale CANE / rattan lattice (lightened, woven rattan). Repaint the butterflies in soft watercolor pastel (peach, lavender, sky blue, mint). Background warm cream. No bamboo.' },
- { replaces: 3283, src: 'BH90210-900 Butterfly Dream.jpg', title: 'Trellis Dream · Cane Lattice · Black Ink',
+ { replaces: 3283, src: BUTTERFLY_DREAM_SRC, title: 'Trellis Dream · Cane Lattice · Black Ink',
prompt: 'Erase every leaf, fern, and plant. Replace the bamboo trellis with a CANE / rattan lattice drawn in pure black ink linework (woven rattan, never bamboo). Redraw the butterflies in pure black ink, fine etching style. Background bone white.' },
];
diff --git a/scripts/regen-bh-abstract-leaves-contemporary.js b/scripts/regen-bh-abstract-leaves-contemporary.js
deleted file mode 100644
index c12b880..0000000
--- a/scripts/regen-bh-abstract-leaves-contemporary.js
+++ /dev/null
@@ -1,275 +0,0 @@
-#!/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); });
← 746cf5e settlement: add 'palm leaves' to negative-prompt language +
·
back to Wallco Ai
·
marketplace: end-to-end integration test for admin payout ex 8e30514 →