[object Object]

← back to Wallco Ai

refiner-compare: add 3rd column — Replicate app render (stability-ai/sdxl). New replicate_compare.js renders 25 via the live app path; progress.json gains a replicate field; compare.html now 3-up; drop static-page overwrite (live page is permanent)

8c46cbd62332f4577f7202ae6355f1be36342f4e · 2026-05-19 12:59:30 -0700 · SteveStudio2

Files touched

Diff

commit 8c46cbd62332f4577f7202ae6355f1be36342f4e
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Tue May 19 12:59:30 2026 -0700

    refiner-compare: add 3rd column — Replicate app render (stability-ai/sdxl). New replicate_compare.js renders 25 via the live app path; progress.json gains a replicate field; compare.html now 3-up; drop static-page overwrite (live page is permanent)
---
 scripts/refiner_compare.js   |  47 +++------------
 scripts/replicate_compare.js | 134 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 143 insertions(+), 38 deletions(-)

diff --git a/scripts/refiner_compare.js b/scripts/refiner_compare.js
index 98bc413..67e2887 100644
--- a/scripts/refiner_compare.js
+++ b/scripts/refiner_compare.js
@@ -109,8 +109,11 @@ const TAG = SHARDS > 1 ? `[shard ${SHARD}/${SHARDS}] ` : '';
   const save = () => {
     const rows = STYLES.map((style, i) => ({
       i, style, brand: FASHION_PALETTES[i % FASHION_PALETTES.length].brand, seed: 770000 + i,
-      base:    haveImg(path.join(OUT, `${i}_base.png`))    ? `${i}_base.png`    : null,
-      refiner: haveImg(path.join(OUT, `${i}_refiner.png`)) ? `${i}_refiner.png` : null,
+      base:      haveImg(path.join(OUT, `${i}_base.png`))      ? `${i}_base.png`      : null,
+      refiner:   haveImg(path.join(OUT, `${i}_refiner.png`))   ? `${i}_refiner.png`   : null,
+      // 3rd column — wallco.ai's live app render via Replicate, filled in by
+      // replicate_compare.js. Counted separately, not in the ComfyUI `done`.
+      replicate: haveImg(path.join(OUT, `${i}_replicate.png`)) ? `${i}_replicate.png` : null,
     }));
     const done = rows.reduce((n, r) => n + (r.base ? 1 : 0) + (r.refiner ? 1 : 0), 0);
     fs.writeFileSync(path.join(OUT, 'progress.json'), JSON.stringify({ done, total: TOTAL, rows }, null, 1));
@@ -153,40 +156,8 @@ const TAG = SHARDS > 1 ? `[shard ${SHARD}/${SHARDS}] ` : '';
   // not to restart this shard while the OTHER shard is still rendering.
   fs.writeFileSync(path.join(OUT, `.shard${SHARD}.done`), new Date().toISOString());
 
-  // static compare page — written only once EVERY render is on disk, so the
-  // first shard to finish doesn't replace the live page out from under the other.
-  const finalRows = STYLES.map((style, i) => ({
-    i, style, brand: FASHION_PALETTES[i % FASHION_PALETTES.length].brand, seed: 770000 + i,
-    base:    haveImg(path.join(OUT, `${i}_base.png`))    ? `${i}_base.png`    : false,
-    refiner: haveImg(path.join(OUT, `${i}_refiner.png`)) ? `${i}_refiner.png` : false,
-  }));
-  if (!finalRows.every(r => r.base && r.refiner)) {
-    console.log(`${TAG}shard done; other shard(s) still rendering — leaving live page up.`);
-    return;
-  }
-  const esc = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
-  const cards = finalRows.map(r => `
-    <div class="row">
-      <div class="meta"><b>#${r.i + 1}</b> · ${esc(r.style)} · <span class="brand">${esc(r.brand)}</span> · seed ${r.seed}</div>
-      <div class="pair">
-        <figure>${r.base ? `<img src="${r.base}" loading="lazy">` : '<div class=miss>FAILED</div>'}<figcaption>base-only SDXL</figcaption></figure>
-        <figure>${r.refiner ? `<img src="${r.refiner}" loading="lazy">` : '<div class=miss>FAILED</div>'}<figcaption>base + refiner</figcaption></figure>
-      </div>
-    </div>`).join('');
-  const html = `<!doctype html><meta charset=utf-8><title>Refiner Comparison — wallco.ai</title>
-<style>
-body{font:15px/1.5 -apple-system,system-ui,sans-serif;background:#1a1712;color:#f4f1ea;margin:0;padding:32px}
-h1{font-weight:600;margin:0 0 4px}.sub{color:#9b8f7a;margin-bottom:28px;font-size:13px}
-.row{margin-bottom:34px;border-bottom:1px solid #3a342a;padding-bottom:24px}
-.meta{font-size:13px;color:#c9bfa7;margin-bottom:10px}.brand{color:#d9a441}
-.pair{display:grid;grid-template-columns:1fr 1fr;gap:18px;max-width:1100px}
-figure{margin:0}figure img{width:100%;border-radius:8px;display:block;background:#000}
-figcaption{text-align:center;font-size:12px;color:#9b8f7a;margin-top:6px;text-transform:uppercase;letter-spacing:.1em}
-.miss{aspect-ratio:1;display:flex;align-items:center;justify-content:center;background:#3a261e;color:#ff8a7a;border-radius:8px}
-</style>
-<h1>SDXL Refiner Comparison</h1>
-<p class="sub">25 wallcovering prompts · identical prompt + seed per pair · left = base-only, right = base + refiner. Generated ${new Date().toISOString()}.</p>
-${cards}`;
-  fs.writeFileSync(path.join(OUT, 'compare.html'), html);
-  console.log(`\nDONE — compare page: http://127.0.0.1:9905/refiner-compare/compare.html`);
+  // The live compare.html keeps polling progress.json, so it shows the final
+  // state on its own — no static-page overwrite needed (and overwriting it
+  // would clobber the 3-column Replicate layout). Just report and exit.
+  console.log(`${TAG}shard ${SHARD} complete — live page: http://127.0.0.1:9905/refiner-compare/compare.html`);
 })();
