← back to Wallco Ai
scripts/cactus-seam-purge.py
194 lines
#!/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}')