← back to Japan Enrich

compute_distributor_colors.py

54 lines

#!/usr/bin/env python3
"""Compute the 'color ID' (dominant hex + palette + color-family + hue) for every distributor
catalog product, LOCALLY ($0, Pillow median-cut — same logic as our sangetsu enrichment).
This is the match key: wallcoverings keep their color across renamed catalogs, so color is the
strongest cross-catalog visual signal. Output: staging/distributor-colors.jsonl. Resumable."""
import json, os, sys, urllib.request, tempfile, colorsys
from concurrent.futures import ThreadPoolExecutor, as_completed
sys.path.insert(0, os.path.dirname(__file__))
from enrich import hex_palette, dominant_family

HERE = os.path.dirname(__file__)
CAT = json.load(open(os.path.join(HERE, 'staging', 'distributor-catalogs.json')))
OUT = os.path.join(HERE, 'staging', 'distributor-colors.jsonl')
UA = 'Mozilla/5.0 (Macintosh)'

def hue_of(hx):
    h = hx.lstrip('#'); r, g, b = [int(h[i:i+2], 16)/255 for i in (0, 2, 4)]
    return round(colorsys.rgb_to_hsv(r, g, b)[0]*360)

def process(p):
    try:
        data = urllib.request.urlopen(urllib.request.Request(p['image_url'], headers={'User-Agent': UA}), timeout=20).read()
        if len(data) < 200: return None
        f = tempfile.NamedTemporaryFile(suffix='.jpg', delete=False); f.write(data); f.close()
        pal = hex_palette(f.name); os.unlink(f.name)
        if not pal: return None
        return {'vendor': p['vendor_code'], 'sku': p['mfr_sku'], 'name': p.get('pattern_name'),
                'color_name': p.get('color_name'), 'image_url': p['image_url'], 'product_url': p.get('product_url'),
                'hex': pal[0]['hex'], 'palette': [x['hex'] for x in pal[:4]],
                'family': dominant_family(pal), 'hue': hue_of(pal[0]['hex'])}
    except Exception:
        return None

def main():
    done = set()
    if os.path.exists(OUT):
        for l in open(OUT):
            try:
                r = json.loads(l); done.add(r['sku'] + '|' + r['vendor'])
            except Exception: pass
    todo = [p for v in CAT.values() for p in v if (p['mfr_sku'] + '|' + p['vendor_code']) not in done]
    print(f"computing color for {len(todo)} distributor products ({len(done)} already done)", flush=True)
    n = 0
    with open(OUT, 'a') as out, ThreadPoolExecutor(max_workers=8) as ex:
        for f in as_completed({ex.submit(process, p): p for p in todo}):
            r = f.result()
            if r: out.write(json.dumps(r, ensure_ascii=False) + '\n'); out.flush()
            n += 1
            if n % 250 == 0: print(f"  {n}/{len(todo)}", flush=True)
    print(f"done — {sum(1 for _ in open(OUT))} products have a color ID", flush=True)

if __name__ == '__main__':
    main()