← back to Wallco Ai
reroll-2: DTD verdict A — drop canny for the 2 glassware-entangled roots (text-led seamless txt2img) + add Gemini/local-llava subject-recovery gate to the retry loop; add free local-llava vision backend (Gemini over spend-cap 429); add gen_textled_seamless to redo module
95d401a6a9f885aa6cf6278e4dcc7555cc66745e · 2026-06-11 16:57:37 -0700 · Steve Abrams
Files touched
M scripts/controlnet-anchored-redo.pyM scripts/controlnet-anchored-reroll-2.pyA scripts/vision-contains-local.py
Diff
commit 95d401a6a9f885aa6cf6278e4dcc7555cc66745e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jun 11 16:57:37 2026 -0700
reroll-2: DTD verdict A — drop canny for the 2 glassware-entangled roots (text-led seamless txt2img) + add Gemini/local-llava subject-recovery gate to the retry loop; add free local-llava vision backend (Gemini over spend-cap 429); add gen_textled_seamless to redo module
---
scripts/controlnet-anchored-redo.py | 36 ++++++++
scripts/controlnet-anchored-reroll-2.py | 148 +++++++++++++++++++++++---------
scripts/vision-contains-local.py | 40 +++++++++
3 files changed, 184 insertions(+), 40 deletions(-)
diff --git a/scripts/controlnet-anchored-redo.py b/scripts/controlnet-anchored-redo.py
index f0cd50c..8d8003f 100644
--- a/scripts/controlnet-anchored-redo.py
+++ b/scripts/controlnet-anchored-redo.py
@@ -261,6 +261,12 @@ def gen_controlnet_seamless(positive, negative, seed, hint_local, cn_strength,
'8': {'class_type': 'VAEDecode', 'inputs': {'samples': ['3', 0], 'vae': ['2000', 0]}},
'9': {'class_type': 'SaveImage', 'inputs': {'filename_prefix': f'wallco_cnar_{seed}', 'images': ['8', 0]}},
}
+ return _submit_and_pull(workflow, out_path)
+
+
+def _submit_and_pull(workflow, out_path):
+ """Submit a ComfyUI workflow, poll /history until node '9' SaveImage yields,
+ pull the bytes to out_path. Shared by the canny + text-led generators."""
submit = comfy_post('/prompt', {'prompt': workflow, 'client_id': 'wallco-cnar'})
pid = json.loads(submit).get('prompt_id')
if not pid:
@@ -299,6 +305,36 @@ def gen_controlnet_seamless(positive, negative, seed, hint_local, cn_strength,
return out_path
+def gen_textled_seamless(positive, negative, seed, out_path):
+ """txt2img native-seamless with NO ControlNet — pure text-led. Used for roots
+ whose source structure (glassware) entangles the animal so badly that a canny
+ edge-map re-imports the wrong geometry no matter how it is cropped (DTD verdict
+ A, 2026-06-11). Same zero-smear circular-pad construction (SeamlessTile +
+ MakeCircularVAE, denoise 1.0) so the tile wraps by construction; the open
+ sole-focal-animal prompt drives a clean single subject with no glassware prior.
+
+ Graph: CheckpointLoader -> SeamlessTile(model) + MakeCircularVAE(vae);
+ CLIPTextEncode(pos/neg); EmptyLatentImage; KSampler(denoise 1.0) ->
+ VAEDecode(circular vae) -> SaveImage. Identical to the canny path minus the
+ LoadImage/Canny/ControlNet nodes."""
+ workflow = {
+ '4': {'class_type': 'CheckpointLoaderSimple', 'inputs': {'ckpt_name': MODEL}},
+ '1000': {'class_type': 'SeamlessTile', 'inputs': {'model': ['4', 0], 'tiling': 'enable', 'copy_model': 'Make a copy'}},
+ '2000': {'class_type': 'MakeCircularVAE', 'inputs': {'vae': ['4', 2], 'tiling': 'enable', 'copy_vae': 'Make a copy'}},
+ '6': {'class_type': 'CLIPTextEncode', 'inputs': {'text': positive, 'clip': ['4', 1]}},
+ '7': {'class_type': 'CLIPTextEncode', 'inputs': {'text': negative, 'clip': ['4', 1]}},
+ '5': {'class_type': 'EmptyLatentImage', 'inputs': {'width': RENDER, 'height': RENDER, 'batch_size': 1}},
+ '3': {'class_type': 'KSampler', 'inputs': {
+ 'seed': seed, 'steps': 30, 'cfg': 7.0, 'sampler_name': SAMPLER,
+ 'scheduler': SCHEDULER, 'denoise': 1.0,
+ 'model': ['1000', 0], 'positive': ['6', 0], 'negative': ['7', 0],
+ 'latent_image': ['5', 0]}},
+ '8': {'class_type': 'VAEDecode', 'inputs': {'samples': ['3', 0], 'vae': ['2000', 0]}},
+ '9': {'class_type': 'SaveImage', 'inputs': {'filename_prefix': f'wallco_cnar_txt_{seed}', 'images': ['8', 0]}},
+ }
+ return _submit_and_pull(workflow, out_path)
+
+
# ---------- composite ----------
def dominant_hex(png_path):
im = Image.open(png_path).convert('RGB').resize((128, 128))
diff --git a/scripts/controlnet-anchored-reroll-2.py b/scripts/controlnet-anchored-reroll-2.py
index fd69de4..b449b98 100644
--- a/scripts/controlnet-anchored-reroll-2.py
+++ b/scripts/controlnet-anchored-reroll-2.py
@@ -73,22 +73,27 @@ ALLOWED_ROOTS = {54076, 54266}
# directly addresses its specific WARN. Tighter crop_frac + smaller motif_px =
# figure sits well inside the boundary = no boundary-touching structure to break
# the wrap, and more open negative space around it.
+#
+# DTD VERDICT A (3/3, 2026-06-11): for THESE TWO roots the canny anchor is the
+# proven cause of subject-loss — the animal is the MINORITY structure entangled
+# with glassware (wine-glass rim / champagne coupes), so any canny crop (auto or
+# hand-targeted) re-imports the glass geometry and the animal vanishes. We DROP
+# canny for both and go text-led (no_canny=True): pure circular-pad seamless
+# txt2img driven by the strong sole-focal-animal prompt, still gated by the
+# seam-retry-until-PASS-both-axes loop (zero-smear + seam guarantee unchanged).
ROOT_CFG = {
- 54076: { # orangutan: kill the white horizontal seam + stop the glass-rim slice
+ 54076: { # orangutan: text-led single orangutan, no glass-rim slice, no seam
'ground': 'natural linen',
- 'crop_frac': 0.26, # tighter than the sample's 0.32 -> a single focal blob
- 'motif_px': 150, # smaller than 175 -> motif lands well inside the frame
- 'cn_strength': 0.40, # lower than 0.5 -> the open prompt wins layout vs. glass
- 'cn_end': 0.45, # release control earlier so surround opens up
- 'cov_lo': 8.0, 'cov_hi': 28.0, # accept an open, non-busy motif tile
+ 'no_canny': True,
+ 'cov_lo': 8.0, 'cov_hi': 30.0, # accept an open, non-busy motif tile
+ # canny params retained only as fallback if --use-canny is forced
+ 'crop_frac': 0.26, 'motif_px': 150, 'cn_strength': 0.40, 'cn_end': 0.45,
},
- 54266: { # frog: drop the champagne-tower clutter, single focal frog, low coverage
+ 54266: { # frog: text-led single tree-frog, drop the champagne-tower clutter
'ground': 'raffia',
- 'crop_frac': 0.24, # very tight -> grab the frog, not the coupe tower
- 'motif_px': 150,
- 'cn_strength': 0.38, # weak anchor: the tower structure must NOT come back
- 'cn_end': 0.42,
- 'cov_lo': 8.0, 'cov_hi': 30.0, # Steve's ~20-30% coverage target, generous space
+ 'no_canny': True,
+ 'cov_lo': 8.0, 'cov_hi': 30.0, # Steve's ~20-30% coverage target, open space
+ 'crop_frac': 0.24, 'motif_px': 150, 'cn_strength': 0.38, 'cn_end': 0.42,
},
}
@@ -101,7 +106,46 @@ def _verify_array(arr, pass_tol=edgeverify.PASS_TOL, warn_tol=edgeverify.WARN_TO
return edgeverify.verify(np.asarray(arr)[:, :, :3], pass_tol, warn_tol)
-def reroll_one(root, cfg, max_retries, pass_tol, warn_tol):
+def _vision_has_animal(png_path, animal):
+ """Subject-recovery gate (yes/no vision). The 5-root sample WARNed on edges;
+ this re-roll ALSO failed 'subject recovered' (the canny re-imported glassware
+ and the animal vanished). So we add a hard vision gate: the motif tile must
+ visibly contain a recognizable <animal>.
+
+ Primary backend = FREE LOCAL Ollama llava (Mac2 localhost, unmetered) because
+ the Gemini API is currently over its monthly spend cap (HTTP 429 — raising it
+ is a Steve-gated money action). Falls back to Gemini only if the local model
+ is unreachable (exit 2). Verified locally: elephant->yes, subjectless
+ glassware->no.
+
+ Keep the question simple and inclusive ("is a recognizable <animal>
+ present?"). An exclusion-clause phrasing ("not just glassware...") made the
+ model answer NO whenever ANY glassware co-existed, false-rejecting a
+ genuinely-present elephant and risking an infinite loop. This gate's only job
+ is to reject tiles where the animal is ABSENT.
+
+ On total failure of both backends -> False (fail-closed; never ship an
+ unconfirmed subject)."""
+ import subprocess as _sp
+ q = f"Is there a recognizable {animal} visible in this wallpaper image?"
+ # 1) local llava (free) — exit 0 = clean answer, exit 2 = unreachable
+ try:
+ r = _sp.run(['python3', os.path.join(ROOT, 'scripts', 'vision-contains-local.py'),
+ png_path, q], capture_output=True, text=True, timeout=140)
+ if r.returncode == 0:
+ return (r.stdout or '').strip().lower().startswith('yes')
+ except Exception:
+ pass
+ # 2) Gemini fallback (metered; may be 429-capped — then returns 'no')
+ try:
+ r = _sp.run(['python3', os.path.join(ROOT, 'scripts', 'vision-contains.py'),
+ png_path, q], capture_output=True, text=True, timeout=40)
+ return (r.stdout or '').strip().lower().startswith('yes')
+ except Exception:
+ return False
+
+
+def reroll_one(root, cfg, max_retries, pass_tol, warn_tol, force_canny=False):
"""Image/file-level retry loop for ONE root. Returns the result dict for the
FINAL passing candidate (and inserts exactly one DB row), or a dict with
ok=False and the per-attempt edge history if it never clears."""
@@ -115,31 +159,37 @@ def reroll_one(root, cfg, max_retries, pass_tol, warn_tol):
subject = cnar.extract_subject(prompt)
animal = cnar.short_animal(prompt)
positive, negative = cnar.build_prompt(subject, animal)
-
- root_img = Image.open(src).convert('RGB')
- # The focal crop is deterministic per root, so re-rolling the crop position
- # would just return the same window. We re-roll the SEED (and, on later
- # attempts, gently jitter the canny strength down) to get a different motif
- # while keeping the same open prompt + circular-pad seamless construction.
- focal, crop_box = cnar.find_focal_crop(root_img, cfg['crop_frac'])
- hint = cnar.build_canny_hint(focal, cfg['motif_px'])
- hint_png = os.path.join(GENDIR, f'{int(time.time()*1000)}_{root}_reroll2_hint.png')
- hint.save(hint_png, 'PNG')
+ no_canny = bool(cfg.get('no_canny')) and not force_canny
+
+ # For the canny fallback path only: the focal crop is deterministic per root,
+ # so re-rolling the crop position returns the same window; we re-roll the SEED
+ # (and jitter canny strength down on later attempts) for a different motif.
+ hint_png = None
+ crop_box = None
+ if not no_canny:
+ root_img = Image.open(src).convert('RGB')
+ focal, crop_box = cnar.find_focal_crop(root_img, cfg['crop_frac'])
+ hint = cnar.build_canny_hint(focal, cfg['motif_px'])
+ hint_png = os.path.join(GENDIR, f'{int(time.time()*1000)}_{root}_reroll2_hint.png')
+ hint.save(hint_png, 'PNG')
attempts = []
ground = cfg['ground']
+ mode = 'text-led' if no_canny else 'canny'
for attempt in range(1, max_retries + 1):
seed = int.from_bytes(os.urandom(4), 'big') % (2**31 - 1)
- # Late attempts: nudge the canny strength further down so a stubborn
- # boundary-crossing structure (glass rim / tower strut) is given even
- # less authority and the model is freer to lay out an open, wrapping tile.
+ # canny-fallback only: nudge strength down on later attempts so a stubborn
+ # boundary-crossing structure is given even less authority.
cn_strength = max(0.30, cfg['cn_strength'] - 0.03 * (attempt - 1))
cn_end = cfg['cn_end']
ts = int(time.time() * 1000)
motif_png = os.path.join(GENDIR, f'{ts}_{seed}_reroll2_motif.png')
try:
- cnar.gen_controlnet_seamless(positive, negative, seed, hint_png,
- cn_strength, cn_end, motif_png)
+ if no_canny:
+ cnar.gen_textled_seamless(positive, negative, seed, motif_png)
+ else:
+ cnar.gen_controlnet_seamless(positive, negative, seed, hint_png,
+ cn_strength, cn_end, motif_png)
except Exception as e:
attempts.append({'attempt': attempt, 'seed': seed, 'error': str(e)[:200]})
continue
@@ -153,19 +203,29 @@ def reroll_one(root, cfg, max_retries, pass_tol, warn_tol):
v_v = ev['axes']['vertical']['verdict']
cov_ok = cfg['cov_lo'] <= cov <= cfg['cov_hi']
motif_pass = (ev['verdict'] == 'PASS' and h_v == 'PASS' and v_v == 'PASS')
- attempts.append({
- 'attempt': attempt, 'seed': seed, 'cn_strength': round(cn_strength, 2),
+ rec = {
+ 'attempt': attempt, 'seed': seed, 'mode': mode,
+ 'cn_strength': round(cn_strength, 2),
'motif_edge': ev['verdict'], 'h': h_v, 'v': v_v,
'h_tiled': ev['axes']['horizontal']['tiled_cross_seam_max'],
'v_tiled': ev['axes']['vertical']['tiled_cross_seam_max'],
'coverage_pct': round(cov, 1), 'cov_ok': cov_ok,
'isolated_motif': ev['isolated_motif'], 'motif': motif_png,
- })
- print(f" root {root} attempt {attempt}/{max_retries}: motif edge={ev['verdict']} "
+ }
+ attempts.append(rec)
+ print(f" root {root} attempt {attempt}/{max_retries} [{mode}]: motif edge={ev['verdict']} "
f"h={h_v} v={v_v} cov={cov:.1f}% cov_ok={cov_ok} seed={seed}", file=sys.stderr)
if not (motif_pass and cov_ok):
- continue # re-roll; do NOT composite, do NOT insert
+ continue # re-roll; do NOT spend a vision call, composite, or insert
+
+ # --- SUBJECT-RECOVERY gate (only after edges+coverage pass, to save cost) ---
+ subject_ok = _vision_has_animal(motif_png, animal)
+ rec['subject_recovered'] = subject_ok
+ print(f" root {root} attempt {attempt}: subject_recovered({animal})={subject_ok}",
+ file=sys.stderr)
+ if not subject_ok:
+ continue # animal not recognizable -> re-roll; do NOT composite/insert
# --- motif cleared BOTH axes + coverage OK: finalize this candidate ---
final_png = os.path.join(GENDIR, f'{ts}_{seed}_reroll2.png')
@@ -192,12 +252,15 @@ def reroll_one(root, cfg, max_retries, pass_tol, warn_tol):
'category': meta.get('category'), 'subject': subject, 'animal': animal,
'ground': ground, 'seed': seed, 'base_hex': base_hex,
'cn_strength': round(cn_strength, 2), 'cn_end': cn_end,
+ 'mode': mode, 'subject_recovered': True,
'crop_box': crop_box, 'coverage_pct': round(cov, 1),
'attempts_used': attempt, 'attempts': attempts,
- 'technique': ('canny-SDXL-CN-anchored native-seamless txt2img '
- '(SeamlessTile+MakeCircularVAE, denoise 1.0, tighter focal '
- 'canny crop) + SEAM-RETRY-until-motif-PASS-both-axes + '
- 'deterministic real-fibre composite — NO smear'),
+ 'technique': (
+ ('text-led native-seamless txt2img (SeamlessTile+MakeCircularVAE, '
+ 'denoise 1.0, NO canny — DTD verdict A) ' if mode == 'text-led'
+ else 'canny-SDXL-CN-anchored native-seamless txt2img (tighter focal crop) ')
+ + '+ SEAM-RETRY-until-motif-PASS-both-axes + Gemini subject-recovery gate '
+ + '+ deterministic real-fibre composite — NO smear'),
'edge_verdict': fev['verdict'], 'edge_seamless': bool(fev['ok']),
'edge_axes': {'h': fev['axes']['horizontal'], 'v': fev['axes']['vertical']},
'motif_edge_verdict': ev['verdict'],
@@ -207,8 +270,10 @@ def reroll_one(root, cfg, max_retries, pass_tol, warn_tol):
# Never cleared within the cap.
return {
- 'root_id': root, 'ok': False,
- 'error': f'no motif cleared PASS-both-axes + coverage in {max_retries} retries',
+ 'root_id': root, 'ok': False, 'mode': mode,
+ 'error': (f'no motif cleared all gates (edge PASS-both-axes + coverage '
+ f'{cfg["cov_lo"]}-{cfg["cov_hi"]}% + Gemini subject-recovery) in '
+ f'{max_retries} retries'),
'ground': ground, 'attempts': attempts,
}
@@ -220,6 +285,8 @@ def main():
ap.add_argument('--max-retries', type=int, default=6)
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('--use-canny', action='store_true',
+ help='force the canny anchor fallback (default: text-led per DTD verdict A)')
args = ap.parse_args()
ids = [int(x) for x in args.ids.split(',') if x.strip()]
@@ -233,7 +300,8 @@ def main():
for rid in ids:
cfg = ROOT_CFG[rid]
try:
- r = reroll_one(rid, cfg, args.max_retries, args.pass_tol, args.warn_tol)
+ r = reroll_one(rid, cfg, args.max_retries, args.pass_tol, args.warn_tol,
+ force_canny=args.use_canny)
except Exception as e:
r = {'root_id': rid, 'ok': False, 'error': str(e), 'ground': cfg['ground']}
results.append(r)
diff --git a/scripts/vision-contains-local.py b/scripts/vision-contains-local.py
new file mode 100644
index 0000000..75809a8
--- /dev/null
+++ b/scripts/vision-contains-local.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+"""vision-contains-local.py — FREE local yes/no vision gate via Ollama llava
+(Mac2 localhost, unmetered). Drop-in replacement for vision-contains.py when the
+Gemini API is over its spend cap (HTTP 429). Used by controlnet-anchored-reroll-2
+as the subject-recovery gate.
+
+Usage: vision-contains-local.py <png_path> <concept-question>
+Prints: yes|no (exit 0 on a clean answer, 2 on unreachable/error -> caller
+fails closed)
+"""
+import base64, json, os, sys, urllib.request
+
+OLLAMA = os.environ.get('OLLAMA_URL', 'http://127.0.0.1:11434')
+MODEL = os.environ.get('OLLAMA_VISION_MODEL', 'llava:latest')
+
+png, question = sys.argv[1], sys.argv[2]
+try:
+ img_b64 = base64.b64encode(open(png, 'rb').read()).decode()
+except Exception as e:
+ print('no', file=sys.stderr); print('no'); sys.exit(2)
+
+body = {
+ 'model': MODEL,
+ 'prompt': question + ' Answer with exactly one word: yes or no.',
+ 'images': [img_b64],
+ 'stream': False,
+ 'options': {'temperature': 0},
+}
+req = urllib.request.Request(f'{OLLAMA}/api/generate',
+ data=json.dumps(body).encode(),
+ headers={'Content-Type': 'application/json'})
+try:
+ with urllib.request.urlopen(req, timeout=120) as r:
+ j = json.load(r)
+ txt = (j.get('response') or '').strip().lower()
+ print('yes' if txt.startswith('yes') else 'no')
+ sys.exit(0)
+except Exception as e:
+ print(f'local-vision error: {e}', file=sys.stderr)
+ print('no'); sys.exit(2)
← 9ec758e Add controlnet-anchored-reroll-2: seam-retry loop for the tw
·
back to Wallco Ai
·
reroll-2: empirical correction — text-led alone fails edges+ fb6e03f →