Implement server-side AI analysis to detect focal points in images, ensuring faces and key subjects are not cut off. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: c1dc1745-dcd7-4247-a1e3-8dd414e4592b Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/517dfa7b-26ac-463d-a6e1-a58c6df97188/drGbo1a Replit-Helium-Checkpoint-Created: true
121 lines
4.3 KiB
TypeScript
121 lines
4.3 KiB
TypeScript
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<string, { x: number; y: number }>();
|
|
|
|
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<string, { x: number; y: number }> {
|
|
const result: Record<string, { x: number; y: number }> = {};
|
|
for (const [key, value] of cache.entries()) {
|
|
result[key] = value;
|
|
}
|
|
return result;
|
|
}
|