← back to Wallco Ai
variants-9roots: edge-seamless is the only hard gate (Steve curates coverage/subject); target 10 children per root for variety
8f035f53d11a5f06be69a20187052a761c8417d6 · 2026-06-12 08:11:09 -0700 · Steve Abrams
Files touched
M scripts/variants-9roots.py
Diff
commit 8f035f53d11a5f06be69a20187052a761c8417d6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 12 08:11:09 2026 -0700
variants-9roots: edge-seamless is the only hard gate (Steve curates coverage/subject); target 10 children per root for variety
---
scripts/variants-9roots.py | 199 ++++++++++++++++++++++++++-------------------
1 file changed, 115 insertions(+), 84 deletions(-)
diff --git a/scripts/variants-9roots.py b/scripts/variants-9roots.py
index 1c4957a..182bd97 100644
--- a/scripts/variants-9roots.py
+++ b/scripts/variants-9roots.py
@@ -1,41 +1,45 @@
#!/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.
+"""variants-9roots.py — generate 10 COMPOSITION variants per Wave-1 root for Steve
+to CURATE (Steve, 2026-06-12: "make 10 each for variety to choose. review live as
+its created completely new skus").
+
+DESIGN CHANGE (2026-06-12): Steve curates the variety himself, so the generator
+enforces ONLY the non-negotiable hard rule — the tile must be EDGE-SEAMLESS on both
+axes (his original "all edges line up and repeat" requirement). Coverage and
+subject-presence are NO LONGER auto-rejects — they are Steve's visual call in the
+curator. This lifts the ~80% rejection of the strict triple-gate so we actually land
+~10 children per root instead of ~1.
+
+Per root we sweep motif SCALE (motif_px 120->210) + focal crop for composition
+variety; for each step we re-roll the seed until the raw motif tile clears
+EDGE-PASS on BOTH axes (in-memory verify-edge-seamless), then composite onto the
+root's held-constant real natural-fibre ground, re-verify the written composite,
+and INSERT exactly one child SKU. Up to 10 children per root.
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.
+all_designs is INSERT-only — a roll that never clears edges inserts NOTHING (no
+orphan). DATABASE_URL self-resolves, never echoed. Does NOT use make_seamless.py /
+force-edge-seamless.py (smear sources). Settlement gate untouched (children of
+already-passed roots, same prompt class).
-Resumable: a (root,variant) cell already recorded PASS in the ledger is skipped, so
-re-invoking continues where it stopped without re-inserting.
+Resumable: a (root,variant) cell already recorded in the ledger is skipped.
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
+ python3 scripts/variants-9roots.py # 9 roots x 10 variants
+ python3 scripts/variants-9roots.py --max-retries 6 # seeds per cell
+ python3 scripts/variants-9roots.py --roots 2656 # one root
+ python3 scripts/variants-9roots.py --contact-only # rebuild contact sheet
"""
import argparse, datetime, json, os, sys, time
from pathlib import Path
+import numpy as np
+from PIL import Image, ImageDraw
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+GENDIR = os.path.join(ROOT, 'data', 'generated')
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')
@@ -49,24 +53,13 @@ ROOT_GROUND = {
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.
+# 10 composition steps — motif SCALE is the lever (sizes the canny hint → motif
+# size + spacing). Small motif = busy/dense repeat; large = open/grand. cn_strength
+# eases down 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),
@@ -74,15 +67,79 @@ for i in range(10):
})
+def edge_only_variant(root, vcfg, ground, max_retries, pass_tol, warn_tol):
+ """Roll seeds until the motif tile clears EDGE-PASS on BOTH axes (the ONLY hard
+ gate), then composite onto the fibre ground, re-verify, and INSERT one child.
+ Returns the result dict (ok=True with new_id/out on success)."""
+ os.makedirs(GENDIR, exist_ok=True)
+ meta = cnar.root_meta_one(root)
+ src = meta.get('local_path')
+ if not src or not os.path.exists(src):
+ return {'root_id': root, 'ok': False, 'error': 'no source file'}
+ prompt = meta.get('prompt') or ''
+ subject = cnar.extract_subject(prompt)
+ animal = cnar.short_animal(prompt)
+ positive, negative = cnar.build_prompt(subject, animal)
+
+ root_img = Image.open(src).convert('RGB')
+ focal, crop_box = cnar.find_focal_crop(root_img, vcfg['crop_frac'])
+ hint = cnar.build_canny_hint(focal, vcfg['motif_px'])
+ hint_png = os.path.join(GENDIR, f"{int(time.time()*1000)}_{root}_var_hint.png")
+ hint.save(hint_png, 'PNG')
+
+ attempts = []
+ for attempt in range(1, max_retries + 1):
+ seed = int.from_bytes(os.urandom(4), 'big') % (2**31 - 1)
+ cn_strength = max(0.30, vcfg['cn_strength'] - 0.03 * (attempt - 1))
+ ts = int(time.time() * 1000)
+ motif_png = os.path.join(GENDIR, f"{ts}_{seed}_var_motif.png")
+ try:
+ cnar.gen_controlnet_seamless(positive, negative, seed, hint_png,
+ cn_strength, vcfg['cn_end'], motif_png)
+ except Exception as e:
+ attempts.append({'attempt': attempt, 'seed': seed, 'error': str(e)[:160]})
+ continue
+ arr = np.asarray(Image.open(motif_png).convert('RGB'))
+ ev = edgeverify.verify(arr[:, :, :3], pass_tol, warn_tol)
+ h_v = ev['axes']['horizontal']['verdict']
+ v_v = ev['axes']['vertical']['verdict']
+ base_hex = cnar.dominant_hex(motif_png)
+ cov = cnar.coverage_pct(motif_png, base_hex)
+ attempts.append({'attempt': attempt, 'seed': seed, 'edge': ev['verdict'],
+ 'h': h_v, 'v': v_v, 'coverage_pct': round(cov, 1)})
+ print(f" root {root} V{vcfg['v']} attempt {attempt}/{max_retries}: "
+ f"edge={ev['verdict']} h={h_v} v={v_v} cov={cov:.1f}% seed={seed}",
+ file=sys.stderr)
+ # EDGE-ONLY hard gate — coverage/subject are Steve's visual call now.
+ if not (ev['verdict'] == 'PASS' and h_v == 'PASS' and v_v == 'PASS'):
+ continue
+ final_png = os.path.join(GENDIR, f"{ts}_{seed}_var.png")
+ cnar.composite_real_fibre(motif_png, ground, base_hex, final_png)
+ fev = edgeverify.verify_path(Path(final_png), pass_tol, warn_tol)
+ if not (fev['verdict'] == 'PASS'
+ and fev['axes']['horizontal']['verdict'] == 'PASS'
+ and fev['axes']['vertical']['verdict'] == 'PASS'):
+ continue # composite regressed the wrap — reroll
+ new_id = cnar.insert_child(root, final_png, positive, negative, ground, cn_strength)
+ return {
+ 'ts': datetime.datetime.now(datetime.timezone.utc).isoformat().replace('+00:00', 'Z'),
+ 'root_id': root, 'animal': animal, 'ground': ground, 'seed': seed,
+ 'base_hex': base_hex, 'coverage_pct': round(cov, 1),
+ 'cn_strength': round(cn_strength, 2), 'attempts_used': attempt,
+ 'edge_verdict': fev['verdict'], 'new_id': new_id, 'out': final_png, 'ok': True,
+ }
+ return {'root_id': root, 'ok': False,
+ 'error': f'no edge-seamless tile in {max_retries} retries', 'attempts': attempts}
+
+
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':
+ if r.get('ok'):
done.add((int(r['root_id']), int(r['variant_n'])))
except Exception:
pass
@@ -92,8 +149,6 @@ def done_cells():
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:
@@ -105,14 +160,10 @@ def build_contact():
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))
+ TH = 150; pad_left, pad_top = 70, 24
+ sheet = Image.new('RGB', (pad_left + 10 * TH, pad_top + len(roots) * TH), (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))
@@ -120,43 +171,34 @@ def build_contact():
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
+ 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))
+ sheet.paste(Image.open(p).convert('RGB').resize((TH - 4, TH - 4)), (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)
+ print(f'contact -> {CONTACT} ({len(cells)}/{len(roots)*10} 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('--roots')
+ ap.add_argument('--max-retries', type=int, default=5)
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')
+ ap.add_argument('--contact-only', action='store_true')
args = ap.parse_args()
-
if args.contact_only:
- build_contact()
- return
+ 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]
@@ -164,40 +206,29 @@ def main():
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)
+ print(f"=> root {root} V{vn}: SKIP (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)
+ f"crop={vc['crop_frac']} (EDGE-ONLY gate) ===", file=sys.stderr)
try:
- r = reroll2.reroll_one(root, cfg, args.max_retries,
- args.pass_tol, args.warn_tol, force_canny=True)
+ r = edge_only_variant(root, vc, ground, args.max_retries, args.pass_tol, args.warn_tol)
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['variant_cfg'] = {'motif_px': vc['motif_px'], '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)
+ print(f"=> root {root} V{vn}: OK new_id={r.get('new_id')} edge={r.get('edge_verdict')} "
+ f"cov={r.get('coverage_pct')}% 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,
+ print(f"=> root {root} V{vn}: no edge-clear ({str(r.get('error'))[:70]})", file=sys.stderr)
+ build_contact()
+ print(json.dumps({'roots': len(roots), 'pass': pass_n, 'skipped': skip_n,
'no_clear': fail_n, 'contact': CONTACT}, indent=2))
build_contact()
← 660e786 Add variants-9roots.py: 10 composition/density variants per
·
back to Wallco Ai
·
Add live variant-curator for Wave-1 drunk-animal composition 96285b0 →