[object Object]

← back to Wallco Ai

generator: every new design self-archives TIF+SVG via build-tif hook

c198fffa489c7e21d6cb949527f3e3a1754cd89d · 2026-06-02 08:26:12 -0700 · Steve Abrams

- generate_designs.js: after each persistDesign, run build-tif.py --force-svg
  (single chokepoint covers standing tick, /api/generator/batch, manual runs)
- build-tif.py: TIFs default to /Volumes/Henry archive (Mac2 boot vol ~93% full);
  WALLCO_TIF_DIR/WALLCO_SVG_DIR overrides; free_gb guards the write volume
- build-tif.py: --force-svg / WALLCO_FORCE_SVG always emits an SVG (raster-embed
  fallback) so every SKU gets a .svg even when not vector-eligible
Root cause of the 2026-06-02 damask tif/svg backlog: comfy path was PNG-only.

Files touched

Diff

commit c198fffa489c7e21d6cb949527f3e3a1754cd89d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 2 08:26:12 2026 -0700

    generator: every new design self-archives TIF+SVG via build-tif hook
    
    - generate_designs.js: after each persistDesign, run build-tif.py --force-svg
      (single chokepoint covers standing tick, /api/generator/batch, manual runs)
    - build-tif.py: TIFs default to /Volumes/Henry archive (Mac2 boot vol ~93% full);
      WALLCO_TIF_DIR/WALLCO_SVG_DIR overrides; free_gb guards the write volume
    - build-tif.py: --force-svg / WALLCO_FORCE_SVG always emits an SVG (raster-embed
      fallback) so every SKU gets a .svg even when not vector-eligible
    Root cause of the 2026-06-02 damask tif/svg backlog: comfy path was PNG-only.
---
 scripts/build-tif.py        | 36 ++++++++++++++++++++++++++++--------
 scripts/generate_designs.js | 20 ++++++++++++++++++++
 2 files changed, 48 insertions(+), 8 deletions(-)

diff --git a/scripts/build-tif.py b/scripts/build-tif.py
index 5453968..69a08a9 100755
--- a/scripts/build-tif.py
+++ b/scripts/build-tif.py
@@ -36,8 +36,15 @@ from psycopg2.extras import RealDictCursor
 from PIL import Image, ImageStat
 
 ROOT = Path(__file__).resolve().parents[1]
-TIF_DIR = ROOT / "data" / "tif"
-SVG_DIR = ROOT / "data" / "svg"
+# TIFs are large (multi-MB) print archives. Mac2's boot volume runs at ~93%, so
+# the canonical archive lives on the Henry external volume alongside the existing
+# 10k-file archive. Fall back to the repo dir when Henry isn't mounted, and allow
+# an explicit override via WALLCO_TIF_DIR. SVGs are small and keep their
+# established home in the repo (data/svg) where the server resolver looks.
+_HENRY_TIF = Path("/Volumes/Henry/wallco-ai-archive/tif")
+TIF_DIR = Path(os.environ.get("WALLCO_TIF_DIR")
+               or (_HENRY_TIF if _HENRY_TIF.is_dir() else ROOT / "data" / "tif"))
+SVG_DIR = Path(os.environ.get("WALLCO_SVG_DIR") or (ROOT / "data" / "svg"))
 TIF_DIR.mkdir(parents=True, exist_ok=True)
 SVG_DIR.mkdir(parents=True, exist_ok=True)
 
@@ -58,8 +65,9 @@ def db():
     )
 
 
-def free_gb(path=ROOT):
-    s = shutil.disk_usage(path)
+def free_gb(path=None):
+    # Guard the volume we actually write the (large) TIFs to — Henry when mounted.
+    s = shutil.disk_usage(path or TIF_DIR)
     return s.free / (1024**3)
 
 
@@ -181,7 +189,13 @@ def compute_max_print(kind, w_px, h_px):
     return round(w_px / DPI, 2), round(h_px / DPI, 2)
 
 
-def build_one(design_id, force=False, check_only=False):
+def build_one(design_id, force=False, check_only=False, force_svg=None):
+    # force_svg: write an SVG even when the design isn't crisp/vector-eligible
+    # (raster-embed fallback). Steve's rule: every SKU gets an SVG file. Default
+    # pulls from WALLCO_FORCE_SVG so the generator hook can opt every new pattern
+    # in without changing the signature for unrelated callers.
+    if force_svg is None:
+        force_svg = os.environ.get("WALLCO_FORCE_SVG") == "1"
     if free_gb() < MIN_FREE_GB:
         print(f"REFUSE: only {free_gb():.1f} GB free (need ≥ {MIN_FREE_GB}).",
               file=sys.stderr)