diff --git a/scripts/replicate_compare.js b/scripts/replicate_compare.js
new file mode 100644
index 0000000..71bc27c
--- /dev/null
+++ b/scripts/replicate_compare.js
@@ -0,0 +1,134 @@
+'use strict';
+/**
+ * Replicate column for the refiner comparison — renders the SAME 25 prompts +
+ * seeds through wallco.ai's LIVE app path: Replicate `stability-ai/sdxl`
+ * (base + expert_ensemble_refiner). Writes `<i>_replicate.png` into
+ * public/refiner-compare/ so compare.html shows it as the 3rd column.
+ *
+ * Cloud API — does NOT touch the local ComfyUI GPUs, runs fully in parallel
+ * with the two refiner_compare shards. Cost ≈ $0.014/image × 25 ≈ $0.35.
+ * Resume-safe: a `<i>_replicate.png` already on disk is skipped.
+ *
+ *   node scripts/replicate_compare.js
+ */
+require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+const { FASHION_PALETTES, paletteString } = require('./fashion_palettes');
+
+const OUT = path.join(__dirname, '..', 'public', 'refiner-compare');
+fs.mkdirSync(OUT, { recursive: true });
+
+// identical style list + seed scheme as refiner_compare.js so all 3 columns
+// of a row are the same prompt + seed.
+const STYLES = [
+  'damask', 'arabesque trellis', 'geometric art-deco', 'botanical floral', 'ogee medallion',
+  'stripe and sprig', 'block-print paisley', 'scrollwork acanthus', 'chinoiserie scenic', 'herringbone tonal',
+  'guilloché lattice', 'stylized fan', 'marbled swirl', 'star tessellation', 'vine and berry',
+  'diamond harlequin', 'rosette grid', 'wave and shell', 'feathered plume', 'interlocking fretwork key',
+  'arts-and-crafts leaf', 'moroccan zellige', 'pinstripe ticking', 'sunburst medallion', 'quatrefoil net',
+];
+
+// ---- Replicate SDXL backend (lifted from scripts/generate_designs.js so this
+// column is exactly the app's production render path) ----------------------
+function loadReplicateToken() {
+  if (process.env.REPLICATE_API_TOKEN) return process.env.REPLICATE_API_TOKEN;
+  const home = require('os').homedir();
+  const candidates = [
+    `${home}/Projects/animate-museum-posts/.env`,
+    `${home}/Projects/secrets-manager/.env`,
+    `${home}/.dw-fleet.env`,
+  ];
+  for (const p of candidates) {
+    if (!fs.existsSync(p)) continue;
+    const m = fs.readFileSync(p, 'utf8').match(/^\s*REPLICATE_API_TOKEN\s*=\s*(\S+)/m);
+    if (m) return m[1].replace(/^["']|["']$/g, '');
+  }
+  return null;
+}
+
+function genReplicate(prompt, seed, outPath) {
+  const TOKEN = loadReplicateToken();
+  if (!TOKEN) throw new Error('REPLICATE_API_TOKEN not found in env or ~/Projects/secrets-manager/.env');
+
+  const MODEL_VERSION = process.env.SDXL_MODEL_VERSION || '7762fd07cf82c948538e41f63f77d685e02b063e37e496e96eefd46c929f9bdc';
+  const WIDTH  = parseInt(process.env.REPLICATE_WIDTH  || '1024', 10);
+  const HEIGHT = parseInt(process.env.REPLICATE_HEIGHT || '1024', 10);
+
+  const positive = `${prompt}, seamless tile, repeating pattern, no edges, fabric pattern, wallpaper design, archival quality, high detail, screen-printed wallpaper quality, solid opaque richly-saturated ink, every motif fully filled with solid color, crisp clean color separation, dimensional embossed relief, three-dimensional rendered depth welcome, tactile luxe material finishes — suede, woven trim, raffia, grasscloth texture`;
+  const negative = 'low quality, blurry, edges, seam, border, frame, signature, watermark, text, hands, fingers, deformed, transparent, see-through, translucent, unfilled shapes, hollow outlines, outline-only motifs, ghosted layers, washed out, faded, low opacity, gaps in the ink, missing fill, gradient bleed, halftone, halftone dots, ben-day dots, dot-screen shading, half-tone gradient texture';
+
+  const body = {
+    version: MODEL_VERSION,
+    input: {
+      prompt: positive, negative_prompt: negative,
+      width: WIDTH, height: HEIGHT,
+      num_inference_steps: 30, guidance_scale: 7.5, seed,
+      refine: 'expert_ensemble_refiner', apply_watermark: false,
+    },
+  };
+
+  const submit = execSync(`curl -sf -m 30 -H 'Authorization: Bearer ${TOKEN}' -H 'Content-Type: application/json' -X POST 'https://api.replicate.com/v1/predictions' -d @-`,
+    { input: JSON.stringify(body), encoding: 'utf8' });
+  let pred;
+  try { pred = JSON.parse(submit); } catch { throw new Error('Replicate submit non-JSON: ' + submit.slice(0, 200)); }
+  if (!pred.id) throw new Error('Replicate: no prediction id — ' + JSON.stringify(pred).slice(0, 300));
+
+  const start = Date.now();
+  let final;
+  while (Date.now() - start < 3 * 60_000) {
+    execSync('sleep 3');
+    const poll = execSync(`curl -sf -m 10 -H 'Authorization: Bearer ${TOKEN}' 'https://api.replicate.com/v1/predictions/${pred.id}'`, { encoding: 'utf8' });
+    const p = JSON.parse(poll);
+    if (p.status === 'succeeded') { final = p; break; }
+    if (p.status === 'failed' || p.status === 'canceled') {
+      throw new Error(`Replicate ${p.status}: ${JSON.stringify(p.error || p.logs || '').slice(0, 300)}`);
+    }
+  }
+  if (!final) throw new Error(`Replicate timed out after 3 min (id=${pred.id})`);
+
+  const imageUrl = Array.isArray(final.output) ? final.output[0] : final.output;
+  if (!imageUrl) throw new Error('Replicate returned no image URL');
+  execSync(`curl -sf -m 60 -L -o ${JSON.stringify(outPath)} ${JSON.stringify(imageUrl)}`);
+  if (!fs.existsSync(outPath) || fs.statSync(outPath).size < 1000) {
+    throw new Error(`Replicate image download failed (${imageUrl})`);
+  }
+  return outPath;
+}
+
+// ---- progress.json: disk-derived (same schema as refiner_compare.js) so this
+// process and the two shard processes can all write it without clobbering ---
+const TOTAL = STYLES.length * 2;   // 50 = the local ComfyUI render count
+const haveImg = p => { try { return fs.statSync(p).size > 1000; } catch { return false; } };
+const save = () => {
+  const rows = STYLES.map((style, i) => ({
+    i, style, brand: FASHION_PALETTES[i % FASHION_PALETTES.length].brand, seed: 770000 + i,
+    base:      haveImg(path.join(OUT, `${i}_base.png`))      ? `${i}_base.png`      : null,
+    refiner:   haveImg(path.join(OUT, `${i}_refiner.png`))   ? `${i}_refiner.png`   : null,
+    replicate: haveImg(path.join(OUT, `${i}_replicate.png`)) ? `${i}_replicate.png` : null,
+  }));
+  const done = rows.reduce((n, r) => n + (r.base ? 1 : 0) + (r.refiner ? 1 : 0), 0);
+  fs.writeFileSync(path.join(OUT, 'progress.json'), JSON.stringify({ done, total: TOTAL, rows }, null, 1));
+};
+
+(async () => {
+  const t0 = Date.now();
+  let ok = 0, fail = 0, skip = 0;
+  console.log(`replicate_compare — 25 app renders via Replicate stability-ai/sdxl`);
+  for (let i = 0; i < STYLES.length; i++) {
+    const style = STYLES[i], pal = FASHION_PALETTES[i % FASHION_PALETTES.length];
+    const seed = 770000 + i;
+    const outPath = path.join(OUT, `${i}_replicate.png`);
+    if (haveImg(outPath)) { skip++; console.log(`  ${i} replicate — skip (already rendered)`); save(); continue; }
+    // core prompt mirrors refiner_compare.js buildPos(); genReplicate appends
+    // the app's own production suffix, so this is the live app render.
+    const prompt = `A seamless wallpaper pattern, ${style} motif, ${paletteString(pal)}`;
+    try { genReplicate(prompt, seed, outPath); ok++; }
+    catch (e) { fail++; console.log(`  ${i} replicate ERR ${e.message}`); }
+    save();
+    console.log(`[replicate ${i + 1}/25] ${style} · ${pal.brand} — ${haveImg(outPath) ? 'ok' : 'FAIL'} · ${((Date.now() - t0) / 60000).toFixed(1)}min`);
+  }
+  save();
+  console.log(`\nDONE — replicate column: ${ok} rendered, ${skip} skipped, ${fail} failed · ${((Date.now() - t0) / 60000).toFixed(1)}min`);
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });

← 9829d20 deploy-kamatera: image sync is incremental + non-destructive  ·  back to Wallco Ai  ·  replicate_compare: read REPLICATE_API_TOKEN only from projec 85d2777 →