[object Object]

← back to Wallco Ai

Purge 2,262 fuzzy-seam cactus tiles (start-fresh); add seam-purge + vision scorer

63e29271d3759728fe48d0ee255e5a037fc085f7 · 2026-05-27 08:18:03 -0700 · Steve Abrams

- cactus-seam-purge.py: rigorous aggressive purge of seamless_tile cactus with
  any fuzzy lens (edge or h_mid/v_mid center, ΔE>5). Rescanned 806 unscanned to
  full coverage via edges-scan scan_path, stores per-lens h_mid/v_mid for audit.
  Murals (mural/mural_panel, 160) explicitly excluded — no center seam.
  Soft-delete = unpublish + user_removed + quarantine PNG (recoverable) + audit.
  Result: 2,262 doomed → quarantined, 541 clean-PASS tiles survive.
- cactus-vision-score.js: free local-Ollama VL quality scorer (for ranking the
  survivors by catalog-resemblance; Mac1 currently slow, run post-purge).

Files touched

Diff

commit 63e29271d3759728fe48d0ee255e5a037fc085f7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 27 08:18:03 2026 -0700

    Purge 2,262 fuzzy-seam cactus tiles (start-fresh); add seam-purge + vision scorer
    
    - cactus-seam-purge.py: rigorous aggressive purge of seamless_tile cactus with
      any fuzzy lens (edge or h_mid/v_mid center, ΔE>5). Rescanned 806 unscanned to
      full coverage via edges-scan scan_path, stores per-lens h_mid/v_mid for audit.
      Murals (mural/mural_panel, 160) explicitly excluded — no center seam.
      Soft-delete = unpublish + user_removed + quarantine PNG (recoverable) + audit.
      Result: 2,262 doomed → quarantined, 541 clean-PASS tiles survive.
    - cactus-vision-score.js: free local-Ollama VL quality scorer (for ranking the
      survivors by catalog-resemblance; Mac1 currently slow, run post-purge).
---
 scripts/cactus-seam-purge.py   | 193 +++++++++++++++++++++++++++++++++++++++++
 scripts/cactus-vision-score.js | 129 +++++++++++++++++++++++++++
 2 files changed, 322 insertions(+)

