← back to Wallco Ai

scripts/edges-batch-scan.py

88 lines

#!/usr/bin/env python3
"""Read-only batch edges-scan over unpublished EDGES_FAIL rows.

Samples N unpublished EDGES_FAIL designs, resolves each PNG on local disk, runs
the 6-lens edges-scan.py, and tallies PASS/WARN/FAIL/missing. Writes a per-row
report JSON. DOES NOT touch the DB, does not flip is_published, does not deploy.
Its only purpose is to gauge the PASS-rate before deciding to promote.

Usage: edges-batch-scan.py [N]   (default 200)
"""
import os, sys, json, subprocess, re

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
GEN = os.path.join(ROOT, 'data', 'generated')
N = int(sys.argv[1]) if len(sys.argv) > 1 else 200


def db_url():
    for line in open(os.path.join(ROOT, '.env')):
        if line.startswith('DATABASE_URL='):
            return line.split('=', 1)[1].strip().strip('"')
    raise SystemExit('no DATABASE_URL in .env')


def fetch_rows(n):
    q = ("select id, coalesce(local_path,'') from all_designs "
         "where is_published=false and coalesce(user_removed,false)=false "
         "and notes ilike '%EDGES_FAIL%' order by id desc limit " + str(int(n)))
    out = subprocess.check_output(['psql', db_url(), '-tAc', q], text=True)
    rows = []
    for line in out.splitlines():
        line = line.strip()
        if '|' in line:
            did, lp = line.split('|', 1)
            rows.append((int(did), lp))
    return rows


def resolve(lp):
    if not lp:
        return None
    cands = [lp, lp if lp.endswith('.png') else lp + '.png']
    base = os.path.basename(lp.rstrip('/'))
    cands += [os.path.join(GEN, base + '.png'),
              os.path.join(GEN, base, 'final.png')]
    return next((c for c in cands if os.path.isfile(c)), None)


def scan(path):
    try:
        out = subprocess.check_output(
            ['python3', os.path.join(ROOT, 'scripts', 'edges-scan.py'), '--path', path],
            text=True, stderr=subprocess.DEVNULL, timeout=60)
        return json.loads(out)
    except Exception as e:
        return {'verdict': 'ERROR', 'error': str(e)[:120]}


def main():
    rows = fetch_rows(N)
    tally = {'PASS': 0, 'WARN': 0, 'FAIL': 0, 'ERROR': 0, 'MISSING': 0}
    detail = []
    passes = []
    for did, lp in rows:
        path = resolve(lp)
        if not path:
            tally['MISSING'] += 1
            detail.append({'id': did, 'verdict': 'MISSING'})
            continue
        r = scan(path)
        v = r.get('verdict', 'ERROR')
        tally[v] = tally.get(v, 0) + 1
        rec = {'id': did, 'verdict': v, 'score': r.get('overall_max') or r.get('score')}
        detail.append(rec)
        if v == 'PASS':
            passes.append(did)
    report = {'requested': N, 'scanned': len(rows), 'tally': tally,
              'pass_ids': passes, 'detail': detail}
    outp = os.path.join(ROOT, 'data', 'edges-batch-scan-report.json')
    with open(outp, 'w') as f:
        json.dump(report, f, indent=2)
    print(json.dumps({'tally': tally, 'pass_count': len(passes),
                      'report': outp}, indent=2))


if __name__ == '__main__':
    main()