← back to Designer Wallcoverings

gemini-enrich-arte9.js

154 lines

const https = require('https');
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 db = new Client({ connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified') });

const PROMPT = `You are an interior design wallcovering analyst. Analyze this wallcovering product image (it may be a room setting showing the wallcovering installed on walls).
Focus on the WALLCOVERING itself, not furniture or decor.
Return ONLY valid JSON with these fields:
{
  "colors": ["<list of 3-6 dominant colors in the wallcovering, e.g. Gold, Cream, Charcoal>"],
  "backgroundColor": "<the single dominant background color of the wallcovering>",
  "tags": ["<5-8 interior design tags, e.g. Luxury, Textured, Organic, Hospitality>"],
  "styles": ["<1-3 design style periods, e.g. Contemporary, Art Deco, Mid-Century Modern>"],
  "patterns": ["<1-3 pattern types, e.g. Geometric, Abstract, Textural, Striped>"],
  "description": "<2 sentence commercial description for an architect or interior designer>"
}
No markdown, no code fences, just raw JSON.`;

// Use vendor_catalog room shot URLs (confirmed working 200 OK)
const products = [
  { id: 123, 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, 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, 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, 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, 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, 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, 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, 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, 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' },
];

function fetchImageAsBase64(url) {
  return new Promise((resolve, reject) => {
    https.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, res => {
      if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
        return fetchImageAsBase64(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', () => {
        const buf = Buffer.concat(chunks);
        if (buf.length < 1000) return reject(new Error(`Image too small: ${buf.length} bytes`));
        resolve(buf.toString('base64'));
      });
      res.on('error', reject);
    }).on('error', reject);
  });
}

function callGemini(b64, productName) {
  return new Promise((resolve, reject) => {
    const payload = JSON.stringify({
      contents: [{
        parts: [
          { text: `Product: ${productName}\n\n${PROMPT}` },
          { inline_data: { mime_type: 'image/jpeg', data: b64 } }
        ]
      }],
      generationConfig: { thinkingConfig: { thinkingBudget: 0 } }
    });
    
    const url = new URL(GEMINI_URL);
    const options = {
      hostname: url.hostname,
      path: url.pathname + url.search,
      method: 'POST',
      headers: { 'Content-Type': 'application/json' }
    };
    
    const req = https.request(options, res => {
      let data = '';
      res.on('data', chunk => data += chunk);
      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 cleaned = text.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
          resolve(JSON.parse(cleaned));
        } catch (e) {
          reject(new Error(`Parse: ${e.message} — ${data.substring(0, 200)}`));
        }
      });
    });
    req.on('error', reject);
    req.write(payload);
    req.end();
  });
}

async function updateDB(product, analysis) {
  // vendor_catalog uses text columns — postgres array literal
  const pgArr = arr => `{${arr.map(c => `"${c.replace(/"/g, '')}"`).join(',')}}`;
  const vc_colors = pgArr(analysis.colors || []);
  const vc_tags = pgArr(analysis.tags || []);
  const vc_styles = pgArr(analysis.styles || []);
  const vc_patterns = pgArr(analysis.patterns || []);

  // arte_catalog uses jsonb columns — JSON array
  const ac_colors = JSON.stringify(analysis.colors || []);
  const ac_tags = JSON.stringify(analysis.tags || []);
  const ac_styles = JSON.stringify(analysis.styles || []);
  const ac_patterns = JSON.stringify(analysis.patterns || []);

  // Update vendor_catalog (text columns)
  await db.query(`
    UPDATE vendor_catalog SET
      ai_colors = $1, ai_background_color = $2, ai_tags = $3,
      ai_styles = $4, ai_patterns = $5, ai_description = $6
    WHERE id = $7
  `, [vc_colors, analysis.backgroundColor, vc_tags, vc_styles, vc_patterns, analysis.description, product.id]);

  // Update arte_catalog (jsonb columns)
  await db.query(`
    UPDATE arte_catalog SET
      ai_colors = $1::jsonb, ai_background_color = $2, ai_tags = $3::jsonb,
      ai_styles = $4::jsonb, ai_patterns = $5::jsonb, ai_description = $6
    WHERE arte_sku = $7
  `, [ac_colors, analysis.backgroundColor, ac_tags, ac_styles, ac_patterns, analysis.description, product.sku]);
}

async function main() {
  await db.connect();
  console.log('Connected. Processing 9 Arte products...\n');
  let ok = 0, fail = 0;
  
  for (const p of products) {
    try {
      process.stdout.write(`${p.sku} ${p.name}... `);
      const b64 = await fetchImageAsBase64(p.img);
      console.log(`img ${Math.round(b64.length/1024)}KB`);
      const analysis = await callGemini(b64, p.name);
      console.log(`  ✅ ${analysis.colors.join(', ')} | bg: ${analysis.backgroundColor}`);
      await updateDB(p, analysis);
      ok++;
      await new Promise(r => setTimeout(r, 1000));
    } catch (e) {
      console.log(`  ❌ ${e.message}`);
      fail++;
    }
  }
  
  console.log(`\nDone: ${ok}/9 succeeded, ${fail} failed`);
  await db.end();
}

main().catch(e => { console.error(e); process.exit(1); });