[object Object]

← 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

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 →