← back to Designer Wallcoverings
contrarian pass 2 fixes: neutral cool-grey lexicon (Mist/Fog/Zinc/Putty) kills Celadon-on-grey; durable enrich-failures.jsonl; marketed-vs-image mismatch review log; drop center-crop (hurt full-bleed swatches)
d8edd9bf1664a94d6499715ec640223a0a6710b6 · 2026-07-07 10:41:10 -0700 · Steve
Files touched
M shopify/scripts/enrich-color-details-local.py
Diff
commit d8edd9bf1664a94d6499715ec640223a0a6710b6
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jul 7 10:41:10 2026 -0700
contrarian pass 2 fixes: neutral cool-grey lexicon (Mist/Fog/Zinc/Putty) kills Celadon-on-grey; durable enrich-failures.jsonl; marketed-vs-image mismatch review log; drop center-crop (hurt full-bleed swatches)
---
shopify/scripts/enrich-color-details-local.py | 33 +++++++++++++++++++++++----
1 file changed, 29 insertions(+), 4 deletions(-)
diff --git a/shopify/scripts/enrich-color-details-local.py b/shopify/scripts/enrich-color-details-local.py
index b5f5791b..63d07fca 100644
--- a/shopify/scripts/enrich-color-details-local.py
+++ b/shopify/scripts/enrich-color-details-local.py
@@ -30,6 +30,16 @@ for line in open(os.path.join(HERE, '..', '.env')):
STORE = ENV['SHOPIFY_STORE_DOMAIN']; TOKEN = ENV['SHOPIFY_ADMIN_TOKEN']
API = f"https://{STORE}/admin/api/2024-10/graphql.json"
OLLAMA = 'http://127.0.0.1:11434/api/generate'; MODEL = 'qwen2.5vl:7b'
+FAIL_LOG = os.path.join(HERE, '..', 'enrich-failures.jsonl') # durable record of failed SKUs
+REVIEW_LOG = os.path.join(HERE, '..', 'enrich-marketed-mismatch.jsonl') # marketed name conflicts with palette
+
+
+def append_jsonl(path, obj):
+ try:
+ with open(path, 'a') as f:
+ f.write(json.dumps(obj) + '\n')
+ except Exception:
+ pass
try:
from PIL import Image
@@ -98,6 +108,9 @@ def iter_published(skip_enriched=True):
def dominant_colors(img_bytes, k=6):
"""Real dominant colors via median-cut. Returns [(hex,pct)] sorted desc, near-dupes merged."""
im = Image.open(io.BytesIO(img_bytes)).convert('RGB')
+ # NOTE: DW product images are full-bleed edge-to-edge swatches (verified by QA),
+ # so a center-crop only distorts the sample (tested: darkened a teal grasscloth to
+ # "Midnight"). No shot-on-white borders to trim → cluster the whole image.
if max(im.size) > 400:
im.thumbnail((400, 400))
q = im.quantize(colors=max(8, k + 4), method=Image.MEDIANCUT)
@@ -174,7 +187,11 @@ NAMED_COLORS = {
'Charcoal': (66, 68, 72), 'Graphite': (74, 76, 80), 'Pewter': (128, 130, 132),
'Gray': (140, 142, 144), 'Silver': (194, 196, 198), 'Dove': (198, 196, 192),
'Ash': (170, 168, 164), 'Smoke': (156, 156, 158), 'Stone': (172, 166, 156),
- 'Flint': (110, 110, 108), 'Black': (38, 38, 40), 'Onyx': (46, 46, 48),
+ 'Flint': (110, 110, 108),
+ # Neutral cool/warm greys covering the 168-192 band so slightly green- or warm-
+ # tinged greys (grasscloth, naturals, greige) don't fall to 'Celadon'/'Sage'.
+ 'Mist': (185, 189, 187), 'Fog': (176, 180, 178), 'Cloud': (196, 198, 196),
+ 'Zinc': (162, 166, 165), 'Driftwood': (176, 172, 164), 'Putty': (184, 178, 168), 'Black': (38, 38, 40), 'Onyx': (46, 46, 48),
'Ebony': (52, 50, 48), 'Jet': (30, 30, 32),
}
_NAMED_ITEMS = list(NAMED_COLORS.items())
@@ -256,13 +273,20 @@ def anchor_marketed(colors, p):
if not resolved or not colors:
return colors
canon, kr = resolved
+ def dist(hexstr):
+ h = hexstr.lstrip('#'); r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
+ return (r - kr[0]) ** 2 + (g - kr[1]) ** 2 + (b - kr[2]) ** 2
if colors[0].get('pct', 0) >= 58:
idx = 0
else:
- def dist(hexstr):
- h = hexstr.lstrip('#'); r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
- return (r - kr[0]) ** 2 + (g - kr[1]) ** 2 + (b - kr[2]) ** 2
idx = min(range(len(colors)), key=lambda i: dist(colors[i]['hex']))
+ # Honesty flag: if the marketed color is nowhere near ANY extracted swatch, the
+ # vendor's colorway name conflicts with the actual image — log for merchant review
+ # (do NOT silently override; the swatches stay truthful to the image).
+ if min(dist(c['hex']) for c in colors) > 95 ** 2:
+ append_jsonl(REVIEW_LOG, {'id': p.get('id'), 'title': p.get('title'),
+ 'marketed': canon, 'palette': [c['name'] for c in colors]})
+ return colors
if all(c['name'] != canon for j, c in enumerate(colors) if j != idx):
colors[idx]['name'] = canon
return colors
@@ -301,6 +325,7 @@ def main():
print(f"[{ok}] {title} … ✓ {summary} ({time.time()-t0:.0f}s)", flush=True)
except Exception as e:
fail += 1
+ append_jsonl(FAIL_LOG, {'id': p['id'], 'title': p['title'], 'error': str(e)[:200]})
print(f"[x] {title} … FAILED: {str(e)[:80]}", flush=True)
if (ok + fail) % 50 == 0:
el = time.time() - t_start
← d43915e2 auto-save: 2026-07-07T10:30:01 (3 files) — pending-approval/
·
back to Designer Wallcoverings
·
enrich: near-solid products trust merchant color name (deep 49c7f068 →