← back to Pj Image Repair

resolve_images.py

103 lines

#!/usr/bin/env python3
"""
TK-10467 — Phillip Jeffries logo-hero repair: resolve real product images.
READ-ONLY / $0 / no-auth. Sources the real product photo for each ACTIVE PJ
product from the PUBLIC phillipjeffries.com product page (og:image), gated to
the real webdamdb CDN + title cross-verification. Does NOT write to Shopify.
Resumable: appends to data/pj_resolved.tsv, skips already-done dw_sku.
"""
import csv, os, re, sys, time, html, json, urllib.request, urllib.error
from concurrent.futures import ThreadPoolExecutor, as_completed

HERE = os.path.dirname(os.path.abspath(__file__))
WORK = os.path.join(HERE, "data", "pj_active_worklist.tsv")
OUT  = os.path.join(HERE, "data", "pj_resolved.tsv")
UA   = "Mozilla/5.0 (compatible; DW-catalog-repair/1.0; +TK-10467)"
REAL_CDN = "cdn2.webdamdb.com"          # real product-image CDN
FALLBACK_MARK = "assets/og-phillip"      # generic shop-landing fallback og:image

def norm(s):
    s = html.unescape(s or "").lower()
    s = re.sub(r"[^a-z0-9]+", " ", s)
    return re.sub(r"\s+", " ", s).strip()

def fetch(url, timeout=25):
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return r.geturl(), r.read().decode("utf-8", "replace")

def resolve(row):
    mfr, dw, handle, title, cur = row
    mfr = (mfr or "").strip()
    out = {"dw_sku": dw, "mfr_sku": mfr, "our_title": title,
           "status": "UNRESOLVED", "image": "", "vendor_title": "",
           "title_match": "", "note": ""}
    if not re.fullmatch(r"\d+", mfr):
        out["note"] = "non-numeric-mfr_sku:needs-search-fallback"
        return out
    url = f"https://www.phillipjeffries.com/product/{mfr}"
    for attempt in range(3):
        try:
            final, body = fetch(url)
            break
        except Exception as e:
            if attempt == 2:
                out["note"] = f"fetch-error:{type(e).__name__}"
                return out
            time.sleep(1.0 + attempt)
    m = re.search(r'<meta property="og:image" content="([^"]+)"', body, re.I)
    t = re.search(r"<title>([^<]*)</title>", body, re.I)
    og = html.unescape(m.group(1)) if m else ""
    vt = html.unescape(t.group(1)).replace(" | Phillip Jeffries", "").strip() if t else ""
    out["vendor_title"] = vt
    if not og or FALLBACK_MARK in og or REAL_CDN not in og:
        out["note"] = "shop-landing-fallback-or-no-real-image"
        return out
    out["image"] = og
    # cross-verify: our "Pattern - Color" vs vendor "Pattern in Color"
    ours = norm(re.sub(r"\bnew\b|\brepeat.*$|by phillip jeffries.*$|at the dw.*$", "", title))
    theirs = norm(vt.replace(" in ", " "))
    otoks = set(ours.split()); ttoks = set(theirs.split())
    inter = otoks & ttoks
    ratio = len(inter) / max(1, len(otoks))
    out["title_match"] = f"{ratio:.2f}"
    out["status"] = "OK" if ratio >= 0.5 else "OK_LOWMATCH"
    return out

def main():
    with open(WORK) as f:
        rows = [line.rstrip("\n").split("\t") for line in f if line.strip()]
    rows = [r for r in rows if len(r) >= 5]
    done = set()
    if os.path.exists(OUT):
        with open(OUT) as f:
            for line in f:
                p = line.split("\t")
                if p and p[0] != "dw_sku":
                    done.add(p[0])
    todo = [r for r in rows if r[1] not in done]
    print(f"worklist={len(rows)} done={len(done)} todo={len(todo)}", flush=True)
    new = not os.path.exists(OUT)
    fh = open(OUT, "a", newline="")
    w = csv.writer(fh, delimiter="\t")
    if new:
        w.writerow(["dw_sku","mfr_sku","status","title_match","image","vendor_title","our_title","note"])
        fh.flush()
    n = 0
    counts = {}
    with ThreadPoolExecutor(max_workers=6) as ex:
        futs = {ex.submit(resolve, r): r for r in todo}
        for fut in as_completed(futs):
            o = fut.result()
            w.writerow([o["dw_sku"],o["mfr_sku"],o["status"],o["title_match"],
                        o["image"],o["vendor_title"],o["our_title"],o["note"]])
            n += 1
            counts[o["status"]] = counts.get(o["status"],0)+1
            if n % 100 == 0:
                fh.flush(); print(f"  {n}/{len(todo)} {counts}", flush=True)
    fh.flush(); fh.close()
    print(f"DONE resolved={n} {counts}", flush=True)

if __name__ == "__main__":
    main()