folx-tv/server/focal-point.ts
sebastjanartic b1b7de007e Improve image display by intelligently focusing on subjects
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
2026-02-28 21:01:13 +00:00

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