← back to Dw Yolo Loop
artmura-site/build-romo-enrich.js
99 lines
#!/usr/bin/env node
/*
* build-romo-enrich.js — attribute Romo products onto the interior-designer lexicon.
* SOURCE OF TRUTH = each live product's own Shopify `tags` in lines/romo.json (covers all 578).
* romo_catalog.dw_sku supplies a precise dominant hex when the key matches (refinement only).
* Emits lines/romo-colors.json { "<dwsku>": { bucket,hue,sat,val,colorway,styles,motifs } }
* keyed to match server.js `colors[r.mfr_sku]`. Read-only; $0 local.
*/
const fs = require('fs'), path = require('path'), { execSync } = require('child_process');
const HERE = __dirname;
const SNAP = path.join(HERE,'lines','romo.json');
const OUT = path.join(HERE,'lines','romo-colors.json');
// ---- controlled STYLE vocab (interior-designer lexicon) ----
const STYLES = new Set(['Contemporary','Traditional','Minimalist','Scandinavian','Coastal','Farmhouse',
'Chinoiserie','Mid-Century Modern','Art Deco','Industrial','Transitional','Modern','Victorian',
'Bohemian','Rustic','Eclectic','Cottage','Organic Modern','Hollywood Regency','Maximalist','Grandmillennial']);
const STYLE_ALIAS = { 'mid-century modern':'Mid-Century Modern','midcentury modern':'Mid-Century Modern' };
// ---- controlled MOTIF vocab; tag(lower) -> canonical motif ----
const MOTIF = {
'grasscloth':'Grasscloth','solid':'Solid','stripe':'Stripe','striped':'Stripe','textured':'Textured',
'texture':'Textured','linen texture':'Textured','silk texture':'Textured','woven':'Textured',
'geometric':'Geometric','floral':'Floral','botanical':'Botanical','herringbone':'Herringbone',
'chevron':'Chevron','damask':'Damask','trellis':'Trellis','animal':'Animalier','animals':'Animalier',
'animalier':'Animalier','toile':'Toile','ikat':'Ikat','paisley':'Paisley','abstract':'Abstract',
'mural':'Mural / Scenic','wood grain':'Faux Bois','faux bois':'Faux Bois','polka dot':'Polka Dot',
'checkerboard':'Check','brick':'Brick','tropical':'Tropical / Palm','border':'Border','moire':'Moiré',
};
// ---- colorway tag(lower) -> one of the 12 frontend BUCKET_HEX keys ----
const B = (k)=>k;
const COLOR_BUCKET = {};
const add=(bucket,names)=>names.forEach(n=>COLOR_BUCKET[n.toLowerCase()]=bucket);
add('white',['White','Off-white','Off White','Bone','Alabaster','Chalk','Ivory','Cream','Cotton','Pearl','Porcelain','Eggshell','Swiss Coffee','Marshmallow','Dove White','Antique White','Snow']);
add('grey', ['Grey','Gray','Ash','Silver','Silver Gray','Light Gray','Light Grey','Dove','Fog','Smoke','Pewter','Nickel','Flannel','Slate','Zinc','Charcoal Gray','Steel']);
add('black',['Black','Ink','Noir','Onyx','Ebony','Jet','Obsidian','Coal','Charcoal','Graphite','Gunmetal']);
add('brown',['Brown','Light Brown','Taupe','Greige','Oatmeal','Flax','String','Mushroom','Putty','Stone','Fawn','Wheat','Sand','Biscuit','Almond','Pebble','Beige','Light Beige','Pale Beige','Tan','Buff','Chamois','Khaki','Camel','Caramel','Pecan','Cognac','Chestnut','Tobacco','Walnut','Mocha','Espresso','Natural','Sepia','Linen']);
add('gold', ['Gold','Pale Gold','Champagne','Brass','Honey','Ochre','Mustard','Maize','Citrine','Saffron','Amber','Buttercream','Yellow']);
add('green',['Green','Celadon','Pistachio','Sage','Eucalyptus','Sea Glass','Moss','Fern','Olive','Loden','Juniper','Hunter','Forest','Emerald','Verdigris','Bottle Green']);
add('teal', ['Teal','Peacock','Aqua','Turquoise']);
add('blue', ['Blue','Powder','Sky','Wedgwood','Cornflower','French Blue','Cerulean','Denim','Slate Blue','Prussian','Indigo','Navy','Midnight']);
add('orange',['Orange','Apricot','Peach','Clay','Terracotta','Persimmon','Pumpkin','Marmalade','Copper','Rust','Sienna','Coral']);
add('red', ['Red','Cinnabar','Brick Red','Oxblood','Claret','Garnet','Burgundy','Crimson','Scarlet']);
add('purple',['Purple','Lavender','Lilac','Wisteria','Orchid','Heather','Mauve','Amethyst','Plum','Aubergine','Violet']);
add('pink', ['Pink','Blush','Rose','Dusty Rose','Magenta','Fuchsia']);
// representative HSV per bucket (for light->dark / color-wheel sorts when no real hex)
const BUCKET_HSV = { white:{h:40,s:.05,v:.95}, grey:{h:0,s:0,v:.62}, black:{h:0,s:0,v:.13},
brown:{h:30,s:.34,v:.45}, red:{h:5,s:.6,v:.55}, orange:{h:26,s:.6,v:.7}, gold:{h:45,s:.5,v:.76},
green:{h:120,s:.38,v:.48}, teal:{h:180,s:.4,v:.5}, blue:{h:218,s:.4,v:.55}, purple:{h:280,s:.34,v:.5}, pink:{h:340,s:.34,v:.72} };
function hexToHsv(hex){
const m=/^#?([0-9a-f]{6})$/i.exec((hex||'').trim()); if(!m) return null;
const n=parseInt(m[1],16), r=(n>>16&255)/255,g=(n>>8&255)/255,b=(n&255)/255;
const mx=Math.max(r,g,b),mn=Math.min(r,g,b),d=mx-mn; let h=0;
if(d){ if(mx===r)h=((g-b)/d)%6; else if(mx===g)h=(b-r)/d+2; else h=(r-g)/d+4; h*=60; if(h<0)h+=360; }
return { h:Math.round(h), s:+(mx?d/mx:0).toFixed(3), v:+mx.toFixed(3) };
}
// optional precise hex from the mirror, keyed by dw_sku
let hexByKey={};
try{
const sql="COPY (SELECT dw_sku||E'\\t'||COALESCE(dominant_color_hex,color_hex,'') FROM romo_catalog WHERE dw_sku IS NOT NULL) TO STDOUT";
const raw=execSync(`psql "host=/tmp dbname=dw_unified" -tAc ${JSON.stringify(sql)}`,{maxBuffer:64*1024*1024}).toString();
for(const line of raw.split('\n')){ const [k,h]=line.split('\t'); if(k&&h&&/^#?[0-9a-f]{6}$/i.test(h)) hexByKey[k]=h.startsWith('#')?h:('#'+h); }
}catch(e){ console.error('(hex refinement skipped:', e.message,')'); }
const snap=JSON.parse(fs.readFileSync(SNAP,'utf8'));
const out={}; let n=0;
for(const p of snap.products){
const key=p.mfr_sku;
const tags=(p.tags||[]).map(t=>String(t));
const low=tags.map(t=>t.toLowerCase());
const styles=new Set(), motifs=new Set();
tags.forEach((t,i)=>{
if(STYLES.has(t)) styles.add(t);
else if(STYLE_ALIAS[low[i]]) styles.add(STYLE_ALIAS[low[i]]);
if(MOTIF[low[i]]) motifs.add(MOTIF[low[i]]);
});
// colorway: first tag that is a known colorway name; bucket from it
let colorway=null, bucket=null;
for(let i=0;i<tags.length;i++){ if(COLOR_BUCKET[low[i]]){ colorway=tags[i]; bucket=COLOR_BUCKET[low[i]]; break; } }
const hsv = hexByKey[key] ? hexToHsv(hexByKey[key]) : (bucket ? BUCKET_HSV[bucket] : null);
const e={ styles:[...styles], motifs:[...motifs] };
if(bucket){ e.bucket=bucket; e.colorway=colorway; }
if(hexByKey[key]) e.hex=hexByKey[key];
if(hsv){ e.hue=hsv.h; e.sat=hsv.s; e.val=hsv.v; }
out[key]=e; n++;
}
fs.writeFileSync(OUT, JSON.stringify(out));
const tally=(sel)=>{const c={};for(const k in out)for(const v of out[k][sel]||[])c[v]=(c[v]||0)+1;return Object.entries(c).sort((a,b)=>b[1]-a[1]);};
const bc={}; for(const k in out){const b=out[k].bucket; if(b)bc[b]=(bc[b]||0)+1;}
console.log(`romo-enrich: ${n} products →`, path.relative(HERE,OUT));
console.log(' precise-hex from DB:', Object.keys(hexByKey).length ? Object.values(out).filter(e=>e.hex).length : 0, '/', n);
console.log(' styles :', tally('styles'));
console.log(' motifs :', tally('motifs'));
console.log(' buckets:', Object.entries(bc).sort((a,b)=>b[1]-a[1]));