[object Object]

← back to Wallco Ai

Add controlnet-anchored-redo.py: SDXL canny-CN anchored native-seamless redo

c63a1ffba50c6ea8cd7b9d20d3537895850d6bfc · 2026-06-11 16:29:32 -0700 · Steve Abrams

Uses the controlnet-canny-sdxl-1.0 model installed on Mac1 ComfyUI to anchor a
single focal motif (DTD 3/3: cropped focal canny hint on blank canvas, not the
full busy root) while circular-pad SeamlessTile+MakeCircularVAE keeps the tile
seamless (zero smear). Composites onto real procedural fibre ground. Inserts a
sacred curator-mode child (is_published/user_removed FALSE, parent=root) with
explicit id (dw_admin has no sequence USAGE) and image_url inline (table is in a
no-replica-identity publication so UPDATE/DELETE error).

Files touched

Diff

commit c63a1ffba50c6ea8cd7b9d20d3537895850d6bfc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jun 11 16:29:32 2026 -0700

    Add controlnet-anchored-redo.py: SDXL canny-CN anchored native-seamless redo
    
    Uses the controlnet-canny-sdxl-1.0 model installed on Mac1 ComfyUI to anchor a
    single focal motif (DTD 3/3: cropped focal canny hint on blank canvas, not the
    full busy root) while circular-pad SeamlessTile+MakeCircularVAE keeps the tile
    seamless (zero smear). Composites onto real procedural fibre ground. Inserts a
    sacred curator-mode child (is_published/user_removed FALSE, parent=root) with
    explicit id (dw_admin has no sequence USAGE) and image_url inline (table is in a
    no-replica-identity publication so UPDATE/DELETE error).
---
 scripts/controlnet-anchored-redo.py | 469 ++++++++++++++++++++++++++++++++++++
 1 file changed, 469 insertions(+)

