Files
digicraft-be/utils/aiReconstruction.ts
Fahri Can Seçer 80dcf4d04a
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
main
2026-02-05 01:29:22 +03:00

180 lines
6.7 KiB
TypeScript

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
};
}