Three-Person Group Photo AI - Core Source Code & Implementation
This page presents the core implementation of the Polaroid Moments Generator 3, demonstrating advanced three-person group photo composition using React, TypeScript, and Google Gemini AI. The application showcases sophisticated multi-subject image processing, story-driven pose generation, and authentic vintage aesthetics.
import React, { useState, useCallback, useEffect } from 'react'; import { ImageUploader } from './components/ImageUploader'; import { ImageGrid } from './components/ImageGrid'; import { Loader } from './components/Loader'; import { generatePolaroidImages } from './services/geminiService'; import { POSE_PROMPTS } from './constants'; const App: React.FC = () => { // Three-person image state management const [image1, setImage1] = useState<File | null>(null); const [image2, setImage2] = useState<File | null>(null); const [image3, setImage3] = useState<File | null>(null); const [generatedImages, setGeneratedImages] = useState<string[]>([]); const [isLoading, setIsLoading] = useState<boolean>(false); const [error, setError] = useState<string | null>(null); // Enhanced validation for three-person requirement const handleGenerateClick = useCallback(async () => { if (!image1 || !image2 || !image3) { setError('Please upload three images to begin.'); return; } setIsLoading(true); setError(null); setGeneratedImages([]); try { // Pass all three images to AI service const results = await generatePolaroidImages( image1, image2, image3, POSE_PROMPTS.map(p => p.prompt) ); setGeneratedImages(results); } catch (err) { handleGenerationError(err); } finally { setIsLoading(false); } }, [image1, image2, image3]); return ( <div className="min-h-screen"> <header> <h1>Polaroid Moments Generator</h1> <p>Create candid, retro-style photos from three images.</p> </header> <ImageUploader onImage1Select={setImage1} onImage2Select={setImage2} onImage3Select={setImage3} /> <button onClick={handleGenerateClick} disabled={!image1 || !image2 || !image3 || isLoading} > Generate 4 Images </button> {isLoading && <Loader />} {generatedImages.length > 0 && ( <ImageGrid images={generatedImages} captions={POSE_PROMPTS.map(p => p.title)} /> )} </div> ); };
export interface PosePrompt { title: string; prompt: string; } // Four narrative-driven poses for three-person compositions export const POSE_PROMPTS: PosePrompt[] = [ { title: "Victory Shout", prompt: `Three friends stand side by side, fists clenched and faces lit with excitement as if they've just won a big game or achieved something together. Their wide smiles and triumphant poses capture a burst of energy and celebration, telling the story of a shared victory moment.` }, { title: "Whispering a Secret", prompt: `Two friends lean in as if one is whispering a secret to the other, while the third friend looks at the camera with a curious or amused expression. This tells a fun little story within the photo.` }, { title: "Celebration Pose", prompt: `Three friends stand close together with arms around each other's shoulders, grinning widely at the camera. One of them points playfully towards the middle friend, as if highlighting him, while all three radiate energy and joy.` }, { title: "Mixed Reactions", prompt: `Three friends strike contrasting poses — one scratches his head with a puzzled look, the middle stands confidently with arms crossed, while the third gestures to himself in surprise. Together, they create a playful scene of disagreement.` } ]; // Enhanced prompting for better three-person coordination export const buildThreePersonPrompt = (basePose: string): string => { return `${basePose} IMPORTANT REQUIREMENTS: - All three people must be clearly visible and well-positioned - Maintain individual facial features and characteristics - Create natural group dynamics and realistic interactions - Apply vintage polaroid aesthetic with authentic color grading - Ensure proper lighting and shadow coordination across all subjects - Generate high-quality, print-ready resolution`; };
import { GoogleGenerativeAI } from '@google/generative-ai'; // Initialize Gemini AI for advanced multi-image processing const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || ''); export async function generatePolaroidImages( image1: File, image2: File, image3: File, prompts: string[] ): Promise<string[]> { try { // Convert all three images to base64 for AI processing const [base64Image1, base64Image2, base64Image3] = await Promise.all([ fileToBase64(image1), fileToBase64(image2), fileToBase64(image3) ]); const model = genAI.getGenerativeModel({ model: 'gemini-1.5-pro' }); const results: string[] = []; // Process each pose with all three images for (const prompt of prompts) { const enhancedPrompt = buildAdvancedThreePersonPrompt(prompt); const result = await model.generateContent([ enhancedPrompt, { inlineData: { data: base64Image1, mimeType: image1.type, }, }, { inlineData: { data: base64Image2, mimeType: image2.type, }, }, { inlineData: { data: base64Image3, mimeType: image3.type, }, }, ]); const response = await result.response; const generatedImageUrl = await processImageResponse(response); results.push(generatedImageUrl); } return results; } catch (error) { console.error('Three-person image generation failed:', error); throw new Error('Failed to generate group polaroid images'); } } // Advanced prompt engineering for three-person coordination function buildAdvancedThreePersonPrompt(basePose: string): string { return `Create a vintage polaroid-style group photo with the following composition: ${basePose} TECHNICAL REQUIREMENTS: - Seamlessly blend all three people into a single cohesive scene - Maintain individual facial features and body characteristics - Create realistic group spacing and natural interactions - Apply authentic vintage polaroid color grading and aesthetic - Ensure consistent lighting and shadow effects across all subjects - Generate in classic polaroid aspect ratio with white border ARTISTIC DIRECTION: - Capture the emotional essence of the pose description - Create authentic 1970s-80s polaroid film aesthetic - Add subtle film grain and characteristic color shifts - Ensure each person's personality shines through - Make the composition feel natural and unposed despite coordination OUTPUT: High-quality vintage polaroid group photo featuring all three subjects`; } // Utility function for base64 conversion async function fileToBase64(file: File): Promise<string> { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === 'string') { // Remove data URL prefix for base64 data const base64 = reader.result.split(',')[1]; resolve(base64); } else { reject(new Error('Failed to convert file to base64')); } }; reader.onerror = reject; reader.readAsDataURL(file); }); }
import React, { useRef, useState } from 'react'; interface ImageUploaderProps { onImage1Select: (file: File | null) => void; onImage2Select: (file: File | null) => void; onImage3Select: (file: File | null) => void; } const ImageUploader: React.FC<ImageUploaderProps> = ({ onImage1Select, onImage2Select, onImage3Select, }) => { const [preview1, setPreview1] = useState<string | null>(null); const [preview2, setPreview2] = useState<string | null>(null); const [preview3, setPreview3] = useState<string | null>(null); const fileInputRef1 = useRef<HTMLInputElement>(null); const fileInputRef2 = useRef<HTMLInputElement>(null); const fileInputRef3 = useRef<HTMLInputElement>(null); // Generic handler for any of the three image slots const handleImageSelect = ( imageNumber: 1 | 2 | 3, event: React.ChangeEvent<HTMLInputElement> ) => { const file = event.target.files?.[0]; if (file && validateImageFile(file)) { // Create preview URL const previewUrl = URL.createObjectURL(file); // Update appropriate state based on image number switch (imageNumber) { case 1: setPreview1(previewUrl); onImage1Select(file); break; case 2: setPreview2(previewUrl); onImage2Select(file); break; case 3: setPreview3(previewUrl); onImage3Select(file); break; } } }; // Validation for uploaded images const validateImageFile = (file: File): boolean => { const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; const maxSize = 10 * 1024 * 1024; // 10MB if (!validTypes.includes(file.type)) { alert('Please upload a valid image file (JPEG, PNG, or WebP)'); return false; } if (file.size > maxSize) { alert('Image size must be less than 10MB'); return false; } return true; }; return ( <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> {[1, 2, 3].map((num) => ( <div key={num} className="upload-slot"> <h3>Person {num}</h3> <div className="upload-area" onClick={() => { if (num === 1) fileInputRef1.current?.click(); if (num === 2) fileInputRef2.current?.click(); if (num === 3) fileInputRef3.current?.click(); }} > {((num === 1 && preview1) || (num === 2 && preview2) || (num === 3 && preview3)) ? ( <img src={num === 1 ? preview1! : num === 2 ? preview2! : preview3!} alt={`Person ${num}`} className="preview-image" /> ) : ( <div className="upload-placeholder"> <span>Click to upload photo</span> </div> )} </div> <input ref={num === 1 ? fileInputRef1 : num === 2 ? fileInputRef2 : fileInputRef3} type="file" accept="image/*" onChange={(e) => handleImageSelect(num as 1 | 2 | 3, e)} style={{ display: 'none' }} /> </div> ))} </div> ); }; export { ImageUploader };