← back to Designer Wallcoverings
onboarding/sangetsu-lilycolor/scripts/enrich-pilot.py
133 lines
#!/usr/bin/env python3
"""Pilot LOCAL swatch enrichment ($0) — reuses enrich-local-hybrid's Pillow palette +
qwen2.5vl on Mac2 ollama (127.0.0.1). Maps dominant hex -> interior-designer lexicon colorway.
Output: one JSONL row per swatch with SKU, hex[], bg_hex, colorway_name, styles[], material,
patterns[]. STAGES ONLY — no publish, no DB write, no Shopify. Reversible.
Usage: enrich-pilot.py <skus_file> <out_jsonl>
skus_file: one mfr_sku per line (image lives at HENRY/<sku>.jpg)
"""
import sys, os, json, io, base64, time, subprocess, urllib.request
HENRY = "/Volumes/Henry/dw-lily-images"
OLLAMA = os.environ.get("ENRICH_OLLAMA_URL", "http://127.0.0.1:11434")
VL_MODEL = os.environ.get("ENRICH_VL_MODEL", "qwen2.5vl:7b")
HERE = os.path.dirname(os.path.abspath(__file__))
PALETTE_PY = os.path.expanduser("~/Projects/enrich-local-hybrid/enrich-palette.py")
LEXICON_PY = os.path.join(HERE, "lexicon-colorway.py")
def palette(img_path, k=6):
r = subprocess.run([sys.executable, PALETTE_PY, img_path, str(k)],
capture_output=True, text=True, timeout=30)
if r.returncode != 0 or not r.stdout:
raise RuntimeError("palette failed: " + (r.stderr or "no output"))
return json.loads(r.stdout) # {palette:[{hex,percentage}], image_b64}
def lexicon_name(hex_str):
r = subprocess.run([sys.executable, LEXICON_PY, hex_str],
capture_output=True, text=True, timeout=10)
try:
return json.loads(r.stdout).get("colorway")
except Exception:
return None
def vl(image_b64, pal):
prompt = (
"This wallcovering swatch image was pixel-sampled into these EXACT colors "
f"(hex + area%): {json.dumps(pal)} . In the SAME ORDER give a designer color name "
"for each hex. Also: backgroundIndex (0-based index of the base/background color), "
"styles (interior-design styles), patterns (motif vocabulary like Damask/Floral/"
"Geometric/Stripe/Grasscloth-texture), material (Grasscloth/Silk/Vinyl/Paper/"
"Non-woven/etc), imageType (scan_swatch|scan_flatbed|photo_full|photo_crop|render), "
"usable (false only if blank/corrupt/not a product), description (one sentence). "
'Never use the word "Wallpaper" — say "Wallcovering".'
)
schema = {"type":"object","properties":{
"colorNames":{"type":"array","items":{"type":"string"}},
"backgroundIndex":{"type":"integer"},
"styles":{"type":"array","items":{"type":"string"}},
"patterns":{"type":"array","items":{"type":"string"}},
"material":{"type":"string"},"imageType":{"type":"string"},
"usable":{"type":"boolean"},"description":{"type":"string"}},
"required":["colorNames","backgroundIndex","styles","patterns","material","usable"]}
body = json.dumps({"model":VL_MODEL,"prompt":prompt,"images":[image_b64],
"stream":False,"format":schema,"keep_alive":"15m",
"options":{"temperature":0.1}}).encode()
req = urllib.request.Request(OLLAMA + "/api/generate", data=body,
headers={"Content-Type":"application/json"})
with urllib.request.urlopen(req, timeout=300) as resp:
raw = json.loads(resp.read().decode())
return json.loads(raw["response"])
def clean(arr, cap=4):
out, seen = [], set()
for v in (arr or []):
s = str(v or "").strip()
if not s or s.lower() in ("none","n/a","na","null","undefined"): continue
k = s.lower()
if k in seen: continue
seen.add(k); out.append(s)
if len(out) >= cap: break
return out
def enrich_one(sku):
img = os.path.join(HENRY, sku + ".jpg")
if not os.path.exists(img):
return {"sku": sku, "error": "image_not_found"}
t0 = time.time()
samp = palette(img, 6)
pal = samp.get("palette", [])
res = vl(samp["image_b64"], pal)
if res.get("usable") is False:
return {"sku": sku, "usable": False, "wall_s": round(time.time()-t0,1)}
names = res.get("colorNames", [])
colors = [{"name": (names[i] if i < len(names) else "").strip(),
"hex": c["hex"], "percentage": c.get("percentage")}
for i, c in enumerate(pal)]
bgi = res.get("backgroundIndex", 0)
if not (isinstance(bgi, int) and 0 <= bgi < len(colors)): bgi = 0
fg = [c for i, c in enumerate(colors) if i != bgi]
dominant = (fg[0] if fg else (colors[0] if colors else {"hex": ""}))
return {
"sku": sku,
"hex": [c["hex"] for c in colors],
"bg_hex": colors[bgi]["hex"] if colors else "",
"bg_name": colors[bgi]["name"] if colors else "",
"dominant_hex": dominant["hex"],
"colorway_name": lexicon_name(dominant["hex"]),
"vl_color_names": [c["name"] for c in colors],
"styles": clean(res.get("styles")),
"material": (res.get("material") or "").strip(),
"patterns": clean(res.get("patterns")),
"imageType": res.get("imageType"),
"description": (res.get("description") or "").strip().replace("Wallpaper","Wallcovering"),
"_provider": "local-hybrid", "_cost": "$0 (local)",
"wall_s": round(time.time()-t0, 1),
}
def main():
skus = [l.strip() for l in open(sys.argv[1]) if l.strip()]
out_path = sys.argv[2]
ok = fail = 0
with open(out_path, "w") as fout:
for i, sku in enumerate(skus, 1):
try:
row = enrich_one(sku)
if row.get("error") or row.get("usable") is False:
fail += 1
else:
ok += 1
fout.write(json.dumps(row, ensure_ascii=False) + "\n")
fout.flush()
w = row.get("wall_s", "?")
print(f"[{i}/{len(skus)}] {sku} ok={ok} fail={fail} {w}s "
f"cw={row.get('colorway_name')} mat={row.get('material')}", flush=True)
except Exception as e:
fail += 1
fout.write(json.dumps({"sku": sku, "error": str(e)[:200]}) + "\n"); fout.flush()
print(f"[{i}/{len(skus)}] {sku} ERROR {str(e)[:120]}", flush=True)
print(f"\nDONE ok={ok} fail={fail} total={len(skus)}", flush=True)
if __name__ == "__main__":
main()