← back to Novasuede Onboard

plan_writes.py

123 lines

#!/usr/bin/env python3
"""
Turn data/enriched.ndjson into the final per-product tag set + body_html.
Tag rule (Steve's decision: "replace descriptors, keep spam"):
  KEEP  : spec/operational, SEO-spam, room, material, SKU codes (DWCC-*, T3..W),
          'novasuede <name>', and THIS product's own vendor name.
  DROP  : color words, Color:/color: tags, interior-style words, mood words,
          and the old AI-Analyzed-v2 marker.
  ADD   : Color: <Primary> [+ secondary], bare colorway, undertone, 2-3 styles,
          and marker 'AI-Lexicon-v3'.
Emits data/plan.ndjson (id,title,old_tags,new_tags,old_body,new_body,added,removed)
and data/before_after.csv for human audit.
"""
import json, csv, re, pathlib

ROOT = pathlib.Path(__file__).resolve().parent
DATA = ROOT / "data"

# ---- KEEP whitelist (exact, case-insensitive) ----
KEEP_EXACT = {t.lower() for t in [
  # spec / operational / material
  "Architectural","Cal 117","Commercial","display_variant","Fabric","Fire Rated",
  "Microfiber Suede","NFPA 260","Novasuede","UFAC Class I","Wallcovering","Upholstery",
  "Class A Fire Rated","100% Nylon Fiber Matrix","Suede","Solid","Textured","Texture",
  "Solid/Textural","100000 double rubs","54 inch wide fabric","by the yard","wyzenbeek rated",
  "ufac class 1","Paper","Non-woven","Fabric-backed Vinyl","Novasuede™ Fabric & Wallcovering",
  # SEO-spam (keep per Steve)
  "aircraft fabric","cal 117 fabric","commercial grade fabric","contract fabric","designer fabric",
  "faux suede fabric","fire rated fabric","hospitality fabric","majilite novasuede",
  "microfiber suede fabric","microfiber wallcovering","microsuede fabric","synthetic suede",
  "upholstery fabric","wallcovering fabric","yacht fabric",
  # room / application
  "Bedroom","Living Room","Office","Hallway","Bathroom","Nursery","Primary Suite","Hotel Lobby",
  # marketing / identity
  "Showroom Line","New Arrival",
]}
KEEP_PREFIX = ("novasuede ",)                 # lowercase vendor SEO tags
KEEP_RE = [re.compile(r"^DWCC-\d+$", re.I), re.compile(r"^T3[A-Z0-9]{2,3}W$", re.I)]
DROP_MARKERS = {"ai-analyzed-v2"}

def keep_tag(tag, vendor_name):
    t = tag.strip()
    if not t: return False
    tl = t.lower()
    if tl in DROP_MARKERS: return False
    if t == vendor_name: return True            # product's own decorative identity
    if tl == vendor_name.lower(): return True
    if tl in KEEP_EXACT: return True
    if tl.startswith(KEEP_PREFIX): return True
    if any(rx.match(t) for rx in KEEP_RE): return True
    return False                                 # everything else = descriptor → drop

def title_case(s):  # 'mid-century modern' stays as given; colorways already cased
    return s

def build(rec):
    vname = rec["vendor_name"]
    old_tags = [x.strip() for x in (rec.get("_old_tags","") ).split(",") if x.strip()]
    kept = [t for t in old_tags if keep_tag(t, vname)]
    prim = (rec.get("primary_colorway") or "").strip().replace("-", " ")
    sec  = (rec.get("secondary_colorway") or "").strip().replace("-", " ")
    und  = (rec.get("undertone") or "").strip().capitalize()
    styles = [s.strip() for s in (rec.get("styles") or []) if s.strip()][:3]
    add = []
    if prim:
        add += [f"Color: {prim}", prim]
    if sec and sec.lower() != prim.lower():
        add += [f"Color: {sec}", sec]
    if und: add.append(und)
    add += styles
    add.append("AI-Lexicon-v3")
    # merge kept + add, de-dup case-insensitively, preserve order (kept first)
    seen=set(); final=[]
    for t in kept + add:
        k=t.lower()
        if k in seen: continue
        seen.add(k); final.append(t)
    # body_html
    desc = (rec.get("description") or "").strip()
    depth = (rec.get("depth") or "").strip()
    color_line = prim + (f" / {sec}" if sec and sec.lower()!=prim.lower() else "")
    style_line = ", ".join(styles)
    body = (f"<p>{desc}</p>\n"
            f"<p><strong>Color:</strong> {color_line} ({und.lower()}, {depth}) &middot; "
            f"<strong>Style:</strong> {style_line} &middot; 100% nylon microfiber suede, "
            f"54&quot; wide, Cal 117 / NFPA 260 / UFAC Class I, 100,000+ double rubs.</p>")
    removed = [t for t in old_tags if t not in final]
    added   = [t for t in final if t not in old_tags]
    return final, body, added, removed

def main():
    raw = {json.loads(l)["id"]: json.loads(l)
           for l in (DATA/"novasuede_raw.ndjson").read_text().splitlines()}
    out = (DATA/"plan.ndjson").open("w")
    csvf = (DATA/"before_after.csv").open("w", newline="")
    w = csv.writer(csvf)
    w.writerow(["id","title","vendor_name","primary","secondary","undertone","depth",
                "styles","removed_tags","added_tags","new_body"])
    n=0; errs=0
    for line in (DATA/"enriched.ndjson").read_text().splitlines():
        rec = json.loads(line)
        if not rec.get("_ok"):
            errs+=1; continue
        rec["_old_tags"] = raw[rec["id"]].get("tags","")
        final, body, added, removed = build(rec)
        plan = {"id":rec["id"],"title":rec["title"],"new_tags":", ".join(final),
                "new_body":body,"old_tags":rec["_old_tags"],
                "added":added,"removed":removed,
                "primary":rec.get("primary_colorway"),"secondary":rec.get("secondary_colorway"),
                "styles":rec.get("styles")}
        out.write(json.dumps(plan)+"\n")
        w.writerow([rec["id"],rec["title"],rec["vendor_name"],rec.get("primary_colorway"),
                    rec.get("secondary_colorway"),rec.get("undertone"),rec.get("depth"),
                    " | ".join(rec.get("styles") or []),
                    " | ".join(removed)," | ".join(added), re.sub("<[^>]+>"," ",body)])
        n+=1
    out.close(); csvf.close()
    print(f"planned {n} products ({errs} enrichment errors skipped)")
    print(f"-> {DATA/'plan.ndjson'}\n-> {DATA/'before_after.csv'}")

if __name__ == "__main__":
    main()