[object Object]

← back to Japan Enrich

greenland: per-colorway color enrichment (Pillow, $0 local, disk-safe)

b644473469f8c412d366ff1330cb2ffd5294aa40 · 2026-07-01 09:53:22 -0700 · Steve Abrams

- enrich_greenland.py: downloads each colorway swatch DOWNSCALED to 256px (~15KB)
  instead of keeping full-res (Greenland serves ~1MB even for thumbnails → 3GB avoided;
  actual footprint ~47MB). Pillow hex + color_family per colorway; material carried from
  the vendor category tree (no vision pass needed). → out/greenland-enriched.jsonl.
- viewer: greenland branch now reads the ENRICH index (hex/color_family/palette/siblings)
  so the color-family sort + swatch nav light up per colorway; ENRICH_FILES + watchFile
  wired for greenland-enriched so it hot-reloads as enrichment lands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit b644473469f8c412d366ff1330cb2ffd5294aa40
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 1 09:53:22 2026 -0700

    greenland: per-colorway color enrichment (Pillow, $0 local, disk-safe)
    
    - enrich_greenland.py: downloads each colorway swatch DOWNSCALED to 256px (~15KB)
      instead of keeping full-res (Greenland serves ~1MB even for thumbnails → 3GB avoided;
      actual footprint ~47MB). Pillow hex + color_family per colorway; material carried from
      the vendor category tree (no vision pass needed). → out/greenland-enriched.jsonl.
    - viewer: greenland branch now reads the ENRICH index (hex/color_family/palette/siblings)
      so the color-family sort + swatch nav light up per colorway; ENRICH_FILES + watchFile
      wired for greenland-enriched so it hot-reloads as enrichment lands.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 enrich_greenland.py    | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++
 viewer-local/server.js | 21 ++++++------
 2 files changed, 98 insertions(+), 9 deletions(-)

