← back to Wallco Ai
Inspiration pool: Replicate backend + settlement gate + placeholder thumbs
4a89b0ed6b0cf0d5dc4a962eeb8facd74e6f2fc8 · 2026-05-19 13:56:43 -0700 · SteveStudio2
- generate route forces GEN_BACKEND=replicate (DTD verdict B) so it runs
synchronously from prod, which has no local ComfyUI
- spawnSync arg-array replaces shell-string execSync (no injection from
LLM-authored prompt text)
- settlementScreen() backstop + settlement-aware Ollama system prompt:
birds/butterflies/bananas/grapes/monstera/palm fronds blocked pre-gen
- response now reports blocked_by_settlement count
- pool cards with empty image_url render hsl-gradient placeholder + initial
Files touched
M public/js/admin.jsM src/admin.js
Diff
commit 4a89b0ed6b0cf0d5dc4a962eeb8facd74e6f2fc8
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Tue May 19 13:56:43 2026 -0700
Inspiration pool: Replicate backend + settlement gate + placeholder thumbs
- generate route forces GEN_BACKEND=replicate (DTD verdict B) so it runs
synchronously from prod, which has no local ComfyUI
- spawnSync arg-array replaces shell-string execSync (no injection from
LLM-authored prompt text)
- settlementScreen() backstop + settlement-aware Ollama system prompt:
birds/butterflies/bananas/grapes/monstera/palm fronds blocked pre-gen
- response now reports blocked_by_settlement count
- pool cards with empty image_url render hsl-gradient placeholder + initial
---
public/js/admin.js | 9 ++--
src/admin.js | 119 ++++++++++++++++++++++++++++++++++++++---------------
2 files changed, 92 insertions(+), 36 deletions(-)
diff --git a/public/js/admin.js b/public/js/admin.js
index 782efe1..1b22205 100644
--- a/public/js/admin.js
+++ b/public/js/admin.js
@@ -65,12 +65,15 @@
gen.disabled = false; gen.textContent = orig; return;
}
const designs = j.designs || [];
+ const made = designs.filter(d => d.id);
+ const blocked = j.blocked_by_settlement || 0;
// Render results above grid
const out = document.createElement('div');
out.style.cssText = 'margin:20px 0;padding:18px;background:var(--card-bg);border:1px solid var(--gold);border-radius:8px';
- out.innerHTML = `<h2 style="font:300 22px/1 var(--serif);margin:0 0 10px;color:var(--ink)">${designs.length} new wallco originals from your pool</h2>
+ out.innerHTML = `<h2 style="font:300 22px/1 var(--serif);margin:0 0 10px;color:var(--ink)">${made.length} new wallco originals from your pool</h2>
+ ${blocked ? `<p style="margin:0 0 10px;font-size:13px;color:#9a3b2e">⚖ ${blocked} prompt${blocked > 1 ? 's' : ''} blocked by the settlement gate (birds / foliage / prohibited motifs) — click again to retry.</p>` : ''}
<div class="p-grid" style="margin-top:10px">
- ${designs.filter(d => !d.error).map(d => `
+ ${made.map(d => `
<a href="/design/${d.id}" class="p-card" style="text-decoration:none;color:inherit">
<div class="p-img" style="background-image:url('${d.image_url}')"></div>
<div class="p-meta">
@@ -80,7 +83,7 @@
</a>`).join('')}
</div>`;
gen.after(out);
- gen.textContent = `✓ Generated ${designs.length} · click to generate 3 more`;
+ gen.textContent = `✓ Generated ${made.length} · click to generate 3 more`;
gen.disabled = false;
} catch (e) {
alert('Generation error: ' + e.message);
diff --git a/src/admin.js b/src/admin.js
index f09270a..b0ccbc8 100644
--- a/src/admin.js
+++ b/src/admin.js
@@ -17,7 +17,7 @@
* GET /api/admin/vendor/:code → JSON: that vendor's products (paginated, filterable)
*/
'use strict';
-const { execSync } = require('child_process');
+const { execSync, spawnSync } = require('child_process');
const PSQL_CMD = (process.platform === 'linux')
? `sudo -n -u postgres psql dw_unified -At -q`
@@ -274,15 +274,25 @@ function mount(app) {
const items = inspirationFor(req.admin.email);
const cards = items.length === 0
? '<div class="empty-state" style="padding:60px;text-align:center;color:var(--ink-faint)">No inspiration items yet. Browse vendors and click + Inspiration.</div>'
- : items.map(i => `
+ : items.map(i => {
+ const label = i.pattern_name || i.mfr_sku || '—';
+ const hasImg = i.image_url && /^(https?:|\/)/.test(i.image_url);
+ // Deterministic hsl placeholder when the pool row has no usable image_url.
+ let hue = 0; for (const c of label) hue = (hue + c.charCodeAt(0)) % 360;
+ const imgStyle = hasImg
+ ? `background-image:url('${i.image_url}')`
+ : `background:linear-gradient(135deg,hsl(${hue} 42% 86%),hsl(${(hue + 40) % 360} 38% 72%))`;
+ const initial = (label.replace(/[^A-Za-z0-9]/g, '').charAt(0) || '·').toUpperCase();
+ return `
<div class="p-card" data-id="${i.id}">
- <div class="p-img" style="background-image:url('${i.image_url}')"></div>
+ <div class="p-img" style="${imgStyle}">${hasImg ? '' : `<span style="display:flex;align-items:center;justify-content:center;height:100%;font:300 38px/1 Georgia,serif;color:rgba(0,0,0,.32)">${initial}</span>`}</div>
<div class="p-meta">
- <div class="p-name">${i.pattern_name || i.mfr_sku || '—'}</div>
- <div class="p-coll">${(i.vendor_code||'').replace(/_/g,' ')}</div>
+ <div class="p-name">${label}</div>
+ <div class="p-coll">${(i.vendor_code || '').replace(/_/g, ' ')}</div>
<button class="p-remove" type="button" data-id="${i.id}">Remove</button>
</div>
- </div>`).join('');
+ </div>`;
+ }).join('');
res.type('html').send(pageShell('Inspiration', `
${adminHeader('/admin/inspiration').replace('{email}', req.admin.email)}
<main class="admin-main">
@@ -359,17 +369,47 @@ function mount(app) {
catch (e) { res.status(500).json({ error: e.message }); }
});
+ // ── Settlement gate (server-side backstop) ────────────────────────────
+ // Defendant-favorable keyword screen for the binding settlement's prohibited
+ // elements. The Ollama prompt-engineer step (below) is instructed to avoid
+ // these up front; this catches anything that slips through before an image
+ // is ever generated. Source of truth: ~/.claude/skills/settlement binding text.
+ const SETTLEMENT_FORBIDDEN =
+ /\b(bananas?|banana\s*(?:leaf|leaves|pods?)|grapes?|birds?|butterfl(?:y|ies)|monstera|palm\s*fronds?|toucans?|parrots?|hummingbirds?)\b/i;
+ function settlementScreen(text) {
+ const m = String(text || '').match(SETTLEMENT_FORBIDDEN);
+ return m ? { verdict: 'BLOCK', term: m[0] } : { verdict: 'OK' };
+ }
+
// ── Generate 3 wallco-originals from the admin's inspiration pool.
- // STRICT NO-LEAK: only pattern_name + collection name are sent to the LLM and Replicate.
- // Vendor names are NEVER included in the prompt that's stored or sent to Replicate.
+ // STRICT NO-LEAK: only pattern_name is sent to the LLM and Replicate.
+ // Vendor names are NEVER included in the prompt that's stored or sent out.
+ // SETTLEMENT GATE: every prompt is screened pre-generation (settlementScreen).
+ // BACKEND: forced to Replicate so the route runs from prod (no local ComfyUI).
app.post('/api/admin/inspiration/generate', gate, async (req, res) => {
try {
const items = inspirationFor(req.admin.email);
if (items.length === 0) return res.status(400).json({ error: 'pool_empty' });
- // 1) Ask Ollama qwen3:14b to translate the items into 3 SDXL prompts (scrubbed)
+ // 1) Ask Ollama qwen3:14b to translate the items into 3 settlement-safe SDXL prompts.
const scrubbedList = items.slice(0, 8).map(i => `- ${i.pattern_name || 'untitled pattern'}`).join('\n');
- const sys = `You are a wallpaper design prompt engineer. Given a short list of pattern names (vendor-anonymous), write 3 distinct SDXL prompts for *new* wallpaper designs that share the same aesthetic family but are NOT copies. Output ONLY a JSON array of 3 strings — each prompt is one paragraph ≤ 200 chars, describing motif + palette + mood + texture. Mention "seamless tile, 24-inch repeat, archival quality". Do NOT mention any vendor name or copy specific patterns.`;
+ const sys = [
+ 'You are a wallpaper design prompt engineer for wallco.ai.',
+ 'Given a short list of pattern names (vendor-anonymous), write 3 distinct SDXL prompts',
+ 'for NEW wallpaper designs that share the aesthetic family but are NOT copies.',
+ '',
+ 'HARD LEGAL CONSTRAINT — a binding settlement prohibits these; every prompt MUST avoid them:',
+ 'birds, butterflies, bananas, banana leaves, banana pods, grapes, monstera leaves, palm fronds,',
+ 'and directional variation among leaves/foliage. If an inspiration name implies them',
+ '(e.g. "scenic", "toile", "chinoiserie"), REINTERPRET with settlement-safe motifs instead:',
+ 'peony & chrysanthemum florals, pagodas, lattice / fretwork / key-fret geometry, ginger jars',
+ '& vases, stylized clouds and mountains, edge-to-edge coverage, one dominant ink family.',
+ 'Crisp opaque screen-print color — never fuzzy or layer-over-layer.',
+ '',
+ 'Output ONLY a JSON array of 3 strings — each prompt one paragraph <= 220 chars,',
+ 'describing motif + palette + mood + texture. Include "seamless tile, 24-inch repeat,',
+ 'archival quality". Never mention a vendor name.'
+ ].join('\n');
const userMsg = `Aesthetic inspiration (names only):\n${scrubbedList}\n\nReturn a JSON array of 3 prompts.`;
const ollamaUrl = process.env.OLLAMA_URL || 'http://192.168.1.133:11434';
@@ -393,36 +433,49 @@ function mount(app) {
}
prompts = prompts.slice(0, 3).filter(p => typeof p === 'string' && p.length > 20);
- // 2) Fire SDXL via the wallco chat tool (re-uses Replicate path + palette extract + insert)
- const { toolGenerateDesign } = (() => {
- // chat.js exports mount(); not the tools. Inline the path here via psql + the same generate_designs.js stub.
- return {};
- })();
- // Simpler: shell out to scripts/generate_designs.js with --prompts flag for each
+ // 2) Generate via Replicate SDXL (DTD verdict B — runs from prod, no local
+ // ComfyUI dependency). spawnSync with an arg array = no shell injection
+ // from the LLM-authored prompt text.
+ const genScript = require('path').join(__dirname, '..', 'scripts', 'generate_designs.js');
const results = [];
for (const prompt of prompts) {
+ // SETTLEMENT GATE — screen before spending a Replicate call.
+ const screen = settlementScreen(prompt);
+ if (screen.verdict === 'BLOCK') {
+ results.push({ error: 'blocked_by_settlement', term: screen.term, prompt: prompt.slice(0, 140) });
+ continue;
+ }
try {
- const out = execSync(`node ${require('path').join(__dirname, '..', 'scripts/generate_designs.js')} --n 1 --kind seamless_tile --category mixed --prompts ${JSON.stringify(prompt)}`,
- { encoding: 'utf8', timeout: 180_000 });
- const idMatch = out.match(/IDs?:\s*(\d+)/);
- if (idMatch) {
- const id = parseInt(idMatch[1], 10);
- const r = psql(`SELECT row_to_json(t) FROM (SELECT id, prompt, dominant_hex, local_path FROM spoon_all_designs WHERE id=${id}) t;`);
- const row = JSON.parse(r);
- // Attach lineage marker (admin pool inspiration). Note: prompts are scrubbed already.
- psql(`UPDATE spoon_all_designs SET request_text='admin-pool: ' || ${esc(req.admin.email)} WHERE id=${id};`);
- results.push({
- id: row.id,
- dominant_hex: row.dominant_hex,
- prompt: row.prompt.slice(0, 200),
- image_url: '/designs/img/' + (row.local_path || '').split('/').pop()
- });
+ const gen = spawnSync('node',
+ [genScript, '--n', '1', '--kind', 'seamless_tile', '--category', 'mixed', '--prompts', prompt],
+ { encoding: 'utf8', timeout: 180_000, env: { ...process.env, GEN_BACKEND: 'replicate' } });
+ if (gen.status !== 0) {
+ results.push({ error: (gen.stderr || gen.stdout || 'generate_designs.js failed').slice(-200) });
+ continue;
+ }
+ const idMatch = String(gen.stdout || '').match(/IDs?:\s*(\d+)/);
+ if (!idMatch) {
+ results.push({ error: 'no_design_id_in_output' });
+ continue;
}
+ const id = parseInt(idMatch[1], 10);
+ const r = psql(`SELECT row_to_json(t) FROM (SELECT id, prompt, dominant_hex, local_path FROM spoon_all_designs WHERE id=${id}) t;`);
+ const row = JSON.parse(r);
+ // Lineage marker — these prompts are vendor-scrubbed and settlement-screened.
+ psql(`UPDATE spoon_all_designs SET request_text='admin-pool: ' || ${esc(req.admin.email)} WHERE id=${id};`);
+ results.push({
+ id: row.id,
+ dominant_hex: row.dominant_hex,
+ prompt: (row.prompt || '').slice(0, 200),
+ image_url: '/designs/img/' + (row.local_path || '').split('/').pop()
+ });
} catch (e) {
- results.push({ error: e.message.slice(0, 200) });
+ results.push({ error: String(e.message || e).slice(0, 200) });
}
}
- res.json({ ok: true, generated: results.length, designs: results });
+ const ok = results.filter(r => r.id).length;
+ const blocked = results.filter(r => r.error === 'blocked_by_settlement').length;
+ res.json({ ok: true, generated: ok, blocked_by_settlement: blocked, designs: results });
} catch (e) {
res.status(500).json({ error: e.message });
}
← d36364c refiner-compare: all 3 columns complete — 50 ComfyUI + 25 Re
·
back to Wallco Ai
·
refiner-compare: dual-method new-item composer — drag-a-hous 6fa0c16 →