[object Object]

← 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 →