[object Object]

← back to Wild Wallcoverings

per-row Regenerate button: 3 fresh gated versions on demand, side-file merge, auto-refresh

a9e930b216494d5e80e2e7634825ae60ec2d1a80 · 2026-07-22 20:19:34 -0700 · Steve Abrams

Files touched

Diff

commit a9e930b216494d5e80e2e7634825ae60ec2d1a80
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 22 20:19:34 2026 -0700

    per-row Regenerate button: 3 fresh gated versions on demand, side-file merge, auto-refresh
---
 data/designs.json                        |   3 +-
 output/designs/fungi-skyline-v1-nano.png | Bin 164855 -> 163141 bytes
 output/designs/regatta-day-v2-sdxl.png   | Bin 82802 -> 98285 bytes
 public/index.html                        |  10 +++++
 scripts/regen_worker.py                  |  61 +++++++++++++++++++++++++++++++
 server.js                                |  29 ++++++++++++++-
 6 files changed, 101 insertions(+), 2 deletions(-)

diff --git a/data/designs.json b/data/designs.json
index ed73b21..23cf39c 100644
--- a/data/designs.json
+++ b/data/designs.json
@@ -2298,7 +2298,8 @@
      "acceptable": true
     },
     "tile": {
-     "verdict": "WARN"
+     "verdict": "EDGES-PASS",
+     "healed": true
     }
    },
    {
diff --git a/output/designs/fungi-skyline-v1-nano.png b/output/designs/fungi-skyline-v1-nano.png
index 0f51026..97987ec 100644
Binary files a/output/designs/fungi-skyline-v1-nano.png and b/output/designs/fungi-skyline-v1-nano.png differ
diff --git a/output/designs/regatta-day-v2-sdxl.png b/output/designs/regatta-day-v2-sdxl.png
index 9d5434c..5eb4b8f 100644
Binary files a/output/designs/regatta-day-v2-sdxl.png and b/output/designs/regatta-day-v2-sdxl.png differ
diff --git a/public/index.html b/public/index.html
index d18ddb2..a9899e1 100644
--- a/public/index.html
+++ b/public/index.html
@@ -155,6 +155,8 @@ function render() {
       `</div></div>
       <div class="rowactions">
         <button class="skip" onclick="choose('${d.id}','none')">Skip — none of these</button>
+        <button class="skip" style="border-color:#5a7;color:#8fc9a8" title="~$0.12 — 3 fresh versions, all rules applied"
+          onclick="regen('${d.id}', this)">🔄 Regenerate — give me 3 new versions</button>
         ${chosenFile === 'none' ? '<span class="status">skipped</span>' : ''}
       </div>`;
     board.appendChild(el);
@@ -198,12 +200,20 @@ async function go() {
   btn.disabled = false;
 }
 
+async function regen(id, btn) {
+  btn.disabled = true;
+  btn.textContent = '⏳ generating 3 fresh versions (~2 min)…';
+  await fetch('/api/regen', { method: 'POST', body: JSON.stringify({ id }) });
+  setTimeout(async () => { await load(); }, 150000);  // refresh when they land
+}
+
 async function load() {
   designs = await (await fetch('/api/designs')).json();
   designs.forEach(d => { if (pending[d.id]) d.choice = { variant: pending[d.id] }; });
   updateGo();
   render();
 }
+setInterval(load, 60000);  // fresh versions + healing badges appear automatically
 sortSel.onchange = render;
 density.oninput = applyDensity;
 applyDensity();
diff --git a/scripts/regen_worker.py b/scripts/regen_worker.py
new file mode 100644
index 0000000..a368c56
--- /dev/null
+++ b/scripts/regen_worker.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+"""Generate 3 fresh versions for one design (viewer 'Regenerate' button).
+
+Text-only pipeline (no reference image ever), palette+scale locked, palette-
+snapped. New variants land in data/regen-additions.json (merged by the server
+at read time) to avoid clobbering designs.json while other writers run.
+Cost ~$0.12 per run (3 nano images).
+
+Usage: GEMINI_API_KEY=... regen_worker.py <design_id>
+"""
+import json, os, subprocess, sys, time
+from datetime import datetime, timezone
+import importlib.util
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ADD = os.path.join(ROOT, "data", "regen-additions.json")
+spec = importlib.util.spec_from_file_location("bg", os.path.join(ROOT, "scripts", "batch_gen.py"))
+bg = importlib.util.module_from_spec(spec); spec.loader.exec_module(bg)
+
+VARIATIONS = [
+    "fresh take: different characters and poses, same style and scale",
+    "fresh take: different composition rhythm, same motif family and scale",
+    "fresh take: bolder maximalist arrangement, same palette and scale",
+]
+
+def main():
+    did = sys.argv[1]
+    briefs = {b["id"]: b for b in json.load(open(os.path.join(ROOT, "data", "briefs.json")))}
+    b = briefs[did]
+    # find next free version number across designs.json + additions + disk
+    outdir = os.path.join(ROOT, "output", "designs")
+    existing = [f for f in os.listdir(outdir) if f.startswith(did + "-v")]
+    maxv = max([int(f.split("-v")[1].split("-")[0]) for f in existing] or [0])
+    adds = {}
+    if os.path.exists(ADD):
+        adds = json.load(open(ADD))
+    new = []
+    for i, var in enumerate(VARIATIONS):
+        ver = maxv + 1 + i
+        fname = f"{did}-v{ver}-nano.png"
+        path = os.path.join(outdir, fname)
+        p = bg.full_prompt(b, "nano", var)
+        r = subprocess.run([sys.executable, os.path.join(ROOT, "scripts", "nano_gen.py"), p, path],
+                           capture_output=True, text=True, timeout=220)
+        if r.returncode != 0:
+            print(fname, "FAIL", r.stderr[-150:], flush=True)
+            continue
+        subprocess.run([sys.executable, os.path.join(ROOT, "scripts", "snap_palette.py"),
+                        path, did], capture_output=True)
+        new.append({"file": fname, "engine": "nano-banana",
+                    "created_at": datetime.now(timezone.utc).isoformat(), "fresh": True})
+        print(fname, "OK", flush=True)
+        time.sleep(1)
+    adds.setdefault(did, [])
+    seen = {v["file"] for v in adds[did]}
+    adds[did] += [v for v in new if v["file"] not in seen]
+    json.dump(adds, open(ADD, "w"), indent=1)
+    print(f"added {len(new)} fresh versions for {did}, est ${len(new)*0.039:.2f}")
+
+if __name__ == "__main__":
+    main()
diff --git a/server.js b/server.js
index eff506a..1f24250 100644
--- a/server.js
+++ b/server.js
@@ -28,11 +28,38 @@ const server = http.createServer((req, res) => {
   if (url.pathname === '/api/designs') {
     const designs = readJson(DESIGNS, []);
     const choices = readJson(CHOICES, {});
-    designs.forEach(d => { d.choice = choices[d.id] || null; });
+    const additions = readJson(path.join(ROOT, 'data', 'regen-additions.json'), {});
+    designs.forEach(d => {
+      d.choice = choices[d.id] || null;
+      const extra = additions[d.id] || [];
+      const seen = new Set(d.variants.map(v => v.file));
+      d.variants = d.variants.concat(extra.filter(v => !seen.has(v.file)));
+    });
     res.writeHead(200, { 'Content-Type': 'application/json' });
     return res.end(JSON.stringify(designs));
   }
 
+  if (url.pathname === '/api/regen' && req.method === 'POST') {
+    let body = '';
+    req.on('data', c => body += c);
+    req.on('end', () => {
+      try {
+        const { id } = JSON.parse(body);
+        const known = readJson(DESIGNS, []).some(d => d.id === id);
+        if (!known || !/^[a-z0-9-]+$/.test(id)) { res.writeHead(400); return res.end('bad id'); }
+        const { exec } = require('child_process');
+        exec(`KEY=$(grep '^GEMINI_API_KEY=' ~/Projects/secrets-manager/.env | cut -d= -f2); ` +
+             `GEMINI_API_KEY="$KEY" python3 ${path.join(ROOT, 'scripts', 'regen_worker.py')} ${id} ` +
+             `>> /tmp/wildwc-regen.log 2>&1 &`);
+        res.writeHead(200, { 'Content-Type': 'application/json' });
+        res.end(JSON.stringify({ ok: true, queued: id }));
+      } catch (e) {
+        res.writeHead(400); res.end(String(e));
+      }
+    });
+    return;
+  }
+
   if (url.pathname === '/api/choose-batch' && req.method === 'POST') {
     let body = '';
     req.on('data', c => body += c);

← a572e80 auto-save: 2026-07-22T20:16:30 (4 files) — data/designs.json  ·  back to Wild Wallcoverings  ·  seamless pass2 script: aggressive denoise for stubborn files 3b219e7 →