← 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
A shopify/collection-hero-fix/tk10056-hero-rotate/hero-rotate.pyA shopify/collection-hero-fix/tk10056-hero-rotate/preview/0.jpgA shopify/collection-hero-fix/tk10056-hero-rotate/preview/1.jpgA shopify/collection-hero-fix/tk10056-hero-rotate/preview/2.jpgA shopify/collection-hero-fix/tk10056-hero-rotate/preview/3.jpgA shopify/collection-hero-fix/tk10056-hero-rotate/room-hero-preview.png
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 →