[object Object]

← back to Wallco Ai

edge_seam_heal: combined mids+edges offset-heal for the edges_only cohort (6/6 PASS sample, scale awaits sign-off)

9ca0509d28fbe1608ab9ba114936082e564777f6 · 2026-05-26 10:02:59 -0700 · Steve Abrams

Files touched

Diff

commit 9ca0509d28fbe1608ab9ba114936082e564777f6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 26 10:02:59 2026 -0700

    edge_seam_heal: combined mids+edges offset-heal for the edges_only cohort (6/6 PASS sample, scale awaits sign-off)
---
 TODO.md                             |   4 +-
 scripts/edge_seam_heal.py           | 316 ++++++++++++++++++++++++++++++++++++
 scripts/refresh_designs_snapshot.py |   5 +-
 3 files changed, 322 insertions(+), 3 deletions(-)

diff --git a/TODO.md b/TODO.md
index afefac8..cdf588b 100644
--- a/TODO.md
+++ b/TODO.md
@@ -7,8 +7,8 @@ up unchecked rows automatically.
 ## Open from 2026-05-26 session
 
 - [ ] Decide which of the 36 fresh fliepaper-bugs Gemini remakes to publish (16 already failed edges-scan, 19 WARN, 1 PASS — all currently `is_published=FALSE`, awaiting per-id review at /design/<id>)
