← 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
A enrich_greenland.pyM viewer-local/server.js
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 →