← back to Wallco Ai
dogs collection generator — 11 breeds (DTD verdict) × 20 designer colorways, opaque repeat-v2 + composition gate, is_published=FALSE
03497c0564b9e9f6b4815a21a0659161448a92e7 · 2026-05-26 14:57:28 -0700 · Steve Abrams
Files touched
A scripts/generate-dogs.js
Diff
commit 03497c0564b9e9f6b4815a21a0659161448a92e7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 26 14:57:28 2026 -0700
dogs collection generator — 11 breeds (DTD verdict) × 20 designer colorways, opaque repeat-v2 + composition gate, is_published=FALSE
---
scripts/generate-dogs.js | 243 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 243 insertions(+)
diff --git a/scripts/generate-dogs.js b/scripts/generate-dogs.js
new file mode 100644
index 0000000..e07ecb4
--- /dev/null
+++ b/scripts/generate-dogs.js
@@ -0,0 +1,243 @@
+#!/usr/bin/env node
+// generate-dogs.js — the wallco.ai "dogs" collection.
+//
+// 11 groups × 20 designer colorways = 220 single-ink solid-silhouette dog
+// designs. Top-10 AKC breeds + one Pit-Lab mix ("Labrabull"), per DTD verdict
+// 2026-05-26 (unanimous 3/3: Q1=A Labrabull mix, Q2=A `dogs · <breed>`
+// sub-grouping). Steve approved Gemini repeat-v2 engine (~$10-20) + the
+// curate-then-publish posture (everything lands is_published=FALSE; a hot-or-not
+// picker gates the actual go-live).
+//
+// "Opaque specs" (Steve directive) is enforced structurally by the canonical
+// lib/repeat-prompt builder: every motif pixel solid alpha 1.0, flat uniform
+// ground, hard die-cut edges, NO gradient / NO ghost layer / NO atmospheric
+// depth. Each design is ONE breed silhouette in ONE designer ink on ONE ground.
+//
+// Pipeline per design (mirrors scripts/gen-poodle-demo.js):
+// buildRepeatPrompt → gemini-2.5-flash-image → gateComposition (>=3 instances,
+// not centered-hero, up to 3 layout attempts) → Pillow palette extract →
+// INSERT spoon_all_designs (category='dogs · <slug>', is_published=FALSE).
+//
+// Idempotent / resumable: a `[cw:<colorway-slug>]` marker is embedded in the
+// stored prompt; on restart we skip any (breed, colorway) row that already
+// exists, so a killed long-running batch resumes cleanly.
+//
+// node scripts/generate-dogs.js # full 220
+// node scripts/generate-dogs.js --breeds 3 --colorways 1 # 3-design smoke test
+// node scripts/generate-dogs.js --only poodle,beagle # named breeds only
+
+require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+const { spawnSync } = require('child_process');
+const { buildRepeatPrompt } = require('../lib/repeat-prompt');
+const { gateComposition } = require('../lib/composition-detector');
+
+const ROOT = path.join(__dirname, '..');
+const GEN_DIR = path.join(ROOT, 'data', 'generated');
+fs.mkdirSync(GEN_DIR, { recursive: true });
+
+// ── DTD-committed 11-group breed list (slug → silhouette motif) ─────────────
+const BREEDS = [
+ { slug: 'labrador-retriever', name: 'Labrador Retriever', motif: 'a Labrador Retriever in three-quarter standing profile — broad clean-lined head, otter tail, deep chest, athletic balanced build. A single solid-color silhouette with no interior detail.' },
+ { slug: 'french-bulldog', name: 'French Bulldog', motif: 'a French Bulldog sitting in profile — distinctive erect bat ears, compact heavy-boned body, short muzzle, wide stance. A single solid-color silhouette with no interior detail.' },
+ { slug: 'golden-retriever', name: 'Golden Retriever', motif: 'a Golden Retriever standing in three-quarter profile — feathered tail and coat fringe, gentle level head carriage, sturdy build. A single solid-color silhouette with no interior detail.' },
+ { slug: 'german-shepherd', name: 'German Shepherd', motif: 'a German Shepherd standing in profile — large erect pointed ears, gently sloping topline, bushy low-set tail, alert poised stance. A single solid-color silhouette with no interior detail.' },
+ { slug: 'poodle', name: 'Poodle', motif: 'a standard Poodle standing in three-quarter profile — classic continental show clip with rounded pompoms on the legs and tail tip, refined arched neck. A single solid-color silhouette with no interior detail.' },
+ { slug: 'bulldog', name: 'Bulldog', motif: 'an English Bulldog standing square in profile — wide low-slung heavily muscled body, broad wrinkled brow, small rose ears, short tail. A single solid-color silhouette with no interior detail.' },
+ { slug: 'rottweiler', name: 'Rottweiler', motif: 'a Rottweiler standing in profile — powerful blocky build, broad deep chest, level back, docked tail, square confident head. A single solid-color silhouette with no interior detail.' },
+ { slug: 'beagle', name: 'Beagle', motif: 'a Beagle standing in three-quarter profile — long low-set drop ears, compact sturdy hound body, gently upright tail. A single solid-color silhouette with no interior detail.' },
+ { slug: 'dachshund', name: 'Dachshund', motif: 'a Dachshund standing in profile — long low body, very short legs, long drop ears, tapered tail held level. A single solid-color silhouette with no interior detail.' },
+ { slug: 'german-shorthaired-pointer',name: 'German Shorthaired Pointer',motif: 'a German Shorthaired Pointer in the classic pointing stance in profile — one foreleg raised and tucked, muzzle and tail extended in a straight line, lean athletic build. A single solid-color silhouette with no interior detail.' },
+ { slug: 'pit-lab-mix', name: 'Pit-Lab Mix', motif: 'a Pit Bull Labrador mix (a "Labrabull" shelter dog) standing in three-quarter profile — broad blocky head, strong muscular athletic body, short smooth coat, medium half-folded drop ears, long tapering tail. A single solid-color silhouette with no interior detail.' },
+];
+
+// ── 20 designer tone-on-tone colorways (ground × ink) ───────────────────────
+// Reuses the validated `designer-zoo-calm` luxe palette so the dogs collection
+// reads as part of the same heritage-house aesthetic. Each is a flat 2-color
+// story: solid ground + single solid ink, contrast tuned so the silhouette
+// reads cleanly while staying tone-on-tone sophisticated.
+const COLORWAYS = [
+ { slug: 'bone-ivory', name: 'Bone Ivory', ground_hex: '#F5F0E8', ground_desc: 'bone ivory', ink_hex: '#2A2622', ink_desc: 'charcoal noir' },
+ { slug: 'midnight-navy', name: 'Midnight Navy', ground_hex: '#1B2A3A', ground_desc: 'midnight navy', ink_hex: '#AEBDCB', ink_desc: 'dove blue-grey' },
+ { slug: 'indigo-library', name: 'Indigo Library', ground_hex: '#20283D', ground_desc: 'deep library indigo', ink_hex: '#C9B07A', ink_desc: 'antique brass' },
+ { slug: 'deep-plum', name: 'Deep Plum', ground_hex: '#2E1D2B', ground_desc: 'deep plum', ink_hex: '#D8C3A8', ink_desc: 'fawn' },
+ { slug: 'bordeaux-velvet', name: 'Bordeaux Velvet', ground_hex: '#3A1820', ground_desc: 'bordeaux velvet', ink_hex: '#E3D2B8', ink_desc: 'champagne' },
+ { slug: 'saddle-mocha', name: 'Saddle Mocha', ground_hex: '#4A342A', ground_desc: 'saddle mocha', ink_hex: '#E8DCC8', ink_desc: 'cream' },
+ { slug: 'greenhouse-glass', name: 'Greenhouse Glass', ground_hex: '#243B33', ground_desc: 'deep greenhouse teal', ink_hex: '#D5DDC9', ink_desc: 'pale sage' },
+ { slug: 'forest-loden', name: 'Forest Loden', ground_hex: '#2A3526', ground_desc: 'forest loden', ink_hex: '#CBB994', ink_desc: 'fawn' },
+ { slug: 'oxblood', name: 'Oxblood', ground_hex: '#3E1A18', ground_desc: 'oxblood', ink_hex: '#E0CFC0', ink_desc: 'oyster' },
+ { slug: 'champagne-linen', name: 'Champagne Linen', ground_hex: '#EFE6D4', ground_desc: 'champagne linen', ink_hex: '#5A4636', ink_desc: 'umber' },
+ { slug: 'smoke-ash', name: 'Smoke Ash', ground_hex: '#4A4A4A', ground_desc: 'smoke ash grey', ink_hex: '#E5E2DC', ink_desc: 'off-white' },
+ { slug: 'manor-saddle', name: 'Manor Saddle', ground_hex: '#5C4327', ground_desc: 'manor saddle brown',ink_hex: '#EDE2CE', ink_desc: 'cream' },
+ { slug: 'antique-tan', name: 'Antique Tan', ground_hex: '#C9B08A', ground_desc: 'antique tan', ink_hex: '#3A2C1E', ink_desc: 'cocoa' },
+ { slug: 'stone-pewter', name: 'Stone Pewter', ground_hex: '#B8B5AC', ground_desc: 'stone pewter', ink_hex: '#33312C', ink_desc: 'charcoal' },
+ { slug: 'antique-brass', name: 'Antique Brass', ground_hex: '#2C2A24', ground_desc: 'charcoal', ink_hex: '#B08A4A', ink_desc: 'antique brass' },
+ { slug: 'slate-mist', name: 'Slate Mist', ground_hex: '#C2CACE', ground_desc: 'slate mist', ink_hex: '#2E363A', ink_desc: 'charcoal blue' },
+ { slug: 'noir-ivory', name: 'Noir Ivory', ground_hex: '#121212', ground_desc: 'noir', ink_hex: '#EDE6D6', ink_desc: 'ivory' },
+ { slug: 'cocoa-cream', name: 'Cocoa Cream', ground_hex: '#6B382C', ground_desc: 'cocoa', ink_hex: '#F5F0E5', ink_desc: 'cream' },
+ { slug: 'malachite', name: 'Malachite', ground_hex: '#0E3B33', ground_desc: 'malachite', ink_hex: '#C8B98C', ink_desc: 'antique gold' },
+ { slug: 'dove-grey', name: 'Dove Grey', ground_hex: '#D3D3CB', ground_desc: 'dove grey', ink_hex: '#2A2A2A', ink_desc: 'noir' },
+];
+
+// ── args ────────────────────────────────────────────────────────────────────
+function parseArgs() {
+ const a = process.argv.slice(2);
+ const o = { breeds: BREEDS.length, colorways: COLORWAYS.length, only: null };
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] === '--breeds') o.breeds = parseInt(a[++i], 10);
+ else if (a[i] === '--colorways') o.colorways = parseInt(a[++i], 10);
+ else if (a[i] === '--only') o.only = a[++i].split(',').map(s => s.trim()).filter(Boolean);
+ }
+ return o;
+}
+
+// ── gemini image ─────────────────────────────────────────────────────────────
+function readKey() {
+ if (process.env.GEMINI_API_KEY) return process.env.GEMINI_API_KEY;
+ const env = fs.readFileSync(path.join(ROOT, '.env'), 'utf8');
+ const m = env.match(/^GEMINI_API_KEY=(.+)$/m);
+ if (!m) throw new Error('GEMINI_API_KEY missing');
+ return m[1].trim();
+}
+const GEMINI_KEY = readKey();
+
+async function geminiImage(prompt) {
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=${GEMINI_KEY}`;
+ const r = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ contents: [{ parts: [{ text: prompt }] }],
+ generationConfig: { responseModalities: ['IMAGE'] },
+ }),
+ });
+ if (!r.ok) throw new Error(`gemini ${r.status}: ${(await r.text()).slice(0, 200)}`);
+ const j = await r.json();
+ const part = j?.candidates?.[0]?.content?.parts?.find(p => p.inline_data || p.inlineData);
+ const b64 = part?.inline_data?.data || part?.inlineData?.data;
+ if (!b64) throw new Error('gemini returned no image');
+ return b64;
+}
+
+// ── psql helpers ─────────────────────────────────────────────────────────────
+function psql(sql) {
+ const r = spawnSync('psql', ['dw_unified', '-At', '-q', '-v', 'ON_ERROR_STOP=1'], { input: sql, encoding: 'utf8' });
+ if (r.status !== 0) throw new Error(r.stderr || r.stdout || 'psql failed');
+ return r.stdout.trim();
+}
+function alreadyExists(category, cwSlug) {
+ // dedupe on category + the [cw:<slug>] marker embedded in the stored prompt
+ const sql = `SELECT 1 FROM spoon_all_designs WHERE category='${category.replace(/'/g, "''")}' AND prompt LIKE '%[cw:${cwSlug}]%' LIMIT 1;`;
+ return psql(sql) === '1';
+}
+function extractPalette(localPath) {
+ const py = `
+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')
+px = list(pal.getdata()); cnt = Counter(px); total = sum(cnt.values())
+print(json.dumps([{'hex':'#{:02x}{:02x}{:02x}'.format(*rgb),'pct':round(n/total*100,2)} for rgb,n in cnt.most_common(5)]))
+`;
+ const r = spawnSync('python3', ['-c', py, localPath], { encoding: 'utf8' });
+ try { const p = JSON.parse(r.stdout.trim()); return { palette: p, dominant: p[0]?.hex || null }; }
+ catch { return { palette: [], dominant: null }; }
+}
+
+// ── one design ───────────────────────────────────────────────────────────────
+async function genOne(breed, cw) {
+ const category = `dogs · ${breed.slug}`;
+ if (alreadyExists(category, cw.slug)) {
+ console.log(` · skip ${breed.slug}/${cw.slug} (already in PG)`);
+ return { skipped: true };
+ }
+ const motif = `${breed.motif} The silhouette is a single flat solid ${cw.ink_desc} (${cw.ink_hex}) shape — no fur texture, no facial detail, no outline, no shading inside it.`;
+ const MAX = 3;
+ let candidate = null, gate = null, usedAttempt = 0;
+ for (let attempt = 0; attempt < MAX; attempt++) {
+ const prompt = buildRepeatPrompt({
+ motifDescription: motif,
+ groundHex: cw.ground_hex,
+ groundDesc: cw.ground_desc,
+ density: 'medium',
+ attempt,
+ });
+ const b64 = await geminiImage(prompt);
+ gate = await gateComposition(b64, { minInstances: 3 }).catch(e => ({ ok: false, reason: 'gate-error: ' + e.message, result: {} }));
+ if (gate.ok) { candidate = b64; usedAttempt = attempt; break; }
+ process.stdout.write(` retry (${gate.reason}) `);
+ }
+ if (!candidate) {
+ // Round-1-sacred: accept the last shot rather than infinite-retry. The
+ // curator pass + edges/ghost gates will catch a genuinely bad one.
+ const fb = buildRepeatPrompt({ motifDescription: motif, groundHex: cw.ground_hex, groundDesc: cw.ground_desc, density: 'medium', attempt: MAX - 1 });
+ candidate = await geminiImage(fb);
+ usedAttempt = MAX - 1;
+ }
+ const seed = crypto.randomInt(1, 2 ** 31 - 1);
+ const filename = `dog_${breed.slug}_${cw.slug}_${Date.now()}_${seed}.png`;
+ const outPath = path.join(GEN_DIR, filename);
+ fs.writeFileSync(outPath, Buffer.from(candidate, 'base64'));
+ const { palette, dominant } = extractPalette(outPath);
+
+ // Stored prompt carries the [cw:<slug>] dedupe marker + a human label.
+ const storedPrompt = `Dogs collection — ${breed.name} silhouette, ${cw.name} colorway [cw:${cw.slug}]. Ground ${cw.ground_desc} ${cw.ground_hex}, single-ink ${cw.ink_desc} ${cw.ink_hex}. ${breed.motif}`;
+ const esc = v => v == null ? 'NULL' : "'" + String(v).replace(/'/g, "''") + "'";
+ const sql = `
+INSERT INTO spoon_all_designs
+ (kind, brand, width_in, height_in, generator, prompt, seed, image_url, local_path,
+ dominant_hex, palette, category, product_line, is_published)
+VALUES
+ ('seamless_tile', 'wallco.ai', 24, 24,
+ 'gemini-2.5-flash-image-repeat-v2',
+ ${esc(storedPrompt)}, ${seed},
+ '/designs/img/by-id/__NEW__', ${esc(outPath)},
+ ${esc(dominant)}, ${esc(JSON.stringify(palette))}::jsonb,
+ ${esc(category)}, ${esc('Studio')}, FALSE)
+RETURNING id;`;
+ const newId = parseInt(psql(sql), 10);
+ if (!Number.isFinite(newId)) throw new Error('insert returned no id');
+ psql(`UPDATE spoon_all_designs SET image_url='/designs/img/by-id/${newId}' WHERE id=${newId};`);
+ return { id: newId, attempt: usedAttempt, gate: gate?.result };
+}
+
+// ── main ─────────────────────────────────────────────────────────────────────
+(async () => {
+ const opt = parseArgs();
+ let breeds = BREEDS.slice(0, opt.breeds);
+ if (opt.only) breeds = BREEDS.filter(b => opt.only.includes(b.slug));
+ const colorways = COLORWAYS.slice(0, opt.colorways);
+ const total = breeds.length * colorways.length;
+ console.log(`generate-dogs: ${breeds.length} breeds × ${colorways.length} colorways = ${total} designs`);
+ console.log(`engine: gemini-2.5-flash-image-repeat-v2 · is_published=FALSE (curate-then-publish)\n`);
+
+ const t0 = Date.now();
+ let ok = 0, skip = 0, fail = 0, retries = 0;
+ const made = [];
+ for (const breed of breeds) {
+ console.log(`\n# ${breed.name} (dogs · ${breed.slug})`);
+ for (const cw of colorways) {
+ try {
+ const r = await genOne(breed, cw);
+ if (r.skipped) { skip++; continue; }
+ if (r.attempt > 0) retries += r.attempt;
+ ok++;
+ made.push(r.id);
+ console.log(` ✓ #${r.id} ${cw.slug} (attempt ${r.attempt}, n=${r.gate?.instance_count ?? '?'})`);
+ } catch (e) {
+ fail++;
+ console.log(` ✗ ${cw.slug}: ${e.message}`);
+ }
+ }
+ }
+ const dur = ((Date.now() - t0) / 1000 / 60).toFixed(1);
+ const billable = ok + retries; // every gemini call (incl. gate retries) is billable
+ console.log(`\n=== done in ${dur} min ===`);
+ console.log(`generated ${ok}, skipped ${skip}, failed ${fail}, gate-retries ${retries}`);
+ console.log(`billable gemini image calls ≈ ${billable} (≈ $${(billable * 0.039).toFixed(2)} @ $0.039/img)`);
+ if (made.length) console.log(`new ids: ${made.join(', ')}`);
+ console.log(`\nnext: curator pass + settlement/edges/ghost gates before any is_published=TRUE`);
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
← bc99681 Add style-aware-sample.js — PG-palette-derived chinoiserie S
·
back to Wallco Ai
·
TODO: record dogs collection (DTD verdict, generate-dogs.js dddb2ad →