← back to Letsbegin
sequin_full_monty.js
202 lines
#!/usr/bin/env node
/**
* Sequin Products Full Monty (63 Eel Skin + Sting Ray)
* 1. AI color analysis via Gemini → custom.color_hex, color_details, background_color, color
* 2. Body rewrite to 2 paragraphs (editorial + AI)
* 3. 2 residential room settings per product
*/
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); });
});
}
function geminiCall(reqBody, model) {
return new Promise((resolve, reject) => {
const data = JSON.stringify(reqBody);
const url = new URL(`https://generativelanguage.googleapis.com/v1beta/models/${model}: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) }
}, res => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => { try { resolve(JSON.parse(Buffer.concat(chunks).toString())); } catch { resolve(null); } });
});
req.on('error', () => resolve(null));
req.setTimeout(120000, () => { req.destroy(); resolve(null); });
req.write(data); req.end();
});
}
async function uploadImage(productId, imgBuffer, alt) {
const tmpFile = '/tmp/seq_room.png';
fs.writeFileSync(tmpFile, imgBuffer);
const fileSize = fs.statSync(tmpFile).size;
const stage = await gql({
query: `mutation { stagedUploadsCreate(input: [{resource: IMAGE, filename: "room.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 r = await gql({
query: `mutation { productCreateMedia(productId: "${pid}", media: [{originalSource: "${target.resourceUrl}", mediaContentType: IMAGE, alt: "${alt}"}]) { media { id } mediaUserErrors { message } } }`
});
return r?.data?.productCreateMedia?.media?.[0]?.id || null;
}
async function main() {
console.log('=== Sequin Full Monty (63 products) ===\n');
const r = await gql({ query: '{ eel: products(first: 50, query: "eel skin sequin AND status:active") { edges { node { id title bodyHtml images(first:1) { edges { node { url } } } metafields(first: 5, keys: ["custom.color_details","custom.color_hex"]) { edges { node { key value } } } } } } sting: products(first: 50, query: "sting ray sequin AND status:active") { edges { node { id title bodyHtml images(first:1) { edges { node { url } } } metafields(first: 5, keys: ["custom.color_details","custom.color_hex"]) { edges { node { key value } } } } } } }' });
const products = [...r.data.eel.edges.map(e => e.node), ...r.data.sting.edges.map(e => e.node)];
console.log(`${products.length} products\n`);
let colors = 0, bodies = 0, rooms = 0, errors = 0;
for (let i = 0; i < products.length; i++) {
const p = products[i];
const imgUrl = p.images?.edges?.[0]?.node?.url;
const hasColors = p.metafields?.edges?.some(m => m.node.key === 'color_details' && m.node.value);
console.log(`[${i+1}/${products.length}] ${p.title}`);
if (!imgUrl) { console.log(' No image — skip'); continue; }
const tmpImg = '/tmp/seq_img.jpg';
await downloadImage(imgUrl, tmpImg);
// 1. AI COLOR ANALYSIS
if (!hasColors) {
const imgB64 = fs.readFileSync(tmpImg).toString('base64');
const colorResult = await geminiCall({
contents: [{ parts: [
{ text: 'Analyze this wallcovering. Return ONLY valid JSON: {"dominantColor":"name","dominantHex":"#XXXXXX","backgroundColor":"name","backgroundHex":"#XXXXXX","colors":[{"name":"Color","hex":"#XXXXXX","percentage":40}]}. 3-6 colors summing to 100%. Title Case.' },
{ inlineData: { mimeType: 'image/jpeg', data: imgB64 } }
]}],
generationConfig: { temperature: 0.2, maxOutputTokens: 500 }
}, 'gemini-2.0-flash');
try {
const text = colorResult?.candidates?.[0]?.content?.parts?.[0]?.text || '';
const clean = text.replace(/```json?\s*/g, '').replace(/```/g, '').trim();
const cd = JSON.parse(clean);
await gql({
query: 'mutation metafieldsSet($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { key } userErrors { message } } }',
variables: { m: [
{ ownerId: p.id, namespace: 'custom', key: 'color_hex', value: cd.dominantHex, type: 'single_line_text_field' },
{ ownerId: p.id, namespace: 'custom', key: 'background_color', value: cd.backgroundColor, type: 'single_line_text_field' },
{ ownerId: p.id, namespace: 'custom', key: 'color_details', value: JSON.stringify(cd.colors), type: 'json' },
{ ownerId: p.id, namespace: 'custom', key: 'color_name', value: cd.dominantColor, type: 'single_line_text_field' },
]}
});
colors++;
console.log(` Color: ${cd.dominantColor} (${cd.dominantHex})`);
} catch { console.log(' Color: parse error'); errors++; }
await sleep(1200);
} else {
console.log(' Color: already done');
colors++;
}
// 2. BODY: 2 paragraphs with AI description
const imgB642 = fs.readFileSync(tmpImg).toString('base64');
const descResult = await geminiCall({
contents: [{ parts: [
{ text: `Describe this sequin wallcovering called "${p.title}" by Glitter Walls in 2 sentences for a design-trade buyer. Mention the texture, shimmer, and recommended commercial/residential use. Do NOT use "wallpaper" — say "wallcovering".` },
{ inlineData: { mimeType: 'image/jpeg', data: imgB642 } }
]}],
generationConfig: { temperature: 0.3, maxOutputTokens: 200 }
}, 'gemini-2.0-flash');
const aiDesc = (descResult?.candidates?.[0]?.content?.parts?.[0]?.text || '').replace(/wallpaper/gi, 'wallcovering').trim();
const firstPara = `${p.title} is a luxurious sequin wallcovering by Glitter Walls. This eye-catching design adds dramatic shimmer and texture to any interior space.`;
const newBody = `<p>${firstPara}</p>\n<p>${aiDesc || 'A stunning decorative wallcovering perfect for feature walls in hospitality, retail, and residential settings.'}</p>`;
await gql({
query: 'mutation ($i: ProductInput!) { productUpdate(input: $i) { product { id } userErrors { message } } }',
variables: { i: { id: p.id, bodyHtml: newBody } }
});
bodies++;
await sleep(1000);
// 3. ROOM SETTINGS: 2 residential
const imgB64Wall = fs.readFileSync(tmpImg).toString('base64');
const roomPrompts = [
{ name: 'Bedroom', prompt: 'This image shows a sequin/glitter wallcovering texture. Use it EXACTLY as the background wall in a luxurious modern bedroom. Place a king bed with velvet bedding, mirrored nightstands with crystal lamps, soft carpet floor. Evening ambient lighting with warm glow. Photorealistic glamorous interior.' },
{ name: 'Living Room', prompt: 'This image shows a sequin/glitter wallcovering texture. Use it EXACTLY as a feature wall behind a modern living room. Place a contemporary sofa, glass coffee table, statement floor lamp, and abstract art on adjacent wall. Soft evening lighting highlighting the shimmer. Photorealistic luxury interior.' },
];
for (const room of roomPrompts) {
console.log(` Room: ${room.name}...`);
const imgResult = await geminiCall({
contents: [{ parts: [
{ text: room.prompt },
{ inlineData: { mimeType: 'image/jpeg', data: imgB64Wall } }
]}],
generationConfig: { responseModalities: ['TEXT', 'IMAGE'] }
}, 'gemini-2.5-flash-image');
for (const part of imgResult?.candidates?.[0]?.content?.parts || []) {
if (part.inlineData) {
const buf = Buffer.from(part.inlineData.data, 'base64');
const mid = await uploadImage(p.id, buf, `${p.title} - ${room.name}`);
if (mid) { rooms++; console.log(` Room: ${room.name} uploaded (${(buf.length/1024).toFixed(0)}KB)`); }
break;
}
}
await sleep(2000);
}
}
console.log('\n=== COMPLETE ===');
console.log(`Colors: ${colors} | Bodies: ${bodies} | Rooms: ${rooms} | Errors: ${errors}`);
}
main().catch(e => { console.error('Fatal:', e); process.exit(1); });