← back to Wallco Ai
wallco redo: real-fibre composite path + img2img native-seamless orchestrator
3a53bd5a4ced1fabd17cf1ebeefb2fe89fd633ee · 2026-06-11 15:04:40 -0700 · Steve Abrams
- make-fibre-ground.py: deterministic SEAMLESS natural-fibre swatch generator
(grasscloth/raffia/linen/sisal/paperweave) — wraps by construction (edge ΔE
0.00), fills the real-fibre-ground gap (no local DW fibre swatch library).
Validated: composite of root #54266 onto raffia reads as genuine woven fibre.
- seamless-bg-swap-file.py: file-input wrapper around seamless-bg-swap.py so the
deterministic real-fibre composite runs on a fresh motif PNG (not just a DB id).
- native-seamless-redo.py: img2img native-seamless orchestrator (VAEEncode root +
SeamlessTile/MakeCircularVAE circular-pad, subject-anchored by starting from the
root latent) then real-fibre composite. Sacred roots, is_published=FALSE.
FINDING (sample BLOCKED, not shipped): proved txt2img circular-pad PASSES edges
cleanly (3.18/2.18) but has no subject anchor -> drift; img2img preserves subject
but breaks native-seamless edges to WARN AND can't open the busy root. No
ControlNet model + no IPAdapter installed on Mac1 ComfyUI = no anchor that works
at denoise-1.0 native-seamless strength. Minimum missing: an SDXL canny/lineart
ControlNet model on Mac1. Probes 58056/58057 removed (FALSE+user_removed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
A data/fibre-grounds/grasscloth.pngA data/fibre-grounds/natural-linen.pngA data/fibre-grounds/paperweave.pngA data/fibre-grounds/raffia.pngA data/fibre-grounds/sisal.pngM data/seam-fix-queue.jsonlA scripts/make-fibre-ground.pyA scripts/native-seamless-redo.pyA scripts/seamless-bg-swap-file.py
Diff
commit 3a53bd5a4ced1fabd17cf1ebeefb2fe89fd633ee
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jun 11 15:04:40 2026 -0700
wallco redo: real-fibre composite path + img2img native-seamless orchestrator
- make-fibre-ground.py: deterministic SEAMLESS natural-fibre swatch generator
(grasscloth/raffia/linen/sisal/paperweave) — wraps by construction (edge ΔE
0.00), fills the real-fibre-ground gap (no local DW fibre swatch library).
Validated: composite of root #54266 onto raffia reads as genuine woven fibre.
- seamless-bg-swap-file.py: file-input wrapper around seamless-bg-swap.py so the
deterministic real-fibre composite runs on a fresh motif PNG (not just a DB id).
- native-seamless-redo.py: img2img native-seamless orchestrator (VAEEncode root +
SeamlessTile/MakeCircularVAE circular-pad, subject-anchored by starting from the
root latent) then real-fibre composite. Sacred roots, is_published=FALSE.
FINDING (sample BLOCKED, not shipped): proved txt2img circular-pad PASSES edges
cleanly (3.18/2.18) but has no subject anchor -> drift; img2img preserves subject
but breaks native-seamless edges to WARN AND can't open the busy root. No
ControlNet model + no IPAdapter installed on Mac1 ComfyUI = no anchor that works
at denoise-1.0 native-seamless strength. Minimum missing: an SDXL canny/lineart
ControlNet model on Mac1. Probes 58056/58057 removed (FALSE+user_removed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
data/fibre-grounds/grasscloth.png | Bin 0 -> 2104943 bytes
data/fibre-grounds/natural-linen.png | Bin 0 -> 1951456 bytes
data/fibre-grounds/paperweave.png | Bin 0 -> 1851233 bytes
data/fibre-grounds/raffia.png | Bin 0 -> 2054199 bytes
data/fibre-grounds/sisal.png | Bin 0 -> 2217115 bytes
data/seam-fix-queue.jsonl | 5 +
scripts/make-fibre-ground.py | 181 ++++++++++++++++++++
scripts/native-seamless-redo.py | 321 +++++++++++++++++++++++++++++++++++
scripts/seamless-bg-swap-file.py | 74 ++++++++
9 files changed, 581 insertions(+)
diff --git a/data/fibre-grounds/grasscloth.png b/data/fibre-grounds/grasscloth.png
new file mode 100644
index 0000000..afbe3b5
Binary files /dev/null and b/data/fibre-grounds/grasscloth.png differ
diff --git a/data/fibre-grounds/natural-linen.png b/data/fibre-grounds/natural-linen.png
new file mode 100644
index 0000000..461c33d
Binary files /dev/null and b/data/fibre-grounds/natural-linen.png differ
diff --git a/data/fibre-grounds/paperweave.png b/data/fibre-grounds/paperweave.png
new file mode 100644
index 0000000..0ee843e
Binary files /dev/null and b/data/fibre-grounds/paperweave.png differ
diff --git a/data/fibre-grounds/raffia.png b/data/fibre-grounds/raffia.png
new file mode 100644
index 0000000..d23a534
Binary files /dev/null and b/data/fibre-grounds/raffia.png differ
diff --git a/data/fibre-grounds/sisal.png b/data/fibre-grounds/sisal.png
new file mode 100644
index 0000000..0902290
Binary files /dev/null and b/data/fibre-grounds/sisal.png differ
diff --git a/data/seam-fix-queue.jsonl b/data/seam-fix-queue.jsonl
index cba63b5..0d5383c 100644
--- a/data/seam-fix-queue.jsonl
+++ b/data/seam-fix-queue.jsonl
@@ -174,3 +174,8 @@
{"ts": "2026-06-11T21:25:00.583895Z", "root_id": 54123, "variant": "edgeforce", "generator": "force-edge-seamless", "category": "drunk-animals", "strip_px": 16, "before_verdict": "FAIL", "before_max": 20.59, "after_verdict": "FAIL", "after_max": 20.37, "improved": true, "root_edge_verdict": "WARN", "edge_verdict": "PASS", "edge_seamless": true, "edge_axes": {"h": "PASS", "v": "PASS"}, "out": "/Users/stevestudio2/Projects/wallco-ai/data/seam-fix/54123_edgeforce.png", "ok": true, "new_id": 58046}
{"ts": "2026-06-11T21:25:00.776639Z", "root_id": 54266, "variant": "edgeforce", "generator": "force-edge-seamless", "category": "drunk-animals", "strip_px": 16, "before_verdict": "FAIL", "before_max": 24.56, "after_verdict": "FAIL", "after_max": 24.53, "improved": true, "root_edge_verdict": "WARN", "edge_verdict": "PASS", "edge_seamless": true, "edge_axes": {"h": "PASS", "v": "PASS"}, "out": "/Users/stevestudio2/Projects/wallco-ai/data/seam-fix/54266_edgeforce.png", "ok": true, "new_id": 58047}
{"ts": "2026-06-11T21:25:00.982885Z", "root_id": 54507, "variant": "edgeforce", "generator": "force-edge-seamless", "category": "drunk-animals", "strip_px": 16, "before_verdict": "FAIL", "before_max": 21.13, "after_verdict": "FAIL", "after_max": 13.26, "improved": true, "root_edge_verdict": "WARN", "edge_verdict": "PASS", "edge_seamless": true, "edge_axes": {"h": "PASS", "v": "PASS"}, "out": "/Users/stevestudio2/Projects/wallco-ai/data/seam-fix/54507_edgeforce.png", "ok": true, "new_id": 58048}
+{"ts": "2026-06-11T21:38:08.261899Z", "root_id": 2662, "variant": "native-seamless", "generator": "native-seamless-sample", "category": "drunk-animals", "subject": "reticulated giraffe with crooked smile, sipping from a coupe glass of ros\u00e9", "ground": "grasscloth", "seed": 531344081, "technique": "native-seamless (ComfyUI SeamlessTile + MakeCircularVAE circular-pad) \u2014 NO post-hoc smear", "edge_verdict": "WARN", "edge_seamless": false, "edge_axes": {"h": {"verdict": "PASS", "line_diff": 0.86, "line_diff_halfdrop": 91.77, "best": 0.86, "tiled_cross_seam_max": 6.62, "edges": "column[0] vs column[W-1]"}, "v": {"verdict": "WARN", "line_diff": 2.8, "line_diff_halfdrop": 50.17, "best": 2.8, "tiled_cross_seam_max": 17.12, "edges": "row[0] vs row[H-1]"}}, "new_id": 58050, "out": "/Users/stevestudio2/Projects/wallco-ai/data/generated/1781213789161_531344081_ns.png", "ok": true}
+{"ts": "2026-06-11T21:39:53.463413Z", "root_id": 54266, "variant": "native-seamless", "generator": "native-seamless-sample", "category": "drunk-animals", "subject": "wide-eyed Amazonian tree frog, pyramid-tower of stacked champagne coupes", "ground": "raffia", "seed": 832831745, "technique": "native-seamless (ComfyUI SeamlessTile + MakeCircularVAE circular-pad) \u2014 NO post-hoc smear", "edge_verdict": "WARN", "edge_seamless": false, "edge_axes": {"h": {"verdict": "PASS", "line_diff": 2.01, "line_diff_halfdrop": 18.95, "best": 2.01, "tiled_cross_seam_max": 10.26, "edges": "column[0] vs column[W-1]"}, "v": {"verdict": "WARN", "line_diff": 4.12, "line_diff_halfdrop": 102.03, "best": 4.12, "tiled_cross_seam_max": 14.49, "edges": "row[0] vs row[H-1]"}}, "new_id": 58051, "out": "/Users/stevestudio2/Projects/wallco-ai/data/generated/1781213888310_832831745_ns.png", "ok": true}
+{"ts": "2026-06-11T21:41:38.659638Z", "root_id": 54076, "variant": "native-seamless", "generator": "native-seamless-sample", "category": "drunk-animals", "subject": "shaggy orange orangutan clutching a banana cocktail, painkiller cocktail in a wide rocks glass with grated nutmeg dusting on top", "ground": "natural linen", "seed": 36110905, "technique": "native-seamless (ComfyUI SeamlessTile + MakeCircularVAE circular-pad) \u2014 NO post-hoc smear", "edge_verdict": "FAIL", "edge_seamless": false, "edge_axes": {"h": {"verdict": "FAIL", "line_diff": 16.94, "line_diff_halfdrop": 85.32, "best": 16.94, "tiled_cross_seam_max": 41.49, "edges": "column[0] vs column[W-1]"}, "v": {"verdict": "WARN", "line_diff": 8.49, "line_diff_halfdrop": 55.26, "best": 8.49, "tiled_cross_seam_max": 39.45, "edges": "row[0] vs row[H-1]"}}, "new_id": 58052, "out": "/Users/stevestudio2/Projects/wallco-ai/data/generated/1781213993518_36110905_ns.png", "ok": true}
+{"ts": "2026-06-11T21:43:23.825010Z", "root_id": 54610, "variant": "native-seamless", "generator": "native-seamless-sample", "category": "drunk-animals", "subject": "wobbly pink champagne elephant, spraying champagne bottle held above the head like a Formula-1 podium", "ground": "sisal", "seed": 1286070489, "technique": "native-seamless (ComfyUI SeamlessTile + MakeCircularVAE circular-pad) \u2014 NO post-hoc smear", "edge_verdict": "WARN", "edge_seamless": false, "edge_axes": {"h": {"verdict": "WARN", "line_diff": 5.73, "line_diff_halfdrop": 22.29, "best": 5.73, "tiled_cross_seam_max": 19.23, "edges": "column[0] vs column[W-1]"}, "v": {"verdict": "WARN", "line_diff": 7.7, "line_diff_halfdrop": 59.48, "best": 7.7, "tiled_cross_seam_max": 47.51, "edges": "row[0] vs row[H-1]"}}, "new_id": 58053, "out": "/Users/stevestudio2/Projects/wallco-ai/data/generated/1781214098716_1286070489_ns.png", "ok": true}
+{"ts": "2026-06-11T21:45:09.029059Z", "root_id": 53896, "variant": "native-seamless", "generator": "native-seamless-sample", "category": "drunk-animals", "subject": "plastered red panda, wide-bowl cognac snifter resting on its belly", "ground": "paperweave", "seed": 1597191492, "technique": "native-seamless (ComfyUI SeamlessTile + MakeCircularVAE circular-pad) \u2014 NO post-hoc smear", "edge_verdict": "WARN", "edge_seamless": false, "edge_axes": {"h": {"verdict": "WARN", "line_diff": 1.39, "line_diff_halfdrop": 77.31, "best": 1.39, "tiled_cross_seam_max": 12.85, "edges": "column[0] vs column[W-1]"}, "v": {"verdict": "WARN", "line_diff": 1.49, "line_diff_halfdrop": 1.6, "best": 1.49, "tiled_cross_seam_max": 108.23, "edges": "row[0] vs row[H-1]"}}, "new_id": 58054, "out": "/Users/stevestudio2/Projects/wallco-ai/data/generated/1781214203876_1597191492_ns.png", "ok": true}
diff --git a/scripts/make-fibre-ground.py b/scripts/make-fibre-ground.py
new file mode 100644
index 0000000..d8f1934
--- /dev/null
+++ b/scripts/make-fibre-ground.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python3
+"""make-fibre-ground.py — DETERMINISTIC seamless natural-fibre texture generator.
+
+WHY THIS EXISTS (the real-fibre-ground gap, 2026-06-11 redo brief):
+ seamless-bg-swap.py modulates a design's flat ground by a REAL fibre-texture
+ photo's high-frequency grain, so the background reads as genuine woven
+ grasscloth/raffia/linen/sisal/paperweave. But wallco-ai has NO local
+ natural-fibre swatch library (only one cached _texture.png from an old run).
+ Prompting SDXL for "grasscloth" gives a flat dead fill (the regression Steve
+ rejected). So we need an honest fibre-texture SOURCE that:
+ - reads as a real woven natural fibre (directional warp/weft strands, slubs,
+ irregular thickness — not a flat noise field),
+ - tiles SEAMLESSLY by construction (so the bg-swap composite stays seamless),
+ - is deterministic + free (no AI, no smear).
+
+ This builds that source PROCEDURALLY. The output is a grayscale-modulation
+ swatch (the bg-swap only uses luminance grain anyway), wrapped on both axes by
+ building it from periodic functions + WRAP-padded noise. The fibre TYPE sets
+ warp/weft frequency, slub density, and strand contrast so grasscloth ≠ raffia
+ ≠ linen ≠ sisal ≠ paperweave.
+
+Usage:
+ python3 scripts/make-fibre-ground.py --type grasscloth --out /abs/grass.png
+ python3 scripts/make-fibre-ground.py --type raffia --size 1024 --seed 7
+
+Output: a seamless RGB PNG (neutral warm-grey) carrying the fibre weave. Feed it
+to seamless-bg-swap.py --texture-image <out> --no-sanitize (it's already clean).
+"""
+import argparse
+import os
+import sys
+
+import numpy as np
+from PIL import Image
+from scipy.ndimage import gaussian_filter
+
+# Per-fibre weave parameters. (warp_freq, weft_freq) = strands across the tile;
+# slub_density = irregular thick spots; strand_contrast = grain depth; aniso =
+# how much warp dominates weft (a coarse raffia is very directional, linen
+# near-isotropic); base_tint = warm-neutral ground colour the weave sits on.
+FIBRE = {
+ # very coarse, strongly horizontal palm-fibre strands, frequent slubs
+ 'raffia': dict(warp=34, weft=22, slub=0.9, contrast=0.34, aniso=0.75, tint=(207, 188, 150)),
+ # fine even horizontal grass strands, tight, subtle slubs
+ 'grasscloth': dict(warp=70, weft=40, slub=0.45, contrast=0.26, aniso=0.62, tint=(205, 196, 168)),
+ # near-isotropic slubby flax weave, soft, irregular thread thickness
+ 'linen': dict(warp=58, weft=56, slub=0.7, contrast=0.22, aniso=0.18, tint=(214, 206, 184)),
+ 'natural linen': dict(warp=58, weft=56, slub=0.7, contrast=0.22, aniso=0.18, tint=(214, 206, 184)),
+ # tight regular sisal weave, pale, low slub, crisp strands
+ 'sisal': dict(warp=80, weft=64, slub=0.3, contrast=0.24, aniso=0.5, tint=(216, 206, 178)),
+ # rolled-paper weave: wide flat horizontal bands, soft, low contrast
+ 'paperweave': dict(warp=24, weft=46, slub=0.35, contrast=0.20, aniso=0.7, tint=(212, 202, 178)),
+}
+
+
+def _seamless_noise(size, scale, seed, octaves=4):
+ """Tileable value-noise via WRAP-padded gaussian-blurred white noise summed
+ over octaves. Wrapping is exact because gaussian_filter(mode='wrap') keeps
+ opposite edges continuous."""
+ rng = np.random.default_rng(seed)
+ H = W = size
+ acc = np.zeros((H, W), np.float64)
+ amp = 1.0
+ total = 0.0
+ s = scale
+ for o in range(octaves):
+ white = rng.standard_normal((H, W))
+ sig = max(0.6, s)
+ n = gaussian_filter(white, sigma=sig, mode='wrap')
+ n /= (n.std() or 1.0)
+ acc += amp * n
+ total += amp
+ amp *= 0.5
+ s *= 0.5
+ return acc / total
+
+
+def _strand_field(size, freq, phase_jitter, seed):
+ """A periodic strand pattern across `freq` strands that wraps exactly:
+ freq must be an integer number of cycles across the tile so sin() closes.
+ Adds wrap-safe per-strand phase jitter so strands aren't a perfect ruler."""
+ H = W = size
+ x = np.linspace(0, 2 * np.pi * freq, W, endpoint=False)
+ # wrap-safe row-wise phase wobble (tileable noise, 1px tall band reused)
+ wobble = _seamless_noise(size, size / max(freq, 1) * 0.5, seed) * phase_jitter
+ line = np.sin(x[None, :] + wobble)
+ # strand "ridges": |sin| gives rounded strand cross-sections
+ return np.abs(line)
+
+
+def _wrap_feather(arr, band):
+ """Force opposite edges of a 2-D float array EQUAL with the heal localized
+ in a `band`-px edge strip (mirror cross-blend, 0.5 at the very edge fading to
+ 0 by `band` inward). Centre untouched. arr[:,0]==arr[:,-1] and arr[0]==arr[-1]
+ after, so the tile wraps."""
+ H, W = arr.shape
+ out = arr.copy()
+ b = int(max(1, min(band, W // 2 - 1, H // 2 - 1)))
+ for i in range(b):
+ w = 0.5 * (1.0 - i / b)
+ out[:, i] = (1 - w) * arr[:, i] + w * arr[:, W - 1 - i]
+ out[:, W - 1 - i] = (1 - w) * arr[:, W - 1 - i] + w * arr[:, i]
+ g2 = out.copy()
+ for j in range(b):
+ w = 0.5 * (1.0 - j / b)
+ out[j, :] = (1 - w) * g2[j, :] + w * g2[H - 1 - j, :]
+ out[H - 1 - j, :] = (1 - w) * g2[H - 1 - j, :] + w * g2[j, :]
+ return out
+
+
+def make_fibre(fibre_type, size=1024, seed=11):
+ p = FIBRE.get(fibre_type)
+ if p is None:
+ raise ValueError(f'unknown fibre type {fibre_type!r}; choices={list(FIBRE)}')
+ H = W = size
+
+ # warp = horizontal strands (vary down the rows) -> use vertical frequency
+ # weft = vertical strands (vary across the cols)
+ warp = _strand_field(size, p['weft'], 0.35, seed + 1).T # transpose: strands run horizontally
+ weft = _strand_field(size, p['warp'], 0.35, seed + 2)
+ aniso = p['aniso']
+ weave = aniso * warp + (1 - aniso) * weft # over/under blend
+
+ # slubs: low-frequency irregular thickening of strands
+ slub = _seamless_noise(size, size / 14.0, seed + 3, octaves=3)
+ slub = (slub - slub.min()) / (float(np.ptp(slub)) or 1.0)
+ weave = weave * (1.0 + p['slub'] * 0.6 * (slub - 0.5))
+
+ # fine fibre grain on top (broken-strand fuzz), wrap-safe
+ grain = _seamless_noise(size, 1.6, seed + 4, octaves=2)
+
+ field = weave + 0.25 * grain
+ field = (field - field.mean()) / (field.std() or 1.0) # zero-mean unit-var
+
+ # GUARANTEE edge wrap: the integer-cycle sin strands are theoretically
+ # tileable, but the additive wobble + grain leave a small residual at the
+ # seam. A thin mirror-wrap band (same technique as seamless-bg-swap's
+ # make_seamless_wrap) forces opposite edges EQUAL while leaving the centre
+ # untouched, so the swatch tiles by construction. Band is ~3% of the tile so
+ # the woven look is preserved everywhere a viewer reads it.
+ field = _wrap_feather(field, max(8, size // 32))
+
+ # map to luminance modulation around the tint
+ tint = np.array(p['tint'], np.float64)
+ mod = p['contrast'] * field # +/- around 0
+ rgb = tint[None, None, :] * (1.0 + mod[..., None])
+ rgb = np.clip(rgb, 0, 255).astype(np.uint8)
+ return Image.fromarray(rgb, 'RGB')
+
+
+def _verify_seamless(img):
+ """Sanity: opposite edges of the fibre swatch must match (so the downstream
+ composite stays seamless). Reports max channel-mean edge ΔE."""
+ a = np.asarray(img, np.float64)
+ lr = float(np.mean(np.abs(a[:, 0, :] - a[:, -1, :])))
+ tb = float(np.mean(np.abs(a[0, :, :] - a[-1, :, :])))
+ return lr, tb
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--type', required=True, help=str(list(FIBRE)))
+ ap.add_argument('--out', required=True)
+ ap.add_argument('--size', type=int, default=1024)
+ ap.add_argument('--seed', type=int, default=11)
+ args = ap.parse_args()
+ img = make_fibre(args.type, args.size, args.seed)
+ os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
+ img.save(args.out, 'PNG', optimize=True)
+ lr, tb = _verify_seamless(img)
+ print(f'wrote {args.out} {img.size} type={args.type} edge ΔE L/R={lr:.2f} T/B={tb:.2f}',
+ file=sys.stderr)
+ # the procedural wrap should be near-zero; warn loudly if not
+ if max(lr, tb) > 6.0:
+ print(f'WARN: fibre swatch edge ΔE {max(lr,tb):.2f} > 6 — not cleanly seamless', file=sys.stderr)
+ sys.exit(1)
+ print(args.out)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/native-seamless-redo.py b/scripts/native-seamless-redo.py
new file mode 100644
index 0000000..0d6df9e
--- /dev/null
+++ b/scripts/native-seamless-redo.py
@@ -0,0 +1,321 @@
+#!/usr/bin/env python3
+"""native-seamless-redo.py — REDO of the BOUNDED sanity sample (2026-06-11).
+
+Steve rejected the first native-seamless sample (#58050-58054): the open+flat
+text prompt overwhelmed SDXL subject adherence (giraffe→waves, frog→leaf-blades)
+and "grasscloth" in the prompt gave a flat dead fill. Brief: "redo them."
+
+THE THREE FIXES (vs native-seamless-sample.py):
+
+1. ZERO smear — UNCHANGED mechanism: native-seamless generation only (ComfyUI
+ SeamlessTile + MakeCircularVAE circular pad). The tile wraps by construction.
+ We NEVER touch make_seamless.py / force-edge-seamless.py (the smear sources).
+
+2. SUBJECT ANCHORING — the first sample was txt2img (denoise 1.0) which let the
+ open/flat prompt drift the subject to abstract. This redo is IMG2IMG: we
+ VAEEncode the ROOT image and run KSampler at denoise≈0.58, so the diffusion
+ STARTS from the actual root composition. The giraffe stays a giraffe; the
+ prompt only nudges it toward open/sparse/two-tone, it can't invent waves.
+ (ControlNet + IPAdapter are NOT installed on this ComfyUI; img2img from the
+ root is the available, correct anchor — it preserves the root pixels' motif
+ identity directly.)
+
+3. REAL natural-fibre GROUND — we do NOT ask SDXL for "grasscloth" (flat dead
+ fill). Instead the clean two-tone img2img tile is run through the existing
+ deterministic composite path (seamless-bg-swap.py) using a REAL woven-fibre
+ swatch built by make-fibre-ground.py. The composite modulates ONLY the flat
+ ground's luminance by genuine warp/weft fibre grain; motif pixels are kept
+ byte-for-byte; both layers wrap, so the composite stays seamless.
+
+4. OPEN / less busy — img2img at moderate denoise + an "open, sparse, fewer
+ elements, generous negative space" prompt opens the dense root toile without
+ abandoning its motif.
+
+SACRED ROOTS: never overwrites a root. Each candidate → NEW file + NEW id with
+is_published=FALSE, user_removed=FALSE, parent_design_id=<root>. The final
+composite is 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/native-seamless-redo.py --ids 2662,54266,54076,54610,53896
+ python3 scripts/native-seamless-redo.py --ids 2662 --denoise 0.6
+"""
+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
+
+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')
+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'))
+
+# Rotate the natural ground across the sample so Steve sees range, not one note.
+GROUNDS = ['grasscloth', 'raffia', 'natural linen', 'sisal', 'paperweave']
+
+
+def extract_subject(prompt: str) -> str:
+ """First two comma clauses = the animal + drink subject; drop the busy
+ 'sunflowers / foliage / reference' bloat that follows."""
+ 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 build_prompt(subject: str):
+ """Open/sparse/two-tone prompt. NOTE — NO fibre-ground prompting here: the
+ ground texture is added deterministically downstream by the composite, so we
+ keep this purely about the MOTIF (flat two-tone silhouette) on a plain flat
+ ground. That plain flat ground is exactly what the composite needs to detect
+ and replace with real fibre."""
+ positive = (
+ f"A single {subject}, rendered as ONE clean flat two-tone silhouette "
+ "motif on a plain solid flat-colour ground. Open airy wallcovering "
+ "repeat — the motif is large, isolated and widely spaced with generous "
+ "empty negative space of bare flat ground around it; sparse minimal "
+ "pattern, calm and uncluttered, NOT busy, NOT edge-to-edge, NOT a dense "
+ "all-over print. The figure is a flat solid-colour silhouette in two "
+ "muted archival ink tones, low saturation tone-on-tone, hard clean "
+ "edges, no shading, no depth, no relief. Elegant heritage toile, "
+ "designer wallcovering."
+ )
+ negative = (
+ "busy, cluttered, dense, edge-to-edge, all-over print, tessellated "
+ "densely, packed motifs, many animals, crowded figures, sunflowers, "
+ "foliage filling the background, leaves filling the background, vines "
+ "everywhere, jungle scene, 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, 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 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(src_path):
+ """Upload the root PNG into ComfyUI's input dir; return the server filename."""
+ r = subprocess.run(
+ ['curl', '-sf', '-m', '60', '-X', 'POST', f'{COMFY}/upload/image',
+ '-F', f'image=@{src_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 gen_img2img_seamless(positive, negative, seed, root_png, denoise, out_path):
+ """img2img native-seamless: VAEEncode(root) -> KSampler(denoise<1) with the
+ SeamlessTile model + MakeCircularVAE circular-pad VAE -> SaveImage. Subject
+ anchored by starting from the root latent; edges wrap by construction (zero
+ post-hoc smear)."""
+ server_name = comfy_upload_image(root_png)
+ workflow = {
+ '4': {'class_type': 'CheckpointLoaderSimple', 'inputs': {'ckpt_name': MODEL}},
+ '10': {'class_type': 'LoadImage', 'inputs': {'image': server_name}},
+ # encode the root through the circular VAE so the start latent already wraps
+ '11': {'class_type': 'VAEEncode', 'inputs': {'pixels': ['10', 0], 'vae': ['2000', 0]}},
+ '6': {'class_type': 'CLIPTextEncode', 'inputs': {'text': positive, 'clip': ['4', 1]}},
+ '7': {'class_type': 'CLIPTextEncode', 'inputs': {'text': negative, 'clip': ['4', 1]}},
+ '3': {'class_type': 'KSampler', 'inputs': {
+ 'seed': seed, 'steps': 30, 'cfg': 7.5, 'sampler_name': SAMPLER,
+ 'scheduler': SCHEDULER, 'denoise': denoise,
+ 'model': ['1000', 0], 'positive': ['6', 0], 'negative': ['7', 0],
+ 'latent_image': ['11', 0]}},
+ '8': {'class_type': 'VAEDecode', 'inputs': {'samples': ['3', 0], 'vae': ['2000', 0]}},
+ '9': {'class_type': 'SaveImage', 'inputs': {'filename_prefix': f'wallco_nsr_{seed}', 'images': ['8', 0]}},
+ # circular-pad nodes — the native-seamless mechanism
+ '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'}},
+ }
+ submit = comfy_post('/prompt', {'prompt': workflow, 'client_id': 'wallco-nsr'})
+ pid = json.loads(submit).get('prompt_id')
+ if not pid:
+ raise RuntimeError('ComfyUI did not return prompt_id')
+ start = time.time()
+ images = 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 entry and 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})')
+ 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
+
+
+def dominant_hex(png_path):
+ """Most-common quantized colour = the flat ground, for the bg-swap mask."""
+ 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 composite_real_fibre(motif_png, ground, base_hex, out_path):
+ """Run the clean img2img motif tile through seamless-bg-swap.py with a REAL
+ procedural fibre swatch so the flat ground reads as woven natural fibre.
+ Returns the bg-swap stdout (contains its own edges-scan)."""
+ slug = ground.replace(' ', '-')
+ fibre_png = os.path.join(FIBREDIR, f'{slug}.png')
+ if not os.path.exists(fibre_png):
+ 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=120)
+ 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_with_prompt(root, out_path, positive, negative, ground):
+ pos = positive.replace("'", "''")
+ neg = negative.replace("'", "''")
+ new_id = int(sfv.psql(
+ "INSERT INTO all_designs "
+ "(category, kind, prompt, negative_prompt, local_path, dominant_hex, "
+ " width_in, height_in, seed, tags, is_published, user_removed, generator, "
+ " parent_design_id, source_dw_sku) "
+ f"SELECT category, kind, '{pos}', '{neg}', '{out_path}', dominant_hex, "
+ " width_in, height_in, seed, "
+ f" (COALESCE(tags, ARRAY[]::text[])) || "
+ f" ARRAY['native-seamless-redo','img2img-anchored','real-fibre-{ground.replace(' ','-')}','open-composition','redo-sample-from-{root}']::text[], "
+ f" FALSE, FALSE, 'native-seamless-redo', {root}, source_dw_sku "
+ f"FROM all_designs WHERE id={root} RETURNING id;"))
+ sfv.psql(f"UPDATE all_designs SET image_url='/designs/img/by-id/' || id WHERE id={new_id};")
+ return new_id
+
+
+def process(root, ground, denoise):
+ os.makedirs(GENDIR, exist_ok=True)
+ meta = sfv.root_meta(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'}
+
+ subject = extract_subject(meta.get('prompt') or
+ sfv.psql(f"SELECT prompt FROM all_designs WHERE id={root};"))
+ positive, negative = build_prompt(subject)
+
+ seed = int.from_bytes(os.urandom(4), 'big') % (2**31 - 1)
+ ts = int(time.time() * 1000)
+ motif_png = os.path.join(GENDIR, f'{ts}_{seed}_nsr_motif.png')
+ final_png = os.path.join(GENDIR, f'{ts}_{seed}_nsr.png')
+
+ # 1. img2img native-seamless (subject-anchored, zero smear)
+ gen_img2img_seamless(positive, negative, seed, src, denoise, motif_png)
+
+ # 2. real-fibre composite (deterministic, both layers wrap)
+ base_hex = dominant_hex(motif_png)
+ composite_real_fibre(motif_png, ground, base_hex, final_png)
+
+ # 3. true-toroidal edge gate on the FINAL composite
+ ev = edgeverify.verify_path(Path(final_png))
+
+ new_id = insert_child_with_prompt(root, final_png, positive, negative, ground)
+
+ return {
+ 'ts': datetime.datetime.now(datetime.timezone.utc).isoformat().replace('+00:00', 'Z'),
+ 'root_id': root, 'variant': 'native-seamless-redo',
+ 'generator': 'native-seamless-redo',
+ 'category': meta.get('category'), 'subject': subject, 'ground': ground,
+ 'seed': seed, 'denoise': denoise, 'base_hex': base_hex,
+ 'technique': 'img2img native-seamless (SeamlessTile+MakeCircularVAE, denoise<1, subject-anchored) + deterministic real-fibre composite (seamless-bg-swap) — 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, '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('--denoise', type=float, default=0.58)
+ 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.denoise)
+ 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"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()
diff --git a/scripts/seamless-bg-swap-file.py b/scripts/seamless-bg-swap-file.py
new file mode 100644
index 0000000..329f1f6
--- /dev/null
+++ b/scripts/seamless-bg-swap-file.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+"""seamless-bg-swap-file.py — file-input wrapper around seamless-bg-swap.py.
+
+seamless-bg-swap.py composites a REAL fibre texture's grain into a design's flat
+ground (deterministic, both layers wrap, motif kept byte-for-byte). But it only
+accepts --id (a row in spoon_all_designs). native-seamless-redo.py needs to run
+the SAME composite on a FRESH motif PNG that is not yet in the DB.
+
+This wrapper reuses seamless-bg-swap.py's composite functions (build_texture_layer,
+build_bg_mask, hex_to_rgb) verbatim on a source FILE + an explicit base-hex. No
+DB, no AI, no smear — identical math, file in / file out.
+
+Usage:
+ python3 scripts/seamless-bg-swap-file.py --src motif.png \
+ --texture-image data/fibre-grounds/grasscloth.png --base-hex '#dccdac' \
+ --out out.png [--strength .6 --feather 40 --mask-tolerance 20 --no-sanitize]
+"""
+import argparse
+import os
+import sys
+from pathlib import Path
+
+import numpy as np
+from PIL import Image
+
+HERE = Path(__file__).resolve().parent
+sys.path.insert(0, str(HERE))
+from importlib import import_module
+bgs = import_module('seamless-bg-swap') # reuse the proven composite functions
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--src', required=True, help='source motif PNG (flat ground + motif)')
+ ap.add_argument('--texture-image', required=True)
+ ap.add_argument('--texture-name', default='real-fibre')
+ ap.add_argument('--base-hex', required=True, help='flat ground colour to replace')
+ ap.add_argument('--out', required=True)
+ ap.add_argument('--strength', type=float, default=0.6)
+ ap.add_argument('--feather', type=int, default=40)
+ ap.add_argument('--mask-tolerance', type=float, default=20.0)
+ ap.add_argument('--mask-feather', type=float, default=1.2)
+ ap.add_argument('--no-sanitize', action='store_true')
+ args = ap.parse_args()
+
+ if not os.path.exists(args.src):
+ print(f'ERROR: src missing {args.src}', file=sys.stderr); sys.exit(2)
+
+ src = Image.open(args.src).convert('RGB')
+ src_arr = np.asarray(src, dtype=np.float64)
+ H, W = src_arr.shape[:2]
+ base_rgb = bgs.hex_to_rgb(args.base_hex)
+
+ tex = bgs.load_texture(args.texture_image, sanitize=not args.no_sanitize)
+ mult = bgs.build_texture_layer(tex, (H, W), args.feather, args.strength)
+ mask, de = bgs.build_bg_mask(src_arr, base_rgb, args.mask_tolerance, args.mask_feather)
+
+ bg_pct = 100.0 * float(mask.mean())
+ print(f'· src {W}x{H} ground={args.base_hex} bg-cover≈{bg_pct:.1f}% '
+ f'texture={args.texture_name} strength={args.strength}', file=sys.stderr)
+
+ bg_layer = np.clip(base_rgb.reshape(1, 1, 3) * mult[..., None], 0, 255)
+ m3 = mask[..., None]
+ out_arr = np.clip(m3 * bg_layer + (1.0 - m3) * src_arr, 0, 255).astype(np.uint8)
+
+ out_path = Path(args.out)
+ out_path.parent.mkdir(parents=True, exist_ok=True)
+ Image.fromarray(out_arr).save(out_path, 'PNG', optimize=True)
+ print(f'· wrote {out_path}', file=sys.stderr)
+ print(str(out_path))
+
+
+if __name__ == '__main__':
+ main()
← 3555103 TODO: native-seamless sanity sample results (smear killed, 2
·
back to Wallco Ai
·
Wire seam-fix wave cadence (DTD verdict C): gated one-wave-p fc72875 →