← 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
M scripts/build-tif.pyM scripts/generate_designs.js
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 →