diff --git a/enrich_greenland.py b/enrich_greenland.py
new file mode 100644
index 0000000..e93840a
--- /dev/null
+++ b/enrich_greenland.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+"""
+Greenland enricher — per-colorway hex + color_family (Pillow, $0 local).
+
+Greenland rows are PATTERNS holding {colorway_skus:[...], sku_images:{sku:url}}.
+Material is already known from the vendor category tree (staging `material`), so
+this pass does NOT run vision — it only needs the per-colorway dominant color for
+the color-family sort/swatch nav.
+
+DISK-SAFE: Greenland serves full-res (~1MB) even for thumbnails, so we DON'T keep
+the originals (3117×1MB ≈ 3GB). Each image is downscaled to a ≤256px swatch on
+download (~15KB) into images/greenland/, and hex is computed from that. Total
+footprint ~47MB, and the swatches double as local swatches if we localize later.
+
+Emits out/greenland-enriched.jsonl — ONE line per colorway SKU:
+  {"mfr_sku":"<sku>", "source":"greenland", "enrich":{"hex":[...], "color_family":..., "material":...}}
+so the viewer's ENRICH index (keyed by normSku(mfr_sku)) picks it up per colorway card.
+"""
+import os, json, io, urllib.request, concurrent.futures as cf
+from enrich import hex_palette, dominant_family, norm_sku
+
+IMG_DIR = "images/greenland"
+STAGING = "staging/greenland-staging.jsonl"
+OUT = "out/greenland-enriched.jsonl"
+os.makedirs(IMG_DIR, exist_ok=True)
+os.makedirs("out", exist_ok=True)
+
+
+def fetch_swatch(sku, url):
+    """Download → downscale to ≤256px → save small jpg. Returns path or None."""
+    from PIL import Image
+    dst = os.path.join(IMG_DIR, norm_sku(sku) + ".jpg")
+    if os.path.exists(dst) and os.path.getsize(dst) > 0:
+        return dst
+    try:
+        req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
+        data = urllib.request.urlopen(req, timeout=45).read()
+        if len(data) < 200:
+            return None
+        im = Image.open(io.BytesIO(data)).convert("RGB")
+        im.thumbnail((256, 256))              # discard full-res; keep only the swatch
+        im.save(dst, "JPEG", quality=82)
+        return dst
+    except Exception:
+        return None
+
+
+def main():
+    rows = [json.loads(l) for l in open(STAGING) if l.strip()]
+    # (sku, url, material) for every colorway that has an image
+    jobs, mat = [], {}
+    for r in rows:
+        imgs = r.get("sku_images") or {}
+        material = r.get("material")
+        for sku, url in imgs.items():
+            if url:
+                jobs.append((sku, url))
+                mat[norm_sku(sku)] = material
+    print(f"greenland: {len(rows)} patterns, {len(jobs)} colorway swatches to enrich", flush=True)
+
+    done = 0
+    with open(OUT, "w") as out, cf.ThreadPoolExecutor(max_workers=8) as ex:
+        futs = {ex.submit(fetch_swatch, s, u): s for s, u in jobs}
+        for fut in cf.as_completed(futs):
+            sku = futs[fut]
+            done += 1
+            path = fut.result()
+            if not path:
+                continue
+            try:
+                hexes = hex_palette(path)
+                fam = dominant_family(hexes)
+            except Exception:
+                continue
+            rec = {"mfr_sku": sku, "source": "greenland",
+                   "enrich": {"hex": hexes, "color_family": fam,
+                              "material": mat.get(norm_sku(sku)), "style": None}}
+            out.write(json.dumps(rec, ensure_ascii=False) + "\n")
+            out.flush()
+            if done % 200 == 0:
+                print(f"  {done}/{len(jobs)}", flush=True)
+    print(f"greenland: enriched → {OUT}", flush=True)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/viewer-local/server.js b/viewer-local/server.js
index a18daca..a07af28 100644
--- a/viewer-local/server.js
+++ b/viewer-local/server.js
@@ -45,6 +45,8 @@ const ENRICH_FILES = [
   path.join(__dirname, '..', 'out', 'pilot-enriched.jsonl'),        // local pilot / full backfill
   path.join(DIR, 'sangetsu-enriched.jsonl'),                        // post-deploy: beside staging
   path.join(__dirname, '..', 'out', 'sangetsu-enriched.jsonl'),     // Sangetsu per-colorway enrich (keyed by colorway SKU)
+  path.join(DIR, 'greenland-enriched.jsonl'),                       // post-deploy: beside staging
+  path.join(__dirname, '..', 'out', 'greenland-enriched.jsonl'),    // Greenland per-colorway hex/family (keyed by colorway SKU)
 ].filter(Boolean);
 function loadEnrich() {
   const idx = {};
@@ -188,7 +190,8 @@ function buildRows() {
     const emit = skus.length ? skus : [r.product_code || `GL-${r.product_id}`];
     for (const sku of emit) {
       const isCw = skus.length > 0;
-      const siblings = isCw ? skus.filter((s) => s !== sku).map((s) => ({ sku: s, hex: null, family: null })) : [];
+      const e = ENRICH[normSku(sku)] || null;             // per-colorway hex/family once enriched
+      const siblings = isCw ? skus.filter((s) => s !== sku).map((s) => siblingChip(s)) : [];
       rows.push({
         source: 'greenland',
         sku,
@@ -209,15 +212,15 @@ function buildRows() {
         is_sample: false,
         colorway_count: isCw ? skus.length : 1,
         siblings,
-        // material comes straight from the vendor category tree (reliable), so surface it
-        // as the enrichment "material" without an image-vision pass; color stays unenriched.
-        enriched: false,
-        color_family: null,
-        style: null,
-        material: r.material || null,
+        // material comes straight from the vendor category tree (reliable); hex/color_family
+        // land per-colorway once enrich_greenland.py (Pillow, $0 local) writes its overlay.
+        enriched: !!e,
+        color_family: e ? e.color_family : null,
+        style: e ? e.style : null,
+        material: (e && e.material) || r.material || null,
         color_name: colors[sku] || null,                   // per-colorway vendor color name
-        hex: null,
-        palette: [],
+        hex: (e && e.hex && e.hex[0]) ? e.hex[0].hex : null,
+        palette: (e && e.hex) ? e.hex.slice(0, 5).map((x) => x.hex) : [],
       });
     }
   }

← cb79303 greenland: scrape vendor API → 880 patterns/3117 colorways,  ·  back to Japan Enrich  ·  auto-save: 2026-07-01T10:11:20 (1 files) — scrape_sangetsu_b 501439c →