← back to Greenland Onboard

scripts/full-03-romance.py

100 lines

#!/usr/bin/env python3
"""Step 6: ROMANCE body_html via LOCAL Ollama (qwen3:14b), $0.
One luxury paragraph PER COLLECTION (~101 calls, cached by collectionCode),
then per-colorway prepend a single color sentence. Template fallback on error.
"""
import json, os, re, sys, urllib.request

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OLLAMA = "http://127.0.0.1:11434/api/generate"
MODEL = "qwen3:14b"

REC = json.load(open(os.path.join(ROOT, "data", "enriched.json")))

# group by collectionCode -> representative row (material, city, name)
by_coll = {}
for r in REC:
    by_coll.setdefault(r["collectionCode"], r)

MATWORD = {
    "Silk": "silk", "Grass": "grasscloth", "Wood": "wood veneer", "Suede": "suede",
    "Linen": "linen", "Raffia": "raffia", "Paper Weave": "paper weave", "Abaca": "abaca",
    "Hemp": "hemp", "Sisal": "sisal", "Jute": "jute", "Velvet": "velvet", "Wool": "wool",
    "Arrowroot": "arrowroot", "Cotton Yarn": "cotton", "Water hyacinth": "water hyacinth",
    "Metal Leaf": "metal leaf", "Jacquard": "jacquard weave",
    "Specialty": "surface", "PE Weaving / PE Woven Material": "woven",
    "XPE Functional Wallcoverings": "textural", "Multi-material": "mixed-media", "Vinyl": "",
}

def strip_think(t):
    return re.sub(r"<think>.*?</think>", "", t, flags=re.S).strip()

def ollama(prompt):
    body = json.dumps({
        "model": MODEL, "prompt": prompt, "stream": False, "think": False,
        "options": {"temperature": 0.8, "num_predict": 320},
    }).encode()
    req = urllib.request.Request(OLLAMA, data=body, headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=180) as resp:
        data = json.loads(resp.read())
    return strip_think(data.get("response", "")).strip()

def template(material_word, city, name):
    mw = material_word or "textural"
    return (f"Woven from natural {mw} fibers, our {name} collection channels the "
            f"unhurried elegance of {city}. Each panel carries a tactile, hand-crafted "
            f"hand that catches the light and softens a room's architecture. Layer it "
            f"behind a channel-tufted headboard or across a study's feature wall for a "
            f"quietly luxurious, coastal-modern finish.")

def gen_collection(r):
    mw = MATWORD.get(r["material"], "textural")
    prompt = (
        "/no_think You are a luxury interior designer writing catalog copy for a high-end "
        "wallcovering house. Write ONE single elegant paragraph (3-4 sentences, no "
        "line breaks, no lists, no preamble, no quotes) romancing a wallcovering "
        f"made of {mw}. Weave in: the material's texture and hand, the refined coastal "
        f"resort feeling of the town '{r['city']}', and one concrete styling suggestion "
        "(a room, a pairing, a mood). Sophisticated, warm, tactile. Do NOT mention any "
        "brand name, SKU, price, or the word 'collection'. Return ONLY the paragraph."
    )
    try:
        txt = ollama(prompt)
        txt = txt.replace("\n", " ").strip().strip('"')
        # basic sanity: at least ~120 chars and no obvious refusal
        if len(txt) < 120 or "as an ai" in txt.lower():
            return template(mw, r["city"], r["collectionName"]), "template"
        return txt, "ollama"
    except Exception as e:
        return template(mw, r["city"], r["collectionName"]), "template"

def color_sentence(color, material):
    mw = MATWORD.get(material, "textural")
    return f"Shown here in {color}, a shade that lends this {mw} its distinctive depth."

def main():
    codes = list(by_coll.keys())
    total = len(codes)
    cache = {}
    src_counter = {"ollama": 0, "template": 0}
    for i, code in enumerate(codes, 1):
        para, src = gen_collection(by_coll[code])
        cache[code] = para
        src_counter[src] += 1
        if i % 10 == 0 or i == total:
            sys.stderr.write(f"  romance {i}/{total} ({src})\n"); sys.stderr.flush()
    # write per-collection cache
    json.dump({"src": src_counter, "paras": cache},
              open(os.path.join(ROOT, "data", "romance-by-collection.json"), "w"), indent=1)
    # apply to each record
    for r in REC:
        para = cache[r["collectionCode"]]
        cs = color_sentence(r["cleanColor"], r["material"])
        r["body_html"] = f"<p>{cs} {para}</p>"
    json.dump(REC, open(os.path.join(ROOT, "data", "enriched.json"), "w"), indent=1)
    print(json.dumps({"collections": total, "src": src_counter,
                      "applied": len(REC)}, indent=2))

if __name__ == "__main__":
    main()