diff --git a/client/src/components/photo-gallery.tsx b/client/src/components/photo-gallery.tsx index 516d2c0..793fef8 100644 --- a/client/src/components/photo-gallery.tsx +++ b/client/src/components/photo-gallery.tsx @@ -110,7 +110,7 @@ function SingleImageCarousel({ onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)} > -
+
{images[index].fileName}): string { + if (!coverImage || !focalPoints) return "center 20%"; + const fp = focalPoints[coverImage]; + if (!fp) return "center 20%"; + return `${fp.x}% ${fp.y}%`; +} + +function MediumCard({ article, focalPoints }: { article: Article; focalPoints?: Record }) { const isVideo = article.category === "Video"; + const objPos = getObjectPosition(article.coverImage, focalPoints); return (
- {article.title} + {article.title}
{isVideo && (
@@ -171,14 +179,15 @@ function MediumCard({ article }: { article: Article }) { ); } -function SideCard({ article }: { article: Article }) { +function SideCard({ article, focalPoints }: { article: Article; focalPoints?: Record }) { const isVideo = article.category === "Video"; + const objPos = getObjectPosition(article.coverImage, focalPoints); return (
- {article.title} + {article.title}
{isVideo && (
@@ -289,7 +298,7 @@ function FeaturedCarousel({ articles, popular, galleryImages, focalPoints }: { a
{side.map((a) => ( - + ))}
@@ -360,26 +369,30 @@ export default function Home() { -
+
{row2Left.map((a) => ( - +
+ +
))} -
+
-
+
-
-
+
+
{row3Middle.map((a) => ( - +
+ +
))} -
+
@@ -387,7 +400,7 @@ export default function Home() { {row4Articles.length > 0 && (
{row4Articles.map((a) => ( - + ))}
@@ -396,7 +409,7 @@ export default function Home() { {row5Articles.length > 0 && (
{row5Articles.map((a) => ( - + ))}
diff --git a/server/focal-point.ts b/server/focal-point.ts index fbdb5d1..00839a1 100644 --- a/server/focal-point.ts +++ b/server/focal-point.ts @@ -9,6 +9,17 @@ const openai = new OpenAI({ const cache = new Map(); +const SYSTEM_PROMPT = `You are an image analysis tool that detects where faces and people are located in photographs. +Analyze the image and find the PRIMARY person or group of people. Report the CENTER of their face(s) as x,y percentages. +- x=0 means far left edge, x=100 means far right edge +- y=0 means very top edge, y=100 means very bottom edge +- For a person's face in the upper third, y should be around 15-35 +- For a person standing centered, x should be around 40-60 +- For a group photo, find the center of the group's faces +- Be PRECISE, do NOT default to 50,50. Actually look at where faces are. +- If there are multiple people, find the most prominent face or group center. +Return ONLY a JSON object like {"x":42,"y":28} with no other text.`; + export async function analyzeFocalPoint(imagePath: string): Promise<{ x: number; y: number }> { const originalPath = imagePath; if (cache.has(originalPath)) { @@ -16,8 +27,7 @@ export async function analyzeFocalPoint(imagePath: string): Promise<{ x: number; } try { - let imageData: string; - let mimeType = "image/webp"; + let imageContent: { type: "image_url"; image_url: { url: string; detail: "auto" | "low" | "high" } }; if (imagePath.startsWith("/uploads/")) { const localPath = path.join(process.cwd(), "client/public", imagePath); @@ -25,61 +35,30 @@ export async function analyzeFocalPoint(imagePath: string): Promise<{ x: number; throw new Error(`File not found: ${localPath}`); } const buffer = fs.readFileSync(localPath); - imageData = buffer.toString("base64"); + const imageData = buffer.toString("base64"); + let mimeType = "image/webp"; if (localPath.endsWith(".jpg") || localPath.endsWith(".jpeg")) mimeType = "image/jpeg"; else if (localPath.endsWith(".png")) mimeType = "image/png"; + imageContent = { type: "image_url", image_url: { url: `data:${mimeType};base64,${imageData}`, detail: "auto" } }; } 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"); + imageContent = { type: "image_url", image_url: { url: imagePath, detail: "auto" } }; } 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: "system", content: SYSTEM_PROMPT }, { 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" } } + { type: "text", text: "Where exactly are the faces/people in this image? Be precise with coordinates. Return only JSON." }, + imageContent ] } ], - max_tokens: 50, + max_tokens: 60, }); const text = response.choices[0]?.message?.content?.trim() || "";