← back to Dw Color Image Audit

scripts/classify_survivors.py

109 lines

#!/usr/bin/env python3
"""
Classify the 29 vision-gate SURVIVORS into 3 dispositions per DTD Q2=a:
  DROP_NAME_LEAK : the vision-detected color is literally present in the declared
                   name -> audit-side parser leak, NOT a real defect (false positive).
  HOLD_BUSY      : busy multi-color / botanical / commercial pattern where the
                   declared color plausibly names one ink -> HUMAN REVIEW, never
                   auto-remap (the memo's hard rule).
  RECRAWL        : solid / textured / neutral-ground product whose image is plainly
                   a different color than the name -> real wrong-image suspect ->
                   re-crawl source full-page to find the correct colorway->image pairing.
Pure-local logic, $0. Writes out/survivor_dispositions.{jsonl,csv}.
"""
import json, os, re

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
IN = os.path.join(ROOT, "out", "vision_gate_results.jsonl")

BASIC = ["red", "orange", "yellow", "green", "blue", "teal", "purple", "violet",
         "pink", "brown", "beige", "tan", "gold", "magenta", "navy", "burgundy",
         "white", "black", "gray", "grey", "cream", "lavender", "turquoise",
         "crimson", "coral", "terracotta", "olive", "khaki", "chestnut"]

# busy / multi-color pattern signals: doubled motif words + botanical/commercial lines
BUSY_RE = re.compile(
    r"\b(flowers?\s+flowers?|trees?\s+trees?|leaves?\s+leaves?|grasses?[- ]leaves?|"
    r"landscapes?\s+landscapes?|waterscapes?\s+waterscapes?|floral|botanical|"
    r"flower|tree|leaf|leaves|grass|vine|garden|meadow|reef|safari|woodland|"
    r"damask|toile|paisley|medley|pinwheel|splash|sunset|coleus|petrouchka)\b", re.I)

# solid / texture lines that SHOULD be one near-solid color (strong wrong-image signal)
SOLID_RE = re.compile(
    r"\b(paintable|faux suede|suede|faux linen|linen|grasscloth|grass cloth|"
    r"raffia|vinyl|texture|weave|woven|silk|solid|plain)\b", re.I)


def tokens(s):
    return set(re.findall(r"[a-z]+", (s or "").lower()))


def main():
    surv = [json.loads(l) for l in open(IN)
            if json.loads(l).get("gate") == "SURVIVOR"]
    out = []
    for r in surv:
        declared = (r["declared_color"] or "")
        dl = declared.lower()
        vis = (r.get("vision_dominant") or "").lower()
        vis_tokens = [t for t in re.findall(r"[a-z]+", vis) if t in BASIC]

        name_leak = any(t in dl for t in vis_tokens)
        is_busy = bool(BUSY_RE.search(declared))
        is_solid = bool(SOLID_RE.search(declared))
        # vision reporting >1 color => inherently multi-ink => can't be a wrong-solid
        # defect; ambiguous busy pattern -> HOLD (a real wrong-image shows ONE color)
        multi_vis = bool(re.search(r"\band\b|multi|gradient|/", vis))
        conf = r.get("vision_confidence") or 0

        if name_leak:
            disp = "DROP_NAME_LEAK"
            why = f"declared name already contains '{[t for t in vis_tokens if t in dl][0]}'"
        elif multi_vis:
            disp = "HOLD_BUSY"
            why = "vision sees multiple colors -> multi-ink/busy -> human review"
        elif is_solid and not is_busy:
            disp = "RECRAWL"
            why = "solid/textured line expected near-solid; image is clearly off-color"
        elif is_busy:
            disp = "HOLD_BUSY"
            why = "busy/botanical pattern; declared may name one ink -> human review"
        elif conf >= 0.95:
            disp = "RECRAWL"
            why = "non-busy ground, high-confidence color contradiction"
        else:
            disp = "HOLD_BUSY"
            why = "ambiguous; conservative hold"
        r2 = dict(r)
        r2["disposition"] = disp
        r2["disposition_reason"] = why
        out.append(r2)

    out.sort(key=lambda r: (r["disposition"], r["vendor"], r["dw_sku"]))
    with open(os.path.join(ROOT, "out", "survivor_dispositions.jsonl"), "w") as f:
        for r in out:
            f.write(json.dumps(r) + "\n")
    import csv
    fields = ["disposition", "vendor", "dw_sku", "mfr_sku", "declared_color",
              "vision_dominant", "vision_confidence", "disposition_reason",
              "image_url", "handle"]
    with open(os.path.join(ROOT, "out", "survivor_dispositions.csv"), "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
        w.writeheader()
        for r in out:
            w.writerow(r)

    from collections import Counter
    c = Counter(r["disposition"] for r in out)
    print("DISPOSITIONS:", dict(c))
    for disp in ["RECRAWL", "HOLD_BUSY", "DROP_NAME_LEAK"]:
        print(f"\n=== {disp} ===")
        for r in out:
            if r["disposition"] == disp:
                print(f"  {r['vendor'][:16]:16} {r['dw_sku']:14} {r['declared_color'][:40]:40} "
                      f"vis={r.get('vision_dominant'):20} ({r['disposition_reason']})")


if __name__ == "__main__":
    main()