← back to Dw Collection Banner Audit

room_render.py

96 lines

#!/usr/bin/env python3
"""
Room-render banner generator for no-source collections.
For each: pull a representative product image from the collection, ask
gemini-2.5-flash-image to place that wallcovering in a photorealistic room,
save the render locally (NOT uploaded to Shopify yet — that write is gated).
Usage: python3 room_render.py sample   # first 3
       python3 room_render.py all       # all 38 genuine no-source
Reads GEMINI_API_KEY from env. Cost ~$0.01-0.04/render (shown).
"""
import json, os, sys, base64, time, urllib.request

HERE = os.path.dirname(os.path.abspath(__file__))
GKEY = os.environ["GEMINI_API_KEY"]
STORE = "designer-laboratory-sandbox.myshopify.com"
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
OUT = os.path.join(HERE, "room_renders"); os.makedirs(OUT, exist_ok=True)
MODEL = "gemini-2.5-flash-image"

rows = json.load(open(os.path.join(HERE, "proposal.json")))
def w(r):
    try: return int(r["new_dim"].split("x")[0])
    except: return 0
genuine = [r for r in rows if r["status"] == "NO_HIRES_SOURCE" and w(r) < 1000 and (r["products"] or 0) > 0]
borderline = [r for r in rows if r["status"] == "NO_HIRES_SOURCE" and w(r) >= 1000 and (r["products"] or 0) > 0]

ROOMS = ["an elegant modern living room", "a refined dining room", "a luxe bedroom feature wall"]

def product_image(handle):
    url = f"https://{STORE}/collections/{handle}/products.json?limit=12"
    prods = None
    for attempt in range(4):
        try:
            prods = json.load(urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": UA}), timeout=25)).get("products", [])
            break
        except urllib.error.HTTPError as e:
            if e.code in (403, 429): time.sleep(2.0*(attempt+1)); continue
            return None
        except Exception:
            time.sleep(1.5)
    if not prods: return None
    for p in prods:
        for im in p.get("images", []):
            if "logo" not in im.get("src","").lower():
                return im["src"]
    return None

def fetch_bytes(src):
    return urllib.request.urlopen(urllib.request.Request(src, headers={"User-Agent": UA}), timeout=30).read()

def gen_room(handle, title, idx):
    src = product_image(handle)
    if not src: return {"handle": handle, "ok": False, "note": "no product image"}
    img_b = fetch_bytes(src)
    b64 = base64.b64encode(img_b).decode()
    mime = "image/png" if src.lower().split("?")[0].endswith(".png") else "image/jpeg"
    room = ROOMS[idx % len(ROOMS)]
    prompt = (f"Photorealistic interior photograph: the wallcovering pattern shown in the reference image "
              f"installed on the main wall of {room}. Elegant, luxury interior design, natural lighting, "
              f"tasteful furniture, magazine-quality. The wallcovering pattern must match the reference exactly. "
              f"Wide landscape composition suitable for a website banner. No text, no watermark.")
    body = json.dumps({
        "contents": [{"parts": [{"text": prompt}, {"inline_data": {"mime_type": mime, "data": b64}}]}],
        "generationConfig": {"imageConfig": {"aspectRatio": "16:9"}}
    }).encode()
    url = f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent?key={GKEY}"
    req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"})
    try:
        res = json.load(urllib.request.urlopen(req, timeout=120))
    except Exception as e:
        return {"handle": handle, "ok": False, "note": f"api err: {str(e)[:100]}"}
    # extract inline image
    try:
        for part in res["candidates"][0]["content"]["parts"]:
            d = part.get("inline_data") or part.get("inlineData")
            if d:
                out = os.path.join(OUT, f"{handle}.png")
                open(out, "wb").write(base64.b64decode(d["data"]))
                return {"handle": handle, "ok": True, "file": out, "src_used": src, "room": room}
        return {"handle": handle, "ok": False, "note": "no image in response"}
    except Exception as e:
        return {"handle": handle, "ok": False, "note": f"parse err: {str(e)[:100]}"}

mode = sys.argv[1] if len(sys.argv) > 1 else "sample"
target = {"sample": genuine[:3], "all": genuine, "borderline": borderline}.get(mode, genuine)
print(f"generating {len(target)} room-render banners (model {MODEL}, ~$0.04 each -> ~${len(target)*0.04:.2f})")
results = []
for i, r in enumerate(target):
    res = gen_room(r["handle"], r["title"], i)
    print(("  OK  " if res["ok"] else "  FAIL ") + res["handle"] + ("" if res["ok"] else " :: " + res.get("note","")))
    results.append(res)
    time.sleep(1.0)
json.dump(results, open(os.path.join(HERE, f"room_renders_{mode}.json"), "w"), indent=1)
ok = sum(1 for r in results if r["ok"])
print(f"\ndone: {ok}/{len(target)} rendered -> {OUT}   est cost ~${ok*0.04:.2f}")