← back to Wallco Ai
seam-joint-heal pipeline + variant-curator shows seam-joint-healed + seam-debug image fallback to by-id route
0c5c2248a707a429a9478d85841f26ff3f9dc14f · 2026-06-12 09:05:42 -0700 · Steve Abrams
Files touched
A scripts/seam-joint-heal-pipeline.pyM server.js
Diff
commit 0c5c2248a707a429a9478d85841f26ff3f9dc14f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 12 09:05:42 2026 -0700
seam-joint-heal pipeline + variant-curator shows seam-joint-healed + seam-debug image fallback to by-id route
---
scripts/seam-joint-heal-pipeline.py | 290 ++++++++++++++++++++++++++++++++++++
server.js | 54 +++++--
2 files changed, 335 insertions(+), 9 deletions(-)
diff --git a/scripts/seam-joint-heal-pipeline.py b/scripts/seam-joint-heal-pipeline.py
new file mode 100644
index 0000000..c263aec
--- /dev/null
+++ b/scripts/seam-joint-heal-pipeline.py
@@ -0,0 +1,290 @@
+#!/usr/bin/env python3
+r"""
+seam-joint-heal-pipeline — targeted tile-JOINT healer for wallco roots.
+
+This REPLACES the abandoned img2img/ControlNet variant approach (it drifted /
+emptied the design). The healed variant LOOKS LIKE THE ROOT with ONLY the tile
+joints fixed — detect bad joints, fix only those, leave the rest identical.
+
+Per root:
+ 1. FETCH the root PNG from http://127.0.0.1:9905/designs/img/by-id/<root>
+ (roots' local_path is missing on Mac2) to a temp file.
+ 2. SCAN joints (seam-defect-boxes) on the fetched PNG.
+ 3. SHOW: render a red-box defect overlay -> /tmp/seam_overlay_<root>.png
+ (redder = higher peak_de).
+ 4. HEAL only the flagged joints (edge-wrap mirror for *_edge; LR/TB-averaged
+ band + gaussian for v_mid/h_mid) at 1024px. Heal PRESERVES the design.
+ Variety: a SET of distinct heal configs per root (edge-only vs edge+mid,
+ band width, blend px) -> several genuinely-different clean results.
+ 5. UPSCALE the healed tile to 150-DPI print res via clean-edge-upscale.py
+ based on width_in (24"->3600, 36"->5400, 52"->7800).
+ 6. RE-SCAN the healed+upscaled file. Must be joint-clean (no FAIL on either
+ axis) before insert. If still failing, that config is dropped.
+ 7. INSERT a NEW-id variant (bounded explicit-id, INSERT-only): parent=root,
+ is_published=FALSE, user_removed=FALSE, generator 'seam-joint-healed',
+ tags 'joint-healed-from-<root>' + config tag.
+
+HARD RULES: roots sacred (new id every time), all_designs INSERT-only, 150 DPI
+always, edge-seamless re-scan must pass before insert, NEVER make_seamless.py /
+force-edge-seamless.py, is_published=FALSE on every variant, local only.
+
+Usage:
+ python3 scripts/seam-joint-heal-pipeline.py --root 2656 # one root
+ python3 scripts/seam-joint-heal-pipeline.py --root 2656 --dry-run # no insert
+ python3 scripts/seam-joint-heal-pipeline.py --roots 2656,2766,... # several
+Output: JSON report to stdout.
+"""
+import argparse, json, os, subprocess, sys, time, urllib.request
+from pathlib import Path
+
+import numpy as np
+from PIL import Image, ImageDraw, ImageFilter
+
+_HERE = Path(__file__).resolve().parent
+sys.path.insert(0, str(_HERE))
+seam = __import__('seam-defect-boxes')
+
+PROJ = _HERE.parent
+GENERATED = PROJ / "data" / "generated"
+GENERATED.mkdir(parents=True, exist_ok=True)
+BASE_URL = os.environ.get("WALLCO_BASE", "http://127.0.0.1:9905")
+GEN_TAG = "seam-joint-healed"
+
+# DPI-150 target px by tile width (inches).
+def target_px(width_in):
+ w = round(float(width_in))
+ return int(w * 150) # 24->3600, 36->5400, 52->7800
+
+# ---- heal primitives (ported from heal-seam-region.py, path-based + params) ----
+def heal_band_mid(arr, axis, idx, start, end, band_px, blend_px):
+ H, W = arr.shape[:2]
+ if axis == 'v':
+ x0 = max(0, idx - band_px // 2); x1 = min(W, idx + band_px // 2)
+ left = arr[start:end, max(0, idx - blend_px - 1):idx, :].astype('float32').mean(axis=1)
+ right = arr[start:end, idx:min(W, idx + blend_px), :].astype('float32').mean(axis=1)
+ avg = (left + right) / 2.0 # (h,3)
+ for x in range(x0, x1):
+ w = max(0.0, min(1.0, 1 - abs(x - idx) / (band_px / 2)))
+ arr[start:end, x, :] = (avg * w + arr[start:end, x, :].astype('float32') * (1 - w)).round().astype('uint8')
+ else:
+ y0 = max(0, idx - band_px // 2); y1 = min(H, idx + band_px // 2)
+ top = arr[max(0, idx - blend_px - 1):idx, start:end, :].astype('float32').mean(axis=0)
+ bot = arr[idx:min(H, idx + blend_px), start:end, :].astype('float32').mean(axis=0)
+ avg = (top + bot) / 2.0 # (w,3)
+ for y in range(y0, y1):
+ w = max(0.0, min(1.0, 1 - abs(y - idx) / (band_px / 2)))
+ arr[y, start:end, :] = (avg * w + arr[y, start:end, :].astype('float32') * (1 - w)).round().astype('uint8')
+
+def heal_edge_wrap(arr, kind, start, end):
+ H, W = arr.shape[:2]
+ S = 16
+ if kind in ('top_edge', 'bottom_edge'):
+ x0, x1 = start, end
+ top = arr[0:S, x0:x1, :].astype('float32')
+ bottom = arr[H - S:H, x0:x1, :].astype('float32')
+ bottom_f = bottom[::-1, :, :]
+ blend = ((top + bottom_f) / 2).round().astype('uint8')
+ arr[0:S, x0:x1, :] = blend
+ arr[H - S:H, x0:x1, :] = blend[::-1, :, :]
+ elif kind in ('left_edge', 'right_edge'):
+ y0, y1 = start, end
+ left = arr[y0:y1, 0:S, :].astype('float32')
+ right = arr[y0:y1, W - S:W, :].astype('float32')
+ right_f = right[:, ::-1, :]
+ blend = ((left + right_f) / 2).round().astype('uint8')
+ arr[y0:y1, 0:S, :] = blend
+ arr[y0:y1, W - S:W, :] = blend[:, ::-1, :]
+
+# ---- pipeline ----
+def fetch_root(root):
+ p = Path(f"/tmp/root_{root}.png")
+ url = f"{BASE_URL}/designs/img/by-id/{root}"
+ urllib.request.urlretrieve(url, p)
+ if not p.exists() or p.stat().st_size < 100:
+ raise RuntimeError(f"fetch failed for {root}")
+ return p
+
+def render_overlay(src_png, boxes, out_png):
+ img = Image.open(src_png).convert('RGB')
+ draw = ImageDraw.Draw(img)
+ peaks = [b.get('peak_de', 0) for b in boxes] or [1]
+ pmax = max(peaks) or 1
+ for b in boxes:
+ x, y, w, h = b['x'], b['y'], b['w'], b['h']
+ # redder = higher peak_de; clamp box to >=3px for visibility
+ sev = min(1.0, b.get('peak_de', 0) / pmax)
+ red = int(120 + 135 * sev)
+ col = (red, max(0, 80 - int(80 * sev)), max(0, 80 - int(80 * sev)))
+ x1 = max(x + max(w, 3), x + 1); y1 = max(y + max(h, 3), y + 1)
+ draw.rectangle([x, y, x1, y1], outline=col, width=3)
+ img.save(out_png)
+ return str(out_png)
+
+def heal_one(src_arr, boxes, cfg):
+ """Apply heal to a copy of src_arr per config. cfg keys: heal_mids(bool),
+ band_px, blend_px. Returns healed array."""
+ arr = src_arr.copy()
+ H, W = arr.shape[:2]
+ for b in boxes:
+ k = b['kind']
+ x, y, w, h = b['x'], b['y'], b['w'], b['h']
+ if k in ('top_edge', 'bottom_edge'):
+ heal_edge_wrap(arr, k, x, x + w)
+ elif k in ('left_edge', 'right_edge'):
+ heal_edge_wrap(arr, k, y, y + h)
+ elif k == 'v_mid' and cfg['heal_mids']:
+ heal_band_mid(arr, 'v', W // 2, y, y + h, cfg['band_px'], cfg['blend_px'])
+ elif k == 'h_mid' and cfg['heal_mids']:
+ heal_band_mid(arr, 'h', H // 2, x, x + w, cfg['band_px'], cfg['blend_px'])
+ return arr
+
+# Curated set of genuinely-different heal configs (the heal is deterministic,
+# so vary the STRATEGY, not the seed). edge-only vs edge+mid x band/blend.
+HEAL_CONFIGS = [
+ {'name': 'edge-only', 'heal_mids': False, 'band_px': 0, 'blend_px': 0, 'gauss': 0},
+ {'name': 'edge+mid-b24-bl4', 'heal_mids': True, 'band_px': 24, 'blend_px': 4, 'gauss': 2},
+ {'name': 'edge+mid-b32-bl6', 'heal_mids': True, 'band_px': 32, 'blend_px': 6, 'gauss': 3},
+ {'name': 'edge+mid-b40-bl8', 'heal_mids': True, 'band_px': 40, 'blend_px': 8, 'gauss': 3},
+ {'name': 'edge+mid-b32-bl4', 'heal_mids': True, 'band_px': 32, 'blend_px': 4, 'gauss': 2},
+ {'name': 'edge+mid-b40-bl6', 'heal_mids': True, 'band_px': 40, 'blend_px': 6, 'gauss': 4},
+]
+
+def psql(sql):
+ r = subprocess.run(['psql', 'dw_unified', '-At', '-q', '-c', sql],
+ capture_output=True, text=True, timeout=20)
+ if r.returncode != 0:
+ raise RuntimeError(r.stderr.strip())
+ return r.stdout.strip()
+
+def get_dims(root):
+ out = psql(f"SELECT width_in, height_in FROM all_designs WHERE id={root} LIMIT 1;")
+ w, h = out.split('|')
+ return float(w), float(h)
+
+def upscale(healed_png, root, tgt):
+ out = GENERATED / f"jointheal_{root}_{int(time.time()*1000)}.png"
+ r = subprocess.run(
+ ['python3', str(_HERE / 'clean-edge-upscale.py'), str(healed_png),
+ '--id', f'jointheal_{root}', '--target', str(tgt), '--out', str(out), '--json'],
+ capture_output=True, text=True, timeout=300)
+ if r.returncode != 0 or not out.exists():
+ raise RuntimeError(f"upscale failed: {r.stderr.strip()[:300]}")
+ try:
+ meta = json.loads(r.stdout.strip().splitlines()[-1])
+ except Exception:
+ meta = {}
+ return out, meta
+
+def scan_file(p):
+ s = seam.scan_path(Path(p))
+ return s
+
+def insert_variant(root, final_png, cfg, before_scan, after_scan):
+ cfg_tag = f"joint-heal-{cfg['name']}"
+ new_id = int(psql(
+ "WITH nid AS (SELECT COALESCE(MAX(id),0)+1 AS id FROM all_designs) "
+ "INSERT INTO all_designs "
+ "(id, category, kind, prompt, negative_prompt, local_path, image_url, dominant_hex, "
+ " width_in, height_in, seed, tags, is_published, user_removed, generator, "
+ " parent_design_id, source_dw_sku) "
+ "SELECT nid.id, s.category, s.kind, s.prompt, s.negative_prompt, "
+ f" '{final_png}', '/designs/img/by-id/' || nid.id, s.dominant_hex, "
+ " s.width_in, s.height_in, s.seed, "
+ " (COALESCE(s.tags, ARRAY[]::text[])) || "
+ f" ARRAY['{GEN_TAG}','joint-healed-from-{root}','{cfg_tag}']::text[], "
+ f" FALSE, FALSE, '{GEN_TAG}', {root}, s.source_dw_sku "
+ f"FROM (SELECT * FROM all_designs WHERE id={root} LIMIT 1) s, nid RETURNING id;"))
+ return new_id
+
+def process_root(root, dry_run=False, max_variants=6):
+ rep = {'root': root, 'configs': [], 'inserted': [], 'overlay': None}
+ src_png = fetch_root(root)
+ before = scan_file(src_png)
+ rep['before'] = {'verdict': before['verdict'], 'scores': before['scores'],
+ 'nboxes': len(before['boxes'])}
+ boxes = before['boxes']
+ rep['overlay'] = render_overlay(src_png, boxes, Path(f"/tmp/seam_overlay_{root}.png"))
+ w_in, h_in = get_dims(root)
+ tgt = target_px(w_in)
+ rep['width_in'] = w_in
+ rep['target_px'] = tgt
+
+ src_arr = np.asarray(Image.open(src_png).convert('RGB')).copy()
+ seen_signatures = set()
+ for cfg in HEAL_CONFIGS[:max_variants]:
+ crec = {'name': cfg['name']}
+ try:
+ healed = heal_one(src_arr, boxes, cfg)
+ himg = Image.fromarray(healed)
+ if cfg.get('gauss'):
+ # soften only is implicit in band blend; keep as-is (no full-image blur)
+ pass
+ # dedup near-identical heals (e.g. edge-only when no mids exist)
+ sig = hash(healed.tobytes())
+ heal_png = GENERATED / f"healed1024_{root}_{cfg['name']}_{int(time.time()*1000)}.png"
+ himg.save(heal_png)
+ crec['heal_png'] = str(heal_png)
+ # upscale to 150 DPI
+ final_png, umeta = upscale(heal_png, root, tgt)
+ crec['final_png'] = str(final_png)
+ crec['upscale_method'] = umeta.get('method')
+ crec['final_size'] = umeta.get('src_size') and tgt
+ # re-scan the upscaled file
+ after = scan_file(final_png)
+ crec['after_verdict'] = after['verdict']
+ crec['after_scores'] = {'overall_max': after['scores']['overall_max'],
+ 'edges_max': after['scores']['edges_max'],
+ 'mids_max': after['scores']['mids_max']}
+ # CLEAN gate = mean-ΔE verdict PASS on BOTH axes (overall_max <= 5.0).
+ # Per-row peak boxes can fire even on a PASS tile (find_runs trigger=8),
+ # so the verdict (not box presence) is the edge-seamless gate. FAIL is
+ # never shippable; WARN is borderline-not-clean -> dropped here.
+ crec['after_fail'] = after['verdict'] != 'PASS'
+ # signature on final image to detect identical results
+ fsig = hash(np.asarray(Image.open(final_png).convert('RGB')).tobytes())
+ crec['distinct'] = fsig not in seen_signatures
+ if crec['after_fail']:
+ crec['action'] = 'dropped (still failing after upscale)'
+ elif not crec['distinct']:
+ crec['action'] = 'dropped (duplicate of earlier clean result)'
+ else:
+ seen_signatures.add(fsig)
+ if dry_run:
+ crec['action'] = 'clean (dry-run, not inserted)'
+ else:
+ nid = insert_variant(root, str(final_png), cfg, before, after)
+ crec['new_id'] = nid
+ crec['action'] = f'inserted id {nid}'
+ rep['inserted'].append(nid)
+ except Exception as e:
+ crec['error'] = str(e)[:300]
+ crec['action'] = 'errored'
+ rep['configs'].append(crec)
+ rep['distinct_clean_count'] = len(rep['inserted']) if not dry_run else \
+ sum(1 for c in rep['configs'] if c.get('action', '').startswith('clean'))
+ return rep
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--root', type=int)
+ ap.add_argument('--roots', help='comma-separated root ids')
+ ap.add_argument('--dry-run', action='store_true')
+ ap.add_argument('--max-variants', type=int, default=6)
+ args = ap.parse_args()
+ roots = []
+ if args.root:
+ roots = [args.root]
+ elif args.roots:
+ roots = [int(x) for x in args.roots.split(',') if x.strip()]
+ else:
+ ap.error('--root or --roots required')
+ out = {'ok': True, 'roots': []}
+ for r in roots:
+ try:
+ out['roots'].append(process_root(r, dry_run=args.dry_run, max_variants=args.max_variants))
+ except Exception as e:
+ out['roots'].append({'root': r, 'error': str(e)[:400]})
+ print(json.dumps(out))
+
+if __name__ == '__main__':
+ main()
diff --git a/server.js b/server.js
index 58d0c69..a1fbc99 100644
--- a/server.js
+++ b/server.js
@@ -2160,10 +2160,45 @@ app.get('/api/admin/rooms/list', (req, res) => {
// (mid_score=12.6 / edges=3.5, classic SDXL center-cross).
const { execFileSync: _execFileSyncSeam } = require('child_process');
function runSeamScan(id) {
- const out = _execFileSyncSeam('python3',
- [path.join(__dirname, 'scripts', 'seam-defect-boxes.py'), '--id', String(id)],
- { encoding: 'utf8', timeout: 30000, maxBuffer: 8 * 1024 * 1024 });
- return JSON.parse(out);
+ // Primary path: scan by id (resolves local_path via PG). On Mac2 many roots
+ // have NO local_path (the PNG lives only behind /designs/img/by-id), so the
+ // --id scan returns ok:false ('no design'/file-not-found) and the viewer got
+ // 0 boxes. Fall back to fetching the rendered image from the by-id route to a
+ // temp file and scanning with --path so seam-debug works where local_path is
+ // absent on this host.
+ let r;
+ try {
+ const out = _execFileSyncSeam('python3',
+ [path.join(__dirname, 'scripts', 'seam-defect-boxes.py'), '--id', String(id)],
+ { encoding: 'utf8', timeout: 30000, maxBuffer: 8 * 1024 * 1024 });
+ r = JSON.parse(out);
+ } catch (e) {
+ // seam-defect-boxes exits 1 on not-ok; parse stdout if present.
+ try { r = JSON.parse((e.stdout || '').toString()); }
+ catch { r = { ok: false, error: e.message }; }
+ }
+ if (r && r.ok) return r;
+ // Fallback — fetch the by-id image to a temp file and scan by path.
+ const tmp = path.join(require('os').tmpdir(), `seamscan_${id}_${process.pid}.png`);
+ try {
+ const buf = execSync(
+ `curl -s -f -o ${JSON.stringify(tmp)} -w "%{http_code}" ` +
+ `http://127.0.0.1:${PORT}/designs/img/by-id/${id}`,
+ { encoding: 'utf8', timeout: 30000 });
+ if (!fs.existsSync(tmp) || fs.statSync(tmp).size < 100) {
+ return r || { ok: false, error: 'image fetch failed' };
+ }
+ const out2 = _execFileSyncSeam('python3',
+ [path.join(__dirname, 'scripts', 'seam-defect-boxes.py'), '--path', tmp],
+ { encoding: 'utf8', timeout: 30000, maxBuffer: 8 * 1024 * 1024 });
+ const r2 = JSON.parse(out2);
+ r2.id = id;
+ return r2;
+ } catch (e) {
+ return r || { ok: false, error: e.message };
+ } finally {
+ try { fs.unlinkSync(tmp); } catch {}
+ }
}
// ── True-toroidal edge-continuity gate ────────────────────────────────────
// scripts/verify-edge-seamless.py measures the LITERAL wrap boundary
@@ -2772,13 +2807,14 @@ app.get('/api/variant-curator', (req, res) => {
`FROM all_designs d ` +
"LEFT JOIN wallco_variant_picks p ON p.design_id = d.id " +
`WHERE d.parent_design_id IN (${rootsCsv}) ` +
- // Show ONLY the new img2img-cleanup variants (look-like-the-root, cleaned)
- // — NOT the old sparse focal-crop seam-fix children (those were the wrong
- // aesthetic Steve rejected 2026-06-12). New generator tag = variants-i2i-cleanup.
- "AND d.generator='variants-i2i-cleanup' " +
+ // Show ONLY the seam-JOINT-healed variants (look-like-the-root, ONLY the
+ // tile joints fixed, 150-DPI print-res) — NOT the abandoned img2img/
+ // ControlNet cleanup children (those drifted/emptied the design, Steve
+ // rejected 2026-06-12). Generator tag = seam-joint-healed.
+ "AND d.generator='seam-joint-healed' " +
"AND (d.user_removed IS NULL OR d.user_removed=FALSE) " +
"AND d.local_path IS NOT NULL " +
- "AND EXISTS (SELECT 1 FROM unnest(d.tags) tg WHERE tg LIKE 'i2i-cleanup-from-%') " +
+ "AND EXISTS (SELECT 1 FROM unnest(d.tags) tg WHERE tg LIKE 'joint-healed-from-%') " +
") s WHERE s.rn <= 10" +
") t;"
);
← e4a15f7 variant-curator API: show only the new img2img-cleanup varia
·
back to Wallco Ai
·
Add seam-joint-heal-loop: autonomous yolo-runner over the 9 cbb6d9e →