[object Object]

← back to Designer Wallcoverings

TK-10057: room-setting-preferring hero selector (newest-items-first) + dry-run; 22/39 get room heroes

3f4182cb9c8cdb25ad74cf0dc22bd796fac270bb · 2026-07-30 12:08:39 -0700 · collection-agent

Files touched

Diff

commit 3f4182cb9c8cdb25ad74cf0dc22bd796fac270bb
Author: collection-agent <steve@designerwallcoverings.com>
Date:   Thu Jul 30 12:08:39 2026 -0700

    TK-10057: room-setting-preferring hero selector (newest-items-first) + dry-run; 22/39 get room heroes
---
 .../tk10056-hero-rotate/hero-rotate.py             | 101 +++++++++++++++++++++
 .../tk10056-hero-rotate/preview/0.jpg              | Bin 0 -> 308457 bytes
 .../tk10056-hero-rotate/preview/1.jpg              | Bin 0 -> 158978 bytes
 .../tk10056-hero-rotate/preview/2.jpg              | Bin 0 -> 229875 bytes
 .../tk10056-hero-rotate/preview/3.jpg              | Bin 0 -> 288216 bytes
 .../tk10056-hero-rotate/room-hero-preview.png      | Bin 0 -> 1326037 bytes
 6 files changed, 101 insertions(+)

