[object Object]

← back to Wallco Ai

fix(regenerate-reverse): fall back to stored prompt when source PNG missing

06ad933726bb2d0142b64238cf2a9744846e0171 · 2026-05-24 01:00:06 -0700 · Steve Abrams

The orphan-PNG sweep (commit 323e1fa) cleared ~33k pre-seamless artifacts
from data/generated/, but it also pruned final designs that the catalog
still references via local_path. As a result, clicking "↻ Reverse-Engineer
+ Regenerate Whole" in /admin/ghost-review on those designs returns
"source PNG missing on disk" and dead-ends the repair flow.

The regenerate-WHOLE flow doesn't strictly need the source — it exists to
start fresh. Step 1 was a Gemini-Vision pass to reverse-engineer a clean
prompt from the source PNG. Without the PNG, the next-best inspiration is
the stored generation prompt in spoon_all_designs.prompt — already known to
have produced this design once. Append the anti-defect directive and skip
straight to Step 2 (image gen).

Behavior:
  - source exists → unchanged vision-reverse-engineered path
  - source missing + d.prompt available → stored-prompt-fallback path
  - source missing + no stored prompt → 404 with detail (genuinely unrecoverable)

Response now includes prompt_origin: 'vision-reverse-engineered' | 'stored-prompt-fallback'
so the UI can show which path ran.

Smart Fix (/api/design/:id/smart-fix) and Crop Fix
(/api/design/:id/crop-fix) still require the source PNG — those are
image-editing operations on the actual pixels, not regen-from-prompt.
Their 404 message stays. Recommended UI hint when source is missing: nudge
admin toward Reverse-Engineer + Regenerate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 06ad933726bb2d0142b64238cf2a9744846e0171
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun May 24 01:00:06 2026 -0700

    fix(regenerate-reverse): fall back to stored prompt when source PNG missing
    
    The orphan-PNG sweep (commit 323e1fa) cleared ~33k pre-seamless artifacts
    from data/generated/, but it also pruned final designs that the catalog
    still references via local_path. As a result, clicking "↻ Reverse-Engineer
    + Regenerate Whole" in /admin/ghost-review on those designs returns
    "source PNG missing on disk" and dead-ends the repair flow.
    
    The regenerate-WHOLE flow doesn't strictly need the source — it exists to
    start fresh. Step 1 was a Gemini-Vision pass to reverse-engineer a clean
    prompt from the source PNG. Without the PNG, the next-best inspiration is
    the stored generation prompt in spoon_all_designs.prompt — already known to
    have produced this design once. Append the anti-defect directive and skip
    straight to Step 2 (image gen).
    
    Behavior:
      - source exists → unchanged vision-reverse-engineered path
      - source missing + d.prompt available → stored-prompt-fallback path
      - source missing + no stored prompt → 404 with detail (genuinely unrecoverable)
    
    Response now includes prompt_origin: 'vision-reverse-engineered' | 'stored-prompt-fallback'
    so the UI can show which path ran.
    
    Smart Fix (/api/design/:id/smart-fix) and Crop Fix
    (/api/design/:id/crop-fix) still require the source PNG — those are
    image-editing operations on the actual pixels, not regen-from-prompt.
    Their 404 message stays. Recommended UI hint when source is missing: nudge
    admin toward Reverse-Engineer + Regenerate.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 server.js | 96 ++++++++++++++++++++++++++++++++++++++-------------------------
 1 file changed, 58 insertions(+), 38 deletions(-)

diff --git a/server.js b/server.js
index a5315ae..2f558f6 100644
--- a/server.js
+++ b/server.js
@@ -7146,44 +7146,64 @@ app.post('/api/design/:id/regenerate-reverse', express.json({ limit: '4kb' }), a
       if (cached) d = { ...cached };
     }
     if (!d) return res.status(404).json({ error: 'design not found' });
-    if (!d.local_path || !fs.existsSync(d.local_path)) {
-      return res.status(404).json({ error: 'source PNG missing on disk' });
-    }
 
-    const srcB64 = fs.readFileSync(d.local_path).toString('base64');
-
-    // Step 1: Vision describes the design as a clean prompt
-    const describePrompt = (
-      `You are reverse-engineering a wallpaper-design generation prompt from this image.\n` +
-      `Write a 2-3 sentence prompt that another AI could use to generate a CLEAN version of this exact design.\n\n` +
-      `Capture: the motifs (what's in the design), their colors, the layout/composition, the overall style/era.\n\n` +
-      `CRITICAL: do NOT reproduce any visible defects. Specifically:\n` +
-      `  - NO ghost layers (no faded duplicates of the same motif at lower opacity)\n` +
-      `  - NO translucent overlays of the whole scene\n` +
-      `  - NO unwanted seam-breaks at the tile edges\n` +
-      `  - NO half-tones, NO atmospheric haze on flat designs\n` +
-      `Describe the INTENDED design — what the artist would have wanted — then add the directive:\n` +
-      `"Render as a flat single-layer screenprint with hard razor-sharp silhouette edges. Tileable repeat. No translucency, no duplicates, no fading, no atmospheric perspective."\n\n` +
-      `Reply with ONLY the prompt text. No preamble, no markdown.`
-    );
     const t0 = Date.now();
