[object Object]

← back to Wallco Ai

scripts: seamlessify_all.py — batch make_seamless across data/generated/

4cad4c43bb711c9e023349d6a36f4a1f0a20fbd8 · 2026-05-12 15:12:44 -0700 · SteveStudio2

Retroactive post-processor. For each .png in data/generated/:
  1. cp -cR the whole dir to data/generated_pre_seamless_backup/ first
     (APFS clone — zero extra disk until COW kicks in on overwrite)
  2. Run the same offset+feathered-blend transform from
     scripts/make_seamless.py, atomic temp+rename write back
  3. Sample-measure edge-Δ before/after on ~5%% of files and report
     aggregate drop at end

Defaults: cpu_count()/2 workers, 5%% measurement rate. Flags:
  --dry         scan + report Δ on first 30 files, no writes
  --sample N    process N random files (smoke test)
  --no-backup   skip the cp -c (blocked by classifier without confirmation)

NOT auto-run. Trigger:
  python3 scripts/seamlessify_all.py
Expected: ~3 min for 1742 files at ~10/s with 4 workers, 3.4G backup.

Files touched

Diff

commit 4cad4c43bb711c9e023349d6a36f4a1f0a20fbd8
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 15:12:44 2026 -0700

    scripts: seamlessify_all.py — batch make_seamless across data/generated/
    
    Retroactive post-processor. For each .png in data/generated/:
      1. cp -cR the whole dir to data/generated_pre_seamless_backup/ first
         (APFS clone — zero extra disk until COW kicks in on overwrite)
      2. Run the same offset+feathered-blend transform from
         scripts/make_seamless.py, atomic temp+rename write back
      3. Sample-measure edge-Δ before/after on ~5%% of files and report
         aggregate drop at end
    
    Defaults: cpu_count()/2 workers, 5%% measurement rate. Flags:
      --dry         scan + report Δ on first 30 files, no writes
      --sample N    process N random files (smoke test)
      --no-backup   skip the cp -c (blocked by classifier without confirmation)
    
    NOT auto-run. Trigger:
      python3 scripts/seamlessify_all.py
    Expected: ~3 min for 1742 files at ~10/s with 4 workers, 3.4G backup.
---
 scripts/seamlessify_all.py | 134 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 134 insertions(+)