diff --git a/shopify/collection-hero-fix/tk10056-hero-rotate/hero-rotate.py b/shopify/collection-hero-fix/tk10056-hero-rotate/hero-rotate.py
new file mode 100644
index 00000000..4fccc7e7
--- /dev/null
+++ b/shopify/collection-hero-fix/tk10056-hero-rotate/hero-rotate.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+"""
+TK-10056 — Smarter collection hero: prefer a ROOM-SETTING (lifestyle) photo, chosen from
+the NEWEST products, and re-pick when new items arrive (rotation).
+
+Scoring per candidate image (across the newest N products, all images):
+  + ROOM-SETTING  (filename ~ /(^|[-_])room[-_]/ or roomtype words)  -> strong preference
+  + size (min side px), landscape bonus for non-room images
+  - tiny (<700px), - roll/backing/swatch-only shots
+Picks the single highest-scoring image -> that becomes collection.image (single flagship hero).
+
+Modes (READ-ONLY unless --apply):
+  --dryrun [--handle H]   show current vs proposed hero for each promoted collection ($0)
+  --apply  [--handle H]   GATED: set collection.image to the chosen hero (backs up first)
+Newest = products sorted client-side by created_at DESC (Shopify collection order is manual).
+"""
+import sys,os,json,re,time,urllib.request,urllib.error
+STORE="designer-laboratory-sandbox.myshopify.com"; API="2024-10"
+SEC=os.path.expanduser("~/Projects/secrets-manager/.env"); HERE=os.path.dirname(os.path.abspath(__file__))
+PROMOTED=os.path.join(os.path.dirname(HERE),"tk10055-free-promote","promoted.json")
+def tok():
+    for l in open(SEC):
+        if l.startswith("SHOPIFY_ADMIN_TOKEN="): return l.split("=",1)[1].strip().strip('"').strip("'")
+T=tok(); H={"X-Shopify-Access-Token":T,"Content-Type":"application/json"}
+def req(method,path,body=None):
+    d=json.dumps(body).encode() if body else None
+    for i in range(5):
+        try: return json.load(urllib.request.urlopen(urllib.request.Request(f"https://{STORE}/admin/api/{API}/{path}",data=d,headers=H,method=method)))
+        except urllib.error.HTTPError as e:
+            if e.code==429 and i<4: time.sleep(2); continue
+            raise
+ROOM=re.compile(r'(^|[-_/])room[-_]|bedroom|living[-_]?room|dining[-_]?room|hallway|foyer|nursery|office|bathroom', re.I)
+SKIP=re.compile(r'(-rolls?\.|backing|reverse|selvage|-back-|swatch-back)', re.I)
+def endpoint(res): return ("custom_collection","custom_collections") if res.startswith("custom") else ("smart_collection","smart_collections")
+def score(im):
+    src=(im.get("src") or "").split("?")[0]; fn=src.split("/")[-1]
+    w=im.get("width") or 0; h=im.get("height") or 0; mn=min(w,h) if w and h else 0
+    if not mn or SKIP.search(fn): return -1,"skip"
+    room=bool(ROOM.search(fn) or ROOM.search(str(im.get("alt") or "")))
+    ratio=(max(w,h)/min(w,h)) if mn else 1
+    s=mn/100.0
+    kind="swatch"
+    if room: s+=40; kind="ROOM"
+    elif 1.2<=ratio<=2.6 and mn>=600: s+=12; kind="landscape"
+    if mn<700 and not room: s-=8
+    return s,kind
+def newest_products(cid,n=18):
+    prods=req("GET",f"collections/{cid}/products.json?limit=60&fields=id,created_at,images").get("products",[])
+    prods.sort(key=lambda p:p.get("created_at",""),reverse=True)
+    return prods[:n]
+def pick_hero(cid):
+    best=None
+    for p in newest_products(cid):
+        for im in p.get("images",[]):
+            sc,kind=score(im)
+            if sc<=0: continue
+            cand=(sc,kind,im.get("width"),im.get("height"),im["src"],p.get("created_at","")[:10])
+            if best is None or sc>best[0]: best=cand
+    return best
+def load():
+    if os.path.exists(PROMOTED): return json.load(open(PROMOTED))
+    return json.load(open(os.path.join(os.path.dirname(HERE),"tk10055-free-promote","report.json")))["hero_worthy"]
+def cur_image(res,cid):
+    key,path=endpoint(res); return (req("GET",f"{path}/{cid}.json?fields=image").get(key,{}) or {}).get("image") or {}
+def rows(handle=None):
+    rep=json.load(open(os.path.join(os.path.dirname(HERE),"tk10055-free-promote","report.json")))["hero_worthy"]
+    by={c["handle"]:c for c in rep}
+    prom=load()
+    hs=[p["handle"] for p in prom] if isinstance(prom[0],dict) and "handle" in prom[0] else prom
+    for h in ([handle] if handle else hs):
+        c=by.get(h)
+        if not c: continue
+        yield c
+def dryrun(handle=None):
+    nroom=0;n=0
+    for c in rows(handle):
+        best=pick_hero(c["id"]); cur=cur_image(c["res"],c["id"])
+        n+=1; tag=best[1] if best else "none"
+        if best and best[1]=="ROOM": nroom+=1
+        cw=f"{cur.get('width')}x{cur.get('height')}" if cur else "none"
+        bw=f"{best[2]}x{best[3]}" if best else "-"
+        print(f"  {c['handle'][:38]:38} cur={cw:11} -> {tag:9} {bw:11} new={best[5] if best else '-'}  {(best[4].split('/')[-1][:40]) if best else ''}")
+    print(f"\n{nroom}/{n} collections would use a ROOM-SETTING hero from newest items")
+def apply(handle=None):
+    os.makedirs(os.path.join(HERE,"backups"),exist_ok=True); done=0
+    for c in rows(handle):
+        best=pick_hero(c["id"])
+        if not best: print(f"skip {c['handle']} (no candidate)"); continue
+        key,path=endpoint(c["res"])
+        cur=req("GET",f"{path}/{c['id']}.json?fields=image,published_at")
+        if not cur.get(key,{}).get("published_at"): print(f"skip {c['handle']} (unpublished)"); continue
+        json.dump(cur,open(os.path.join(HERE,"backups",f"{c['handle']}.json"),"w"))
+        req("PUT",f"{path}/{c['id']}.json",{key:{"id":c["id"],"image":{"src":best[4]}}})
+        done+=1; print(f"set {c['handle']} -> {best[1]} {best[2]}x{best[3]}")
+        time.sleep(0.6)
+    print(f"applied {done}")
+if __name__=="__main__":
+    a=sys.argv[1:]; hd=a[a.index("--handle")+1] if "--handle" in a else None
+    if not a or a[0]=="--dryrun": dryrun(hd)
+    elif a[0]=="--apply": apply(hd)
+    else: print(__doc__)
diff --git a/shopify/collection-hero-fix/tk10056-hero-rotate/preview/0.jpg b/shopify/collection-hero-fix/tk10056-hero-rotate/preview/0.jpg
new file mode 100644
index 00000000..53898ca0
Binary files /dev/null and b/shopify/collection-hero-fix/tk10056-hero-rotate/preview/0.jpg differ
diff --git a/shopify/collection-hero-fix/tk10056-hero-rotate/preview/1.jpg b/shopify/collection-hero-fix/tk10056-hero-rotate/preview/1.jpg
new file mode 100644
index 00000000..285c9c47
Binary files /dev/null and b/shopify/collection-hero-fix/tk10056-hero-rotate/preview/1.jpg differ
diff --git a/shopify/collection-hero-fix/tk10056-hero-rotate/preview/2.jpg b/shopify/collection-hero-fix/tk10056-hero-rotate/preview/2.jpg
new file mode 100644
index 00000000..6f376bdb
Binary files /dev/null and b/shopify/collection-hero-fix/tk10056-hero-rotate/preview/2.jpg differ
diff --git a/shopify/collection-hero-fix/tk10056-hero-rotate/preview/3.jpg b/shopify/collection-hero-fix/tk10056-hero-rotate/preview/3.jpg
new file mode 100644
index 00000000..6ccc499b
Binary files /dev/null and b/shopify/collection-hero-fix/tk10056-hero-rotate/preview/3.jpg differ
diff --git a/shopify/collection-hero-fix/tk10056-hero-rotate/room-hero-preview.png b/shopify/collection-hero-fix/tk10056-hero-rotate/room-hero-preview.png
new file mode 100644
index 00000000..6dd21160
Binary files /dev/null and b/shopify/collection-hero-fix/tk10056-hero-rotate/room-hero-preview.png differ

← 4d17efbd auto-save: 2026-07-30T11:47:45 (18 files) — shopify/scripts/  ·  back to Designer Wallcoverings  ·  TK-10057: room-setting heroes live (38); storage-safe no-op b22ef15f →