← back to Letsbegin
ani_full_monty.js
324 lines
#!/usr/bin/env node
/**
* ANI- Series Full Monty
* 1. Migrate global.* → custom.* specs
* 2. AI color analysis via Gemini
* 3. Update body to 2 paragraphs (editorial + AI)
* 4. Generate 2 residential room settings per product (bedroom + living room)
* 5. Upload room settings to Shopify
*/
const https = require('https');
const http = require('http');
const fs = require('fs');
const { execFileSync } = require('child_process');
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');
}
function gql(body) {
return new Promise((resolve, reject) => {
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) },
}, res => { let c = ''; res.on('data', d => c += d); res.on('end', () => { try { resolve(JSON.parse(c)); } catch { resolve({ error: c.slice(0,300) }); } }); });
req.on('error', reject);
req.setTimeout(60000, () => { req.destroy(); reject(new Error('timeout')); });
req.write(data); req.end();
});
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function downloadImage(url, dest) {
return new Promise((resolve, reject) => {
const mod = url.startsWith('https') ? https : http;
const file = fs.createWriteStream(dest);
mod.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, res => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
file.close(); return downloadImage(res.headers.location, dest).then(resolve).catch(reject);
}
res.pipe(file);
file.on('finish', () => { file.close(); resolve(); });
}).on('error', e => { file.close(); reject(e); });
});
}
async function geminiAnalyze(imgPath, prompt) {
const imgB64 = fs.readFileSync(imgPath).toString('base64');
const req = {
contents: [{ parts: [
{ text: prompt },
{ inlineData: { mimeType: 'image/jpeg', data: imgB64 } }
]}],
generationConfig: { temperature: 0.2, maxOutputTokens: 500 }
};
fs.writeFileSync('/tmp/ani_gemini_req.json', JSON.stringify(req));
const result = execFileSync('curl', ['-s',
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`,
'-H', 'Content-Type: application/json',
'-d', `@/tmp/ani_gemini_req.json`
], { timeout: 30000 }).toString();
const parsed = JSON.parse(result);
return parsed?.candidates?.[0]?.content?.parts?.[0]?.text || '';
}
function geminiGenerateImage(imgPath, roomPrompt) {
return new Promise((resolve, reject) => {
const imgB64 = fs.readFileSync(imgPath).toString('base64');
const reqBody = JSON.stringify({
contents: [{ parts: [
{ text: roomPrompt },
{ inlineData: { mimeType: 'image/jpeg', data: imgB64 } }
]}],
generationConfig: { responseModalities: ['TEXT', 'IMAGE'] }
});
const url = new URL(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image: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(reqBody) }
}, res => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => {
try {
const parsed = JSON.parse(Buffer.concat(chunks).toString());
for (const part of parsed?.candidates?.[0]?.content?.parts || []) {
if (part.inlineData) {
return resolve(Buffer.from(part.inlineData.data, 'base64'));
}
}
resolve(null);
} catch (e) { resolve(null); }
});
});
req.on('error', () => resolve(null));
req.setTimeout(120000, () => { req.destroy(); resolve(null); });
req.write(reqBody);
req.end();
});
}
async function uploadImageToShopify(productId, imgBuffer, alt) {
const tmpFile = '/tmp/ani_room_upload.png';
fs.writeFileSync(tmpFile, imgBuffer);
const fileSize = fs.statSync(tmpFile).size;
const stage = await gql({
query: `mutation { stagedUploadsCreate(input: [{resource: IMAGE, filename: "room-setting.png", mimeType: "image/png", httpMethod: POST, fileSize: "${fileSize}"}]) { stagedTargets { url parameters { name value } resourceUrl } userErrors { message } } }`
});
const target = stage?.data?.stagedUploadsCreate?.stagedTargets?.[0];
if (!target) return null;
const curlArgs = ['-s', '-X', 'POST', target.url];
for (const p of target.parameters) curlArgs.push('-F', `${p.name}=${p.value}`);
curlArgs.push('-F', `file=@${tmpFile}`);
execFileSync('curl', curlArgs, { timeout: 30000 });
const pid = productId.includes('/') ? productId : `gid://shopify/Product/${productId}`;
const mediaResult = await gql({
query: `mutation { productCreateMedia(productId: "${pid}", media: [{originalSource: "${target.resourceUrl}", mediaContentType: IMAGE, alt: "${alt}"}]) { media { id } mediaUserErrors { message } } }`
});
return mediaResult?.data?.productCreateMedia?.media?.[0]?.id || null;
}
function cleanWidth(v) { const m = (v||'').match(/([\d.]+)/); return m ? `${m[1]} Inches` : null; }
function cleanRepeat(v) {
if (!v || v === 'N/A') return null;
const m = v.match(/([\d.]+)/);
return m ? `${m[1]} Inches` : null;
}
function cleanFire(v) {
if (!v) return null;
let f = v.replace(/["']/g, '').trim();
if (/class\s*a/i.test(f) && !f.includes('ASTM')) f = 'ASTM E-84 Class A';
return f;
}
function cleanMaterial(v) { return v ? v.replace(/wallpaper/gi, 'Wallcovering') : null; }
function toTitleCase(s) {
if (!s) return '';
return s.replace(/\w\S*/g, t => t.charAt(0).toUpperCase() + t.substr(1).toLowerCase());
}
async function main() {
console.log('=== ANI- Series Full Monty ===\n');
// Fetch all 16 ANI products with ALL metafields
const r = await gql({
query: `{ products(first: 20, query: "sku:ANI-* AND status:active") {
edges { node { id title bodyHtml vendor images(first:1) { edges { node { url width height } } }
metafields(first: 30) { edges { node { namespace key value } } }
} }
} }`
});
const products = r?.data?.products?.edges?.map(e => e.node) || [];
console.log(`Found ${products.length} ANI products\n`);
let specsOk = 0, colorsOk = 0, bodiesOk = 0, roomsOk = 0, errors = 0;
for (let i = 0; i < products.length; i++) {
const p = products[i];
const pid = p.id;
console.log(`\n[${i+1}/${products.length}] ${p.title}`);
// Collect existing metafields
const globals = {};
const customs = {};
for (const m of p.metafields.edges) {
if (m.node.namespace === 'global') globals[m.node.key] = m.node.value;
if (m.node.namespace === 'custom') customs[m.node.key] = m.node.value;
}
// === 1. SPECS: Migrate global → custom ===
const mf = [];
const add = (k, v, t = 'single_line_text_field') => {
if (v && v.toString().trim() && !customs[k]) {
mf.push({ ownerId: pid, namespace: 'custom', key: k, value: v.toString().trim(), type: t });
}
};
add('width', cleanWidth(globals['width']));
add('pattern_repeat', cleanRepeat(globals['repeat']));
add('fire_rating', cleanFire(globals['fire_rating']));
add('material', cleanMaterial(globals['Construction'] || globals['Contents']), 'multi_line_text_field');
add('collection_name', globals['Collection']);
add('match_type', globals['MATCH'] || globals['Match']);
add('brand', globals['Brand'] || p.vendor);
add('packaging', globals['packaged']);
add('unit_of_measure', globals['unit_of_measure']);
add('origin', globals['Country'] || globals['Country-of-Origin']);
add('pattern_name', customs['pattern_name'] || p.title);
add('color', globals['color'] || globals['Color-Way']);
if (mf.length > 0) {
try {
await gql({ query: 'mutation metafieldsSet($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { key } userErrors { message } } }', variables: { m: mf } });
specsOk++;
console.log(` Specs: ${mf.length} fields set`);
} catch { errors++; }
} else {
console.log(' Specs: already complete');
specsOk++;
}
// === 2. AI COLOR ANALYSIS ===
const imgUrl = p.images?.edges?.[0]?.node?.url;
let aiDesc = '';
if (imgUrl && !customs['color_details']) {
try {
const tmpImg = '/tmp/ani_img.jpg';
await downloadImage(imgUrl, tmpImg);
const colorJson = await geminiAnalyze(tmpImg,
'Analyze this wallcovering. Return ONLY valid JSON: {"dominantColor":"name","dominantHex":"#XXXXXX","backgroundColor":"name","backgroundHex":"#XXXXXX","colors":[{"name":"Color","hex":"#XXXXXX","percentage":40}]}. Use 3-6 colors summing to 100%. Title Case names.'
);
const colorText = colorJson.replace(/```json?\s*/g, '').replace(/```/g, '').trim();
const colorData = JSON.parse(colorText);
// Also get AI description
aiDesc = await geminiAnalyze(tmpImg,
`Describe this wallcovering called "${p.title}" in 2 sentences for a commercial/architectural buyer. Mention the pattern, colors, and recommended use. Do NOT use the word "wallpaper" — use "wallcovering".`
);
// Set color metafields
const colorMf = [
{ ownerId: pid, namespace: 'custom', key: 'color_hex', value: colorData.dominantHex, type: 'single_line_text_field' },
{ ownerId: pid, namespace: 'custom', key: 'background_color', value: colorData.backgroundColor, type: 'single_line_text_field' },
{ ownerId: pid, namespace: 'custom', key: 'color_details', value: JSON.stringify(colorData.colors), type: 'json' },
];
await gql({ query: 'mutation metafieldsSet($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { key } userErrors { message } } }', variables: { m: colorMf } });
colorsOk++;
console.log(` Colors: ${colorData.dominantColor} (${colorData.dominantHex})`);
} catch (e) {
console.log(` Colors: ERROR ${e.message?.slice(0,50)}`);
errors++;
}
} else {
console.log(' Colors: already done or no image');
colorsOk++;
}
// === 3. BODY: 2 paragraphs ===
const existingBody = (p.bodyHtml || '').replace(/<[^>]+>/g, '').trim();
let firstPara = existingBody.length > 20 ? existingBody : `${p.title} is a charming wallcovering by ${p.vendor}. This design brings whimsical character to any interior space.`;
let secondPara = aiDesc ? aiDesc.replace(/wallpaper/gi, 'wallcovering').trim() : `A delightful decorative wallcovering perfect for bedrooms, nurseries, and living spaces.`;
const newBody = `<p>${firstPara}</p>\n<p>${secondPara}</p>`;
try {
await gql({
query: 'mutation ($i: ProductInput!) { productUpdate(input: $i) { product { id } userErrors { message } } }',
variables: { i: { id: pid, bodyHtml: newBody } }
});
bodiesOk++;
console.log(` Body: 2 paragraphs set`);
} catch { errors++; }
// === 4. ROOM SETTINGS: 2 residential (bedroom + living room) ===
if (imgUrl) {
const tmpImg = '/tmp/ani_img.jpg';
if (!fs.existsSync(tmpImg) || fs.statSync(tmpImg).size < 1000) {
await downloadImage(imgUrl, tmpImg);
}
// Build tiled wall from the product image
try {
const dims = execFileSync('identify', ['-format', '%wx%h', tmpImg]).toString().trim();
const [imgW, imgH] = dims.split('x').map(Number);
// Tile horizontally (3 strips), crop to wall proportions
execFileSync('convert', [tmpImg, tmpImg, tmpImg, '+append', '-gravity', 'center', '-crop', `${imgW*3}x${Math.min(imgH, imgW*2)}+0+0`, '+repage', '-resize', '2000x1200', '-quality', '90', '/tmp/ani_wall.jpg']);
const rooms = [
{ name: 'Bedroom', prompt: `This image is a wallcovering pattern tiled on a wall. Use it EXACTLY as the background wall. Place in front: a modern queen bed with white and sage green bedding, two wood nightstands with ceramic lamps, light oak hardwood floor. Warm morning light from right window. Photorealistic interior design magazine style.` },
{ name: 'Living Room', prompt: `This image is a wallcovering pattern tiled on a wall. Use it EXACTLY as the background wall. Place in front: a mid-century modern sofa in cream linen, a round walnut coffee table, a potted fiddle leaf fig plant, and a soft area rug. Natural afternoon light. Photorealistic interior design photography.` },
];
for (const room of rooms) {
console.log(` Room: generating ${room.name}...`);
const imgBuf = await geminiGenerateImage('/tmp/ani_wall.jpg', room.prompt);
if (imgBuf) {
const mediaId = await uploadImageToShopify(pid, imgBuf, `${p.title} - ${room.name} room setting`);
if (mediaId) {
roomsOk++;
console.log(` Room: ${room.name} uploaded (${(imgBuf.length/1024).toFixed(0)}KB)`);
}
} else {
console.log(` Room: ${room.name} — Gemini returned no image`);
}
await sleep(2000); // Rate limit between image generations
}
} catch (e) {
console.log(` Rooms: ERROR ${e.message?.slice(0,60)}`);
errors++;
}
}
await sleep(1000);
}
console.log('\n=== COMPLETE ===');
console.log(`Products: ${products.length}`);
console.log(`Specs migrated: ${specsOk}`);
console.log(`Colors analyzed: ${colorsOk}`);
console.log(`Bodies rewritten: ${bodiesOk}`);
console.log(`Room settings generated: ${roomsOk}`);
console.log(`Errors: ${errors}`);
}
main().catch(e => { console.error('Fatal:', e); process.exit(1); });