← back to Designer Wallcoverings
enrich: replace LLM naming with deterministic ~115-color interior-design lookup table (DTD verdict A) — reliable+consistent+instant; redmean nearest-match, dark-red family added, --force re-enrich
75188a05dd5d1cbb9c965c76e5eced07e5e2b7a1 · 2026-07-07 10:06:12 -0700 · Steve
Files touched
M shopify/scripts/enrich-color-details-local.py
Diff
commit 75188a05dd5d1cbb9c965c76e5eced07e5e2b7a1
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jul 7 10:06:12 2026 -0700
enrich: replace LLM naming with deterministic ~115-color interior-design lookup table (DTD verdict A) — reliable+consistent+instant; redmean nearest-match, dark-red family added, --force re-enrich
---
shopify/scripts/enrich-color-details-local.py | 109 ++++++++++++++++++++------
1 file changed, 86 insertions(+), 23 deletions(-)
diff --git a/shopify/scripts/enrich-color-details-local.py b/shopify/scripts/enrich-color-details-local.py
index 45079a29..3569ca3b 100644
--- a/shopify/scripts/enrich-color-details-local.py
+++ b/shopify/scripts/enrich-color-details-local.py
@@ -111,24 +111,86 @@ def dominant_colors(img_bytes, k=6):
return out
-def name_hexes(img_b64, hexes):
- """Ask qwen2.5vl to name each REAL hex, in order. Falls back to the hex if it fails."""
- prompt = ("This wallcovering/fabric image has these dominant colors as hex codes: "
- + json.dumps(hexes) + ". For EACH hex, in the SAME order, give a short interior-design "
- "color name (e.g. Sage, Oatmeal, Wedgwood Blue, Slate, Coral). "
- 'Return ONLY JSON {"names":[...]} with the same count and order.')
- body = json.dumps({'model': MODEL, 'stream': False, 'format': 'json', 'prompt': prompt,
- 'images': [img_b64], 'options': {'temperature': 0.1}}).encode()
- try:
- r = urllib.request.urlopen(urllib.request.Request(OLLAMA, body, {'Content-Type': 'application/json'}), timeout=150)
- names = json.loads(json.load(r)['response']).get('names', [])
- except Exception:
- names = []
- out = []
- for i, h in enumerate(hexes):
- nm = str(names[i]).strip()[:24] if i < len(names) and names[i] else h
- out.append(nm)
- return out
+# Deterministic interior-design color lexicon (name -> RGB). Nearest-match names a
+# real pixel hex reliably & consistently — beats LLM naming which drifts to "Slate"
+# on muted tones (DTD verdict A, 2026-07-07). ~110 designer colorway names.
+NAMED_COLORS = {
+ 'White': (250, 250, 248), 'Alabaster': (237, 234, 224), 'Chalk': (240, 238, 230),
+ 'Ivory': (240, 234, 214), 'Cream': (238, 230, 205), 'Eggshell': (230, 222, 202),
+ 'Vanilla': (238, 228, 196), 'Bone': (226, 218, 200), 'Oatmeal': (214, 204, 182),
+ 'Linen': (224, 214, 196), 'Parchment': (226, 214, 186), 'Sand': (206, 188, 154),
+ 'Wheat': (216, 196, 152), 'Straw': (210, 190, 140), 'Buff': (200, 178, 140),
+ 'Beige': (208, 194, 168), 'Greige': (188, 178, 162), 'Mushroom': (176, 164, 148),
+ 'Taupe': (160, 148, 130), 'Fawn': (176, 154, 128), 'Camel': (176, 146, 104),
+ 'Tan': (196, 168, 128), 'Khaki': (176, 160, 110), 'Ochre': (192, 150, 74),
+ 'Honey': (206, 160, 78), 'Amber': (198, 146, 60), 'Gold': (196, 162, 84),
+ 'Antique Gold': (170, 138, 80), 'Brass': (176, 148, 92), 'Bronze': (146, 116, 68),
+ 'Mustard': (188, 158, 62), 'Saffron': (206, 158, 60), 'Marigold': (222, 158, 54),
+ 'Apricot': (226, 176, 128), 'Peach': (232, 190, 156), 'Melon': (236, 178, 140),
+ 'Coral': (224, 130, 104), 'Terracotta': (188, 108, 78), 'Rust': (168, 92, 58),
+ 'Sienna': (150, 90, 56), 'Copper': (170, 104, 66), 'Clay': (176, 120, 94),
+ 'Salmon': (226, 150, 130), 'Blush': (222, 184, 176), 'Petal': (228, 196, 190),
+ 'Rose': (198, 138, 138), 'Dusty Rose': (188, 148, 146), 'Mauve': (168, 132, 136),
+ 'Pink': (222, 160, 176), 'Fuchsia': (196, 84, 130), 'Raspberry': (168, 68, 96),
+ 'Wine': (120, 52, 66), 'Burgundy': (110, 44, 52), 'Merlot': (96, 40, 48),
+ 'Maroon': (112, 46, 50), 'Brick': (150, 66, 58), 'Red': (176, 56, 52),
+ 'Cranberry': (140, 44, 58), 'Oxblood': (78, 38, 38), 'Mahogany': (102, 52, 44),
+ 'Garnet': (112, 42, 46), 'Currant': (94, 40, 44), 'Claret': (100, 44, 52), 'Plum': (108, 70, 92), 'Aubergine': (78, 56, 74),
+ 'Eggplant': (86, 62, 82), 'Grape': (98, 74, 108), 'Purple': (116, 84, 140),
+ 'Violet': (128, 100, 160), 'Lilac': (192, 174, 200), 'Lavender': (198, 188, 214),
+ 'Wisteria': (170, 156, 190), 'Periwinkle': (150, 156, 196), 'Iris': (110, 110, 168),
+ 'Indigo': (70, 74, 120), 'Navy': (44, 54, 84), 'Midnight': (34, 42, 68),
+ 'Sapphire': (52, 72, 120), 'Cobalt': (56, 82, 140), 'Royal Blue': (60, 90, 156),
+ 'Blue': (74, 108, 160), 'Denim': (86, 110, 140), 'Chambray': (128, 150, 178),
+ 'Wedgwood Blue': (120, 150, 176), 'Cornflower': (128, 150, 200), 'Sky': (170, 196, 216),
+ 'Powder Blue': (188, 206, 220), 'Steel Blue': (110, 132, 152), 'Slate Blue': (98, 118, 140),
+ 'Slate': (108, 116, 124), 'Teal': (48, 106, 112), 'Peacock': (40, 96, 110),
+ 'Deep Teal': (36, 78, 86), 'Aqua': (110, 180, 186), 'Aquamarine': (128, 178, 178),
+ 'Turquoise': (100, 178, 176), 'Seafoam': (168, 206, 194), 'Celadon': (176, 196, 176),
+ 'Sage': (150, 168, 138), 'Eucalyptus': (140, 156, 138), 'Willow': (150, 164, 120),
+ 'Fern': (108, 132, 92), 'Moss': (114, 128, 84), 'Olive': (118, 118, 74),
+ 'Olive Drab': (104, 104, 68), 'Loden': (90, 104, 72), 'Forest': (60, 96, 72),
+ 'Hunter Green': (52, 84, 64), 'Emerald': (48, 120, 92), 'Green': (78, 132, 90),
+ 'Pistachio': (188, 202, 150), 'Chartreuse': (176, 196, 100), 'Lime': (168, 188, 90),
+ 'Pear': (196, 200, 130), 'Citron': (200, 196, 110), 'Pale Green': (198, 210, 176),
+ 'Mint': (186, 214, 188), 'Chocolate': (78, 56, 44), 'Espresso': (58, 44, 38),
+ 'Coffee': (86, 64, 50), 'Mocha': (118, 96, 78), 'Walnut': (108, 80, 56),
+ 'Chestnut': (128, 84, 58), 'Cocoa': (108, 82, 66), 'Brown': (120, 88, 62),
+ 'Pecan': (150, 112, 78), 'Caramel': (176, 130, 82), 'Toffee': (168, 122, 80),
+ '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),
+ 'Ebony': (52, 50, 48), 'Jet': (30, 30, 32),
+}
+_NAMED_ITEMS = list(NAMED_COLORS.items())
+
+
+def table_name(hexstr):
+ """Nearest interior-design color name for a real pixel hex (perceptual-ish weights)."""
+ h = hexstr.lstrip('#')
+ r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
+ best, bd = 'Neutral', 1e18
+ for nm, (cr, cg, cb) in _NAMED_ITEMS:
+ # redmean weighting for better perceptual distance
+ rm = (r + cr) / 2
+ d = (2 + rm / 256) * (r - cr) ** 2 + 4 * (g - cg) ** 2 + (2 + (255 - rm) / 256) * (b - cb) ** 2
+ if d < bd:
+ bd = d; best = nm
+ return best
+
+
+def name_hexes(hexes):
+ """Deterministic table naming; de-dupes repeated names (e.g. two 'Teal' -> 'Teal', 'Deep Teal')."""
+ names, used = [], {}
+ for hx_ in hexes:
+ nm = table_name(hx_)
+ if nm in used: # differentiate a duplicate by lightness qualifier
+ h = hx_.lstrip('#'); lum = 0.3 * int(h[0:2], 16) + 0.59 * int(h[2:4], 16) + 0.11 * int(h[4:6], 16)
+ nm = ('Light ' if lum > 150 else 'Deep ') + nm
+ used[nm] = 1
+ names.append(nm)
+ return names
def write_metafield(gid, colors):
@@ -143,8 +205,7 @@ def write_metafield(gid, colors):
def enrich_one(p, dry):
img_bytes = urllib.request.urlopen(p['featuredImage']['url'], timeout=40).read()
hexpct = dominant_colors(img_bytes)
- b64 = base64.b64encode(img_bytes).decode()
- names = name_hexes(b64, [h for h, _ in hexpct])
+ names = name_hexes([h for h, _ in hexpct]) # deterministic table (no model call)
colors = [{'name': names[i], 'hex': hexpct[i][0], 'pct': hexpct[i][1]} for i in range(len(hexpct))]
if not dry:
write_metafield(p['id'], colors)
@@ -160,10 +221,12 @@ def main():
if '--limit' in args: limit = int(args[args.index('--limit') + 1])
if '--all-published' in args:
- # Resumable full-catalog run: skips products that already have color_details.
- print(f"FULL-CATALOG enrichment — hybrid (PIL pixels + {MODEL} naming), $0 local, resumable\n", flush=True)
+ force = '--force' in args # re-enrich ALL (ignore existing color_details)
+ # Resumable full-catalog run: skips products already in the current schema unless --force.
+ print(f"FULL-CATALOG enrichment — hybrid (PIL pixels + deterministic table naming), $0 local, "
+ f"{'FORCE re-enrich all' if force else 'resumable'}\n", flush=True)
ok = fail = 0; t_start = time.time()
- for p in iter_published(skip_enriched=True):
+ for p in iter_published(skip_enriched=not force):
t0 = time.time(); title = p['title'][:46]
try:
summary = enrich_one(p, dry)
← 55858e64 Alan Campbell: reclassify wallcovering->fabric (type/title/t
·
back to Designer Wallcoverings
·
contrarian fixes: (1) read color_details BEFORE legacy color b21d7edb →