← 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]));