[object Object]

← back to Wallco Ai

add refiner_compare.js — 25-prompt base vs base+refiner side-by-side eval

9bda674d35e818e0b6bb11c1039a859f84e22674 · 2026-05-19 11:15:03 -0700 · SteveStudio2

Files touched

Diff

commit 9bda674d35e818e0b6bb11c1039a859f84e22674
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Tue May 19 11:15:03 2026 -0700

    add refiner_compare.js — 25-prompt base vs base+refiner side-by-side eval
---
 scripts/refiner_compare.js | 137 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 137 insertions(+)

diff --git a/scripts/refiner_compare.js b/scripts/refiner_compare.js
new file mode 100644
index 0000000..680dab7
--- /dev/null
+++ b/scripts/refiner_compare.js
@@ -0,0 +1,137 @@
+'use strict';
+/**
+ * Refiner comparison — 25 wallcovering prompts, each rendered TWICE via ComfyUI:
+ *   base-only SDXL   vs   base + refiner (expert-ensemble)
+ * Identical prompt + identical seed per pair, so any difference is the refiner.
+ *
+ * Does NOT touch the database. Writes images + a side-by-side compare page to
+ * public/refiner-compare/ — served at http://127.0.0.1:9905/refiner-compare/compare.html
+ *
+ *   node scripts/refiner_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 COMFY = process.env.COMFY_URL || 'http://127.0.0.1:8188';
+const BASE_MODEL = 'sd_xl_base_1.0.safetensors';
+const REFINER_MODEL = 'sd_xl_refiner_1.0.safetensors';
+const OUT = path.join(__dirname, '..', 'public', 'refiner-compare');
+fs.mkdirSync(OUT, { recursive: true });
+
+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',
+];
+
+const NEG = 'low quality, blurry, edges, seam, border, frame, signature, watermark, text, hands, fingers, ' +
+  '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';
+
+function buildPos(style, pal) {
+  return `A seamless wallpaper pattern, ${style} motif, ${paletteString(pal)}, ` +
+    'seamless tile, repeating pattern, no edges, fabric pattern, wallpaper design, high detail, archival quality, ' +
+    'screen-printed wallpaper, solid opaque flat ink, flat color blocks, every shape fully filled with solid color, ' +
+    'even saturated pigment coverage, crisp screen-print layers';
+}
+
+function baseWorkflow(seed, POS) {
+  return {
+    '3': { class_type: 'KSampler', inputs: { seed, steps: 28, cfg: 7.5, sampler_name: 'euler', scheduler: 'normal', denoise: 1.0, model: ['4', 0], positive: ['6', 0], negative: ['7', 0], latent_image: ['5', 0] } },
+    '4': { class_type: 'CheckpointLoaderSimple', inputs: { ckpt_name: BASE_MODEL } },
+    '5': { class_type: 'EmptyLatentImage', inputs: { width: 1024, height: 1024, batch_size: 1 } },
+    '6': { class_type: 'CLIPTextEncode', inputs: { text: POS, clip: ['4', 1] } },
+    '7': { class_type: 'CLIPTextEncode', inputs: { text: NEG, clip: ['4', 1] } },
+    '8': { class_type: 'VAEDecode', inputs: { samples: ['3', 0], vae: ['4', 2] } },
+    '9': { class_type: 'SaveImage', inputs: { filename_prefix: `cmp_base_${seed}`, images: ['8', 0] } },
+  };
+}
+
+function refinerWorkflow(seed, POS) {
+  const STEPS = 30, SWITCH = 24;
+  return {
+    '4':  { class_type: 'CheckpointLoaderSimple', inputs: { ckpt_name: BASE_MODEL } },
+    '15': { class_type: 'CheckpointLoaderSimple', inputs: { ckpt_name: REFINER_MODEL } },
+    '5':  { class_type: 'EmptyLatentImage', inputs: { width: 1024, height: 1024, batch_size: 1 } },
+    '6':  { class_type: 'CLIPTextEncode', inputs: { text: POS, clip: ['4', 1] } },
+    '7':  { class_type: 'CLIPTextEncode', inputs: { text: NEG, clip: ['4', 1] } },
+    '16': { class_type: 'CLIPTextEncode', inputs: { text: POS, clip: ['15', 1] } },
+    '17': { class_type: 'CLIPTextEncode', inputs: { text: NEG, clip: ['15', 1] } },
+    '10': { class_type: 'KSamplerAdvanced', inputs: { add_noise: 'enable', noise_seed: seed, steps: STEPS, cfg: 7.5, sampler_name: 'euler', scheduler: 'normal', start_at_step: 0, end_at_step: SWITCH, return_with_leftover_noise: 'enable', model: ['4', 0], positive: ['6', 0], negative: ['7', 0], latent_image: ['5', 0] } },
+    '11': { class_type: 'KSamplerAdvanced', inputs: { add_noise: 'disable', noise_seed: seed, steps: STEPS, cfg: 7.5, sampler_name: 'euler', scheduler: 'normal', start_at_step: SWITCH, end_at_step: 10000, return_with_leftover_noise: 'disable', model: ['15', 0], positive: ['16', 0], negative: ['17', 0], latent_image: ['10', 0] } },
+    '8':  { class_type: 'VAEDecode', inputs: { samples: ['11', 0], vae: ['15', 2] } },
+    '9':  { class_type: 'SaveImage', inputs: { filename_prefix: `cmp_ref_${seed}`, images: ['8', 0] } },
+  };
+}
+
+function render(workflow, outPath) {
+  const submit = execSync(`curl -sf -m 30 -H 'Content-Type: application/json' -X POST '${COMFY}/prompt' -d @-`,
+    { input: JSON.stringify({ prompt: workflow, client_id: 'refiner-compare' }), encoding: 'utf8' });
+  const pid = JSON.parse(submit).prompt_id;
+  if (!pid) throw new Error('no prompt_id');
+  const start = Date.now();
+  while (Date.now() - start < 6 * 60_000) {
+    execSync('sleep 2');
+    let hist;
+    try { hist = JSON.parse(execSync(`curl -sf -m 5 '${COMFY}/history/${pid}'`, { encoding: 'utf8' })); }
+    catch { continue; }
+    const e = hist[pid];
+    if (e && e.outputs && e.outputs['9'] && e.outputs['9'].images) {
+      const im = e.outputs['9'].images[0];
+      const url = `${COMFY}/view?filename=${encodeURIComponent(im.filename)}&subfolder=${encodeURIComponent(im.subfolder || '')}&type=${encodeURIComponent(im.type || 'output')}`;
+      execSync(`curl -sf -m 60 -o ${JSON.stringify(outPath)} ${JSON.stringify(url)}`);
+      return fs.existsSync(outPath) && fs.statSync(outPath).size > 1000;
+    }
+  }
+  return false;
+}
+
+(async () => {
+  const rows = [];
+  const t0 = Date.now();
+  for (let i = 0; i < STYLES.length; i++) {
+    const style = STYLES[i];
+    const pal = FASHION_PALETTES[i % FASHION_PALETTES.length];
+    const seed = 770000 + i;
+    const POS = buildPos(style, pal);
+    const baseFile = `${i}_base.png`, refFile = `${i}_refiner.png`;
+    let okB = false, okR = false;
+    try { okB = render(baseWorkflow(seed, POS), path.join(OUT, baseFile)); } catch (e) { console.log(`  ${i} base ERR ${e.message}`); }
+    try { okR = render(refinerWorkflow(seed, POS), path.join(OUT, refFile)); } catch (e) { console.log(`  ${i} refiner ERR ${e.message}`); }
+    rows.push({ i, style, brand: pal.brand, seed, base: okB ? baseFile : null, refiner: okR ? refFile : null });
+    console.log(`[${i + 1}/25] ${style} · ${pal.brand} — base:${okB ? 'ok' : 'FAIL'} refiner:${okR ? 'ok' : 'FAIL'} · ${((Date.now() - t0) / 1000 / 60).toFixed(1)}min`);
+    fs.writeFileSync(path.join(OUT, 'progress.json'), JSON.stringify({ done: i + 1, total: 25, rows }, null, 1));
+  }
+
+  // compare page
+  const esc = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
+  const cards = rows.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`);
+})();

← 25f96e9 comfy: optional SDXL base+refiner workflow (COMFY_REFINER_MO  ·  back to Wallco Ai  ·  generator: ban halftones, allow 3D/dimensional designs, add af70673 →