Improve image focus by centering on people and faces

Update AI prompts for more accurate focal point detection and adjust image rendering in various components (MediumCard, SideCard, SingleImageCarousel) to utilize these focal points, enhancing visual composition.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 4e108b3d-fbcd-43ec-8d2f-0a6ed15b2c47
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
This commit is contained in:
sebastjanartic 2026-02-28 21:16:53 +00:00
parent 3c23a20652
commit 4046bafaab
3 changed files with 49 additions and 57 deletions

View File

@ -110,7 +110,7 @@ function SingleImageCarousel({
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
<div className="relative w-full overflow-hidden rounded-b-lg aspect-square">
<div className="relative w-full overflow-hidden rounded-b-lg aspect-[9/16]">
<img
src={images[index].large || images[index].thumb}
alt={images[index].fileName}

View File

@ -137,14 +137,22 @@ function GalleryHeroCard({ images }: { images: GalleryImage[] }) {
);
}
function MediumCard({ article }: { article: Article }) {
function getObjectPosition(coverImage: string | null, focalPoints?: Record<string, { x: number; y: number }>): 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<string, { x: number; y: number }> }) {
const isVideo = article.category === "Video";
const objPos = getObjectPosition(article.coverImage, focalPoints);
return (
<Link href={`/article/${article.slug}`}>
<div className="relative group cursor-pointer rounded-lg overflow-hidden h-full bg-card border border-card-border flex flex-col" data-testid={`card-medium-${article.id}`}>
<div className="relative flex-shrink-0">
<div className="overflow-hidden">
<img src={thumbUrl(article.coverImage)} alt={article.title} className="w-full aspect-video object-cover transition-transform duration-500 group-hover:scale-105" style={{ objectPosition: "center 20%" }} loading="lazy" />
<img src={thumbUrl(article.coverImage)} alt={article.title} className="w-full aspect-video object-cover transition-transform duration-500 group-hover:scale-105" style={{ objectPosition: objPos }} loading="lazy" />
</div>
{isVideo && (
<div className="absolute inset-0 flex items-center justify-center">
@ -171,14 +179,15 @@ function MediumCard({ article }: { article: Article }) {
);
}
function SideCard({ article }: { article: Article }) {
function SideCard({ article, focalPoints }: { article: Article; focalPoints?: Record<string, { x: number; y: number }> }) {
const isVideo = article.category === "Video";
const objPos = getObjectPosition(article.coverImage, focalPoints);
return (
<Link href={`/article/${article.slug}`}>
<div className="relative group cursor-pointer rounded-lg overflow-hidden bg-card border border-card-border h-full flex flex-col" data-testid={`card-side-${article.id}`}>
<div className="relative flex-shrink-0 flex-1 min-h-0">
<div className="overflow-hidden h-full">
<img src={thumbUrl(article.coverImage)} alt={article.title} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" style={{ objectPosition: "center 20%" }} loading="lazy" />
<img src={thumbUrl(article.coverImage)} alt={article.title} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" style={{ objectPosition: objPos }} loading="lazy" />
</div>
{isVideo && (
<div className="absolute inset-0 flex items-center justify-center">
@ -289,7 +298,7 @@ function FeaturedCarousel({ articles, popular, galleryImages, focalPoints }: { a
</div>
<div className="lg:col-span-3 grid grid-cols-1 gap-3 grid-rows-2 lg:h-[420px]">
{side.map((a) => (
<SideCard key={a.id} article={a} />
<SideCard key={a.id} article={a} focalPoints={focalPoints} />
))}
</div>
<div className="lg:col-span-2 lg:h-[420px]">
@ -360,26 +369,30 @@ export default function Home() {
<FeaturedCarousel articles={articles} popular={popular} galleryImages={galleryImages} focalPoints={focalPoints} />
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="columns-1 sm:columns-2 lg:columns-4 gap-4 space-y-4 [column-fill:_balance]">
{row2Left.map((a) => (
<MediumCard key={a.id} article={a} />
<div key={a.id} className="break-inside-avoid">
<MediumCard article={a} focalPoints={focalPoints} />
</div>
))}
<div className="aspect-[4/5] sm:aspect-auto">
<div className="break-inside-avoid">
<PhotoGalleryWidget />
</div>
<div className="aspect-[4/5] sm:aspect-auto">
<div className="break-inside-avoid">
<RecipeWidget />
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="aspect-[4/5] sm:aspect-auto">
<div className="columns-1 sm:columns-2 lg:columns-4 gap-4 space-y-4 [column-fill:_balance]">
<div className="break-inside-avoid">
<HoroscopeWidget />
</div>
{row3Middle.map((a) => (
<MediumCard key={a.id} article={a} />
<div key={a.id} className="break-inside-avoid">
<MediumCard article={a} focalPoints={focalPoints} />
</div>
))}
<div className="aspect-[4/5] sm:aspect-auto">
<div className="break-inside-avoid">
<NewsWidget />
</div>
</div>
@ -387,7 +400,7 @@ export default function Home() {
{row4Articles.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{row4Articles.map((a) => (
<MediumCard key={a.id} article={a} />
<MediumCard key={a.id} article={a} focalPoints={focalPoints} />
))}
<NativeAdCard />
</div>
@ -396,7 +409,7 @@ export default function Home() {
{row5Articles.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{row5Articles.map((a) => (
<MediumCard key={a.id} article={a} />
<MediumCard key={a.id} article={a} focalPoints={focalPoints} />
))}
<NativeAdCard />
</div>

View File

@ -9,6 +9,17 @@ const openai = new OpenAI({
const cache = new Map<string, { x: number; y: number }>();
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() || "";