← back to Wallco Ai
add read-only color-name vs dominant-hex drift audit (10171 root-cause)
5713b85223adda54e0e7365ff9660ebba6d85a9e · 2026-06-01 16:35:06 -0700 · Steve Abrams
Files touched
A scripts/audit-colorname-hex-drift.py
Diff
commit 5713b85223adda54e0e7365ff9660ebba6d85a9e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jun 1 16:35:06 2026 -0700
add read-only color-name vs dominant-hex drift audit (10171 root-cause)
---
scripts/audit-colorname-hex-drift.py | 195 +++++++++++++++++++++++++++++++++++
1 file changed, 195 insertions(+)
diff --git a/scripts/audit-colorname-hex-drift.py b/scripts/audit-colorname-hex-drift.py
new file mode 100644
index 0000000..4ff6123
--- /dev/null
+++ b/scripts/audit-colorname-hex-drift.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python3
+"""audit-colorname-hex-drift.py — READ-ONLY scope audit for the color-name vs
+dominant-hex decoupling Steve flagged on /design/10171 ("Crimson Studio /
+Midnight Navy" displayed over a #FFCFD8 pale-pink chip).
+
+Root cause: the LIVE prod data/designs.json snapshot carries titles +
+"style · colorway" categories whose color WORD was generated by an OLDER
+refresh_designs_snapshot.py — before the lightness-aware colorway_name() and
+ground_hex() ground-sampling fixes landed (both 2026-05-31). The current Mac2
+script already derives the color name from dominant_hex correctly. So the fix
+is to REBUILD the snapshot on Mac2 with the current script (no code change
+needed for the colorway logic) and ship it.
+
+This script reproduces the LIVE display name (as the prod payload shows it) and
+the CORRECTED name the current colorway_name() would produce, and reports how
+many published rows would change. NO writes. Pure audit.
+
+Compares against the LIVE prod catalog pulled via the public /api/designs API
+(no auth, no DB write) so the number reflects what shoppers actually see.
+
+Usage:
+ python3 scripts/audit-colorname-hex-drift.py # pulls live API
+ python3 scripts/audit-colorname-hex-drift.py /tmp/prod_all.json # cached
+"""
+import sys, json, re, colorsys, urllib.request
+
+# ── current snapshot colorway_name() (verbatim from refresh_designs_snapshot.py)
+COLORWAYS = {
+ 'bands': [
+ (15, {'dark': 'Oxblood', 'mid': 'Claret', 'light': 'Blush'}),
+ (35, {'dark': 'Terracotta', 'mid': 'Amber', 'light': 'Apricot'}),
+ (55, {'dark': 'Ochre', 'mid': 'Honey', 'light': 'Chamois'}),
+ (80, {'dark': 'Loden', 'mid': 'Olive', 'light': 'Chartreuse'}),
+ (155, {'dark': 'Bottle Green','mid': 'Sage', 'light': 'Celadon'}),
+ (200, {'dark': 'Verdigris', 'mid': 'Teal', 'light': 'Eau de Nil'}),
+ (250, {'dark': 'Prussian', 'mid': 'Sapphire', 'light': 'Delft'}),
+ (290, {'dark': 'Aubergine', 'mid': 'Amethyst', 'light': 'Wisteria'}),
+ (330, {'dark': 'Damson', 'mid': 'Plum', 'light': 'Mauve'}),
+ (361, {'dark': 'Garnet', 'mid': 'Rosewood', 'light': 'Rose'}),
+ ],
+ 'neutral': {'vlight': 'Alabaster', 'light': 'Oyster', 'mid': 'Greige',
+ 'dark': 'Pewter', 'vdark': 'Charcoal'},
+}
+
+def hex_to_hls(hex_):
+ if not hex_ or not hex_.startswith('#'):
+ return None
+ h = hex_.lstrip('#')
+ if len(h) == 3:
+ h = ''.join(c * 2 for c in h)
+ if len(h) < 6 or any(c not in '0123456789abcdefABCDEF' for c in h[:6]):
+ return None
+ r, g, b = int(h[0:2], 16) / 255, int(h[2:4], 16) / 255, int(h[4:6], 16) / 255
+ return colorsys.rgb_to_hls(r, g, b)
+
+def colorway_name(hex_):
+ hls = hex_to_hls(hex_)
+ if hls is None:
+ return None
+ h, l, s = hls
+ if s < 0.10:
+ n = COLORWAYS['neutral']
+ if l > 0.85: return n['vlight']
+ if l > 0.65: return n['light']
+ if l > 0.42: return n['mid']
+ if l > 0.22: return n['dark']
+ return n['vdark']
+ deg = h * 360
+ for maxd, shades in COLORWAYS['bands']:
+ if deg < maxd:
+ return shades['light' if l > 0.62 else 'dark' if l < 0.34 else 'mid']
+ return 'Rose'
+
+# coarse chromatic zone, for "hard" (>=2 buckets apart) disagreement
+def zone(hex_):
+ hls = hex_to_hls(hex_)
+ if hls is None:
+ return None
+ h, s, l = hls
+ if s < 0.12: return 'Neutral'
+ d = h * 360
+ if d >= 345 or d < 15: return 'Red'
+ if d < 60: return 'Warm'
+ if d < 170: return 'Green'
+ if d < 260: return 'Blue'
+ if d < 290: return 'Violet'
+ return 'Pink'
+
+# named color word → coarse zone (titles + category slugs)
+WORD_ZONE = {
+ 'crimson': 'Red', 'scarlet': 'Red', 'oxblood': 'Red', 'bordeaux': 'Red',
+ 'garnet': 'Red', 'ruby': 'Red', 'claret': 'Red', 'rosewood': 'Red',
+ 'terracotta': 'Warm', 'amber': 'Warm', 'rust': 'Warm', 'copper': 'Warm',
+ 'honey': 'Warm', 'gold': 'Warm', 'brass': 'Warm', 'ochre': 'Warm',
+ 'apricot': 'Warm', 'chamois': 'Warm', 'saffron': 'Warm', 'mustard': 'Warm',
+ 'olive': 'Green', 'loden': 'Green', 'moss': 'Green', 'forest': 'Green',
+ 'sage': 'Green', 'celadon': 'Green', 'emerald': 'Green', 'jade': 'Green',
+ 'chartreuse': 'Green', 'verdant': 'Green',
+ 'teal': 'Blue', 'verdigris': 'Blue', 'navy': 'Blue', 'indigo': 'Blue',
+ 'sapphire': 'Blue', 'cobalt': 'Blue', 'azure': 'Blue', 'midnight': 'Blue',
+ 'prussian': 'Blue', 'delft': 'Blue',
+ 'violet': 'Violet', 'plum': 'Violet', 'aubergine': 'Violet',
+ 'amethyst': 'Violet', 'wisteria': 'Violet', 'damson': 'Violet',
+ 'rose': 'Pink', 'blush': 'Pink', 'pink': 'Pink', 'mauve': 'Pink',
+ 'fuchsia': 'Pink', 'magenta': 'Pink',
+}
+
+SUFFIXES = r'(Studio|Atelier|Folio|Salon|Reverie|House|Manor|Origin|Drift|' \
+ r'Daydream|Halcyon|Vespers|Idyll|Repose|Slumber|Haze|Lullaby)'
+TITLE_RE = re.compile(r'^([A-Za-z]+)\s+' + SUFFIXES + r'\s+No\.\d+', re.I)
+IN_RE = re.compile(r'\bin\s+([A-Za-z][A-Za-z\- ]*?)\s*$', re.I)
+
+def title_color_word(title):
+ if not title:
+ return None
+ m = TITLE_RE.match(title.strip())
+ if m:
+ return m.group(1).lower()
+ m = IN_RE.search(title.strip())
+ if m:
+ return m.group(1).strip().split()[-1].lower()
+ return None
+
+def load():
+ if len(sys.argv) > 1:
+ return json.load(open(sys.argv[1]))
+ items, page = [], 1
+ while True:
+ url = f'https://wallco.ai/api/designs?limit=2000&page={page}'
+ with urllib.request.urlopen(url, timeout=30) as r:
+ d = json.loads(r.read())
+ items += d['items']
+ if page >= d['pages']:
+ break
+ page += 1
+ seen = {}
+ for it in items:
+ seen[it['id']] = it
+ return list(seen.values())
+
+def main():
+ items = load()
+ pub = [d for d in items if d.get('is_published') and not d.get('user_removed')]
+
+ title_total = title_hard = 0
+ cat_total = cat_hard = 0
+ would_rename = 0
+ examples = []
+
+ for d in pub:
+ hexd = d.get('dominant_hex') or ''
+ corrected = colorway_name(hexd)
+ zh = zone(hexd)
+
+ # title color word
+ tw = title_color_word(d.get('title') or '')
+ if tw and tw in WORD_ZONE:
+ title_total += 1
+ zw = WORD_ZONE[tw]
+ if zh and zw != 'Neutral' and zh != 'Neutral' and zw != zh:
+ title_hard += 1
+ if corrected and len(examples) < 30:
+ examples.append((d['id'], d.get('title'), hexd, zw, zh, corrected))
+
+ # category "style · slug" color word
+ cat = d.get('category') or ''
+ if ' · ' in cat:
+ cw = cat.split(' · ', 1)[1].strip().lower().split('-')[0]
+ if cw in WORD_ZONE:
+ cat_total += 1
+ zw = WORD_ZONE[cw]
+ if zh and zw != 'Neutral' and zh != 'Neutral' and zw != zh:
+ cat_hard += 1
+
+ # would the current snapshot give a DIFFERENT color word than what's displayed?
+ if corrected and tw and corrected.lower() != tw:
+ would_rename += 1
+
+ print(f"LIVE published (not user_removed): {len(pub)}")
+ print()
+ print(f"TITLE color-word vs hex (evaluable): {title_total}")
+ print(f" HARD chromatic mismatch (Steve's 10171): {title_hard}")
+ print(f"CATEGORY 'style · slug' color vs hex (eval): {cat_total}")
+ print(f" HARD chromatic mismatch: {cat_hard}")
+ print()
+ print(f"Rows whose displayed color WORD != the name ")
+ print(f"the CURRENT colorway_name(hex) would produce: {would_rename}")
+ print(f" (== records a Mac2 snapshot rebuild would visibly re-title)")
+ print()
+ print("Sample hard mismatches (id | live title | hex | named-zone -> hex-zone | corrected):")
+ for e in examples:
+ print(f" {e[0]:>6} | {e[1]:<28} | {e[2]:<9} | {e[3]}->{e[4]:<7} | {e[5]}")
+
+if __name__ == '__main__':
+ main()
← 4746851 PDP: explicit Wallpaper/Wall Mural format toggle on every de
·
back to Wallco Ai
·
PDP v11-best (live theme): Wallpaper/Wall Mural toggle on ev bd05114 →