diff --git a/scripts/controlnet-anchored-redo.py b/scripts/controlnet-anchored-redo.py
new file mode 100644
index 0000000..f0cd50c
--- /dev/null
+++ b/scripts/controlnet-anchored-redo.py
@@ -0,0 +1,469 @@
+#!/usr/bin/env python3
+"""controlnet-anchored-redo.py — REDO of the BOUNDED 5-root sanity sample using
+an SDXL CANNY ControlNet to anchor the subject while hitting an OPEN composition.
+
+WHY THIS SUPERSEDES native-seamless-redo.py (2026-06-11):
+  The img2img redo anchored the subject by starting from the root latent, but the
+  root tiles are EDGE-TO-EDGE BUSY toiles (sunflowers everywhere / barcode vertical
+  stripes / an abstract red-cyan blob). img2img inherits that busyness, fighting the
+  graphic-designer open-composition brief. The ControlNet model
+  `controlnet-canny-sdxl-1.0.safetensors` was installed on Mac1 ComfyUI after Steve's
+  approval, which unlocks the right architecture.
+
+DTD VERDICT (3/3, 2026-06-11): feed the ControlNet a CROPPED single focal motif of
+the root (the one clean animal head) centred on an otherwise-BLANK canvas, at
+LOW-MODERATE strength (~0.5), NOT the full busy root. A canny map of the full root
+is a dense spatial prior that re-imposes the edge-to-edge clutter and makes an open
+layout structurally impossible. Cropping decouples WHICH-animal (canny anchor) from
+LAYOUT (blank surround + open prompt + circular-pad), so all four goals co-exist.
+
+THE FOUR GOALS, each by mechanism:
+  1. ZERO smear   — native circular-pad seamless: SeamlessTile + MakeCircularVAE,
+     txt2img at denoise 1.0. The tile wraps by construction. We NEVER touch
+     make_seamless.py / force-edge-seamless.py (retired smear sources).
+  2. SUBJECT anchored — the root's single focal crop -> Canny edge map, fed through
+     ControlNetApplyAdvanced (canny SDXL CN) at strength ~0.5 over a partial step
+     window. The motif identity (giraffe head, frog, etc.) survives; the model is
+     free to rebuild open surround.
+  3. REAL fibre ground — the clean two-tone open motif tile is composited onto a
+     seamless procedural natural-fibre swatch (make-fibre-ground.py) via the
+     deterministic seamless-bg-swap path. Both layers wrap; motif kept byte-for-byte.
+  4. OPEN / less busy — blank canny surround + open/sparse prompt + a focal-scale
+     canny crop sized so the primary motif lands ~150-200px tall in a 512 render.
+
+SACRED ROOTS: never overwrites a root. Each candidate -> NEW file + NEW id with
+is_published=FALSE, user_removed=FALSE, parent_design_id=<root>. NEVER published live.
+The composite's edges are verified with verify-edge-seamless.py on the WRITTEN file;
+the real verdict is recorded (a WARN/FAIL is recorded as such, never a fake PASS).
+
+NOT a bulk run. Caps at --ids. DATABASE_URL self-resolves (never echoed).
+
+Usage:
+  python3 scripts/controlnet-anchored-redo.py --ids 2662,54266,54076,54610,53896
+  python3 scripts/controlnet-anchored-redo.py --ids 2662 --cn-strength 0.55
+"""
+import argparse, datetime, json, os, re, subprocess, sys, time
+from pathlib import Path
+from urllib.parse import quote
+
+import numpy as np
+from PIL import Image, ImageFilter
+from scipy.ndimage import gaussian_filter
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+GENDIR = os.path.join(ROOT, 'data', 'generated')
+FIBREDIR = os.path.join(ROOT, 'data', 'fibre-grounds')
+QUEUE = os.path.join(ROOT, 'data', 'seam-fix-queue.jsonl')
+
+sys.path.insert(0, os.path.join(ROOT, 'scripts'))
+from importlib import import_module
+sfv = import_module('seam-fix-variants')            # psql / root_meta
+edgeverify = import_module('verify-edge-seamless')  # true-toroidal edge gate
+mkfibre = import_module('make-fibre-ground')        # procedural fibre swatch
+
+COMFY = os.environ.get('COMFY_URL', 'http://192.168.1.133:8188')
+MODEL = os.environ.get('COMFY_MODEL', 'sd_xl_base_1.0.safetensors')
+CN_MODEL = os.environ.get('COMFY_CN_MODEL', 'controlnet-canny-sdxl-1.0.safetensors')
+SAMPLER = os.environ.get('COMFY_SAMPLER', 'dpmpp_2m_sde')
+SCHEDULER = os.environ.get('COMFY_SCHEDULER', 'karras')
+POLL_TIMEOUT = int(os.environ.get('COMFY_POLL_TIMEOUT_SEC', '900'))
+RENDER = 1024  # SDXL native; verify+brief are framed at 512 — we downscale notes
+
+# Rotate the natural ground across the sample so Steve sees range, not one note.
+GROUNDS = ['grasscloth', 'raffia', 'natural linen', 'sisal', 'paperweave']
+
+
+# ---------- subject extraction ----------
+def extract_subject(prompt: str) -> str:
+    if not prompt:
+        return 'a single tipsy animal holding one cocktail'
+    parts = [p.strip() for p in prompt.split(',')]
+    subject = ', '.join(parts[:2]) if len(parts) >= 2 else parts[0]
+    subject = re.split(r'\.\s', subject)[0]
+    return subject.strip().rstrip('.')
+
+
+def short_animal(prompt: str) -> str:
+    """Just the animal noun (for a tighter focal prompt)."""
+    p = (prompt or '').lower()
+    for animal in ['giraffe', 'tree frog', 'frog', 'orangutan', 'elephant',
+                   'red panda', 'panda', 'monkey', 'lemur', 'sloth', 'fox',
+                   'otter', 'raccoon', 'koala', 'parrot', 'flamingo']:
+        if animal in p:
+            return animal
+    return 'animal'
+
+
+# ---------- focal crop ----------
+def _detail_density(gray):
+    """Local edge-energy map: where the subject's fine detail concentrates."""
+    gx = np.abs(np.diff(gray, axis=1, append=gray[:, -1:]))
+    gy = np.abs(np.diff(gray, axis=0, append=gray[-1:, :]))
+    energy = gx + gy
+    return gaussian_filter(energy, sigma=24)
+
+
+def find_focal_crop(root_img, crop_frac=0.42):
+    """Find the densest-detail square region away from the tile edges — that is
+    where the animal subject (eyes/face/contours) concentrates vs. flat ground or
+    repetitive filler. Returns a centred square crop of the root.
+
+    crop_frac sets crop side as a fraction of the root's short edge; smaller =
+    tighter focal motif = more open surround once placed on the blank canvas."""
+    g = np.asarray(root_img.convert('L'), np.float64)
+    H, W = g.shape
+    side = int(min(H, W) * crop_frac)
+    side = max(64, min(side, min(H, W)))
+    dens = _detail_density(g)
+    # integral image for fast windowed sum of density
+    ii = dens.cumsum(0).cumsum(1)
+    ii = np.pad(ii, ((1, 0), (1, 0)))
+
+    def win_sum(y, x):
+        return (ii[y + side, x + side] - ii[y, x + side]
+                - ii[y + side, x] + ii[y, x])
+
+    # bias slightly toward centre (avoid grabbing a cropped half-subject at edge)
+    best, by, bx = -1.0, 0, 0
+    step = max(8, side // 16)
+    cy, cx = (H - side) / 2.0, (W - side) / 2.0
+    for y in range(0, H - side + 1, step):
+        for x in range(0, W - side + 1, step):
+            centre_bias = 1.0 - 0.35 * (abs(y - cy) / max(cy, 1) + abs(x - cx) / max(cx, 1)) / 2.0
+            s = win_sum(y, x) * centre_bias
+            if s > best:
+                best, by, bx = s, y, x
+    return root_img.crop((bx, by, bx + side, by + side)), (bx, by, side)
+
+
+def build_canny_hint(focal_crop, motif_px=180):
+    """Place the focal crop, scaled so the subject reads ~motif_px tall in a 512
+    frame (scaled to RENDER), centred on a BLACK canvas. Black surround => Canny
+    yields no edges there => CN exerts no constraint => the model leaves it open.
+
+    CRITICAL: feather the crop's outer ring to black with a radial alpha so the
+    crop's square boundary does NOT survive as a hard edge through Canny — a hard
+    square produced the faint frame-overlay ghost-box in the first smoke test.
+    The feather kills the box; only the subject's interior contours remain as
+    canny edges."""
+    target = int(round(motif_px * (RENDER / 512.0)))
+    target = max(96, min(target, RENDER - 32))
+    crop = focal_crop.convert('RGB').resize((target, target), Image.LANCZOS)
+
+    # Feather to black over a LONG, VERY gradual radial ramp so NO detectable
+    # edge forms at any radius — the first feather (sharp ring at r=0.82) created
+    # a circular gradient that Canny picked up as a ring outline ('framed cameo'
+    # defect). Here alpha falls linearly from 1.0 at the centre to 0 at r=1.0,
+    # and we blur it heavily, so the luminance gradient everywhere is below
+    # Canny's gradient threshold — only the SUBJECT's own internal contours
+    # survive as edges, no box and no ring.
+    yy, xx = np.mgrid[0:target, 0:target].astype(np.float64)
+    cx = cy = (target - 1) / 2.0
+    r = np.sqrt(((xx - cx) / cx) ** 2 + ((yy - cy) / cy) ** 2)  # 0 centre .. ~1.41 corners
+    alpha = np.clip(1.0 - r, 0.0, 1.0)                          # gentle whole-radius ramp
+    alpha = gaussian_filter(alpha, sigma=target * 0.06)         # heavy blur => no hard ring
+    crop_arr = np.asarray(crop, np.float64) * alpha[..., None]  # fade toward black
+
+    canvas = np.zeros((RENDER, RENDER, 3), np.float64)
+    off = (RENDER - target) // 2
+    canvas[off:off + target, off:off + target] = crop_arr
+    return Image.fromarray(np.clip(canvas, 0, 255).astype(np.uint8), 'RGB')
+
+
+# ---------- comfy plumbing ----------
+def comfy_post(path, data=None, timeout=30):
+    url = f'{COMFY}{path}'
+    if data is not None:
+        r = subprocess.run(
+            ['curl', '-sf', '-m', str(timeout), '-H', 'Content-Type: application/json',
+             '-X', 'POST', url, '-d', '@-'],
+            input=json.dumps(data), capture_output=True, text=True)
+    else:
+        r = subprocess.run(['curl', '-sf', '-m', str(timeout), url],
+                           capture_output=True, text=True)
+    if r.returncode != 0:
+        raise RuntimeError(f'comfy {path} failed rc={r.returncode}: {r.stderr[:200]}')
+    return r.stdout
+
+
+def comfy_upload_image(local_path, name_hint):
+    r = subprocess.run(
+        ['curl', '-sf', '-m', '60', '-X', 'POST', f'{COMFY}/upload/image',
+         '-F', f'image=@{local_path}', '-F', 'overwrite=true'],
+        capture_output=True, text=True)
+    if r.returncode != 0:
+        raise RuntimeError(f'comfy upload failed: {r.stderr[:200]}')
+    name = json.loads(r.stdout).get('name')
+    if not name:
+        raise RuntimeError(f'comfy upload returned no name: {r.stdout[:200]}')
+    return name
+
+
+def build_prompt(subject, animal):
+    positive = (
+        f"A single {subject}. ONE clean bold flat two-tone {animal} as the SOLE "
+        "focal motif, large and isolated and centred, with generous EMPTY "
+        "negative space of bare flat ground all around it. Open airy wallcovering "
+        "repeat, sparse and minimal, calm and uncluttered, NOT busy, NOT "
+        "edge-to-edge, NOT a dense all-over print, plenty of breathing room. The "
+        "figure is a SOLID flat dark-ink silhouette in one deep archival ink "
+        "colour on a clean light flat ground, STRONG figure-ground contrast, "
+        "crisp graphic two-tone block-print, hard clean confident edges, no "
+        "shading, no gradient, no depth, no relief, NOT pale, NOT washed out, NOT "
+        "faded. Elegant heritage toile, refined block-print designer wallcovering."
+    )
+    negative = (
+        "busy, cluttered, dense, edge-to-edge, all-over print, tessellated "
+        "densely, packed motifs, many animals, crowded figures, repeated heads, "
+        "rows of faces, sunflowers, foliage filling the background, leaves filling "
+        "the background, vines everywhere, jungle scene, vertical stripes, barcode "
+        "stripes, columns of shapes, abstract shapes, abstract waves, abstract "
+        "blobs, leaf blades, unrecognizable subject, embossed, relief, raised "
+        "texture, 3D, dimensional, depth, drop shadow, ambient occlusion, ghost "
+        "layer, ghosted, faded background copies, gradient fill, halftone, "
+        "ben-day dots, outline only, hollow shape, pale, washed out, faded, "
+        "pastel, low contrast, ghostly, translucent, neon, fluorescent, "
+        "saturated, rainbow, more than 4 colours, seam visible, hard seam, frame, "
+        "border, picture frame, pasted-on rectangle, plate behind motif, "
+        "signature, watermark, text, blurry, low quality"
+    )
+    return positive, negative
+
+
+def gen_controlnet_seamless(positive, negative, seed, hint_local, cn_strength,
+                            cn_end, out_path):
+    """txt2img native-seamless with a canny ControlNet anchoring the focal motif.
+
+    Graph: CheckpointLoader -> SeamlessTile(model) + MakeCircularVAE(vae);
+    LoadImage(hint) -> Canny -> ControlNetApplyAdvanced(positive,negative) at
+    strength<1 over [0, cn_end]; EmptyLatentImage(seamless via circular VAE on
+    decode); KSampler(denoise 1.0) -> VAEDecode(circular vae) -> SaveImage."""
+    hint_name = comfy_upload_image(hint_local, 'cn_hint')
+    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]}},
+        '20': {'class_type': 'ControlNetLoader', 'inputs': {'control_net_name': CN_MODEL}},
+        '10': {'class_type': 'LoadImage', 'inputs': {'image': hint_name}},
+        '21': {'class_type': 'Canny', 'inputs': {'image': ['10', 0], 'low_threshold': 0.45, 'high_threshold': 0.85}},
+        '22': {'class_type': 'ControlNetApplyAdvanced', 'inputs': {
+            'positive': ['6', 0], 'negative': ['7', 0],
+            'control_net': ['20', 0], 'image': ['21', 0],
+            'strength': cn_strength, 'start_percent': 0.0, 'end_percent': cn_end}},
+        '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': ['22', 0], 'negative': ['22', 1],
+            '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_{seed}', 'images': ['8', 0]}},
+    }
+    submit = comfy_post('/prompt', {'prompt': workflow, 'client_id': 'wallco-cnar'})
+    pid = json.loads(submit).get('prompt_id')
+    if not pid:
+        raise RuntimeError('ComfyUI did not return prompt_id')
+    start = time.time()
+    images = None
+    last_err = None
+    while time.time() - start < POLL_TIMEOUT:
+        time.sleep(2)
+        try:
+            hist = json.loads(comfy_post(f'/history/{pid}', timeout=5))
+        except Exception:
+            continue
+        entry = hist.get(pid)
+        if not entry:
+            continue
+        status = entry.get('status', {})
+        if status.get('status_str') == 'error':
+            msgs = status.get('messages', [])
+            last_err = json.dumps(msgs)[:400]
+            raise RuntimeError(f'ComfyUI execution error: {last_err}')
+        if entry.get('outputs', {}).get('9', {}).get('images'):
+            images = entry['outputs']['9']['images']
+            break
+    if not images:
+        raise RuntimeError(f'ComfyUI timed out after {POLL_TIMEOUT}s (pid={pid}) {last_err or ""}')
+    im = images[0]
+    view = (f"/view?filename={quote(im['filename'])}"
+            f"&subfolder={quote(im.get('subfolder',''))}"
+            f"&type={quote(im.get('type','output'))}")
+    raw = subprocess.run(['curl', '-sf', '-m', '60', f'{COMFY}{view}'], capture_output=True)
+    if raw.returncode != 0 or len(raw.stdout) < 1000:
+        raise RuntimeError(f'ComfyUI image pull failed ({view})')
+    with open(out_path, 'wb') as f:
+        f.write(raw.stdout)
+    return out_path
+
+
+# ---------- composite ----------
+def dominant_hex(png_path):
+    im = Image.open(png_path).convert('RGB').resize((128, 128))
+    arr = np.asarray(im).reshape(-1, 3)
+    q = (arr // 16 * 16).astype(np.uint8)
+    vals, counts = np.unique(q, axis=0, return_counts=True)
+    r, g, b = vals[counts.argmax()]
+    return f'#{r:02x}{g:02x}{b:02x}'
+
+
+def coverage_pct(png_path, base_hex, tol=26):
+    """Rough motif-coverage estimate: fraction of pixels NOT near the ground hex."""
+    arr = np.asarray(Image.open(png_path).convert('RGB').resize((256, 256)), np.float64)
+    base = np.array([int(base_hex[i:i + 2], 16) for i in (1, 3, 5)], np.float64)
+    d = np.sqrt(((arr - base) ** 2).sum(2))
+    return float((d > tol).mean()) * 100.0
+
+
+def composite_real_fibre(motif_png, ground, base_hex, out_path):
+    slug = ground.replace(' ', '-')
+    fibre_png = os.path.join(FIBREDIR, f'{slug}.png')
+    if not os.path.exists(fibre_png):
+        os.makedirs(FIBREDIR, exist_ok=True)
+        mkfibre.make_fibre(ground, 1024, 11).save(fibre_png, 'PNG')
+    cmd = ['python3', os.path.join(ROOT, 'scripts', 'seamless-bg-swap-file.py'),
+           '--src', motif_png,
+           '--texture-image', fibre_png, '--texture-name', f'real-{slug}',
+           '--base-hex', base_hex,
+           '--strength', '0.6', '--feather', '40',
+           '--mask-tolerance', '20', '--mask-feather', '1.2',
+           '--no-sanitize', '--out', out_path]
+    r = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
+    if r.returncode != 0 or not os.path.exists(out_path):
+        raise RuntimeError(f'bg-swap composite failed rc={r.returncode}: {r.stderr[-300:]}')
+    return r.stdout
+
+
+def insert_child(root, out_path, positive, negative, ground, cn_strength):
+    """Insert the child row. dw_admin has INSERT on all_designs but NO USAGE on
+    spoon_all_designs_id_seq, so we supply an EXPLICIT id = max(id)+1 (never touch
+    the sequence). Safe for this bounded, single-threaded 5-row sanity sample —
+    no concurrent inserts. is_published / user_removed FALSE; parent = root."""
+    pos = positive.replace("'", "''")
+    neg = negative.replace("'", "''")
+    # image_url is built IN the INSERT (not a later UPDATE): all_designs is in a
+    # logical-replication PUBLICATION with no replica identity, so UPDATE without
+    # a PK identity errors. The new id is the MAX(id)+1 computed once in a CTE so
+    # image_url can reference the same value the row is inserted with.
+    new_id = int(sfv.psql(
+        "WITH nid AS (SELECT COALESCE(MAX(id),0)+1 AS id FROM all_designs) "
+        "INSERT INTO all_designs "
+        "(id, category, kind, prompt, negative_prompt, local_path, image_url, dominant_hex, "
+        " width_in, height_in, seed, tags, is_published, user_removed, generator, "
+        " parent_design_id, source_dw_sku) "
+        "SELECT nid.id, "
+        f"  s.category, s.kind, '{pos}', '{neg}', '{out_path}', "
+        "  '/designs/img/by-id/' || nid.id, s.dominant_hex, "
+        "  s.width_in, s.height_in, s.seed, "
+        f"  (COALESCE(s.tags, ARRAY[]::text[])) || "
+        f"  ARRAY['controlnet-anchored-redo','canny-sdxl-cn','real-fibre-{ground.replace(' ','-')}','open-composition','focal-crop','redo-sample-from-{root}']::text[], "
+        f"  FALSE, FALSE, 'controlnet-anchored-redo', {root}, s.source_dw_sku "
+        f"FROM (SELECT * FROM all_designs WHERE id={root} LIMIT 1) s, nid RETURNING id;"))
+    return new_id
+
+
+def root_meta_one(root):
+    """Local meta fetch with LIMIT 1 — some root ids are duplicated in all_designs
+    (snapshot import dupes), and sfv.root_meta json.loads-es ALL matching rows and
+    chokes on 'Extra data'. We take exactly one."""
+    row = sfv.psql(
+        "SELECT row_to_json(t) FROM (SELECT id, category, kind, prompt, local_path "
+        f"FROM all_designs WHERE id={root} LIMIT 1) t;")
+    if not row:
+        raise RuntimeError(f'no design {root}')
+    return json.loads(row.splitlines()[0])
+
+
+def process(root, ground, cn_strength, cn_end, crop_frac, motif_px):
+    os.makedirs(GENDIR, exist_ok=True)
+    meta = 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')
+    if not prompt:
+        pr = sfv.psql(f"SELECT COALESCE(prompt,'') FROM all_designs WHERE id={root} LIMIT 1;").splitlines()
+        prompt = pr[0] if pr else ''
+    subject = extract_subject(prompt)
+    animal = short_animal(prompt)
+    positive, negative = build_prompt(subject, animal)
+
+    root_img = Image.open(src).convert('RGB')
+    focal, crop_box = find_focal_crop(root_img, crop_frac)
+    hint = build_canny_hint(focal, motif_px)
+
+    seed = int.from_bytes(os.urandom(4), 'big') % (2**31 - 1)
+    ts = int(time.time() * 1000)
+    hint_png = os.path.join(GENDIR, f'{ts}_{seed}_cnar_hint.png')
+    motif_png = os.path.join(GENDIR, f'{ts}_{seed}_cnar_motif.png')
+    final_png = os.path.join(GENDIR, f'{ts}_{seed}_cnar.png')
+    hint.save(hint_png, 'PNG')
+
+    # 1+2. controlnet-anchored native-seamless txt2img (zero smear, subject anchored)
+    gen_controlnet_seamless(positive, negative, seed, hint_png, cn_strength, cn_end, motif_png)
+
+    # composition stats on the raw motif tile (before fibre)
+    base_hex = dominant_hex(motif_png)
+    cov = coverage_pct(motif_png, base_hex)
+
+    # 3. real-fibre composite (deterministic, both layers wrap)
+    composite_real_fibre(motif_png, ground, base_hex, final_png)
+
+    # 4. true-toroidal edge gate on the FINAL composite
+    ev = edgeverify.verify_path(Path(final_png))
+
+    new_id = 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, 'variant': 'controlnet-anchored-redo',
+        'generator': 'controlnet-anchored-redo',
+        'category': meta.get('category'), 'subject': subject, 'animal': animal,
+        'ground': ground, 'seed': seed, 'base_hex': base_hex,
+        'cn_strength': cn_strength, 'cn_end': cn_end, 'crop_box': crop_box,
+        'coverage_pct': round(cov, 1),
+        'technique': 'canny-SDXL-CN-anchored native-seamless txt2img (SeamlessTile+MakeCircularVAE, denoise 1.0, cropped focal canny hint) + deterministic real-fibre composite — NO smear',
+        'edge_verdict': ev.get('verdict'), 'edge_seamless': bool(ev.get('ok')),
+        'edge_axes': {'h': ev.get('axes', {}).get('horizontal', {}),
+                      'v': ev.get('axes', {}).get('vertical', {})},
+        'new_id': new_id, 'hint': hint_png, 'motif': motif_png, 'out': final_png, 'ok': True,
+    }
+
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument('--ids', required=True)
+    ap.add_argument('--ground', help='force one ground for all (else rotates)')
+    ap.add_argument('--cn-strength', type=float, default=0.5, help='canny CN strength (DTD: ~0.5)')
+    ap.add_argument('--cn-end', type=float, default=0.55, help='CN end_percent (release control late so layout opens)')
+    ap.add_argument('--crop-frac', type=float, default=0.32, help='focal crop side / root short edge (tighter = one head, less filler)')
+    ap.add_argument('--motif-px', type=int, default=175, help='target motif height in a 512 frame')
+    args = ap.parse_args()
+    ids = [int(x) for x in args.ids.split(',') if x.strip()]
+
+    results = []
+    for i, rid in enumerate(ids):
+        ground = args.ground or GROUNDS[i % len(GROUNDS)]
+        try:
+            r = process(rid, ground, args.cn_strength, args.cn_end, args.crop_frac, args.motif_px)
+        except Exception as e:
+            r = {'root_id': rid, 'ok': False, 'error': str(e), 'ground': ground}
+        results.append(r)
+        if r.get('ok'):
+            with open(QUEUE, 'a') as q:
+                q.write(json.dumps(r) + '\n')
+        v = r.get('edge_verdict', r.get('error', '?'))
+        ax = r.get('edge_axes', {})
+        h = (ax.get('h') or {}).get('verdict'); vv = (ax.get('v') or {}).get('verdict')
+        print(f"root {rid} ground={r.get('ground')}: edge={v} h={h} v={vv} "
+              f"cov={r.get('coverage_pct')}% new_id={r.get('new_id')}", file=sys.stderr)
+
+    print(json.dumps(results, indent=2))
+    sys.exit(0 if all(r.get('ok') for r in results) else 1)
+
+
+if __name__ == '__main__':
+    main()

← fc72875 Wire seam-fix wave cadence (DTD verdict C): gated one-wave-p  ·  back to Wallco Ai  ·  TODO: ControlNet redo sample landed (1 PASS, 2 near-miss WAR b9e1b4d →