← back to Letsbegin
nina_names_colors.js
159 lines
const https = require('https');
const http = require('http');
const fs = require('fs');
const STORE = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = process.env.SHOPIFY_PRODUCT_TOKEN;
const GEMINI_KEY = process.env.GEMINI_API_KEY;
if (!GEMINI_KEY) {
throw new Error('GEMINI_API_KEY environment variable is required');
}
if (!TOKEN) {
throw new Error('SHOPIFY_PRODUCT_TOKEN environment variable is required');
}
const catalog = JSON.parse(fs.readFileSync('/tmp/nina_catalog.json'));
const byMfr = {};
for (const c of catalog) { if (c.mfr_sku) byMfr[c.mfr_sku.toUpperCase()] = c; }
console.log(`Loaded ${catalog.length} catalog entries\n`);
function gql(body) {
return new Promise((res, rej) => {
const data = JSON.stringify(body);
const req = https.request({ hostname: STORE, path: '/admin/api/2024-10/graphql.json', method: 'POST',
headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
}, r => { let c=''; r.on('data',d=>c+=d); r.on('end',()=>res(JSON.parse(c))); });
req.on('error', rej); req.write(data); req.end();
});
}
function gemini(reqBody) {
return new Promise((res) => {
const data = JSON.stringify(reqBody);
const url = new URL('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=' + GEMINI_KEY);
const req = https.request({ hostname: url.hostname, path: url.pathname + url.search, method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
}, r => { let c=''; r.on('data',d=>c+=d); r.on('end',()=>{ try{res(JSON.parse(c))}catch{res(null)} }); });
req.on('error', ()=>res(null)); req.write(data); req.end();
});
}
function download(url, dest) {
return new Promise((res, rej) => {
const mod = url.startsWith('https') ? https : http;
const file = fs.createWriteStream(dest);
mod.get(url, {headers:{'User-Agent':'Mozilla/5.0'}}, r => {
if(r.statusCode>=300&&r.headers.location){file.close();return download(r.headers.location,dest).then(res).catch(rej)}
r.pipe(file); file.on('finish',()=>{file.close();res()});
}).on('error',e=>{file.close();rej(e)});
});
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
function toTitleCase(s) {
if (!s) return '';
return s.replace(/\w\S*/g, t => t.charAt(0).toUpperCase() + t.substr(1).toLowerCase()).trim();
}
async function main() {
console.log('=== Nina Campbell: Real Names + Colors ===\n');
let cursor = null;
const products = [];
while (true) {
const after = cursor ? `, after: "${cursor}"` : '';
const r = await gql({ query: `{ products(first: 50, query: "vendor:\\"Nina Campbell\\" AND status:active"${after}) { edges { cursor node { id title images(first:1) { edges { node { url } } } metafields(first: 3, keys:["custom.manufacturer_sku","custom.color_name"]) { edges { node { key value } } } } } pageInfo { hasNextPage } } }` });
const edges = r?.data?.products?.edges || [];
if (edges.length === 0) break;
for (const e of edges) { products.push(e.node); cursor = e.cursor; }
if (!r?.data?.products?.pageInfo?.hasNextPage) break;
await sleep(500);
}
console.log(`${products.length} products\n`);
const usedNames = {};
let catalogMatch = 0, aiColor = 0, errors = 0;
for (let i = 0; i < products.length; i++) {
const p = products[i];
const metas = {};
for (const m of p.metafields.edges) metas[m.node.key] = m.node.value;
const mfrSku = (metas.manufacturer_sku || '').toUpperCase();
let patternName = '';
let colorName = '';
// Try catalog match
const match = byMfr[mfrSku];
if (match) {
// Extract clean pattern name (remove collection prefix like "Les Rêves ")
const pn = match.pattern_name || '';
// Pattern name often includes collection: "Fontibre Kershaw Plain Black/Ivory"
// Keep it as-is but use the color_name field
patternName = pn.split(/\s+[-–]\s+/)[0].trim() || pn;
colorName = toTitleCase(match.color_name);
catalogMatch++;
}
// If no color from catalog, use AI on the image
if (!colorName) {
const imgUrl = p.images?.edges?.[0]?.node?.url;
if (imgUrl) {
try {
const tmp = '/tmp/nina_color.jpg';
await download(imgUrl, tmp);
const b64 = fs.readFileSync(tmp).toString('base64');
// Get pattern base from existing title
if (!patternName) {
patternName = p.title.split('|')[0].replace(/wallcovering/gi,'').trim();
}
if (!usedNames[patternName]) usedNames[patternName] = new Set();
const used = [...usedNames[patternName]].join(', ');
const cr = await gemini({
contents: [{ parts: [
{ text: 'You are an interior designer. What is the dominant color of this wallcovering? Use specific designer terms (Ivory, Charcoal, Sage, Blush, Cobalt, etc.).' + (used ? ' NOT: ' + used + '.' : '') + ' Reply with ONLY 1-2 words.' },
{ inlineData: { mimeType: 'image/jpeg', data: b64 } }
]}],
generationConfig: { temperature: 0.4, maxOutputTokens: 15 }
});
colorName = (cr?.candidates?.[0]?.content?.parts?.[0]?.text || '').trim().replace(/[\"'\n]/g, '');
aiColor++;
} catch { errors++; }
await sleep(1200);
}
}
if (!colorName) colorName = '';
if (!patternName) patternName = p.title.split('|')[0].trim();
// Track used names
if (!usedNames[patternName]) usedNames[patternName] = new Set();
if (colorName) usedNames[patternName].add(colorName);
// Build title
const newTitle = colorName
? `${patternName} - ${colorName} Wallcovering | Nina Campbell`
: `${patternName} Wallcovering | Nina Campbell`;
// Update title + color metafield
await gql({
query: 'mutation ($i: ProductInput!) { productUpdate(input: $i) { product { id } userErrors { message } } }',
variables: { i: { id: p.id, title: newTitle } }
});
if (colorName) {
await gql({
query: 'mutation metafieldsSet($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { key } userErrors { message } } }',
variables: { m: [
{ ownerId: p.id, namespace: 'custom', key: 'color_name', value: colorName, type: 'single_line_text_field' },
{ ownerId: p.id, namespace: 'custom', key: 'pattern_name', value: patternName, type: 'single_line_text_field' },
]}
});
}
if ((i+1) % 25 === 0) console.log(`[${i+1}/${products.length}] catalog:${catalogMatch} ai:${aiColor} errors:${errors}`);
await sleep(500);
}
console.log(`\n=== COMPLETE ===`);
console.log(`Catalog matches: ${catalogMatch} | AI colors: ${aiColor} | Errors: ${errors}`);
}
main().catch(e => { console.error('Fatal:', e); process.exit(1); });