[object Object]

← 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

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 →