← back to Wallco Ai
Replicate fallback: sanctioned feather seam-heal + fix post-process skip
f9f8f896e21961cbdacbd036c615b702e674babe · 2026-06-01 16:57:36 -0700 · Steve Abrams
- New scripts/seam-heal-feather.py: thin-band Gaussian feather seam repair
(the Smart-Fix technique Steve kept when the composite make_seamless.py was
disabled 2026-05-24). Self-gates — heals ONLY tiles that FAIL the seam gate,
leaves passing tiles byte-for-byte untouched (Round-1 sacred). Validated: a
SEAMLESS_FAIL_LR tile (lr=51.4) heals to SEAMLESS (lr=2.8/tb=5.1).
- generate_designs.js: comfy->replicate fallback no longer early-returns, so
fallback output now gets quantize-no-ghost + the seam heal (it silently
skipped BOTH before -> raw seams -> gate fail). Tracks usedBackend; heal runs
only on non-comfy backends (comfy is already circular-padded). WALLCO_SEAM_HEAL=0
opts out.
- deploy-kamatera.sh: ship seam-heal-feather.py + quantize-no-ghost.py (now
runtime deps of the fallback path; scripts/ is excluded from main rsync).
Files touched
M deploy-kamatera.shM scripts/generate_designs.jsA scripts/seam-heal-feather.py
Diff
commit f9f8f896e21961cbdacbd036c615b702e674babe
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jun 1 16:57:36 2026 -0700
Replicate fallback: sanctioned feather seam-heal + fix post-process skip
- New scripts/seam-heal-feather.py: thin-band Gaussian feather seam repair
(the Smart-Fix technique Steve kept when the composite make_seamless.py was
disabled 2026-05-24). Self-gates — heals ONLY tiles that FAIL the seam gate,
leaves passing tiles byte-for-byte untouched (Round-1 sacred). Validated: a
SEAMLESS_FAIL_LR tile (lr=51.4) heals to SEAMLESS (lr=2.8/tb=5.1).
- generate_designs.js: comfy->replicate fallback no longer early-returns, so
fallback output now gets quantize-no-ghost + the seam heal (it silently
skipped BOTH before -> raw seams -> gate fail). Tracks usedBackend; heal runs
only on non-comfy backends (comfy is already circular-padded). WALLCO_SEAM_HEAL=0
opts out.
- deploy-kamatera.sh: ship seam-heal-feather.py + quantize-no-ghost.py (now
runtime deps of the fallback path; scripts/ is excluded from main rsync).
---
deploy-kamatera.sh | 2 +-
scripts/generate_designs.js | 62 ++++++++++++--------
scripts/seam-heal-feather.py | 137 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 177 insertions(+), 24 deletions(-)
diff --git a/deploy-kamatera.sh b/deploy-kamatera.sh
index 66e3032..594478e 100755
--- a/deploy-kamatera.sh
+++ b/deploy-kamatera.sh
@@ -105,7 +105,7 @@ rsync -az "$LOCAL_DIR/scripts/refresh_designs_snapshot.py" \
# for those features to work. List grows narrowly as endpoints add deps;
# don't unbox scripts/ wholesale.
echo "[2d/6] ship runtime-endpoint scripts..."
-for script in scripts/seam-defect-boxes.py scripts/mint-shopify-download-token.js scripts/generate_designs.js scripts/generator_tick.js; do
+for script in scripts/seam-defect-boxes.py scripts/mint-shopify-download-token.js scripts/generate_designs.js scripts/generator_tick.js scripts/seam-heal-feather.py scripts/quantize-no-ghost.py; do
if [ -f "$LOCAL_DIR/$script" ]; then
rsync -az "$LOCAL_DIR/$script" "$REMOTE:$REMOTE_DIR/$script" && echo " $script shipped"
fi
diff --git a/scripts/generate_designs.js b/scripts/generate_designs.js
index f3854e4..ea0798e 100644
--- a/scripts/generate_designs.js
+++ b/scripts/generate_designs.js
@@ -572,25 +572,32 @@ function generate(prompt, seed, outPath, meta) {
}
}
+ // Track which backend ACTUALLY rendered the pixels (may differ from `backend`
+ // after a comfy→replicate fallback). The seam post-process keys off this:
+ // comfy output is circular-padded (truly seamless) and needs no heal; every
+ // other backend ships padding='zeros' and gets the deterministic feather heal.
+ let usedBackend = backend;
try {
callBackend(backend);
} catch (err) {
if (err instanceof BackendUnreachableError) {
// Check for optional comfy→replicate fallback (WALLCO_COMFY_FALLBACK=replicate).
const fallback = process.env.WALLCO_COMFY_FALLBACK;
- if (backend === 'comfy' && fallback === 'replicate') {
- const tok = loadReplicateToken();
- if (tok) {
- console.warn(` ⚠ ComfyUI unreachable — falling back to Replicate (seam quality degraded)`);
- genReplicate(prompt, seed, outPath, meta);
- // Re-tag what actually ran so downstream logs show "replicate" not "comfy"
- return; // success via fallback; post-process below continues normally
- }
+ if (backend === 'comfy' && fallback === 'replicate' && loadReplicateToken()) {
+ console.warn(` ⚠ ComfyUI unreachable — falling back to Replicate (seam quality degraded; feather-heal will run)`);
+ genReplicate(prompt, seed, outPath, meta);
+ usedBackend = 'replicate';
+ // FALL THROUGH to the post-process (quantize-no-ghost + seam heal). The
+ // old code early-returned here, which silently skipped BOTH steps on the
+ // fallback path — that's why Replicate-fallback tiles shipped raw and
+ // failed the seam gate. They now get the same cleanup as any other tile.
+ } else {
+ // No fallback available — re-throw so main() can emit the BACKEND_UNREACHABLE marker
+ throw err;
}
- // No fallback available — re-throw so main() can emit the BACKEND_UNREACHABLE marker
+ } else {
throw err;
}
- throw err;
}
// 2026-05-22 — Ghost-layer post-process. SDXL produces multi-opacity ghost
// copies of repeat motifs ~100% of the time even with strong anti-prompts
@@ -608,23 +615,32 @@ function generate(prompt, seed, outPath, meta) {
}
}
}
- // Seamless post-process — collapse edge-pixel Δ via offset-and-blend so the
- // tile repeats cleanly (raw-download / Spoonflower print files need this;
- // the room renderer hides seams on a composite but a print does not).
- // Steve 2026-05-19: every pattern MUST be seamlessly tileable. The ONLY
- // exception is a large mural panel (~11ft tall × 120" wide) — a single
- // non-repeating image. make_seamless is therefore MANDATORY for every
- // non-mural design (no env opt-out); murals skip it.
+ // Seamless post-process. Steve 2026-05-19: every pattern MUST be seamlessly
+ // tileable; the only exception is a mural panel (single non-repeating image).
+ //
+ // Two cases:
+ // • comfy backend → already circular-padded (SeamlessTile + MakeCircularVAE),
+ // truly seamless at the pixel level. No heal needed (make_seamless.py was a
+ // deliberate no-op anyway). Leave it byte-for-byte — Round-1 sacred.
+ // • any other backend (Replicate fallback / flux / stub) → padding='zeros',
+ // hard edge-wrap mismatch. Apply the SANCTIONED thin-band Gaussian feather
+ // repair (scripts/seam-heal-feather.py = the Smart-Fix technique Steve KEPT
+ // when he disabled the composite-overlay make_seamless on 2026-05-24). The
+ // script self-gates: it heals ONLY when the tile actually FAILS the seam
+ // gate, and leaves passing tiles untouched. Opt out with WALLCO_SEAM_HEAL=0.
const isMural = !!meta && (meta.kind === 'mural' || /mural/i.test(meta.category || ''));
- if (!isMural) {
- const seamlessScript = path.join(__dirname, 'make_seamless.py');
- if (fs.existsSync(seamlessScript) && fs.existsSync(outPath)) {
- const r = spawnSync('python3', [seamlessScript, outPath, outPath], { encoding: 'utf8' });
+ if (!isMural && usedBackend !== 'comfy' && process.env.WALLCO_SEAM_HEAL !== '0') {
+ const healScript = path.join(__dirname, 'seam-heal-feather.py');
+ if (fs.existsSync(healScript) && fs.existsSync(outPath)) {
+ const tol = process.env.SEAMLESS_TOLERANCE || '12';
+ const r = spawnSync('python3', [healScript, outPath, outPath, '--tolerance', tol], { encoding: 'utf8' });
if (r.status !== 0) {
- console.error('[make_seamless] FAILED — pattern is NOT seamless:', (r.stderr || r.stdout || '').slice(-200));
+ console.error('[seam-heal] FAILED — tile may still be non-seamless:', (r.stderr || r.stdout || '').slice(-200));
+ } else if (r.stdout && r.stdout.trim()) {
+ console.log('[seam-heal]', r.stdout.trim());
}
} else {
- console.error('[make_seamless] script or output missing — pattern NOT verified seamless');
+ console.error('[seam-heal] script or output missing — tile NOT verified seamless');
}
}
return outPath;
diff --git a/scripts/seam-heal-feather.py b/scripts/seam-heal-feather.py
new file mode 100644
index 0000000..b87a03b
--- /dev/null
+++ b/scripts/seam-heal-feather.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+"""seam-heal-feather.py — SANCTIONED deterministic seam repair for the
+non-circular-padded render path (Replicate fallback / flux / stub).
+
+WHY THIS EXISTS
+ The gold seam path is ComfyUI circular padding (SeamlessTile + MakeCircularVAE),
+ which makes the UNet+VAE Conv2d wrap so the tile is truly seamless at the
+ pixel level. When Mac1 ComfyUI is down and generation falls back to Replicate
+ stability-ai/sdxl (padding='zeros'), the output has hard edge-wrap mismatches
+ that FAIL the seam gate (scripts/seamless-check.py, edge ΔE > tolerance).
+
+ make_seamless.py is DELIBERATELY a no-op (disabled 2026-05-24): its old
+ binary-mask COMPOSITE-overlay variant left a visible rectangular boundary on
+ centered-hero designs. The technique Steve explicitly KEPT is the Smart-Fix
+ "thin-band Gaussian feather" — shift the tile by half so the seam moves to the
+ centre, then feather a thin band across the new centre cross. NO composite,
+ NO binary mask, NO rectangular boundary. That is exactly what this script does
+ (same algorithm/params as scripts/fix-seam.py shift_and_heal).
+
+ROUND-1-SACRED
+ We do NOT reroll and we do NOT touch tiles that already pass. By default the
+ heal fires ONLY when the tile FAILS the seam gate (it would be unpublishable
+ as-is). Pass --force to heal regardless; pass --check-only to just print the
+ verdict and exit (0 = seamless, 1 = would-heal) without writing.
+
+Usage:
+ python3 scripts/seam-heal-feather.py <in> [<out>] [--tolerance 12] [--force] [--check-only]
+ (out defaults to in — in-place)
+"""
+import argparse
+import json
+import shutil
+import sys
+from pathlib import Path
+
+import numpy as np
+from PIL import Image, ImageFilter
+
+ISOLATED_MOTIF_VAR_THRESHOLD = 25.0
+
+
+def edge_verdict(img, tolerance):
+ """Mirror scripts/seamless-check.py exactly (downscale to 512, straight +
+ half-drop diffs, isolated-motif variance) so the heal decision matches the
+ gate that will judge the output downstream."""
+ im = img.copy()
+ im.thumbnail((512, 512))
+ a = np.asarray(im.convert('RGB'), dtype=np.float32)
+ h, w, _ = a.shape
+ top, bot = a[0, :, :], a[h - 1, :, :]
+ left, right = a[:, 0, :], a[:, w - 1, :]
+ tb = float(np.mean(np.abs(top - bot)))
+ lr = float(np.mean(np.abs(left - right)))
+ tb_hd = float(np.mean(np.abs(top - np.roll(bot, w // 2, axis=0))))
+ lr_hd = float(np.mean(np.abs(left - np.roll(right, h // 2, axis=0))))
+
+ def var(strip):
+ return float(np.var(strip, axis=0).sum())
+
+ top_v, bot_v, left_v, right_v = var(top), var(bot), var(left), var(right)
+ straight_ok = (tb <= tolerance) and (lr <= tolerance)
+ halfdrop_ok = (tb_hd <= tolerance) and (lr_hd <= tolerance)
+ isolated = (top_v < ISOLATED_MOTIF_VAR_THRESHOLD and bot_v < ISOLATED_MOTIF_VAR_THRESHOLD and
+ left_v < ISOLATED_MOTIF_VAR_THRESHOLD and right_v < ISOLATED_MOTIF_VAR_THRESHOLD)
+ ok = (not isolated) and (straight_ok or halfdrop_ok)
+ code = ('ISOLATED_MOTIF' if isolated else
+ 'SEAMLESS' if straight_ok else
+ 'SEAMLESS_HALFDROP' if halfdrop_ok else 'SEAMLESS_FAIL')
+ return {'ok': ok, 'code': code, 'tb': round(tb, 2), 'lr': round(lr, 2),
+ 'tb_hd': round(tb_hd, 2), 'lr_hd': round(lr_hd, 2)}
+
+
+def shift_and_heal(im):
+ """Shift the tile by half (wrap) so the original edge seam lands on the
+ centre cross, then feather a thin Gaussian band over that cross. Pure
+ quad-shift + feather — NO composite overlay. Identical technique + params to
+ fix-seam.py shift_and_heal, with the band width scaled to resolution."""
+ im = im.convert('RGB')
+ w, h = im.size
+ out = Image.new('RGB', (w, h))
+ out.paste(im.crop((w // 2, h // 2, w, h)), (0, 0))
+ out.paste(im.crop((0, h // 2, w // 2, h)), (w // 2, 0))
+ out.paste(im.crop((w // 2, 0, w, h // 2)), (0, h // 2))
+ out.paste(im.crop((0, 0, w // 2, h // 2)), (w // 2, h // 2))
+ band = max(12, round(w * 0.012)) # ~12px at 1024, scales up for bigger tiles
+ blur = 2.5
+ vband = out.crop((w // 2 - band, 0, w // 2 + band, h)).filter(ImageFilter.GaussianBlur(blur))
+ out.paste(vband, (w // 2 - band, 0))
+ hband = out.crop((0, h // 2 - band, w, h // 2 + band)).filter(ImageFilter.GaussianBlur(blur))
+ out.paste(hband, (0, h // 2 - band))
+ return out
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('input')
+ ap.add_argument('output', nargs='?', default=None)
+ ap.add_argument('--tolerance', type=float, default=12.0)
+ ap.add_argument('--force', action='store_true',
+ help='heal even if the tile already passes the gate')
+ ap.add_argument('--check-only', action='store_true',
+ help='print verdict, write nothing (exit 0=seamless, 1=would-heal)')
+ args = ap.parse_args()
+
+ src = Path(args.input)
+ out = Path(args.output) if args.output else src
+ if not src.exists():
+ print(f'ERR: input not found: {src}', file=sys.stderr)
+ sys.exit(2)
+
+ img = Image.open(src)
+ before = edge_verdict(img, args.tolerance)
+
+ if args.check_only:
+ print(json.dumps({'phase': 'check', **before}))
+ sys.exit(0 if before['ok'] else 1)
+
+ if before['ok'] and not args.force:
+ # Round-1 sacred: a tile that already passes is left byte-for-byte alone.
+ if str(src.resolve()) != str(out.resolve()):
+ shutil.copyfile(src, out)
+ print(json.dumps({'phase': 'skip', 'reason': 'already-seamless', **before}))
+ sys.exit(0)
+
+ healed = shift_and_heal(img)
+ healed.save(out, 'PNG', optimize=True)
+ after = edge_verdict(healed, args.tolerance)
+ print(json.dumps({'phase': 'healed',
+ 'before': {k: before[k] for k in ('code', 'tb', 'lr', 'tb_hd', 'lr_hd')},
+ 'after': {k: after[k] for k in ('code', 'tb', 'lr', 'tb_hd', 'lr_hd')}}))
+ # Exit non-zero only if the heal failed to clear the gate (shouldn't happen —
+ # the shifted edges are former interior content — but surface it if it does).
+ sys.exit(0 if after['ok'] else 1)
+
+
+if __name__ == '__main__':
+ main()
← 64c2db6 security: admin-gate the entire /api/generator/* surface + /
·
back to Wallco Ai
·
render-time color-name consistency on /api/design/:id + 404 0288f88 →