← Back to Project Overview

📸 Polaroid Moments Generator 3

Three-Person Group Photo AI - Core Source Code & Implementation

React 19.1+ TypeScript Gemini AI Three-Person

📋 About This Code Showcase

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.

🎯 Key Implementation Highlights:
  • Three-person image upload and processing with advanced AI coordination
  • Story-driven pose generation with emotional narrative templates
  • Complex group composition algorithms for realistic social dynamics
  • Vintage polaroid aesthetic processing with authentic color grading
  • Type-safe React architecture with modern hooks and components
📱 App.tsx - Three-Person Application Component
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>
  );
};
            
🎭 constants.ts - Story-Driven Pose Prompts
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`;
};
            
🤖 geminiService.ts - Three-Person AI Processing
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);
  });
}
            
📤 ImageUploader.tsx - Three-Image Upload Component
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 };
            

🔧 Technical Implementation Notes

🎯 Three-Person Architecture Challenges

🚀 Enhanced Features Over Two-Person Version

🛡️ Quality Assurance & Error Handling

← Back to Project Overview | Portfolio Home