← back to Wallco Ai
Add variants-9roots.py: 10 composition/density variants per Wave-1 root (motif-scale sweep, permissive coverage gate, ledger-resumable, insert-only PASS children)
660e786a75e8b3089597aa90cfdfd8212b354963 · 2026-06-12 07:49:37 -0700 · Steve Abrams
Files touched
A scripts/variants-9roots.py
Diff
commit 660e786a75e8b3089597aa90cfdfd8212b354963
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 12 07:49:37 2026 -0700
Add variants-9roots.py: 10 composition/density variants per Wave-1 root (motif-scale sweep, permissive coverage gate, ledger-resumable, insert-only PASS children)
---
scripts/variants-9roots.py | 206 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 206 insertions(+)
diff --git a/scripts/variants-9roots.py b/scripts/variants-9roots.py
new file mode 100644
index 0000000..1c4957a
--- /dev/null
+++ b/scripts/variants-9roots.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python3
+"""variants-9roots.py — generate 10 COMPOSITION/DENSITY variants for each of the 9
+Wave-1 seam-fix roots (Steve, 2026-06-12: "create 10 variants each").
+
+Each of the 9 roots already has ONE PASS seam-fix candidate. This produces up to 10
+ADDITIONAL children per root that vary the COMPOSITION ONLY — ground is held
+constant per root; we sweep motif scale (motif_px), focal crop, and the coverage
+band from OPEN (small, sparse motifs) to DENSE (large, closer-packed motifs). Steve
+then curates the 10×9 grid and keeps the composition he likes per root.
+
+Mechanism: reuses the PROVEN reroll-2 machinery byte-for-byte via
+reroll2.reroll_one(force_canny=True) — cropped-focal canny anchor + circular-pad
+native-seamless txt2img (SeamlessTile + MakeCircularVAE, denoise 1.0 = ZERO smear)
++ in-memory both-axes seam gate + free-local subject-recovery gate + real-fibre
+composite. reroll_one INSERTS exactly one DB row per call, and ONLY for a candidate
+that clears the seam gate on BOTH axes — a non-clearing cell inserts nothing.
+
+SACRED ROOTS: never overwrites a root. Every variant -> NEW file + NEW id,
+is_published=FALSE, user_removed=FALSE, parent_design_id=<root>. Never published.
+all_designs is INSERT-only — so a cell that doesn't clear within --max-retries
+leaves NO orphan. DATABASE_URL self-resolves, never echoed. Does NOT use
+make_seamless.py / force-edge-seamless.py.
+
+Resumable: a (root,variant) cell already recorded PASS in the ledger is skipped, so
+re-invoking continues where it stopped without re-inserting.
+
+Usage:
+ python3 scripts/variants-9roots.py # all 9 roots × 10 variants
+ python3 scripts/variants-9roots.py --max-retries 3 # throttle seeds per cell
+ python3 scripts/variants-9roots.py --roots 2656,3391 --contact-only
+"""
+import argparse, datetime, json, os, sys, time
+from pathlib import Path
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, os.path.join(ROOT, 'scripts'))
+from importlib import import_module
+reroll2 = import_module('controlnet-anchored-reroll-2')
+cnar = import_module('controlnet-anchored-redo')
+edgeverify = import_module('verify-edge-seamless')
+
+LEDGER = os.path.join(ROOT, 'data', 'variants-9roots.jsonl')
+CONTACT = '/tmp/variants_9roots.png'
+
+# 9 Wave-1 roots, ground HELD CONSTANT per root (Steve: hold ground, vary comp).
+ROOT_GROUND = {
+ 2656: 'raffia', 2766: 'natural linen', 3391: 'sisal',
+ 53999: 'grasscloth', 54002: 'raffia', 54092: 'sisal',
+ 54110: 'paperweave', 54112: 'grasscloth', 54290: 'paperweave',
+}
+
+# 10 composition steps. motif_px is the REAL composition lever: it sizes the canny
+# hint, so it sets motif SCALE + spacing. At a similar coverage, a SMALL motif reads
+# as a busy/dense repeat (many little figures) and a LARGE motif reads as open/grand
+# (few big figures). We sweep motif_px 120->210 + crop_frac 0.22->0.34 to span that.
+#
+# The coverage band is held PERMISSIVE + uniform (14-40%) for every variant — the
+# overnight data shows canny reliably lands ~22-34% coverage, so a tight low-coverage
+# band (the original 10-24% V1) almost never clears and just starves the open cells.
+# Decoupling the accept-gate from the composition lever = high fill rate AND genuine
+# scale variation. cn_strength eases down slightly as the motif grows so the bigger
+# figure keeps open ground around it.
+VARIANTS = []
+for i in range(10):
+ VARIANTS.append({
+ 'v': i + 1,
+ 'no_canny': False,
+ 'cov_lo': 14.0,
+ 'cov_hi': 40.0,
+ 'motif_px': 120 + 10 * i,
+ 'crop_frac': round(0.22 + 0.013 * i, 3),
+ 'cn_strength': round(0.52 - 0.01 * i, 2),
+ 'cn_end': 0.55,
+ })
+
+
+def done_cells():
+ """(root, v) cells already recorded PASS in the ledger — skip on re-run."""
+ done = set()
+ try:
+ with open(LEDGER) as f:
+ for line in f:
+ try:
+ r = json.loads(line)
+ if r.get('ok') and r.get('edge_verdict') == 'PASS':
+ done.add((int(r['root_id']), int(r['variant_n'])))
+ except Exception:
+ pass
+ except FileNotFoundError:
+ pass
+ return done
+
+
+def build_contact():
+ """Contact sheet: rows = roots, cols = variants. Reads the ledger 'out' paths."""
+ from PIL import Image, ImageDraw
+ cells = {}
+ try:
+ with open(LEDGER) as f:
+ for line in f:
+ try:
+ r = json.loads(line)
+ except Exception:
+ continue
+ if r.get('ok') and r.get('out') and os.path.exists(r['out']):
+ cells[(int(r['root_id']), int(r['variant_n']))] = r['out']
+ except FileNotFoundError:
+ print('no ledger yet — nothing to contact-sheet', file=sys.stderr)
+ return
+ roots = sorted(ROOT_GROUND)
+ TH = 150
+ pad_left, pad_top = 70, 24
+ W = pad_left + 10 * TH
+ H = pad_top + len(roots) * TH
+ sheet = Image.new('RGB', (W, H), (245, 244, 240))
+ d = ImageDraw.Draw(sheet)
+ for c in range(10):
+ d.text((pad_left + c * TH + TH // 2 - 6, 6), f'V{c+1}', fill=(40, 40, 40))
+ for ri, root in enumerate(roots):
+ y = pad_top + ri * TH
+ d.text((6, y + TH // 2 - 4), str(root), fill=(40, 40, 40))
+ for c in range(10):
+ p = cells.get((root, c + 1))
+ x = pad_left + c * TH
+ if p:
+ try:
+ im = Image.open(p).convert('RGB').resize((TH - 4, TH - 4))
+ sheet.paste(im, (x + 2, y + 2))
+ except Exception:
+ d.rectangle([x + 2, y + 2, x + TH - 2, y + TH - 2], outline=(200, 0, 0))
+ else:
+ d.rectangle([x + 2, y + 2, x + TH - 2, y + TH - 2], outline=(210, 200, 190))
+ sheet.save(CONTACT)
+ filled = len(cells)
+ print(f'contact sheet -> {CONTACT} ({filled}/{len(roots)*10} cells filled)',
+ file=sys.stderr)
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--roots', help='comma-subset of the 9 (default all)')
+ ap.add_argument('--max-retries', type=int, default=3) # throttle seeds/cell
+ ap.add_argument('--pass-tol', type=float, default=edgeverify.PASS_TOL)
+ ap.add_argument('--warn-tol', type=float, default=edgeverify.WARN_TOL)
+ ap.add_argument('--contact-only', action='store_true',
+ help='just rebuild the contact sheet from the ledger and exit')
+ args = ap.parse_args()
+
+ if args.contact_only:
+ build_contact()
+ return
+
+ roots = sorted(ROOT_GROUND)
+ if args.roots:
+ want = {int(x) for x in args.roots.split(',') if x.strip()}
+ roots = [r for r in roots if r in want]
+
+ done = done_cells()
+ total = len(roots) * len(VARIANTS)
+ pass_n = skip_n = fail_n = 0
+ for root in roots:
+ ground = ROOT_GROUND[root]
+ for vc in VARIANTS:
+ vn = vc['v']
+ if (root, vn) in done:
+ skip_n += 1
+ print(f"=> root {root} V{vn}: SKIP (already PASS in ledger)", file=sys.stderr)
+ continue
+ cfg = {'ground': ground, 'no_canny': vc['no_canny'],
+ 'cov_lo': vc['cov_lo'], 'cov_hi': vc['cov_hi'],
+ 'crop_frac': vc['crop_frac'], 'motif_px': vc['motif_px'],
+ 'cn_strength': vc['cn_strength'], 'cn_end': vc['cn_end']}
+ print(f"\n=== root {root} V{vn}/10 ground={ground} motif_px={vc['motif_px']} "
+ f"cov={vc['cov_lo']:.0f}-{vc['cov_hi']:.0f}% crop={vc['crop_frac']} ===",
+ file=sys.stderr)
+ try:
+ r = reroll2.reroll_one(root, cfg, args.max_retries,
+ args.pass_tol, args.warn_tol, force_canny=True)
+ except Exception as e:
+ r = {'root_id': root, 'ok': False, 'error': str(e), 'ground': ground}
+ r['variant_n'] = vn
+ r['variant_cfg'] = {'motif_px': vc['motif_px'], 'cov_lo': vc['cov_lo'],
+ 'cov_hi': vc['cov_hi'], 'crop_frac': vc['crop_frac']}
+ r['generator'] = 'variants-9roots'
+ r['variant'] = f'comp-v{vn}'
+ with open(LEDGER, 'a') as q:
+ q.write(json.dumps(r) + '\n')
+ if r.get('ok'):
+ pass_n += 1
+ print(f"=> root {root} V{vn}: OK new_id={r.get('new_id')} "
+ f"edge={r.get('edge_verdict')} attempts={r.get('attempts_used')}",
+ file=sys.stderr)
+ else:
+ fail_n += 1
+ print(f"=> root {root} V{vn}: no-clear ({r.get('error','?')[:80]})",
+ file=sys.stderr)
+ build_contact() # refresh after every cell so Steve can watch it fill
+
+ print(json.dumps({'roots': len(roots), 'variants_each': len(VARIANTS),
+ 'total_cells': total, 'pass': pass_n, 'skipped': skip_n,
+ 'no_clear': fail_n, 'contact': CONTACT}, indent=2))
+ build_contact()
+
+
+if __name__ == '__main__':
+ main()
← a0bab98 TODO: log replace-colors-3-methods work (AI-recolor gate fix
·
back to Wallco Ai
·
variants-9roots: edge-seamless is the only hard gate (Steve 8f035f5 →