← 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
M scripts/refiner_compare.jsA scripts/replicate_compare.js
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, '&').replace(/</g, '<').replace(/>/g, '>');
- 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 →