import { PrismaClient } from '@prisma/client'; import { GoogleGenAI, Type, Part } from "@google/genai"; import fs from 'fs'; import path from 'path'; import sharp from 'sharp'; interface ReconstructionOptions { projectId: string; prisma: PrismaClient; ai: GoogleGenAI; storageRoot: string; } export async function reconstructProjectMetadata({ projectId, prisma, ai, storageRoot }: ReconstructionOptions) { console.log(`[AI-Reconstruction] Starting analysis for project: ${projectId}`); const project = await prisma.project.findUnique({ where: { id: projectId }, include: { assets: true, seoData: true } }); if (!project) throw new Error("Project not found"); // FIND MASTER ASSET const masterAsset = project.assets.find(a => a.type === 'master' || a.type === 'MASTER' || a.type === 'upscaled'); if (!masterAsset) { console.warn(`[AI-Reconstruction] No master/upscaled asset found for ${projectId}.`); throw new Error("No master image found to analyze."); } // RESOLVE PATH let realPath = path.join(storageRoot, masterAsset.path); if (!fs.existsSync(realPath)) { // Try fallback path (sometimes path already includes 'projects/' prefix, sometimes not relative to storage root in same way) // If masterAsset.path starts with 'projects/', and storageRoot ends with 'storage', simple join usually works. // Try stripping 'projects/' prefix if needed. const altPath = path.join(storageRoot, masterAsset.path.replace(/^projects\//, '')); if (fs.existsSync(altPath)) { realPath = altPath; } else { // Try assuming it's relative to CWD if local debug const localPath = path.resolve(process.cwd(), '../storage', masterAsset.path); if (fs.existsSync(localPath)) realPath = localPath; } } if (!fs.existsSync(realPath)) { throw new Error(`Master asset file not found on disk at: ${realPath}`); } // PROCESS IMAGE const rawBuffer = fs.readFileSync(realPath); // Resize for AI Latency Optimization const resizedBuffer = await sharp(rawBuffer) .resize({ width: 1024, height: 1024, fit: 'inside' }) .toBuffer(); const base64Image = resizedBuffer.toString('base64'); console.log(`[AI-Reconstruction] Image processed (${rawBuffer.length} -> ${resizedBuffer.length} bytes). calling Gemini...`); // CALL GEMINI 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. - "jsonLd" should be a valid schema.org Product or CreativeWork object. 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": { "@context": "https://schema.org", "@type": "Product", "name": "...", "description": "...", "image": "...", "brand": "..." } } ` }, { inlineData: { data: base64Image, mimeType: "image/png" } } ]; const result = await ai.models.generateContent({ model: "gemini-flash-latest", // 1.5 Flash (Confirmed Stable) 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 JSON responseText = responseText.replace(/```json/g, '').replace(/```/g, '').trim(); let metadata; try { metadata = JSON.parse(responseText); } catch (e) { console.error("JSON Parse Error:", responseText); throw new Error("Failed to parse AI response JSON"); } const finalTitle = metadata.title || `Recovered Project ${project.niche?.substring(0, 10) || 'Art'}`; const finalDescription = metadata.description || "Project metadata recovered from image analysis."; const finalKeywords = Array.isArray(metadata.keywords) ? metadata.keywords : ["Recovered"]; // UPDATE DB // Update Project await prisma.project.update({ where: { id: projectId }, data: { niche: metadata.niche || "Recovered Art", productType: metadata.productType || "Wall Art" } }); // Update SeoData const updatedSeo = await prisma.seoData.upsert({ where: { projectId: projectId }, create: { projectId: projectId, 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 || {}) } }); // Strategy Object Return for frontend return { seoTitle: finalTitle, description: finalDescription, keywords: finalKeywords, printingGuide: "Standard", suggestedPrice: "5.00", jsonLd: metadata.jsonLd // return object, endpoint might stringify or not }; }