-    let describeJ;
-    try {
-      describeJ = await geminiCall({
-        model: 'gemini-2.0-flash',
-        parts: [
-          { inline_data: { mime_type: 'image/png', data: srcB64 } },
-          { text: describePrompt },
-        ],
-        note: 'regenerate-reverse-describe-' + id,
-      });
-    } catch (e) {
-      if (e.code === 'NO_KEY') return res.status(500).json({ error: 'GEMINI_API_KEY not set' });
-      return res.status(502).json({ error: 'vision describe failed: ' + e.message });
-    }
-    const newPrompt = (geminiText(describeJ) || '').trim();
-    if (!newPrompt || newPrompt.length < 30) {
-      return res.status(502).json({ error: 'gemini vision returned empty/short prompt: ' + newPrompt.slice(0, 200) });
+    const sourceMissing = !d.local_path || !fs.existsSync(d.local_path);
+    let newPrompt;
+    let promptOrigin;
+
+    if (sourceMissing) {
+      // Fallback path: source PNG was swept (orphan-PNG cleanup) — use the
+      // stored generation prompt directly. The point of regenerate-WHOLE is
+      // to start fresh, so the source PNG is inspiration, not a requirement.
+      // If there's no stored prompt either, we genuinely can't regenerate.
+      const storedPrompt = String(d.prompt || '').trim();
+      if (!storedPrompt || storedPrompt.length < 20) {
+        return res.status(404).json({
+          error: 'source PNG missing on disk AND no stored prompt to regenerate from',
+          local_path: d.local_path || null,
+          stored_prompt_len: storedPrompt.length,
+        });
+      }
+      newPrompt = storedPrompt +
+        '\n\nRender as a flat single-layer screenprint with hard razor-sharp silhouette edges. ' +
+        'Tileable repeat. No translucency, no duplicates of motifs, no fading, no atmospheric perspective.';
+      promptOrigin = 'stored-prompt-fallback';
+    } else {
+      const srcB64 = fs.readFileSync(d.local_path).toString('base64');
+      // Step 1: Vision describes the design as a clean prompt
+      const describePrompt = (
+        `You are reverse-engineering a wallpaper-design generation prompt from this image.\n` +
+        `Write a 2-3 sentence prompt that another AI could use to generate a CLEAN version of this exact design.\n\n` +
+        `Capture: the motifs (what's in the design), their colors, the layout/composition, the overall style/era.\n\n` +
+        `CRITICAL: do NOT reproduce any visible defects. Specifically:\n` +
+        `  - NO ghost layers (no faded duplicates of the same motif at lower opacity)\n` +
+        `  - NO translucent overlays of the whole scene\n` +
+        `  - NO unwanted seam-breaks at the tile edges\n` +
+        `  - NO half-tones, NO atmospheric haze on flat designs\n` +
+        `Describe the INTENDED design — what the artist would have wanted — then add the directive:\n` +
+        `"Render as a flat single-layer screenprint with hard razor-sharp silhouette edges. Tileable repeat. No translucency, no duplicates, no fading, no atmospheric perspective."\n\n` +
+        `Reply with ONLY the prompt text. No preamble, no markdown.`
+      );
+      let describeJ;
+      try {
+        describeJ = await geminiCall({
+          model: 'gemini-2.0-flash',
+          parts: [
+            { inline_data: { mime_type: 'image/png', data: srcB64 } },
+            { text: describePrompt },
+          ],
+          note: 'regenerate-reverse-describe-' + id,
+        });
+      } catch (e) {
+        if (e.code === 'NO_KEY') return res.status(500).json({ error: 'GEMINI_API_KEY not set' });
+        return res.status(502).json({ error: 'vision describe failed: ' + e.message });
+      }
+      newPrompt = (geminiText(describeJ) || '').trim();
+      if (!newPrompt || newPrompt.length < 30) {
+        return res.status(502).json({ error: 'gemini vision returned empty/short prompt: ' + newPrompt.slice(0, 200) });
+      }
+      promptOrigin = 'vision-reverse-engineered';
     }
 
     // Step 2: Generate fresh image from the reverse-engineered prompt
@@ -7236,8 +7256,8 @@ RETURNING id`);
     psqlExecLocal(`UPDATE spoon_all_designs SET image_url='/designs/img/by-id/${newId}', is_published=TRUE WHERE id=${newId}`);
     psqlExecLocal(`UPDATE spoon_all_designs SET is_published=FALSE WHERE id=${id}`);
     const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
-    appendFixEvent({ kind: 'regenerate-reverse', src_id: id, new_id: newId, elapsed_s: elapsed, new_prompt: newPrompt.slice(0, 400) });
-    return res.json({ ok: true, new_id: newId, new_prompt: newPrompt, elapsed_s: elapsed, src_id: id });
+    appendFixEvent({ kind: 'regenerate-reverse', src_id: id, new_id: newId, elapsed_s: elapsed, prompt_origin: promptOrigin, new_prompt: newPrompt.slice(0, 400) });
+    return res.json({ ok: true, new_id: newId, new_prompt: newPrompt, prompt_origin: promptOrigin, elapsed_s: elapsed, src_id: id });
   } catch (e) {
     console.error('[regenerate-reverse]', e);
     return res.status(500).json({ error: e.message });

← b6c85f3 feat(ghost-review): bulk-select with marquee drag + Delete &  ·  back to Wallco Ai  ·  add dedupe-flagged-jsonl.js + watch-and-dedupe.sh a7d1745 →