← back to Wallco Ai
generator: wire product_line column on INSERT + backfill designs.json
28ef4d5283d3ef1cf412dcbdf315d855ffbf3f96 · 2026-05-21 20:08:02 -0700 · Steve Abrams
- generate_designs.js: PRODUCT_LINE_MAP (cactus-11ft-mural+tree-mural→Scenic,
cactus-pine-scenic→Studio) computed from --category and persisted in the
INSERT VALUES; NULL for unmapped categories.
- night-builder.js: colorway-variant INSERT now carries product_line in
column list + SELECT clause so child rows inherit the parent's line.
- scripts/sync-designs-json-product-line.js: one-off PG→JSON backfill,
atomic write (tmp+fsync+rename). 142 rows now carry product_line.
- Verified end-to-end with GEN_BACKEND=stub on cactus-pine-scenic (test row
id 39181, product_line='Studio', flagged is_published=false + note).
Files touched
M scripts/generate_designs.jsM scripts/night-builder.jsA scripts/sync-designs-json-product-line.js
Diff
commit 28ef4d5283d3ef1cf412dcbdf315d855ffbf3f96
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 21 20:08:02 2026 -0700
generator: wire product_line column on INSERT + backfill designs.json
- generate_designs.js: PRODUCT_LINE_MAP (cactus-11ft-mural+tree-mural→Scenic,
cactus-pine-scenic→Studio) computed from --category and persisted in the
INSERT VALUES; NULL for unmapped categories.
- night-builder.js: colorway-variant INSERT now carries product_line in
column list + SELECT clause so child rows inherit the parent's line.
- scripts/sync-designs-json-product-line.js: one-off PG→JSON backfill,
atomic write (tmp+fsync+rename). 142 rows now carry product_line.
- Verified end-to-end with GEN_BACKEND=stub on cactus-pine-scenic (test row
id 39181, product_line='Studio', flagged is_published=false + note).
---
scripts/generate_designs.js | 37 +++++++++++--
scripts/night-builder.js | 21 ++++++--
scripts/sync-designs-json-product-line.js | 88 +++++++++++++++++++++++++++++++
3 files changed, 139 insertions(+), 7 deletions(-)
diff --git a/scripts/generate_designs.js b/scripts/generate_designs.js
index e7981dc..ed516ec 100644
--- a/scripts/generate_designs.js
+++ b/scripts/generate_designs.js
@@ -239,8 +239,23 @@ function genComfy(prompt, seed, outPath) {
// artifacts Steve has been rejecting (10333, 10335, 10346, 10350, 10353,
// 10392). It DIRECTLY contradicts the prompt's "flat 2-tone" anchor.
// NEW suffix reinforces sparse + flat + 2-4 tone.
- const positivePrompt = `${prompt}, seamless tile, flat single-layer screen-print, two to four ink tones maximum, hard crisp razor-sharp edges, generous negative space, sparse minimal composition, archival quality designer wallcovering — Zuber, Designers Guild, Thibaut, Arte, Romo, Maya Romanoff register`;
- const negativePrompt = `dimensional, embossed, relief, 3D, three-dimensional, depth, atmospheric perspective, ghost layer, ghosted, faded background, layered bleed, multi-tone interior shading, fur texture, anatomical detail inside silhouette, crosshatching, engraving lines, halftone, ben-day dots, dot screen, watercolor wash, airbrush, soft edges, blurry, low quality, seam visible, border, frame, signature, watermark, text, hands, fingers, busy, cluttered, edge-to-edge, tessellated densely, more than 4 colors, ${NEGATIVE_PROMPT_ADDON}`;
+ // 2026-05-21 Steve directive — SOLID COLORS ONLY, never overlapping or
+ // translucent layers. Each motif fills with one solid ink; every ink is
+ // fully opaque; no gradients, no semi-transparent overlays, no multiply
+ // blends, no ghost backgrounds, no bleed between layers.
+ // 2026-05-22 Steve directive — STRONGER anti-ghost language. Previous version
+ // said "no overlapping translucent layers" but SDXL was still rendering
+ // SECONDARY motifs (flowers, leaves behind the main subject) with outline-only
+ // / hollow interiors that show the background through them. The model treats
+ // outline-fill rendering as a legit toile/chinoiserie convention and overrides
+ // the explicit instruction.
+ //
+ // Fix — re-anchor as a STENCIL/DIE-CUT aesthetic: every motif is a solid
+ // filled silhouette, NEVER an outline drawing. Background motifs must be
+ // colored fills, not white-interior shapes. Negative prompt explicitly bans
+ // outline-only / hollow / linework rendering.
+ const positivePrompt = `${prompt}, vinyl die-cut sticker aesthetic, seamless tile, every single shape rendered as a SOLID FILLED SILHOUETTE in one opaque ink color, NO outline-only motifs, NO hollow shapes, NO line drawings, NO see-through interiors, EVERY flower leaf branch fully filled like a paper-cut stencil, hard crisp razor-sharp edges, flat single-layer screen-print, two to four ink tones maximum, generous negative space between filled shapes, sparse minimal composition, archival quality designer wallcovering`;
+ const negativePrompt = `outline only, line drawing, linework, hollow shape, hollow interior, unfilled motif, see-through, white interior, transparent fill, translucent fill, outline drawing of a flower, outline drawing of a leaf, ink outline without fill, line-art rendered motif, hand-drawn ink lines, etching, engraving, gradient fill, drop shadow, ambient occlusion, soft shadow, semi-transparent, alpha overlay, multiply blend, ghost background, ghost layer, ghosted, layered bleed, bleed between inks, overlapping translucent layers, faded background, multi-tone interior shading, dimensional, embossed, relief, 3D, three-dimensional, depth, atmospheric perspective, fur texture, anatomical detail inside silhouette, crosshatching, halftone, ben-day dots, dot screen, watercolor wash, airbrush, soft edges, blurry, low quality, seam visible, border, frame, signature, watermark, text, hands, fingers, busy, cluttered, edge-to-edge, tessellated densely, more than 4 colors, ${NEGATIVE_PROMPT_ADDON}`;
// Optional SDXL refiner — set COMFY_REFINER_MODEL=sd_xl_refiner_1.0.safetensors
// to run base+refiner (expert-ensemble), matching what Replicate's SDXL does.
@@ -354,7 +369,18 @@ function psql(sql) {
return r.stdout.trim();
}
+// Category → product_line mapping (Steve directive 2026-05-21). New rows
+// write product_line directly to PG on INSERT so the in-memory productLine(d)
+// helper in server.js doesn't have to do category→line classification at
+// read time. Categories not in this map insert NULL (no line chip).
+const PRODUCT_LINE_MAP = {
+ 'cactus-11ft-mural': 'Scenic',
+ 'tree-mural': 'Scenic',
+ 'cactus-pine-scenic': 'Studio',
+};
+
function persistDesign({ kind, prompt, seed, localPath, width, height, panels, category }) {
+ const productLine = PRODUCT_LINE_MAP[category] || null;
// Extract palette via Pillow
const py = `
from PIL import Image
@@ -374,7 +400,7 @@ print(json.dumps(palette))
const palJson = JSON.stringify(palette).replace(/'/g, "''");
const promptEsc = prompt.replace(/'/g, "''");
const sql = `
-INSERT INTO spoon_all_designs (kind, width_in, height_in, panels, generator, prompt, seed, dominant_hex, palette, local_path, category, is_published)
+INSERT INTO spoon_all_designs (kind, width_in, height_in, panels, generator, prompt, seed, dominant_hex, palette, local_path, category, product_line, is_published)
VALUES (
'${kind}', ${width}, ${height}, ${panels == null ? 'NULL' : panels},
'${BACKEND}', '${promptEsc}', ${seed},
@@ -382,6 +408,7 @@ VALUES (
'${palJson}'::jsonb,
'${localPath.replace(/'/g, "''")}',
'${category.replace(/'/g, "''")}',
+ ${productLine ? "'" + productLine + "'" : 'NULL'},
FALSE
) RETURNING id;`;
const idStr = psql(sql);
@@ -450,6 +477,10 @@ function main() {
}
}
console.log(`\nCreated ${created.length}/${opt.n} designs · IDs: ${created.join(',')}`);
+ // 2026-05-21 — exit non-zero when zero designs landed in PG. Previously this
+ // function always returned with exit 0 even on ComfyUI timeout or psql failure,
+ // which let parallel runners count failures as successes. Now: 0/N → exit 1.
+ if (created.length === 0) process.exit(1);
}
main();
diff --git a/scripts/night-builder.js b/scripts/night-builder.js
index 96fed35..3f70bf8 100755
--- a/scripts/night-builder.js
+++ b/scripts/night-builder.js
@@ -789,9 +789,22 @@ function generateOne(track, cw) {
// psqlAt returns tuples-only, no header/footer — clean single-value reads
const lp = execSync(`/opt/homebrew/opt/postgresql@14/bin/psql dw_unified -At -c "SELECT local_path FROM spoon_all_designs WHERE id=${id};"`, { encoding: 'utf8' }).trim();
if (lp && fs.existsSync(lp)) {
- const q = spawnSync('python3', [path.join(__dirname, 'quantize-2tone.py'), lp], { encoding: 'utf8' });
+ // Per-category post-quantize policy (Steve 2026-05-20):
+ // - silhouette tracks (muybridge, lips, single-stamp): strict 2-tone
+ // - everything else: up to 4 tones with ghost-merge (kills the
+ // hovering shadow-layer SDXL likes to add without flattening
+ // legitimate multi-color compositions like drips/florals/lips
+ // with red-lip+skin+shadow+ground).
+ const silhouetteCategories = new Set(['muybridge-plate', 'lips']);
+ const isSilhouette = silhouetteCategories.has(category);
+ const qScript = isSilhouette ? 'quantize-2tone.py' : 'quantize-no-ghost.py';
+ const qEnv = { ...process.env,
+ QUANT_COLORS: isSilhouette ? '2' : '4',
+ GHOST_LUMA_GAP: '35',
+ };
+ const q = spawnSync('python3', [path.join(__dirname, qScript), lp], { encoding: 'utf8', env: qEnv });
if (q.status === 0) {
- logLine(` 2tone OK id=${id}: ${(q.stdout || '').trim().split('->')[1] || '?'}`);
+ logLine(` quant OK id=${id} (${isSilhouette ? '2-tone' : '4-tone+noghost'}): ${(q.stdout || '').trim().split('->')[1] || '?'}`);
// COLORWAY-VARIANTS — for every 2-toned design, generate 3 free
// recolors. Steve directive 2026-05-20. Each variant becomes a
// child row in spoon_all_designs with parent_design_id=id.
@@ -814,8 +827,8 @@ function generateOne(track, cw) {
// PLACEHOLDER. Two separate psql calls — INSERT...RETURNING
// captures id, then a follow-up UPDATE writes the real URL.
const insertSql = `
- INSERT INTO spoon_all_designs (brand, kind, width_in, height_in, panels, generator, prompt, image_url, local_path, dominant_hex, category, is_published, notes, parent_design_id, created_at)
- SELECT brand, kind, width_in, height_in, panels, generator, prompt || E'\\n[colorway-variant of #${id}]', '/designs/img/by-id/PLACEHOLDER', '${v.path.replace(/'/g, "''")}', '${v.ground.replace(/'/g, "''")}', '${cwTitle.replace(/'/g, "''")}', TRUE, E'colorway variant: ${v.slug} (ground=${v.ground}, figure=${v.figure}) of parent #${id}', ${id}, NOW()
+ INSERT INTO spoon_all_designs (brand, kind, width_in, height_in, panels, generator, prompt, image_url, local_path, dominant_hex, category, product_line, is_published, notes, parent_design_id, created_at)
+ SELECT brand, kind, width_in, height_in, panels, generator, prompt || E'\\n[colorway-variant of #${id}]', '/designs/img/by-id/PLACEHOLDER', '${v.path.replace(/'/g, "''")}', '${v.ground.replace(/'/g, "''")}', '${cwTitle.replace(/'/g, "''")}', product_line, TRUE, E'colorway variant: ${v.slug} (ground=${v.ground}, figure=${v.figure}) of parent #${id}', ${id}, NOW()
FROM spoon_all_designs WHERE id=${id}
RETURNING id;`;
try {
diff --git a/scripts/sync-designs-json-product-line.js b/scripts/sync-designs-json-product-line.js
new file mode 100644
index 0000000..9021b69
--- /dev/null
+++ b/scripts/sync-designs-json-product-line.js
@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+// One-off backfill — sync the `product_line` PG column into data/designs.json
+// so the JSON-file-backed DESIGNS array carries the field at server startup
+// (server.js productLine(d) helper short-circuits on d.product_line).
+//
+// Strategy:
+// 1. Read current data/designs.json
+// 2. Bulk SELECT id, product_line FROM spoon_all_designs WHERE
+// product_line IS NOT NULL — single query, no N+1 round-trips
+// 3. For each JSON row whose id matches, set row.product_line
+// 4. Atomic write — tmpfile + fsync + rename
+//
+// Steve directive 2026-05-21 — independent of Agent A; no server.js touch.
+
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const ROOT = path.join(__dirname, '..');
+const SRC = path.join(ROOT, 'data', 'designs.json');
+
+function log(m) { console.log(`[sync-product-line] ${m}`); }
+
+function psql(sql) {
+ const onLinux = (process.platform === 'linux');
+ if (onLinux) {
+ return execSync(`sudo -n -u postgres psql dw_unified -At -q -c "${sql.replace(/"/g, '\\"')}"`, { encoding: 'utf8' }).trim();
+ }
+ // Mac2 — local trust auth
+ return execSync(`psql dw_unified -At -q -c "${sql.replace(/"/g, '\\"')}"`, { encoding: 'utf8' }).trim();
+}
+
+function main() {
+ if (!fs.existsSync(SRC)) {
+ console.error(`MISSING: ${SRC}`);
+ process.exit(1);
+ }
+ log(`reading ${SRC}`);
+ const raw = fs.readFileSync(SRC, 'utf8');
+ const rows = JSON.parse(raw);
+ log(`parsed ${rows.length} rows`);
+
+ // Bulk fetch — single round trip
+ log('querying PG for product_line map');
+ const out = psql("SELECT id || '|' || product_line FROM spoon_all_designs WHERE product_line IS NOT NULL;");
+ const map = new Map();
+ for (const line of out.split('\n')) {
+ if (!line) continue;
+ const [idStr, line2] = line.split('|');
+ const id = parseInt(idStr, 10);
+ if (Number.isFinite(id) && line2) map.set(id, line2.trim());
+ }
+ log(`PG map has ${map.size} entries`);
+
+ let touched = 0;
+ let alreadyHad = 0;
+ for (const r of rows) {
+ if (!r || r.id == null) continue;
+ const want = map.get(Number(r.id));
+ if (want) {
+ if (r.product_line === want) {
+ alreadyHad += 1;
+ } else {
+ r.product_line = want;
+ touched += 1;
+ }
+ }
+ }
+ log(`wrote product_line on ${touched} rows; ${alreadyHad} already had it`);
+
+ // Atomic write — tmp + fsync + rename
+ const tmp = SRC + '.tmp.' + process.pid;
+ const fd = fs.openSync(tmp, 'w');
+ try {
+ fs.writeSync(fd, JSON.stringify(rows, null, 2));
+ fs.fsyncSync(fd);
+ } finally {
+ fs.closeSync(fd);
+ }
+ fs.renameSync(tmp, SRC);
+ log(`wrote ${SRC} atomically (${rows.length} rows)`);
+
+ // Coverage report
+ const linesCount = rows.filter(r => r && r.product_line).length;
+ log(`coverage — ${linesCount} rows in designs.json now carry product_line`);
+}
+
+main();
← b0577b9 feat(scenic+studio): sub-brand rename — product_line column
·
back to Wallco Ai
·
wallco / Scenic + Studio sub-brand landings, Studio trade-ga 9e6a142 →