[object Object]

← 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

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 →