← back to Wallco Ai
designer-studio compose: auto-retry up to 3× on seamless-QA rejection (SEAMLESS_FAIL_LR) before erroring — single run_id spans attempts, frontend poll window 240s→420s
04255011d7b5d56fc21286bf26fb1b8d04639bf2 · 2026-05-31 18:31:45 -0700 · Steve Abrams
Files touched
Diff
commit 04255011d7b5d56fc21286bf26fb1b8d04639bf2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun May 31 18:31:45 2026 -0700
designer-studio compose: auto-retry up to 3× on seamless-QA rejection (SEAMLESS_FAIL_LR) before erroring — single run_id spans attempts, frontend poll window 240s→420s
---
server.js | 82 ++++++++++++++++++++++++++++++++++++---------------------------
1 file changed, 47 insertions(+), 35 deletions(-)
diff --git a/server.js b/server.js
index 59500b9..582d86a 100644
--- a/server.js
+++ b/server.js
@@ -21886,26 +21886,15 @@ app.post('/api/studio/compose', (req, res) => {
// /api/studio/compose/status for completion.
const logFile = path.join(__dirname, 'data', 'tmp', `studio-run-${runId}.log`);
try { fs.mkdirSync(path.dirname(logFile), { recursive: true }); } catch {}
- const out = fs.openSync(logFile, 'a');
- const child = spawn('node', [
- path.join(__dirname, 'scripts', 'generate_designs.js'),
- '--n', '1', '--kind', 'seamless_tile',
- '--category', (styles[0] || 'mixed').toLowerCase(),
- '--prompts', prompt,
- ], { cwd: __dirname, env: { ...process.env, GEN_BACKEND: process.env.GEN_BACKEND || 'replicate' },
- stdio: ['ignore', out, out], timeout: 180000 });
-
- child.on('exit', (code) => {
- try { fs.closeSync(out); } catch {}
- // Attribute from the LOG, not the global max id (concurrent-generator race).
- let createdIds = [];
- try {
- const logText = fs.readFileSync(logFile, 'utf8');
- const m = logText.match(/Created\s+\d+\/\d+\s+designs\s*·?\s*IDs?:\s*([0-9,\s]+)/i);
- if (m) createdIds = m[1].split(',').map(s => parseInt(s.trim(), 10)).filter(Number.isFinite);
- } catch {}
- const ok = code === 0 && createdIds.length > 0;
- const designId = createdIds[0] || null;
+ const category = (styles[0] || 'mixed').toLowerCase();
+ // Auto-retry (2026-05-31): SDXL+seamless-QA rejects some renders
+ // (SEAMLESS_FAIL_LR). Rather than fail the user's compose on the first
+ // unlucky tile, re-spawn the generator up to MAX_ATTEMPTS times — each
+ // generate_designs.js run already self-retries once internally, so this is
+ // a 2nd-order safety net. The single run_id spans all attempts; the
+ // frontend just keeps polling. Only the FINAL outcome is written.
+ const MAX_ATTEMPTS = 3;
+ const finalize = (ok, designId, msg) => {
if (ok && designId) {
try {
const safePrompt = prompt.replace(/'/g, "''");
@@ -21916,20 +21905,43 @@ app.post('/api/studio/compose', (req, res) => {
}
try {
psqlQuery(`UPDATE wallco_generator_runs SET
- status='${ok ? 'done' : 'error'}',
- design_id=${designId || 'NULL'},
- error=${ok ? 'NULL' : `'${(createdIds.length === 0
- ? `0/1 designs survived the quality gate (code ${code})`
- : `exited code ${code}`).replace(/'/g,"''")}'`},
+ status='${ok ? 'done' : 'error'}', design_id=${designId || 'NULL'},
+ error=${ok ? 'NULL' : `'${String(msg||'generation failed').replace(/'/g,"''")}'`},
finished_at=now() WHERE id=${runId};`);
} catch {}
- });
- child.on('error', (err) => {
- try { fs.closeSync(out); } catch {}
- try { psqlQuery(`UPDATE wallco_generator_runs SET status='error',
- error='${String(err.message||'spawn failed').replace(/'/g,"''")}',
- finished_at=now() WHERE id=${runId};`); } 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' },
+ stdio: ['ignore', out, out], timeout: 180000 });
+ child.on('exit', (code) => {
+ try { fs.closeSync(out); } catch {}
+ // Attribute from the LAST "Created N/N · IDs:" log line (this attempt's),
+ // not the global max id (concurrent-generator race).
+ let createdIds = [];
+ try {
+ const logText = fs.readFileSync(logFile, 'utf8');
+ 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 (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 {}
+ attempt(n + 1);
+ } else {
+ finalize(false, null, `0/1 designs survived the seamless quality gate after ${MAX_ATTEMPTS} attempts (last code ${code})`);
+ }
+ });
+ child.on('error', (err) => {
+ try { fs.closeSync(out); } catch {}
+ if (n < MAX_ATTEMPTS) { attempt(n + 1); }
+ else { finalize(false, null, String(err.message||'spawn failed')); }
+ });
+ };
+ attempt(1);
// Respond immediately — generation continues in the background; frontend polls.
res.json({ ok: true, run_id: parseInt(runId, 10), status: 'running', prompt });
@@ -22435,16 +22447,16 @@ document.getElementById('btn-compose').addEventListener('click', async function(
// in <1s — no more 60s nginx 504. Poll the status endpoint until done.
if (started && started.run_id && started.status === 'running') {
var startedAt = Date.now();
- while (Date.now() - startedAt < 240000) {
+ 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, 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">· seamless tiling + quality gate (auto-retries up to 3× for a clean tile), 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; }
} catch(_) {}
}
- if (!j || j.status === 'running') { j = { ok:false, error:'Still rendering after 4 min — it may appear in "Your saved renders" shortly.' }; }
+ if (!j || j.status === 'running') { j = { ok:false, error:'Still rendering after 7 min — it may appear in "Your saved renders" shortly.' }; }
}
btn.disabled = false; btn.textContent = 'Compose new design';
if (j.ok) {
← b793327 Publish luxe-v2 batch L5 image-reviewed (41906,41917,41920,4
·
back to Wallco Ai
·
Pattern generation always anchors on a REAL natural wallcove ebf027d →