diff --git a/scripts/cactus-seam-purge.py b/scripts/cactus-seam-purge.py
new file mode 100644
index 0000000..fb088a9
--- /dev/null
+++ b/scripts/cactus-seam-purge.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+"""cactus-seam-purge.py — rigorous seam-defect purge for the Cactus Curator.
+
+Steve's directive: delete any cactus SEAMLESS TILE with fuzzy edges OR a fuzzy
+internal center seam (the "2x2 internal-grid" defect, both horiz + vert
+centerlines). Aggressive threshold: any of the 6 lenses (top/bottom/left/right/
+h_mid/v_mid) over ΔE 5 (i.e. anything that isn't a clean PASS) is purged.
+
+Scope guard: ONLY kind='seamless_tile'. Murals (kind in mural / mural_panel)
+are single scenes / butt-join panels with no meaningful center seam — they are
+NEVER touched.
+
+Steps: (1) ensure full scan coverage by re-scanning every unscanned tile via
+edges-scan.py's scan_path (free, local, per-lens). (2) report the exact doomed
+count. (3) with --execute, soft-delete the doomed set: is_published=FALSE,
+user_removed=TRUE, web_viewer=FALSE, quarantine the PNG (recoverable), tag
+'seam-purge', append to data/cactus-decisions.jsonl. Files we could not scan
+(missing PNG) are reported and KEPT, never blind-deleted.
+
+  python3 scripts/cactus-seam-purge.py            # rescan + dry-run report
+  python3 scripts/cactus-seam-purge.py --execute  # also perform the purge
+"""
+import importlib.util
+import json
+import os
+import shutil
+import subprocess
+import sys
+from datetime import datetime, timezone
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+EXECUTE = '--execute' in sys.argv
+PASS_MAX = 5.0          # any lens > this == "fuzzy" (not a clean PASS)
+QUAR = ROOT / 'data' / 'generated_cactus_quarantine'
+AUDIT = ROOT / 'data' / 'cactus-decisions.jsonl'
+
+# import scan_path from the hyphenated edges-scan.py
+_spec = importlib.util.spec_from_file_location('edges_scan', str(ROOT / 'scripts' / 'edges-scan.py'))
+_es = importlib.util.module_from_spec(_spec); _spec.loader.exec_module(_es)
+scan_path = _es.scan_path
+
+
+def psql(sql, read=True):
+    r = subprocess.run(['psql', 'dw_unified', '-At', '-F', '|', '-c', sql],
+                       capture_output=True, text=True)
+    if r.returncode != 0:
+        raise RuntimeError('psql: ' + r.stderr.strip())
+    return r.stdout.strip()
+
+
+def psql_stdin(sql):
+    r = subprocess.run(['psql', 'dw_unified', '-At', '-q'], input=sql,
+                       capture_output=True, text=True)
+    if r.returncode != 0:
+        raise RuntimeError('psql: ' + r.stderr.strip())
+    return r.stdout.strip()
+
+
+def seam_quality(edge, mid):
+    worst = max(edge or 0, mid or 0)
+    return max(0, min(100, round((1 - worst / 12.0) * 100)))
+
+
+# ── per-lens columns (rigorous audit trail) ────────────────────────────────
+psql_stdin("""
+ALTER TABLE wallco_cactus_rank ADD COLUMN IF NOT EXISTS seam_hmid_raw numeric;
+ALTER TABLE wallco_cactus_rank ADD COLUMN IF NOT EXISTS seam_vmid_raw numeric;
+INSERT INTO wallco_cactus_rank (design_id)
+  SELECT id FROM all_designs WHERE category ILIKE '%cactus%' AND kind='seamless_tile'
+  ON CONFLICT (design_id) DO NOTHING;
+""")
+
+# ── 1. rescan unscanned seamless_tile cactus ───────────────────────────────
+unscanned = psql("""
+  SELECT d.id, d.local_path FROM all_designs d
+  JOIN wallco_cactus_rank r ON r.design_id=d.id
+  WHERE d.category ILIKE '%cactus%' AND d.kind='seamless_tile'
+    AND NOT COALESCE(d.user_removed,false)
+    AND r.seam_edge_raw IS NULL AND d.local_path IS NOT NULL;
+""")
+rows = [ln.split('|', 1) for ln in unscanned.split('\n') if ln.strip()]
+print(f'[purge] rescanning {len(rows)} unscanned tiles…')
+
+
+def _flush(batch):
+    if not batch:
+        return
+    tup = ','.join(
+        f"({i},{sq},{e},{m},{hm},{vm},'{v}',now())" for (i, sq, e, m, hm, vm, v) in batch)
+    psql_stdin(f"""
+      INSERT INTO wallco_cactus_rank
+        (design_id, seam_score, seam_edge_raw, seam_mid_raw, seam_hmid_raw, seam_vmid_raw, seam_verdict, seam_scored_at)
+      VALUES {tup}
+      ON CONFLICT (design_id) DO UPDATE SET
+        seam_score=EXCLUDED.seam_score, seam_edge_raw=EXCLUDED.seam_edge_raw,
+        seam_mid_raw=EXCLUDED.seam_mid_raw, seam_hmid_raw=EXCLUDED.seam_hmid_raw,
+        seam_vmid_raw=EXCLUDED.seam_vmid_raw, seam_verdict=EXCLUDED.seam_verdict,
+        seam_scored_at=EXCLUDED.seam_scored_at;
+    """)
+
+
+missing, scanned, vals = 0, 0, []
+for i, (rid, lp) in enumerate(rows):
+    if not lp or not os.path.isfile(lp):
+        missing += 1
+        continue
+    try:
+        r = scan_path(Path(lp))
+    except Exception as e:
+        missing += 1
+        continue
+    if not r.get('ok'):
+        missing += 1
+        continue
+    e_ = r['edges_max']; m_ = r['mids_max']
+    hm = r['lenses']['h_mid']['score']; vm = r['lenses']['v_mid']['score']
+    vals.append((int(rid), seam_quality(e_, m_), e_, m_, hm, vm, r['verdict']))
+    scanned += 1
+    if len(vals) >= 400:
+        _flush(vals); vals = []
+    if (i + 1) % 100 == 0:
+        print(f'\r[purge]   scanned {i+1}/{len(rows)} (missing {missing})', end='')
+
+_flush(vals)
+print(f'\n[purge] rescan complete — scanned {scanned}, unscannable(missing PNG) {missing}')
+
+# ── 2. doomed set: any lens > PASS_MAX (edges OR center fuzzy) ──────────────
+doomed = psql(f"""
+  SELECT d.id, d.local_path FROM all_designs d
+  JOIN wallco_cactus_rank r ON r.design_id=d.id
+  WHERE d.category ILIKE '%cactus%' AND d.kind='seamless_tile'
+    AND NOT COALESCE(d.user_removed,false)
+    AND r.seam_edge_raw IS NOT NULL
+    AND (r.seam_edge_raw > {PASS_MAX} OR r.seam_mid_raw > {PASS_MAX});
+""")
+doomed_rows = [ln.split('|', 1) for ln in doomed.split('\n') if ln.strip()]
+
+clean = psql(f"""
+  SELECT count(*) FROM all_designs d JOIN wallco_cactus_rank r ON r.design_id=d.id
+  WHERE d.category ILIKE '%cactus%' AND d.kind='seamless_tile' AND NOT COALESCE(d.user_removed,false)
+    AND r.seam_edge_raw IS NOT NULL AND r.seam_edge_raw<={PASS_MAX} AND r.seam_mid_raw<={PASS_MAX};
+""")
+unscannable = psql(f"""
+  SELECT count(*) FROM all_designs d JOIN wallco_cactus_rank r ON r.design_id=d.id
+  WHERE d.category ILIKE '%cactus%' AND d.kind='seamless_tile' AND NOT COALESCE(d.user_removed,false)
+    AND r.seam_edge_raw IS NULL;
+""")
+murals = psql("""SELECT count(*) FROM all_designs WHERE category ILIKE '%cactus%'
+  AND kind IN ('mural','mural_panel') AND NOT COALESCE(user_removed,false);""")
+
+print('\n===== SEAM PURGE REPORT (kind=seamless_tile only) =====')
+print(f'  DOOMED (fuzzy edge or center, ΔE>{PASS_MAX:.0f}): {len(doomed_rows)}')
+print(f'  KEEP — clean PASS all 6 lenses           : {clean}')
+print(f'  KEEP — unscannable (missing PNG)          : {unscannable}')
+print(f'  UNTOUCHED — murals / mural_panels         : {murals}')
+print('=======================================================')
+
+if not EXECUTE:
+    print('\n[dry-run] no changes made. Re-run with --execute to purge.')
+    sys.exit(0)
+
+# ── 3. execute soft-delete: quarantine PNG + flag + audit ──────────────────
+QUAR.mkdir(parents=True, exist_ok=True)
+moved, ids = 0, []
+with open(AUDIT, 'a') as af:
+    for rid, lp in doomed_rows:
+        ids.append(int(rid))
+        if lp and os.path.isfile(lp):
+            dest = QUAR / os.path.basename(lp)
+            try:
+                if not dest.exists():
+                    shutil.move(lp, dest); moved += 1
+            except Exception as e:
+                print(f'  quarantine fail {rid}: {e}')
+        af.write(json.dumps({'ts': datetime.now(timezone.utc).isoformat(),
+                             'id': int(rid), 'action': 'bad', 'reason': 'seam-purge-aggressive'}) + '\n')
+
+# bulk DB flip in chunks
+for k in range(0, len(ids), 500):
+    chunk = ids[k:k + 500]
+    idlist = ','.join(str(x) for x in chunk)
+    psql_stdin(f"""
+      UPDATE all_designs SET is_published=FALSE, user_removed=TRUE, web_viewer=FALSE,
+        tags = array_remove(COALESCE(tags,ARRAY[]::text[]),'seam-purge') || ARRAY['seam-purge']::text[]
+      WHERE id IN ({idlist});
+    """)
+
+print(f'\n[purge] EXECUTED — soft-deleted {len(ids)} tiles, quarantined {moved} PNGs.')
+print(f'[purge] recoverable from {QUAR}')
+remaining = psql("""SELECT count(*) FROM all_designs WHERE category ILIKE '%cactus%'
+  AND kind='seamless_tile' AND NOT COALESCE(user_removed,false);""")
+print(f'[purge] live seamless_tile cactus remaining: {remaining}')
diff --git a/scripts/cactus-vision-score.js b/scripts/cactus-vision-score.js
new file mode 100644
index 0000000..7c7b4be
--- /dev/null
+++ b/scripts/cactus-vision-score.js
@@ -0,0 +1,129 @@
+#!/usr/bin/env node
+'use strict';
+/*
+ * cactus-vision-score.js — FREE local-vision quality scorer for the Cactus
+ * Curator. For every cactus design lacking a vision_score, asks a local
+ * Ollama vision model (qwen2.5vl:7b on Mac1 by default — unmetered) to rate
+ * 0..100 how much the tile looks like a REAL, sellable, high-end-catalog
+ * wallcovering ("most like our dw_unified catalog patterns"). Writes the
+ * score into wallco_cactus_rank.vision_score; the curator's rank_score
+ * (0.6*vision + 0.4*seam) is computed live in the list query, so the grid
+ * re-ranks as scores land.
+ *
+ * Resumable (skips already-scored), throttled by the model itself, keeps the
+ * model warm via keep_alive. Reads images from local_path when present, else
+ * from the running server's /designs/img/by-id/<id>.
+ *
+ *   node scripts/cactus-vision-score.js [--limit N] [--host H:PORT] [--model M]
+ *
+ * Env: OLLAMA_VISION_HOST (default 192.168.1.133:11434), OLLAMA_VISION_MODEL
+ * (default qwen2.5vl:7b), WALLCO_PORT (local server for by-id, default 9905).
+ */
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+const { execSync } = require('child_process');
+const { psqlQuery, psqlExecLocal, pgEsc } = require('../lib/db');
+
+const args = process.argv.slice(2);
+const flag = (n, d) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : d; };
+const LIMIT = parseInt(flag('--limit', '0'), 10) || 0;
+const HOST = flag('--host', process.env.OLLAMA_VISION_HOST || '192.168.1.133:11434');
+const MODEL = flag('--model', process.env.OLLAMA_VISION_MODEL || 'qwen2.5vl:7b');
+const SRV_PORT = process.env.WALLCO_PORT || '9905';
+const TMP = path.join(os.tmpdir(), 'cactus-vision');
+fs.mkdirSync(TMP, { recursive: true });
+
+const PROMPT =
+  'You are a luxury wallcovering buyer evaluating an AI-generated wallpaper tile for our high-end catalog. ' +
+  'Score 0-100 how much it looks like a REAL, professional, sellable wallcovering (Schumacher / Thibaut / Designtex grade): ' +
+  'clean even repeat, cohesive refined palette, NO AI smears or garbled/melted motifs, no harsh visible seams, ' +
+  'balanced composition, tasteful not cartoonish. 100 = indistinguishable from a premium catalog product; ' +
+  '0 = obviously broken AI junk. Reply with ONLY the integer, nothing else.';
+
+// ensure a rank row exists for every cactus design, then pick the unscored set
+psqlExecLocal(`
+  INSERT INTO wallco_cactus_rank (design_id)
+  SELECT id FROM all_designs WHERE category ILIKE '%cactus%'
+  ON CONFLICT (design_id) DO NOTHING;
+`);
+
+const idsRaw = psqlQuery(
+  `SELECT d.id, COALESCE(d.local_path,'') FROM all_designs d
+   JOIN wallco_cactus_rank r ON r.design_id = d.id
+   WHERE d.category ILIKE '%cactus%' AND r.vision_score IS NULL
+   ORDER BY d.id ${LIMIT ? 'LIMIT ' + LIMIT : ''};`
+).trim();
+
+const rows = idsRaw ? idsRaw.split('\n').map(l => { const [id, lp] = l.split('|'); return { id: id.trim(), lp: (lp || '').trim() }; }) : [];
+console.log(`[vision] ${rows.length} cactus designs to score · model=${MODEL} @ ${HOST}`);
+if (!rows.length) process.exit(0);
+
+async function getJpegB64(row) {
+  const out = path.join(TMP, row.id + '.jpg');
+  let src = row.lp && fs.existsSync(row.lp) ? row.lp : null;
+  if (!src) {
+    // fetch from the running server's by-id route
+    const r = await fetch(`http://127.0.0.1:${SRV_PORT}/designs/img/by-id/${row.id}`);
+    if (!r.ok) throw new Error('img fetch ' + r.status);
+    const buf = Buffer.from(await r.arrayBuffer());
+    src = path.join(TMP, row.id + '.src');
+    fs.writeFileSync(src, buf);
+  }
+  // downscale to 512 jpeg (fast, small base64) — sips is on every mac
+  execSync(`sips -Z 512 -s format jpeg ${JSON.stringify(src)} --out ${JSON.stringify(out)}`, { stdio: 'ignore' });
+  const b64 = fs.readFileSync(out).toString('base64');
+  try { fs.unlinkSync(out); if (src.endsWith('.src')) fs.unlinkSync(src); } catch {}
+  return b64;
+}
+
+async function score(b64) {
+  const body = JSON.stringify({
+    model: MODEL, prompt: PROMPT, images: [b64], stream: false,
+    keep_alive: '30m', options: { temperature: 0.1, num_predict: 12 },
+  });
+  const ctl = AbortSignal.timeout(180000);
+  const r = await fetch(`http://${HOST}/api/generate`, {
+    method: 'POST', headers: { 'Content-Type': 'application/json' }, body, signal: ctl,
+  });
+  if (!r.ok) throw new Error('ollama ' + r.status);
+  const j = await r.json();
+  return (j.response || '').trim();
+}
+
+function parseScore(reply) {
+  const m = String(reply).match(/\d{1,3}/);
+  if (!m) return null;
+  return Math.max(0, Math.min(100, parseInt(m[0], 10)));
+}
+
+(async () => {
+  let ok = 0, fail = 0;
+  const t0 = Date.now();
+  for (let i = 0; i < rows.length; i++) {
+    const row = rows[i];
+    try {
+      const b64 = await getJpegB64(row);
+      const reply = await score(b64);
+      const s = parseScore(reply);
+      psqlExecLocal(`UPDATE wallco_cactus_rank
+        SET vision_score=${s == null ? 'NULL' : s},
+            vision_raw=${pgEsc(reply.slice(0, 120))},
+            vision_model=${pgEsc(MODEL)},
+            vision_scored_at=now()
+        WHERE design_id=${row.id};`);
+      if (s == null) { fail++; }
+      else { ok++; }
+    } catch (e) {
+      fail++;
+      // leave vision_score NULL so a re-run retries this id
+      if (fail <= 5 || fail % 25 === 0) console.warn(`\n[vision] id ${row.id} failed: ${e.message}`);
+    }
+    if ((i + 1) % 10 === 0 || i === rows.length - 1) {
+      const rate = (i + 1) / ((Date.now() - t0) / 1000);
+      const eta = ((rows.length - i - 1) / rate / 60).toFixed(1);
+      process.stdout.write(`\r[vision] ${i + 1}/${rows.length}  ok=${ok} fail=${fail}  ${rate.toFixed(2)}/s  ETA ${eta}m   `);
+    }
+  }
+  console.log(`\n[vision] done — scored ${ok}, failed ${fail}`);
+})();

← adae983 drunk-animals: allow marijuana imagery (joints/buds/cannabis  ·  back to Wallco Ai  ·  Cactus curator: show created date+time per card; add marquee 08ff786 →