@@ -236,8 +250,11 @@ def build_one(design_id, force=False, check_only=False):
 
     svg_bytes = None
     svg_is_vector = False
-    if eligible:
-        svg_bytes, svg_is_vector = write_svg(img, svg_path, src, vector_attempted=True)
+    if eligible or force_svg:
+        # Attempt real vectorization only when the design is crisp enough to be
+        # worth it; otherwise (force_svg on a fuzzy design) go straight to the
+        # raster-embed SVG so every SKU still gets a scalable .svg file.
+        svg_bytes, svg_is_vector = write_svg(img, svg_path, src, vector_attempted=eligible)
         print(f"  ✓ SVG  {svg_path}  {svg_bytes:,} bytes "
               f"({'real vector' if svg_is_vector else 'embedded raster — install vtracer for real vectors'})")
 
@@ -383,6 +400,8 @@ def main():
     p.add_argument("design_id", type=int, nargs="?")
     p.add_argument("--force", action="store_true")
     p.add_argument("--check", action="store_true")
+    p.add_argument("--force-svg", dest="force_svg", action="store_true",
+                   help="Always emit an SVG (raster-embed fallback) even for fuzzy/non-crisp designs")
     p.add_argument("--bulk-eligible-only", action="store_true",
                    help="Scan all published designs; write TIF+SVG only for those ≤8 dominant colors")
     p.add_argument("--rebuild", action="store_true",
@@ -393,7 +412,8 @@ def main():
         sys.exit(bulk_eligible_only(rebuild=args.rebuild, concurrency=args.concurrency))
     if args.design_id is None:
         p.error("design_id is required (or pass --bulk-eligible-only)")
-    sys.exit(build_one(args.design_id, force=args.force, check_only=args.check))
+    sys.exit(build_one(args.design_id, force=args.force, check_only=args.check,
+                       force_svg=(args.force_svg or None)))
 
 
 if __name__ == "__main__":
diff --git a/scripts/generate_designs.js b/scripts/generate_designs.js
index 955df34..7b8d86f 100644
--- a/scripts/generate_designs.js
+++ b/scripts/generate_designs.js
@@ -914,6 +914,26 @@ async function main() {
       });
       console.log(`  ✓ #${id} · ${thisWidth}" · seed ${seed} · ${path.basename(outPath)} · ©`);
       created.push(id);
+
+      // ── TIF + SVG print-master (every new pattern must self-archive) ──────
+      // The publish gate refuses a NULL tif_path, so a design with no TIF can
+      // never go live. This is the single chokepoint every generation path
+      // funnels through (standing tick, /api/generator/batch, manual runs), so
+      // building the 150-DPI TIF (+SVG if crisp) right here guarantees no design
+      // is ever left tif-less again. (Root cause of the 2026-06-02 damask backlog:
+      // the comfy path produced PNG-only and this step did not exist.) Synchronous
+      // + non-fatal: build-tif self-guards on disk and a failure only warns —
+      // it never blocks the design that was just created.
+      try {
+        const bt = spawnSync('python3', [path.join(__dirname, 'build-tif.py'), String(id), '--force-svg'],
+          { encoding: 'utf8', timeout: 180000, env: { ...process.env, WALLCO_FORCE_SVG: '1' } });
+        if (bt.status === 0) {
+          const last = (bt.stdout || '').trim().split('\n').slice(-2).join(' · ');
+          if (last) console.log(`  ✓ archive: ${last}`);
+        } else {
+          console.warn('  ⚠ tif/svg build skipped:', (bt.stderr || bt.stdout || '').slice(-180));
+        }
+      } catch (e) { console.warn('  ⚠ tif/svg build error:', e.message); }
     } catch (e) {
       if (e instanceof BackendUnreachableError) {
         // Emit a machine-parseable marker so the batch-route log-scanner in

← 2b09de8 edges-review + seam-debug + PDP: status filter + per-surface  ·  back to Wallco Ai  ·  PDP: '✓ smart-fix → #child' pill linking a design to its pub 0bbf5ba →