← back to Designer Wallcoverings
enrich-remaining-13.js
137 lines
const https = require('https');
const http = require('http');
const { Client } = require('pg');
const GEMINI_KEY = '${GOOGLE_API_KEY}';
const GEMINI_MODEL = 'gemini-3.5-flash';
const PG_URL = (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified');
const PRODUCTS = [
// Arte — use packshot_url from arte_catalog
{ vc_id: 123, vendor: 'arte', sku: '19433', name: 'Horus Desert Sun', img: 'https://edge.arte-international.com/media/a0686a22-83f6-46a5-86fe-b17566b88245/conversions/EssentialsLuxor_Horus_19433_Packshot_Web_LR-medium-two-thirds.jpg' },
{ vc_id: 16, vendor: 'arte', sku: '23401', name: 'Amarna Bister', img: 'https://edge.arte-international.com/media/d4610242-66e0-4b12-8723-df90ddb40e04/conversions/Memphis_Amarna_23401_Packshot_Web_HR-medium-two-thirds.jpg' },
{ vc_id: 15, vendor: 'arte', sku: '23412', name: 'Sakkara Antique Gold', img: 'https://edge.arte-international.com/media/6fe8a7ec-ecdd-49c4-b398-223293c48eed/conversions/Memphis_Sakkara_23412_Packshot_Web_HR-medium-two-thirds.jpg' },
{ vc_id: 172, vendor: 'arte', sku: '23413', name: 'Sakkara Off-white', img: 'https://edge.arte-international.com/media/a45badcf-d133-44b1-bcd6-1d74b4f13141/conversions/Memphis_Sakkara_23413_Packshot_Web_HR-medium-two-thirds.jpg' },
{ vc_id: 72, vendor: 'arte', sku: '23421', name: 'Ibis Biscuit', img: 'https://edge.arte-international.com/media/fb1738af-4e34-4d36-9a3e-7392f590381a/conversions/Memphis_Ibis_23421_Packshot_Web_LR-medium-two-thirds.jpg' },
{ vc_id: 75, vendor: 'arte', sku: '23422', name: 'Ibis Chestnut', img: 'https://edge.arte-international.com/media/2899330e-abba-481e-9098-ffd76f1731cf/conversions/Memphis_Ibis_23422_Packshot_Web_LR-medium-two-thirds.jpg' },
{ vc_id: 78, vendor: 'arte', sku: '23431', name: 'Siwa Bone', img: 'https://edge.arte-international.com/media/2ef17968-181d-4da5-8c8a-59f719a3c0fd/conversions/Memphis_Siwa_23431_Packshot_Web_LR-medium-two-thirds.jpg' },
{ vc_id: 83, vendor: 'arte', sku: '23433', name: 'Siwa Biscuit', img: 'https://edge.arte-international.com/media/fac5d958-0b0e-437f-abbe-a07a2c147676/conversions/Memphis_Siwa_23433_Packshot_Web_LR-medium-two-thirds.jpg' },
{ vc_id: 183, vendor: 'arte', sku: '29221', name: 'Galon Grey Linen', img: 'https://edge.arte-international.com/media/f92ad97a-a59b-431c-84ad-0a7520995f6a/conversions/Allures_Galon_29221_Flatshot_Web_LR-medium-two-thirds.jpg' },
// Black Edition — use vendor_catalog image_url
{ vc_id: 1751, vendor: 'black_edition', sku: 'W924/01', name: 'Mizumi Panel Carbon', img: 'https://static.theromogroup.com/rb/cache/image/720x720/catalog/product/W/9/W924-01FP-mizumi-panel-charcoal_00.jpg' },
{ vc_id: 1752, vendor: 'black_edition', sku: 'W924/02', name: 'Mizumi Panel Midnight', img: 'https://static.theromogroup.com/rb/cache/image/720x720/catalog/product/W/9/W924-02FP-mizumi-panel-midnight_00.jpg' },
{ vc_id: 1753, vendor: 'black_edition', sku: 'W924/03', name: 'Mizumi Panel Basalt', img: 'https://static.theromogroup.com/rb/cache/image/720x720/catalog/product/W/9/W924-03FP-mizumi-panel-stone_00.jpg' },
{ vc_id: 1754, vendor: 'black_edition', sku: 'W924/04', name: 'Mizumi Panel Viridian', img: 'https://static.theromogroup.com/rb/cache/image/720x720/catalog/product/W/9/W924-04FP-mizumi-panel-teal_02.jpg' },
];
const PROMPT = `You are an interior design color analyst for luxury wallcoverings.
Analyze this wallcovering image and return a JSON object with these fields:
- "colors": array of 3-6 prominent colors visible (e.g. ["Gold", "Ivory", "Charcoal"])
- "backgroundColor": the single dominant background color (e.g. "Ivory")
- "styles": array of 1-3 design styles (e.g. ["Contemporary", "Art Deco"])
- "patterns": array of 1-2 pattern types (e.g. ["Geometric", "Textured"])
- "tags": array of 4-8 interior design tags (e.g. ["luxury", "metallic", "hospitality", "accent wall"])
- "description": a 2-sentence commercial description for an interior designer audience. NEVER use the word "wallpaper" — always say "wallcovering".
Return ONLY the JSON object, no markdown fences, no explanation.`;
function fetchImageBase64(url) {
return new Promise((resolve, reject) => {
const mod = url.startsWith('https') ? https : http;
mod.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
return fetchImageBase64(res.headers.location).then(resolve).catch(reject);
}
if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => resolve(Buffer.concat(chunks).toString('base64')));
res.on('error', reject);
}).on('error', reject);
});
}
function callGemini(imageBase64, mimeType) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({
contents: [{
parts: [
{ text: PROMPT },
{ inline_data: { mime_type: mimeType || 'image/jpeg', data: imageBase64 } }
]
}],
generationConfig: { temperature: 0.3, maxOutputTokens: 1024, thinkingConfig: { thinkingBudget: 0 } }
});
const url = new URL(`https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_KEY}`);
const opts = { method: 'POST', hostname: url.hostname, path: url.pathname + url.search, headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } };
const req = https.request(opts, (res) => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => {
try {
const data = JSON.parse(Buffer.concat(chunks).toString());
if (data.error) return reject(new Error(data.error.message));
let text = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
text = text.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
resolve(JSON.parse(text));
} catch (e) { reject(e); }
});
});
req.on('error', reject);
req.write(body);
req.end();
});
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
async function main() {
const db = new Client({ connectionString: PG_URL });
await db.connect();
let success = 0, failed = 0;
for (const p of PRODUCTS) {
try {
console.log(`[${p.vendor}] Analyzing ${p.sku} — ${p.name}...`);
const imgB64 = await fetchImageBase64(p.img);
const ai = await callGemini(imgB64);
const colorsStr = (ai.colors || []).join(', ');
const stylesStr = (ai.styles || []).join(', ');
const patternsStr = (ai.patterns || []).join(', ');
const tagsStr = (ai.tags || []).join(', ');
// Update vendor_catalog
await db.query(`UPDATE vendor_catalog SET
ai_colors = $1, ai_background_color = $2, ai_styles = $3,
ai_patterns = $4, ai_tags = $5, ai_description = $6
WHERE id = $7`,
[colorsStr, ai.backgroundColor, stylesStr, patternsStr, tagsStr, ai.description, p.vc_id]);
// Also update arte_catalog if Arte
if (p.vendor === 'arte') {
await db.query(`UPDATE arte_catalog SET
ai_colors = $1, ai_background_color = $2, ai_styles = $3,
ai_patterns = $4, ai_tags = $5, ai_description = $6, ai_accepted_at = NOW()
WHERE arte_sku = $7`,
[JSON.stringify(ai.colors), ai.backgroundColor, JSON.stringify(ai.styles),
JSON.stringify(ai.patterns), JSON.stringify(ai.tags), ai.description, p.sku]);
}
console.log(` ✅ Colors: ${colorsStr} | BG: ${ai.backgroundColor}`);
success++;
await sleep(1200); // rate limit
} catch (err) {
console.error(` ❌ FAILED ${p.sku}: ${err.message}`);
failed++;
}
}
await db.end();
console.log(`\nDone: ${success} enriched, ${failed} failed out of ${PRODUCTS.length}`);
}
main().catch(console.error);