diff --git a/scripts/seamlessify_all.py b/scripts/seamlessify_all.py
new file mode 100644
index 0000000..0cf0afc
--- /dev/null
+++ b/scripts/seamlessify_all.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+"""Retroactively post-process every PNG in data/generated/ into a seamless tile.
+
+Strategy:
+  1. APFS clone-copy the directory to data/generated_pre_seamless_backup/
+     (cp -c) — zero extra disk until files diverge via COW.
+  2. For each .png in data/generated/: read, run make_seamless transform,
+     atomically write back via temp+rename.
+  3. Sample edge-Δ before/after on a random subset and print aggregate stats.
+  4. Multiprocessing — defaults to cpu_count()//2 workers (leave room for
+     the pm2 wallco-ai process).
+
+Run with --dry to scan and report what WOULD change without writing.
+Run with --sample N to process only N files (smoke-test).
+"""
+
+import argparse, os, sys, random, time, multiprocessing as mp, subprocess
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+SRC  = ROOT / 'data' / 'generated'
+BAK  = ROOT / 'data' / 'generated_pre_seamless_backup'
+
+
+def seam_delta(im):
+    """Return (h_delta, v_delta) — mean per-channel pixel Δ on edge columns/rows."""
+    px = im.load(); w, h = im.size
+    s_h = sum(abs(px[0,y][c] - px[w-1,y][c]) for y in range(h) for c in range(3))
+    s_v = sum(abs(px[x,0][c] - px[x,h-1][c]) for x in range(w) for c in range(3))
+    return s_h / (h * 3), s_v / (w * 3)
+
+
+def process_one(args):
+    from PIL import Image, ImageFilter, ImageChops
+    path_str, measure = args
+    p = Path(path_str)
+    try:
+        im = Image.open(p).convert('RGB')
+        w, h = im.size
+        before = seam_delta(im) if measure else None
+
+        feather = max(8, min(w, h) // 8)
+        shifted = ImageChops.offset(im, w // 2, h // 2)
+        mask = Image.new('L', (w, h), 0)
+        inner = Image.new('L', (max(1, w - 2*feather), max(1, h - 2*feather)), 255)
+        mask.paste(inner, (feather, feather))
+        mask = mask.filter(ImageFilter.GaussianBlur(feather // 2))
+        out = Image.composite(im, shifted, mask)
+
+        # Atomic write via temp + rename
+        tmp = p.with_name(p.stem + '.seamless-tmp' + p.suffix)
+        out.save(tmp, optimize=True)
+        tmp.replace(p)
+
+        after = seam_delta(out) if measure else None
+        return {'ok': True, 'path': p.name, 'before': before, 'after': after}
+    except Exception as e:
+        return {'ok': False, 'path': p.name, 'error': str(e)}
+
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument('--dry', action='store_true', help='measure but do not write')
+    ap.add_argument('--sample', type=int, default=0, help='process only N files (smoke test)')
+    ap.add_argument('--workers', type=int, default=max(1, mp.cpu_count() // 2))
+    ap.add_argument('--measure-rate', type=float, default=0.05,
+                    help='fraction of files to measure Δ on (default 5%%)')
+    ap.add_argument('--no-backup', action='store_true', help='skip the cp -c backup step')
+    args = ap.parse_args()
+
+    files = sorted(SRC.glob('*.png'))
+    if args.sample:
+        random.seed(0)
+        files = random.sample(files, min(args.sample, len(files)))
+    print(f'found {len(files)} files in {SRC}')
+    if not files:
+        return
+
+    if not args.dry and not args.no_backup:
+        if BAK.exists():
+            print(f'[skip] backup already exists at {BAK}')
+        else:
+            print(f'creating APFS clone backup at {BAK} (zero extra disk until COW)...')
+            r = subprocess.run(['cp', '-cR', str(SRC), str(BAK)], capture_output=True, text=True)
+            if r.returncode != 0:
+                print(f'cp -c failed, falling back to regular cp -R: {r.stderr}', file=sys.stderr)
+                subprocess.check_call(['cp', '-R', str(SRC), str(BAK)])
+            else:
+                print(f'  backup complete')
+
+    # Build per-file task list with a sampling flag for measurement
+    rng = random.Random(7)
+    tasks = [(str(p), rng.random() < args.measure_rate) for p in files]
+    if args.dry:
+        # in dry mode, measure ALL files but write none
+        for i, p in enumerate(files):
+            from PIL import Image
+            im = Image.open(p).convert('RGB')
+            h, v = seam_delta(im)
+            print(f'  {p.name}: h-Δ {h:6.2f} v-Δ {v:6.2f}')
+            if i > 30: break
+        return
+
+    t0 = time.time()
+    ok = fail = 0
+    before_h, after_h, before_v, after_v = [], [], [], []
+
+    with mp.Pool(args.workers) as pool:
+        for i, res in enumerate(pool.imap_unordered(process_one, tasks, chunksize=8), 1):
+            if res['ok']:
+                ok += 1
+                if res['before'] and res['after']:
+                    bh, bv = res['before']; ah, av = res['after']
+                    before_h.append(bh); after_h.append(ah)
+                    before_v.append(bv); after_v.append(av)
+            else:
+                fail += 1
+                print(f'  FAIL {res["path"]}: {res["error"]}', file=sys.stderr)
+            if i % 100 == 0 or i == len(tasks):
+                rate = i / (time.time() - t0)
+                print(f'  {i}/{len(tasks)} done ({rate:.1f}/s)')
+
+    elapsed = time.time() - t0
+    print(f'\ncomplete — {ok} ok, {fail} fail, {elapsed:.1f}s ({len(tasks)/elapsed:.1f}/s)')
+    if before_h:
+        bh_avg = sum(before_h)/len(before_h); ah_avg = sum(after_h)/len(after_h)
+        bv_avg = sum(before_v)/len(before_v); av_avg = sum(after_v)/len(after_v)
+        print(f'\nmeasured on {len(before_h)} sampled files:')
+        print(f'  h-Δ before: {bh_avg:6.2f}   after: {ah_avg:6.2f}   ({100*(bh_avg-ah_avg)/bh_avg:.1f}% drop)')
+        print(f'  v-Δ before: {bv_avg:6.2f}   after: {av_avg:6.2f}   ({100*(bv_avg-av_avg)/bv_avg:.1f}% drop)')
+
+
+if __name__ == '__main__':
+    main()

← 61dcaab studio: post-process every generated pattern to be truly sea  ·  back to Wallco Ai  ·  wall-calc: 'Save defaults' link pins W×H across PDPs with cl 3cf89a9 →