← 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
M data/designs.jsonM output/designs/fungi-skyline-v1-nano.pngM output/designs/regatta-day-v2-sdxl.pngM public/index.htmlA scripts/regen_worker.pyM server.js
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 →