← 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
M scripts/fix-seam.pyM server.js
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 →