← 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()