← back to Designer Wallcoverings

ai-enrich-gaps2.js

134 lines

const https = require('https');
const http = require('http');
const { Client } = require('pg');

const GEMINI_KEY = '${GOOGLE_API_KEY}';
const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent?key=${GEMINI_KEY}`;

const PRODUCTS = [
  {id:123,vendor:'arte',sku:'19433',name:'Horus Desert Sun',img:'https://edge.arte-international.com/media/4960d9f1-bc0a-4bcb-9a82-c09d9e5e7d98/conversions/EssentialsLuxor_Horus_19436_Roomshot_Web_LR-medium-two-thirds.jpg'},
  {id:16,vendor:'arte',sku:'23401',name:'Amarna Bister',img:'https://edge.arte-international.com/media/cc56d30c-5e1c-4b22-878e-b437681e6f1b/conversions/Memphis_Amarna_23400_Roomshot_Web_LR-medium-two-thirds.jpg'},
  {id:15,vendor:'arte',sku:'23412',name:'Sakkara Antique Gold',img:'https://edge.arte-international.com/media/a580214f-25ff-472e-b88c-6a2bd8dfab64/conversions/Memphis_Sakkara_23410_Roomshot_Web_LR-medium-two-thirds.jpg'},
  {id:172,vendor:'arte',sku:'23413',name:'Sakkara Off-white',img:'https://edge.arte-international.com/media/a580214f-25ff-472e-b88c-6a2bd8dfab64/conversions/Memphis_Sakkara_23410_Roomshot_Web_LR-medium-two-thirds.jpg'},
  {id:72,vendor:'arte',sku:'23421',name:'Ibis Biscuit',img:'https://edge.arte-international.com/media/2183e2bc-0620-43c4-81ae-6364abed3937/conversions/Memphis_Ibis_23420_Roomshot_Web_LR-medium-two-thirds.jpg'},
  {id:75,vendor:'arte',sku:'23422',name:'Ibis Chestnut',img:'https://edge.arte-international.com/media/2183e2bc-0620-43c4-81ae-6364abed3937/conversions/Memphis_Ibis_23420_Roomshot_Web_LR-medium-two-thirds.jpg'},
  {id:78,vendor:'arte',sku:'23431',name:'Siwa Bone',img:'https://edge.arte-international.com/media/428898de-d288-40f3-b463-1714c59de939/conversions/Memphis_Siwa_23432_Roomshot_Web_LR-medium-two-thirds.jpg'},
  {id:83,vendor:'arte',sku:'23433',name:'Siwa Biscuit',img:'https://edge.arte-international.com/media/428898de-d288-40f3-b463-1714c59de939/conversions/Memphis_Siwa_23432_Roomshot_Web_LR-medium-two-thirds.jpg'},
  {id:183,vendor:'arte',sku:'29221',name:'Galon Grey Linen',img:'https://edge.arte-international.com/media/8571da7a-7bf9-4abc-adf0-76863cee5413/conversions/Allures_Galon9224_1_Roomshot_Web_LR-medium-two-thirds.jpg'},
  {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'},
  {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'},
  {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'},
  {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 wallcovering product analyst. Analyze this wallcovering image and return ONLY valid JSON (no markdown, no backticks):
{
  "colors": ["<list 3-6 actual colors you see, e.g. Warm Beige, Soft Gold>"],
  "backgroundColor": "<the single dominant background color>",
  "styles": ["<1-3 design styles, e.g. Contemporary, Art Deco, Minimalist>"],
  "patterns": ["<1-2 pattern types, e.g. Geometric, Textured, Striped>"],
  "tags": ["<5-8 interior design tags, e.g. luxury, neutral, organic>"],
  "description": "<2 sentences describing this wallcovering for a commercial interior designer>"
}`;

function fetchImage(url) {
  const mod = url.startsWith('https') ? https : http;
  return new Promise((resolve, reject) => {
    mod.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (res) => {
      if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
        return fetchImage(res.headers.location).then(resolve).catch(reject);
      }
      if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`));
      const chunks = [];
      res.on('data', c => chunks.push(c));
      res.on('end', () => resolve(Buffer.concat(chunks)));
      res.on('error', reject);
    }).on('error', reject);
  });
}

function callGemini(b64, productName) {
  const body = JSON.stringify({
    contents: [{ parts: [
      { text: `Product: ${productName}\n\n${PROMPT}` },
      { inlineData: { mimeType: 'image/jpeg', data: b64 } }
    ]}],
    generationConfig: { temperature: 0.2, maxOutputTokens: 1024, thinkingConfig: { thinkingBudget: 0 } }
  });

  return new Promise((resolve, reject) => {
    const url = new URL(GEMINI_URL);
    const req = https.request({
      hostname: url.hostname, path: url.pathname + url.search,
      method: 'POST',
      headers: { 'Content-Type': 'application/json' }
    }, (res) => {
      let data = '';
      res.on('data', c => data += c);
      res.on('end', () => {
        try {
          const json = JSON.parse(data);
          if (json.error) return reject(new Error(json.error.message));
          const text = json.candidates?.[0]?.content?.parts?.[0]?.text || '';
          const clean = text.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
          resolve(JSON.parse(clean));
        } catch (e) { reject(new Error(`Parse: ${e.message}`)); }
      });
    });
    req.on('error', reject);
    req.write(body);
    req.end();
  });
}

async function main() {
  const db = new Client({ connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified') });
  await db.connect();

  let success = 0, fail = 0;

  for (let i = 0; i < PRODUCTS.length; i++) {
    const p = PRODUCTS[i];
    console.log(`\n[${i+1}/13] ${p.vendor} ${p.sku} - ${p.name}`);

    try {
      const imgBuf = await fetchImage(p.img);
      console.log(`  Image: ${(imgBuf.length/1024).toFixed(0)}KB`);
      const b64 = imgBuf.toString('base64');

      const ai = await callGemini(b64, p.name);
      console.log(`  Colors: ${(ai.colors||[]).join(', ')}`);
      console.log(`  BG: ${ai.backgroundColor} | Styles: ${(ai.styles||[]).join(', ')}`);

      const res = 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
      `, [ai.colors, ai.backgroundColor, ai.styles, ai.patterns, ai.tags, ai.description, p.id]);

      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
          WHERE arte_sku = $7
        `, [ai.colors, ai.backgroundColor, ai.styles, ai.patterns, ai.tags, ai.description, p.sku]);
      }

      console.log(`  Saved (${res.rowCount} row)`);
      success++;
    } catch (err) {
      console.log(`  FAIL: ${err.message}`);
      fail++;
    }

    if (i < PRODUCTS.length - 1) await new Promise(r => setTimeout(r, 1200));
  }

  await db.end();
  console.log(`\nDONE: ${success} success, ${fail} failed out of 13`);
}

main().catch(console.error);