import { PrismaClient, Prisma } from '@prisma/client'; import { GoogleGenAI, Type, Part } from "@google/genai"; import fs from 'fs'; import path from 'path'; import dotenv from 'dotenv'; import sharp from 'sharp'; // Correct top-level import // Load environment variables manually if helpful, but usually preloaded dotenv.config(); const prisma = new PrismaClient(); // Adjust API Key retrieval if needed const API_KEY = process.env.GEMINI_API_KEY || process.env.API_KEY || ""; const ai = new GoogleGenAI({ apiKey: API_KEY }); const STORAGE_ROOT = path.join(process.cwd(), '..', 'storage'); async function reconstruct() { console.log("šŸš‘ STARTING AI METADATA RECONSTRUCTION (v2)..."); // We target "Recovered Project" in particular // Note: Project model does not have strategy column, so we rely on Niche check const recoveredProjects = await prisma.project.findMany({ where: { OR: [ { niche: { contains: "Recovered Project" } }, { seoData: { is: null } } ] }, include: { assets: true, seoData: true } }); console.log(`šŸ” Found ${recoveredProjects.length} projects needing reconstruction.`); let successCount = 0; for (const project of recoveredProjects) { try { // Find Master Asset const masterAsset = project.assets.find(a => a.type === 'master' || a.type === 'MASTER'); if (!masterAsset) { // console.log(`āš ļø Skiping ${project.id}: No master asset.`); continue; } const imagePath = path.join(STORAGE_ROOT, masterAsset.path.replace('projects/', 'projects/')); // Correction: masterAsset.path is relative to storage root? // masterAsset.path usually is "projects/UUID/master/file.png" // STORAGE_ROOT is ".../storage" // Join safely. const fullPath = path.join(STORAGE_ROOT, ...masterAsset.path.split('/').slice(1)); // Wait, if path is "projects/..." and STORAGE_ROOT has "storage", we need to check real fs path. // Let's try direct join first. let realPath = path.join(process.cwd(), '../storage', masterAsset.path); if (!fs.existsSync(realPath)) { // Try alternative path (sometimes assets stored with leading slash?) realPath = path.join(process.cwd(), '../storage', masterAsset.path.replace(/^projects\//, '')); } if (!fs.existsSync(realPath)) { console.log(`āŒ Image not found on disk: ${realPath}`); continue; } // Read Image const rawBuffer = fs.readFileSync(realPath); // Resize if too large (Optimization) const resizedBuffer = await sharp(rawBuffer) .resize({ width: 1024, height: 1024, fit: 'inside' }) .toBuffer(); const base64Image = resizedBuffer.toString('base64'); // CALL GEMINI console.log(`šŸ¤– Analyzing ${project.id.substring(0, 8)}...`); // Construct Prompt Parts const parts: Part[] = [ { text: ` You are a Professional Market Analyst and SEO Specialist. Analyze this image (which is a digital product for Etsy) to reconstruct its lost metadata. IMPORTANT: - Keep "description" SHORT and CONCISE (max 50 words). - Return exactly 13 "keywords". - Do not hallucinate long text. Return ONLY a JSON object: { "niche": "specific niche string", "productType": "Wall Art", "title": "SEO Title", "description": "Short Marketing Description", "keywords": ["tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", "tag8", "tag9", "tag10", "tag11", "tag12", "tag13"], "creativity": "Balanced", "jsonLd": { ...valid schema... } } ` }, { inlineData: { data: base64Image, mimeType: "image/png" } } ]; const result = await ai.models.generateContent({ model: "gemini-flash-latest", // 1.5 Flash (Confirmed available) contents: { parts }, config: { responseMimeType: "application/json", responseSchema: { type: Type.OBJECT, properties: { niche: { type: Type.STRING }, productType: { type: Type.STRING }, title: { type: Type.STRING }, description: { type: Type.STRING }, keywords: { type: Type.ARRAY, items: { type: Type.STRING } }, creativity: { type: Type.STRING }, jsonLd: { type: Type.OBJECT, properties: { "@context": { type: Type.STRING }, "@type": { type: Type.STRING }, name: { type: Type.STRING }, description: { type: Type.STRING }, image: { type: Type.STRING }, brand: { type: Type.STRING } } } } } } }); let responseText = result.text; if (!responseText) throw new Error("Empty AI Response"); // Clean markdown fences responseText = responseText.replace(/```json/g, '').replace(/```/g, '').trim(); let metadata; try { metadata = JSON.parse(responseText); } catch (jsonErr) { throw new Error("Failed to parse JSON: " + responseText.substring(0, 100)); } // Defaults if missing const finalTitle = metadata.title || `Recovered Project ${project.niche.substring(0, 10)}`; const finalDescription = metadata.description || "Project metadata recovered from image analysis."; const finalKeywords = Array.isArray(metadata.keywords) ? metadata.keywords : ["Recovered"]; // UDPATE DATABASE await prisma.project.update({ where: { id: project.id }, data: { niche: metadata.niche || "Recovered Art", productType: metadata.productType || "Wall Art" } }); await prisma.seoData.upsert({ where: { projectId: project.id }, create: { projectId: project.id, title: finalTitle, description: finalDescription, keywords: JSON.stringify(finalKeywords), jsonLd: JSON.stringify(metadata.jsonLd || {}), printingGuide: "Standard", suggestedPrice: "5.00" }, update: { title: finalTitle, description: finalDescription, keywords: JSON.stringify(finalKeywords), jsonLd: JSON.stringify(metadata.jsonLd || {}) } }); process.stdout.write('+'); successCount++; } catch (err: any) { console.error(`āŒ Error ${project.id}:`, err.message); // Log preview if it was a parse error if (err.message.includes('JSON')) { console.log("Response Preview:", err.stack); // Stack is too noisy, but okay for now. } } } console.log(`\nšŸŽ‰ RECONSTRUCTION COMPLETE. Fixed ${successCount} projects.`); await prisma.$disconnect(); } reconstruct();