[object Object]

← 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

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 →