← back to Designer Wallcoverings

enrich-arte-fix.js

141 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_URL = (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified');

const PRODUCTS = [
  { id: 123, sku: '19433', pattern: 'Horus', color: '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', pattern: 'Amarna', color: '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', pattern: 'Sakkara', color: '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', pattern: 'Sakkara', color: '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', pattern: 'Ibis', color: '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', pattern: 'Ibis', color: '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', pattern: 'Siwa', color: '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', pattern: 'Siwa', color: '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', pattern: 'Galon', color: '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 downloadImage(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 downloadImage(res.headers.location).then(resolve).catch(reject);
      }
      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, mime, prompt) {
  return new Promise((resolve, reject) => {
    const body = JSON.stringify({
      contents: [{ parts: [
        { text: prompt },
        { inline_data: { mime_type: mime, data: b64 } }
      ]}],
      generationConfig: { thinkingConfig: { thinkingBudget: 0 } }
    });
    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', 'Content-Length': Buffer.byteLength(body) },
      timeout: 30000
    }, (res) => {
      const chunks = [];
      res.on('data', c => chunks.push(c));
      res.on('end', () => {
        try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
        catch(e) { reject(e); }
      });
    });
    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
    req.write(body);
    req.end();
  });
}

function parseGeminiJson(text) {
  let clean = text.replace(/^```(?:json)?\s*/m, '').replace(/\s*```\s*$/m, '').trim();
  return JSON.parse(clean);
}

// For vendor_catalog (text columns): PostgreSQL array literal
function toPgArray(arr) {
  if (!arr || !arr.length) return '{}';
  return '{' + arr.map(s => '"' + s.replace(/"/g, '\\"') + '"').join(',') + '}';
}

async function main() {
  const db = new Client({ connectionString: DB_URL });
  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}/9] arte ${p.sku} (${p.pattern} - ${p.color}) ━━━`);
    try {
      const imgBuf = await downloadImage(p.img);
      if (!imgBuf.length) throw new Error('Empty image');
      const b64 = imgBuf.toString('base64');
      console.log(`  Downloaded: ${(imgBuf.length / 1024).toFixed(0)}KB`);

      const prompt = `You are an interior design wallcovering expert. Analyze this wallcovering image. The vendor color name is "${p.color}" and the pattern is "${p.pattern}".

Return ONLY valid JSON with no markdown formatting:
{
  "colors": ["list 3-6 actual colors visible in the wallcovering"],
  "backgroundColor": "the dominant background color",
  "tags": ["5-8 interior design tags like Luxury, Textured, Modern, Elegant"],
  "styles": ["1-3 design period styles like Contemporary, Art Deco, Mid-Century"],
  "patterns": ["1-3 pattern types like Geometric, Textured, Striped, Floral"],
  "description": "Two-sentence commercial description for an architectural wallcovering retailer. Never use the word wallpaper."
}`;

      const resp = await callGemini(b64, 'image/jpeg', prompt);
      const text = resp?.candidates?.[0]?.content?.parts?.[0]?.text;
      if (!text) throw new Error('No Gemini text: ' + JSON.stringify(resp?.error || 'unknown').slice(0, 200));

      const data = parseGeminiJson(text);
      console.log(`  Colors: ${data.colors?.join(', ')}`);
      console.log(`  BG: ${data.backgroundColor}`);
      console.log(`  Tags: ${data.tags?.join(', ')}`);

      // vendor_catalog uses TEXT columns (pg array format)
      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`,
        [toPgArray(data.colors), data.backgroundColor||'', toPgArray(data.tags), toPgArray(data.styles), toPgArray(data.patterns), data.description||'', p.id]);

      // arte_catalog uses JSONB columns (JSON format)
      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`,
        [JSON.stringify(data.colors), data.backgroundColor||'', JSON.stringify(data.tags), JSON.stringify(data.styles), JSON.stringify(data.patterns), data.description||'', p.sku]);

      // Verify both
      const v1 = await db.query('SELECT ai_colors FROM vendor_catalog WHERE id=$1', [p.id]);
      const v2 = await db.query('SELECT ai_colors FROM arte_catalog WHERE arte_sku=$1', [p.sku]);
      const ok1 = v1.rows[0]?.ai_colors && v1.rows[0].ai_colors !== '{}';
      const ok2 = v2.rows[0]?.ai_colors && JSON.stringify(v2.rows[0].ai_colors) !== '[]';
      if (ok1 && ok2) {
        console.log(`  ✅ Saved (vendor_catalog + arte_catalog)`); success++;
      } else {
        console.log(`  ⚠️ Partial: vc=${ok1}, ac=${ok2}`); fail++;
      }
    } catch (err) {
      console.log(`  ❌ Error: ${err.message}`); fail++;
    }
    await new Promise(r => setTimeout(r, 1200));
  }

  await db.end();
  console.log(`\n━━━━━━━━━━━━━━━━━━━━━━`);
  console.log(`RESULTS: ✅ ${success}  ❌ ${fail}  (of 9)`);
}

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