201 lines
8.1 KiB
TypeScript
201 lines
8.1 KiB
TypeScript
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();
|