[object Object]

← back to Wallco Ai

designer-studio compose: heal-first instead of re-roll — keep the round-1 tile (WALLCO_SEAMLESS_GATE=0), heal its seam in place via fix-seam.py (PIL shift-and-heal), re-roll only as last resort when ghost/frame gate rejects; fix-seam.py --inplace now no-ops on already-seamless tiles (Round-1 sacred)

e6c546dbcedf25aac59d74d631524ca35ed43fc9 · 2026-05-31 18:49:18 -0700 · Steve Abrams

Files touched

Diff

commit e6c546dbcedf25aac59d74d631524ca35ed43fc9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun May 31 18:49:18 2026 -0700

    designer-studio compose: heal-first instead of re-roll — keep the round-1 tile (WALLCO_SEAMLESS_GATE=0), heal its seam in place via fix-seam.py (PIL shift-and-heal), re-roll only as last resort when ghost/frame gate rejects; fix-seam.py --inplace now no-ops on already-seamless tiles (Round-1 sacred)
---
 scripts/fix-seam.py |  6 ++++++
 server.js           | 35 ++++++++++++++++++++++++++++++-----
 2 files changed, 36 insertions(+), 5 deletions(-)

diff --git a/scripts/fix-seam.py b/scripts/fix-seam.py
index 544c99c..b74f85b 100644
--- a/scripts/fix-seam.py
+++ b/scripts/fix-seam.py
@@ -109,6 +109,12 @@ def process_path(in_path, out_path=None, inplace=False):
     im = Image.open(in_path).convert("RGB")
     lr, tb = edge_diff(im)
     result = {'path': in_path, 'lr_diff': round(lr,1), 'tb_diff': round(tb,1)}
+    # Round-1 sacred: only heal a tile that actually has a visible seam — a
+    # clean tile must be left untouched (shift_and_heal would needlessly move
+    # the composition + blur a band).
+    if not ((lr > THRESHOLD) or (tb > THRESHOLD)):
+        result['status'] = 'already-seamless'
+        return result
     fixed = shift_and_heal(im)
     lr2, tb2 = edge_diff(fixed)
     dest = in_path if inplace else (out_path or in_path.replace('.png', '__seamfixed.png'))
diff --git a/server.js b/server.js
index 582d86a..2fd2c3e 100644
--- a/server.js
+++ b/server.js
@@ -21910,12 +21910,32 @@ app.post('/api/studio/compose', (req, res) => {
           finished_at=now() WHERE id=${runId};`);
       } catch {}
     };
+    // HEAL-FIRST (Round-1 outputs are sacred): rather than re-rolling a fresh
+    // generation when the tile isn't perfectly seamless, we KEEP the round-1
+    // composition and heal its seam in place with scripts/fix-seam.py (PIL
+    // shift-and-heal — no-op if already seamless). We only re-roll as a LAST
+    // resort, and only when the round produced NO usable tile at all (the
+    // ghost/frame gates legitimately rejected it).
+    const healSeam = (designId) => {
+      try {
+        const lp = psqlQuery(`SELECT local_path FROM spoon_all_designs WHERE id=${designId} LIMIT 1;`).trim();
+        if (lp && fs.existsSync(lp)) {
+          const fr = spawnSync('python3', [
+            path.join(__dirname, 'scripts', 'fix-seam.py'), '--path', lp, '--inplace',
+          ], { cwd: __dirname, encoding: 'utf8', timeout: 60000 });
+          try { fs.appendFileSync(logFile, `\n— seam-heal #${designId}: ${(fr.stdout||fr.stderr||'').trim().slice(-200)} —\n`); } catch {}
+        }
+      } catch (e) { try { fs.appendFileSync(logFile, `\n— seam-heal #${designId} error: ${e.message} —\n`); } catch {} }
+    };
     const attempt = (n) => {
       const out = fs.openSync(logFile, 'a');
       const child = spawn('node', [
         path.join(__dirname, 'scripts', 'generate_designs.js'),
         '--n', '1', '--kind', 'seamless_tile', '--category', category, '--prompts', prompt,
-      ], { cwd: __dirname, env: { ...process.env, GEN_BACKEND: process.env.GEN_BACKEND || 'replicate' },
+      ], { cwd: __dirname,
+           // WALLCO_SEAMLESS_GATE=0 keeps the round-1 tile instead of letting the
+           // generator skip/re-roll it on a seam defect — we heal it ourselves below.
+           env: { ...process.env, GEN_BACKEND: process.env.GEN_BACKEND || 'replicate', WALLCO_SEAMLESS_GATE: '0' },
            stdio: ['ignore', out, out], timeout: 180000 });
       child.on('exit', (code) => {
         try { fs.closeSync(out); } catch {}
@@ -21927,12 +21947,17 @@ app.post('/api/studio/compose', (req, res) => {
           const ms = [...logText.matchAll(/Created\s+\d+\/\d+\s+designs\s*·?\s*IDs?:\s*([0-9,\s]+)/gi)];
           if (ms.length) createdIds = ms[ms.length-1][1].split(',').map(s => parseInt(s.trim(),10)).filter(Number.isFinite);
         } catch {}
-        if (code === 0 && createdIds.length > 0) { finalize(true, createdIds[0], null); return; }
+        if (code === 0 && createdIds.length > 0) {
+          healSeam(createdIds[0]);              // heal the round-1 composition, don't re-roll
+          finalize(true, createdIds[0], null);
+          return;
+        }
+        // No usable round-1 output (ghost/frame gate rejected, or hard failure) → re-roll, last resort.
         if (n < MAX_ATTEMPTS) {
-          try { fs.appendFileSync(logFile, `\n— attempt ${n}/${MAX_ATTEMPTS} did not pass the seamless quality gate (code ${code}); auto-retrying —\n`); } catch {}
+          try { fs.appendFileSync(logFile, `\n— attempt ${n}/${MAX_ATTEMPTS} produced no usable tile (code ${code}); re-rolling —\n`); } catch {}
           attempt(n + 1);
         } else {
-          finalize(false, null, `0/1 designs survived the seamless quality gate after ${MAX_ATTEMPTS} attempts (last code ${code})`);
+          finalize(false, null, `no design survived the ghost/frame quality gate after ${MAX_ATTEMPTS} attempts (last code ${code})`);
         }
       });
       child.on('error', (err) => {
@@ -22450,7 +22475,7 @@ document.getElementById('btn-compose').addEventListener('click', async function(
     while (Date.now() - startedAt < 420000) {
       await new Promise(function(r){ setTimeout(r, 3000); });
       var elapsed = Math.round((Date.now()-startedAt)/1000);
-      stat.innerHTML = '<div style="padding:10px;background:#fff3cd;color:#856404;border-radius:4px">Generating your pattern… '+elapsed+'s <span style="color:#999">· seamless tiling + quality gate (auto-retries up to 3× for a clean tile), safe to wait</span></div>';
+      stat.innerHTML = '<div style="padding:10px;background:#fff3cd;color:#856404;border-radius:4px">Generating your pattern… '+elapsed+'s <span style="color:#999">· rendering + seam-healing for a clean tileable repeat, safe to wait</span></div>';
       try {
         var st = await get('/api/studio/compose/status?run_id='+encodeURIComponent(started.run_id));
         if (st && st.status && st.status !== 'running') { j = st; break; }

← 55871ad Generate + publish fresh mica-ground drunk-animals set (5492  ·  back to Wallco Ai  ·  v11-best PDP: mobile polish — id chip to bottom-right (clear c0e3414 →