← 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
A scripts/seamlessify_all.py
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 →