← back to Greenland Onboard
auto-save: 2026-07-12T10:17:36 (1 files) — scripts/full-03-romance.py
a784bc0873e4e60951efbbecb9374abba3a38a70 · 2026-07-12 10:17:42 -0700 · Steve Abrams
Files touched
A scripts/full-03-romance.py
Diff
commit a784bc0873e4e60951efbbecb9374abba3a38a70
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 12 10:17:42 2026 -0700
auto-save: 2026-07-12T10:17:36 (1 files) — scripts/full-03-romance.py
---
scripts/full-03-romance.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 99 insertions(+)
diff --git a/scripts/full-03-romance.py b/scripts/full-03-romance.py
new file mode 100644
index 0000000..73eac4c
--- /dev/null
+++ b/scripts/full-03-romance.py
@@ -0,0 +1,99 @@
+#!/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": "artisanal", "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,
+ "options": {"temperature": 0.8, "num_predict": 220},
+ }).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 = (
+ "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()
← 800bae3 Full-catalog steps 2-5: PIL hex+color-clean, tags, packaging
·
back to Greenland Onboard
·
Full-catalog step 6: romance body_html via local qwen3:14b ( 5b87e99 →