import OpenAI from "openai"; import fs from "fs"; import path from "path"; const openai = new OpenAI({ apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY, baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL, }); const cache = new Map(); export async function analyzeFocalPoint(imagePath: string): Promise<{ x: number; y: number }> { const originalPath = imagePath; if (cache.has(originalPath)) { return cache.get(originalPath)!; } try { let imageData: string; let mimeType = "image/webp"; if (imagePath.startsWith("/uploads/")) { const localPath = path.join(process.cwd(), "client/public", imagePath); if (!fs.existsSync(localPath)) { throw new Error(`File not found: ${localPath}`); } const buffer = fs.readFileSync(localPath); imageData = buffer.toString("base64"); if (localPath.endsWith(".jpg") || localPath.endsWith(".jpeg")) mimeType = "image/jpeg"; else if (localPath.endsWith(".png")) mimeType = "image/png"; } else if (imagePath.startsWith("http")) { const response = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "system", content: "You analyze images to find the main subject (person's face or group center). Return ONLY a JSON object with x and y as percentages (0-100). x=50 means horizontal center, y=20 means near top. Example: {\"x\":50,\"y\":30}" }, { role: "user", content: [ { type: "text", text: "Where is the main subject (face/person) in this image? Return only JSON." }, { type: "image_url", image_url: { url: imagePath, detail: "low" } } ] } ], max_tokens: 50, }); const text = response.choices[0]?.message?.content?.trim() || ""; const match = text.match(/\{[^}]+\}/); if (match) { const parsed = JSON.parse(match[0]); const point = { x: Math.max(0, Math.min(100, Number(parsed.x) || 50)), y: Math.max(0, Math.min(100, Number(parsed.y) || 30)), }; cache.set(originalPath, point); return point; } throw new Error("Could not parse response"); } else { throw new Error(`Unsupported path: ${imagePath}`); } const dataUrl = `data:${mimeType};base64,${imageData}`; const response = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "system", content: "You analyze images to find the main subject (person's face or group center). Return ONLY a JSON object with x and y as percentages (0-100) indicating where the main subject/face is located. x=50 means horizontal center, y=20 means near the top. Example: {\"x\":45,\"y\":30}" }, { role: "user", content: [ { type: "text", text: "Where is the main subject (face/person) in this image? Return only JSON with x,y percentages." }, { type: "image_url", image_url: { url: dataUrl, detail: "low" } } ] } ], max_tokens: 50, }); const text = response.choices[0]?.message?.content?.trim() || ""; console.log(`[focal-point] ${imagePath}: AI response = ${text}`); const match = text.match(/\{[^}]+\}/); if (match) { const parsed = JSON.parse(match[0]); const point = { x: Math.max(0, Math.min(100, Number(parsed.x) || 50)), y: Math.max(0, Math.min(100, Number(parsed.y) || 30)), }; cache.set(originalPath, point); return point; } } catch (e) { console.log("[focal-point] AI analysis failed for", imagePath, ":", (e as Error).message); } const fallback = { x: 50, y: 30 }; cache.set(originalPath, fallback); return fallback; } export async function analyzeAllArticleImages(articles: Array<{ coverImage: string | null }>) { for (const article of articles) { if (!article.coverImage) continue; await analyzeFocalPoint(article.coverImage); } console.log("[focal-point] Analyzed", cache.size, "images"); } export function getCachedFocalPoints(): Record { const result: Record = {}; for (const [key, value] of cache.entries()) { result[key] = value; } return result; }