[object Object]

← 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

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 →