-- [ ] Address the 1,058 edge-wrap defect cohort (top/bottom/left/right ΔE > 12 from the edges sweep — mid_seam_heal doesn't help, needs a different shift-and-blend fix at the tile edges)
-- [ ] Address the 2,883 catastrophic-defect cohort (both edges AND mids broken — mid_seam_heal fixes the mids portion only; survivors will still flag edges-fail on rescan)
+- [ ] **APPROVE: scale the edge-heal to the 630 non-ghost edges_only cohort.** Tool built + validated: `scripts/edge_seam_heal.py` (combined mids+edges feather — np.roll offset-heal for the wrap seam PLUS the mid_seam_heal center-band blur, since the "edges_only" rows actually carry WARN-range mids). 6-sample across 4 generator families → **6/6 PASS** (edges 13-22 → 1.3-3.2, mids → 1.7-2.5). Validated rows in PG: 50603-50608 (is_published=FALSE per strict gate). Scale run `python3 scripts/edge_seam_heal.py --all --workers 8` was auto-mode-gated (630-row prod-DB mass insert needs Steve sign-off). All stay is_published=FALSE; promotion + deploy is a separate decision. snapshot allowlist already includes 'pil-edge-heal'. (cohort is 1,064 total; 428 carry BLEED_GHOST and are excluded — a ghost feather can't fix opacity defects.)
+- [ ] Address the 2,883 catastrophic-defect cohort (both edges AND mids broken). The combined edge-heal tool's `--grid both` path covers the edge component; the center-band blur covers the mids component — so the same `edge_seam_heal.py --all --grid both` should land many at PASS. Needs its own sample-gate validation first (the 'both' cohort is more degraded than edges_only) + ghost-exclusion (same BLEED_GHOST carve-out).
 - [x] WARN cohort (11,947) classified — DO NOT bulk-heal. 67% mids-only / 30% both / 2% edges-only. But the cohort is already fully unpublished (0/200 still published — bleed_guard culled it since the AM sweep) and ≥24% carry a BLEED_GHOST verdict the mid-seam-blend can't fix. Worse: the pil-* bleed_guard exemption (68b78757) means a heal of a ghost-defective WARN would skip ghost-recheck and could auto-publish the defect. Recovery (if ever) = ghost-removal FIRST (opacity-snap / Gemini img-to-img), then edges-heal, then re-gate — a real project, not a $0 sweep. Leaving the cohort culled.
 - [x] Deploy heal results to Kamatera — DONE. Two ./deploy-kamatera.sh runs: first shipped code + 1.4 GB PNGs but designs.json was missing pil-mid-heal (refresh script allowlist gap, fixed in e189ef09); second shipped the corrected snapshot. Prod /health count 3,516 → 5,864 (+2,348 edges-PASS heals live). 4,981 WARN-tier heals stayed is_published=FALSE per the strict gate.
 - [ ] Wire the /admin/elements drawer "Use as reference → / Blend 2 → / Compose N →" CTA to actual Gemini img-to-img edit (currently a stub alert)
diff --git a/scripts/edge_seam_heal.py b/scripts/edge_seam_heal.py
new file mode 100644
index 0000000..7c728dd
--- /dev/null
+++ b/scripts/edge_seam_heal.py
@@ -0,0 +1,316 @@
+#!/usr/bin/env python3
+"""edge_seam_heal — production batch tool for the tile-edge wrap defect.
+
+Defect: pixel discontinuity at the WRAP boundary — row 0 vs row H-1 and/or
+col 0 vs col W-1. The 6-lens edges-agent flags this as `grid=edges_only`
+(midlines clean, edges fail). Usually a tile-prep regression: a center-crop
+or recolor that broke the seamless wrap a source had. mid_seam_heal does NOT
+help these — it blurs the interior midlines, which are already clean here.
+
+Fix (the classic offset-heal): np.roll the image by (H/2, W/2). This moves
+the wrap seam from the edges to the interior midlines, where we can blur a
+band exactly the way mid_seam_heal does. Then np.roll back by -(H/2, W/2).
+np.roll is exactly invertible, so every pixel OUTSIDE the blurred band is
+byte-identical to the source — composition fully preserved, only the wrap
+seam is feathered.
+
+Round-1-sacred pattern (per feedback_round_one_outputs_are_sacred):
+  - Source PNG NEVER modified or deleted
+  - Healed PNG written to data/generated/edgeheal_<src_id>_<ts>.png
+  - New all_designs row INSERTed:
+      generator='pil-edge-heal'
+      parent_design_id=<src_id>
+      is_published=FALSE  (mirrors mid_seam_heal's strict gate — auto-publish
+                          would bypass the edges-agent quality bar. Promotion
+                          requires an explicit edges-agent PASS.)
+      notes='edge_heal of #<src_id>: PIL roll+band-blur BAND=12 SIGMA=4.0'
+  - Source gets notes append: ' | EDGE_HEALED_BY: <new_id>'
+
+Ghost-defective rows are excluded from --all (a BLEED_GHOST verdict can't be
+fixed by an edge feather, so healing them just burns disk). They're left in
+the unpublished pool for the ghost-removal pipeline.
+
+Usage:
+  python3 edge_seam_heal.py --id 4135                # one design
+  python3 edge_seam_heal.py --id 4135 --id 10275     # explicit sample set
+  python3 edge_seam_heal.py --all                    # every edges_only FAIL (non-ghost)
+  python3 edge_seam_heal.py --all --grid both        # also the 'both' cohort
+  python3 edge_seam_heal.py --all --workers 8
+  python3 edge_seam_heal.py --limit 50               # cap for smoke test
+  python3 edge_seam_heal.py --dry-run                # plan only, no writes
+"""
+import argparse
+import multiprocessing as mp
+import subprocess
+import sys
+import tempfile
+import time
+from pathlib import Path
+
+WALLCO_ROOT = Path.home() / 'Projects' / 'wallco-ai'
+OUT_DIR = WALLCO_ROOT / 'data' / 'generated'
+BAND_PX = 12
+SIGMA = 4.0
+
+
+def fetch_targets(grid: str, limit: int | None, ids: list[int] | None):
+    """Return target rows for the edge-wrap heal.
+
+    grid=edges_only → notes LIKE '%EDGES_FAIL: %grid=edges_only%'
+    grid=both       → edges_only OR both (the edge component of catastrophic)
+    """
+    where = []
+    if ids:
+        where = [f"id IN ({','.join(str(int(i)) for i in ids)})"]
+    else:
+        where.append("(notes IS NULL OR notes NOT LIKE '%EDGE_HEALED_BY:%')")
+        where.append("(user_removed IS NULL OR user_removed = FALSE)")
+        where.append("local_path IS NOT NULL")
+        # A ghost feather can't be undone by an edge blur — skip them.
+        where.append("(notes NOT LIKE '%BLEED_GHOST%')")
+        where.append("(settlement_verdict IS NULL OR settlement_verdict NOT LIKE '%GHOST%')")
+        if grid == 'edges_only':
+            where.append("notes LIKE '%EDGES_FAIL:%grid=edges_only%'")
+        elif grid == 'both':
+            where.append("(notes LIKE '%EDGES_FAIL:%grid=edges_only%' "
+                         "OR notes LIKE '%EDGES_FAIL:%grid=both%')")
+        else:
+            raise ValueError(f'bad --grid {grid}')
+
+    sql = (
+        "SELECT id, local_path, dominant_hex, category, kind, "
+        "COALESCE(palette::text, 'null') AS palette_json, "
+        "width_in, height_in, panels "
+        "FROM all_designs WHERE " + ' AND '.join(where)
+        + " ORDER BY id"
+    )
+    if limit:
+        sql += f' LIMIT {int(limit)}'
+    sql += ';'
+
+    r = subprocess.run(
+        ['psql', 'dw_unified', '-At', '-F', chr(31), '-c', sql],
+        check=True, capture_output=True, text=True,
+    )
+    rows = []
+    for line in r.stdout.splitlines():
+        if not line.strip():
+            continue
+        parts = line.split(chr(31))
+        if len(parts) < 9:
+            continue
+        try:
+            rows.append({
+                'id': int(parts[0]),
+                'local_path': parts[1],
+                'dominant_hex': parts[2] if parts[2] != '' else None,
+                'category': parts[3] or 'mixed',
+                'kind': parts[4] or 'seamless_tile',
+                'palette_json': parts[5] if parts[5] not in ('', 'null') else 'null',
+                'width_in': parts[6] if parts[6] != '' else None,
+                'height_in': parts[7] if parts[7] != '' else None,
+                'panels': parts[8] if parts[8] != '' else None,
+            })
+        except ValueError:
+            continue
+    return rows
+
+
+def heal_image(src_path: Path, out_path: Path):
+    """Combined seam-heal — feathers BOTH the interior midlines and the wrap edges.
+
+    The edges_only cohort is mislabelled-clean on mids: in the 6-sample
+    validation the "clean" midlines actually scored 7.5-10.9 ΔE (WARN range),
+    so an edge-only feather lands the result at WARN, not PASS — and the strict
+    gate won't promote WARN. Healing both in one pass took all 6 samples to a
+    clean PASS (edges 1.3-3.2, mids 1.7-2.5). So this tool always does both:
+
+      1) interior-midline blur (the mid_seam_heal operation)
+      2) np.roll by (H/2, W/2) → wrap seam moves to the interior → blur → roll
+         back (np.roll is exactly invertible, so only the feathered bands differ
+         from the source; the rest is byte-identical — composition preserved).
+    """
+    from PIL import Image, ImageFilter
+    import numpy as np
+    img = Image.open(src_path).convert('RGB')
+    W, H = img.size
+    arr = np.asarray(img).copy()
+    hm, wm = H // 2, W // 2
+
+    # 1) interior midlines
+    blur = np.asarray(img.filter(ImageFilter.GaussianBlur(radius=SIGMA)))
+    arr[hm - BAND_PX:hm + BAND_PX, :, :] = blur[hm - BAND_PX:hm + BAND_PX, :, :]
+    arr[:, wm - BAND_PX:wm + BAND_PX, :] = blur[:, wm - BAND_PX:wm + BAND_PX, :]
+
+    # 2) wrap edges via the offset trick
+    rolled = np.roll(arr, (hm, wm), axis=(0, 1))
+    rblur = np.asarray(Image.fromarray(rolled).filter(ImageFilter.GaussianBlur(radius=SIGMA)))
+    rolled[hm - BAND_PX:hm + BAND_PX, :, :] = rblur[hm - BAND_PX:hm + BAND_PX, :, :]
+    rolled[:, wm - BAND_PX:wm + BAND_PX, :] = rblur[:, wm - BAND_PX:wm + BAND_PX, :]
+    healed = np.roll(rolled, (-hm, -wm), axis=(0, 1))
+
+    Image.fromarray(healed).save(out_path, 'PNG', optimize=True)
+    return out_path.stat().st_size
+
+
+def worker(row: dict):
+    src_id = row['id']
+    try:
+        src_path = Path(row['local_path'])
+        if not src_path.exists():
+            return {'id': src_id, 'ok': False, 'error': 'file_missing'}
+        ts = int(time.time() * 1000)
+        out_filename = f'edgeheal_{src_id}_{ts}.png'
+        out_path = OUT_DIR / out_filename
+        bytes_written = heal_image(src_path, out_path)
+        return {
+            'id': src_id, 'ok': True,
+            'out_path': str(out_path), 'out_filename': out_filename,
+            'bytes': bytes_written,
+            'category': row['category'], 'kind': row['kind'],
+            'dominant_hex': row['dominant_hex'], 'palette_json': row['palette_json'],
+            'width_in': row['width_in'], 'height_in': row['height_in'],
+            'panels': row['panels'],
+        }
+    except Exception as exc:
+        return {'id': src_id, 'ok': False, 'error': f'heal_exception: {exc}'}
+
+
+def sql_escape(s: str) -> str:
+    return s.replace("'", "''")
+
+
+def persist(results: list[dict]):
+    ok = [r for r in results if r.get('ok')]
+    if not ok:
+        return 0
+
+    sql_parts = ['BEGIN;']
+    sql_parts.append(
+        'CREATE TEMP TABLE _eheal_in (src_id BIGINT PRIMARY KEY, '
+        'kind TEXT, category TEXT, dominant_hex TEXT, palette JSONB, '
+        'width_in INTEGER, height_in INTEGER, panels INTEGER, '
+        'local_path TEXT, image_url TEXT, prompt TEXT, seed BIGINT, '
+        'notes TEXT) ON COMMIT DROP;'
+    )
+
+    seed_base = int(time.time())
+    rows_for_sql = []
+    for i, r in enumerate(ok):
+        seed = (seed_base + i) & 0x7fffffff
+        kind = sql_escape(r['kind'])
+        cat = sql_escape(r['category'])
+        dh = "NULL" if not r['dominant_hex'] else f"'{sql_escape(r['dominant_hex'])}'"
+        pal_json = r.get('palette_json') or 'null'
+        if pal_json in ('', 'None'):
+            pal_json = 'null'
+        pal = f"'{sql_escape(pal_json)}'::jsonb"
+        wi = r['width_in'] if r['width_in'] not in (None, '') else 'NULL'
+        hi = r['height_in'] if r['height_in'] not in (None, '') else 'NULL'
+        pn = r['panels'] if r['panels'] not in (None, '') else 'NULL'
+        local_path = sql_escape(r['out_path'])
+        img_url = sql_escape(f"/designs/img/{r['out_filename']}")
+        msg = sql_escape(f"edge_heal of #{r['id']}: PIL combined mids+edges band-blur BAND={BAND_PX} SIGMA={SIGMA}")
+        rows_for_sql.append(
+            f"({r['id']}, '{kind}', '{cat}', {dh}, {pal}, {wi}, {hi}, {pn}, "
+            f"'{local_path}', '{img_url}', '{msg}', {seed}, '{msg}')"
+        )
+
+    CHUNK = 200
+    for i in range(0, len(rows_for_sql), CHUNK):
+        batch = rows_for_sql[i:i + CHUNK]
+        sql_parts.append(
+            'INSERT INTO _eheal_in (src_id, kind, category, dominant_hex, palette, '
+            'width_in, height_in, panels, local_path, image_url, prompt, seed, notes) VALUES '
+            + ','.join(batch) + ';'
+        )
+
+    sql_parts.append("""
+WITH ins AS (
+  INSERT INTO all_designs
+    (kind, brand, width_in, height_in, panels, generator, prompt, seed,
+     image_url, local_path, dominant_hex, palette, category,
+     is_published, parent_design_id, notes)
+  SELECT
+    h.kind, 'wallco.ai', h.width_in, h.height_in, h.panels, 'pil-edge-heal',
+    h.prompt, h.seed, h.image_url, h.local_path, h.dominant_hex, h.palette,
+    h.category, FALSE, h.src_id, h.notes
+  FROM _eheal_in h
+  RETURNING id AS new_id, parent_design_id AS src_id
+)
+UPDATE all_designs ad
+SET notes = CASE
+  WHEN ad.notes IS NULL OR ad.notes = '' THEN ' | EDGE_HEALED_BY: ' || ins.new_id::text
+  ELSE ad.notes || ' | EDGE_HEALED_BY: ' || ins.new_id::text
+END
+FROM ins
+WHERE ad.id = ins.src_id;
+""")
+
+    sql_parts.append('SELECT count(*) FROM _eheal_in;')
+    sql_parts.append('COMMIT;')
+
+    full_sql = '\n'.join(sql_parts)
+    with tempfile.NamedTemporaryFile(mode='w', suffix='.sql', delete=False) as tf:
+        tf.write(full_sql)
+        sql_path = tf.name
+
+    r = subprocess.run(
+        ['psql', 'dw_unified', '-At', '-q', '-v', 'ON_ERROR_STOP=1', '-f', sql_path],
+        capture_output=True, text=True,
+    )
+    if r.returncode != 0:
+        print(f'PSQL FAILED:\n  stdout: {r.stdout[:500]}\n  stderr: {r.stderr[:500]}',
+              file=sys.stderr)
+        return 0
+    return len(ok)
+
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument('--id', type=int, action='append', help='Specific id (repeatable)')
+    ap.add_argument('--all', action='store_true', help='Every matching design')
+    ap.add_argument('--grid', choices=['edges_only', 'both'], default='edges_only')
+    ap.add_argument('--limit', type=int, default=None)
+    ap.add_argument('--workers', type=int, default=8)
+    ap.add_argument('--dry-run', action='store_true', help='List targets, no writes')
+    args = ap.parse_args()
+
+    if not (args.id or args.all or args.limit):
+        ap.error('Pass --id, --all, or --limit')
+
+    targets = fetch_targets(args.grid, args.limit, args.id)
+    print(f'targets: {len(targets)} ({args.grid} cohort)', flush=True)
+    if not targets:
+        print('nothing to do', flush=True)
+        return
+
+    if args.dry_run:
+        print('--dry-run — first 10 ids:', [t['id'] for t in targets[:10]])
+        return
+
+    OUT_DIR.mkdir(parents=True, exist_ok=True)
+    t0 = time.time()
+    results = []
+    counts = {'ok': 0, 'err': 0}
+
+    with mp.Pool(args.workers) as pool:
+        for i, res in enumerate(pool.imap_unordered(worker, targets, chunksize=4), 1):
+            results.append(res)
+            counts['ok' if res.get('ok') else 'err'] += 1
+            if i % 250 == 0 or i == len(targets):
+                rate = i / max(0.001, time.time() - t0)
+                eta = (len(targets) - i) / max(0.001, rate)
+                print(f'  [{i:>5}/{len(targets)}] ok={counts["ok"]} err={counts["err"]} '
+                      f'rate={rate:.1f}/s eta={eta:.0f}s', flush=True)
+
+    pg_written = persist(results)
+    elapsed = time.time() - t0
+    print(f'\nedge-heal done in {elapsed:.1f}s — '
+          f'baked={counts["ok"]} err={counts["err"]} pg_inserted={pg_written}', flush=True)
+    for r in [x for x in results if x.get('ok')][:8]:
+        print(f"  id={r['id']:>6} → {r['out_filename']}  {r['bytes']//1024}KB", flush=True)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/scripts/refresh_designs_snapshot.py b/scripts/refresh_designs_snapshot.py
index 20fc50b..a2c9421 100644
--- a/scripts/refresh_designs_snapshot.py
+++ b/scripts/refresh_designs_snapshot.py
@@ -84,7 +84,10 @@ rows = psql_json(
     # scripts/mid_seam_heal.py land in the snapshot. Without this they ship as
     # orphan PNGs (rsync delivers them, but designs.json doesn't reference them,
     # so the prod DESIGNS array can't render them).
-    "FROM spoon_all_designs WHERE generator IN ('replicate','gemini-2.5-flash-image-edit','comfy','pil-mid-heal') ORDER BY id) t;"
+    # 2026-05-26 — added 'pil-edge-heal' (scripts/edge_seam_heal.py) for the same
+    # reason: the tile-edge-wrap cohort heals. Rows are is_published=FALSE until an
+    # edges-agent PASS promotes them, so public /api/designs still filters them out.
+    "FROM spoon_all_designs WHERE generator IN ('replicate','gemini-2.5-flash-image-edit','comfy','pil-mid-heal','pil-edge-heal') ORDER BY id) t;"
 )
 
 import re as _re

← e0387ff geometric pilot: bulk-verify ghost-detector (stable 3-vote)  ·  back to Wallco Ai  ·  Revert seamless guard to TILE_CATEGORIES — mirror genComfy t 0b10f36 →