├── example_face.jpg ├── requirements.txt ├── .gitignore ├── LICENSE ├── .github └── workflows │ └── python-check.yml ├── FaceTracker.css ├── package.json ├── examples ├── react-basic │ ├── App.jsx │ └── App.css └── README.md ├── FaceTracker.jsx ├── CONTRIBUTING.md ├── generate.sh ├── useGazeTracking.js ├── QUICKSTART.md ├── generate_missing.py ├── main.py └── README.md /example_face.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylan02/face_looker/HEAD/example_face.jpg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Face Looker Dependencies 2 | # For generating gaze-direction face images using Replicate AI 3 | 4 | # Replicate API client 5 | replicate>=0.22.0 6 | 7 | # Image processing 8 | pillow>=10.0.0 9 | 10 | # Progress bars for generation 11 | tqdm>=4.66.0 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | MANIFEST 23 | 24 | # Virtual environments 25 | .env 26 | .venv 27 | env/ 28 | venv/ 29 | ENV/ 30 | env.bak/ 31 | venv.bak/ 32 | 33 | # IDE 34 | .vscode/ 35 | .idea/ 36 | *.swp 37 | *.swo 38 | *~ 39 | 40 | # OS 41 | .DS_Store 42 | .DS_Store? 43 | ._* 44 | .Spotlight-V100 45 | .Trashes 46 | ehthumbs.db 47 | Thumbs.db 48 | 49 | # Project specific 50 | out/ 51 | *.jpg 52 | *.jpeg 53 | *.png 54 | *.webp 55 | !example_face.jpg 56 | 57 | # API keys and secrets 58 | .env.local 59 | .env.development.local 60 | .env.test.local 61 | .env.production.local 62 | 63 | # Logs 64 | *.log 65 | npm-debug.log* 66 | yarn-debug.log* 67 | yarn-error.log* 68 | 69 | # Node modules (if any) 70 | node_modules/ 71 | 72 | # Generated files 73 | index.csv -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kylan O'Connor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/python-check.yml: -------------------------------------------------------------------------------- 1 | name: Python Code Check 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | check: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | 29 | - name: Lint with flake8 30 | run: | 31 | pip install flake8 32 | # Stop build if there are Python syntax errors or undefined names 33 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 34 | # Exit-zero treats all errors as warnings 35 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 36 | 37 | - name: Check imports 38 | run: | 39 | python -c "import main; print('✅ main.py imports successfully')" 40 | 41 | -------------------------------------------------------------------------------- /FaceTracker.css: -------------------------------------------------------------------------------- 1 | .face-tracker { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | min-height: 300px; 6 | background: #f0f0f0; 7 | border-radius: 8px; 8 | overflow: hidden; 9 | cursor: none; /* Hide cursor for better effect */ 10 | } 11 | 12 | .face-image { 13 | width: 100%; 14 | height: 100%; 15 | object-fit: contain; 16 | transition: opacity 0.1s ease-out; 17 | } 18 | 19 | .face-loading { 20 | position: absolute; 21 | top: 50%; 22 | left: 50%; 23 | transform: translate(-50%, -50%); 24 | color: #666; 25 | font-size: 14px; 26 | } 27 | 28 | .face-tracker-error { 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | height: 200px; 33 | color: #e74c3c; 34 | background: #fdf2f2; 35 | border: 1px solid #f5c6cb; 36 | border-radius: 4px; 37 | padding: 20px; 38 | } 39 | 40 | .face-debug { 41 | position: absolute; 42 | top: 10px; 43 | left: 10px; 44 | background: rgba(0, 0, 0, 0.8); 45 | color: white; 46 | padding: 8px 12px; 47 | border-radius: 4px; 48 | font-family: monospace; 49 | font-size: 12px; 50 | line-height: 1.4; 51 | } 52 | 53 | /* Responsive adjustments */ 54 | @media (max-width: 768px) { 55 | .face-tracker { 56 | min-height: 250px; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "face-looker", 3 | "version": "1.0.0", 4 | "description": "Generate AI-powered gaze-tracking face images and use them to create interactive React components that follow the cursor", 5 | "main": "useGazeTracking.js", 6 | "scripts": { 7 | "test": "echo \"No tests yet\" && exit 0" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/yourusername/face_looker.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "face-tracking", 16 | "gaze-tracking", 17 | "interactive", 18 | "replicate", 19 | "ai", 20 | "cursor-tracking", 21 | "mouse-tracking", 22 | "animation", 23 | "portfolio" 24 | ], 25 | "author": "Kylan O'Connor", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/yourusername/face_looker/issues" 29 | }, 30 | "homepage": "https://github.com/yourusername/face_looker#readme", 31 | "peerDependencies": { 32 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 33 | }, 34 | "files": [ 35 | "useGazeTracking.js", 36 | "FaceTracker.jsx", 37 | "FaceTracker.css", 38 | "main.py", 39 | "generate_missing.py", 40 | "requirements.txt", 41 | "README.md", 42 | "LICENSE", 43 | "examples/" 44 | ] 45 | } 46 | 47 | -------------------------------------------------------------------------------- /examples/react-basic/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FaceTracker from '../../FaceTracker'; 3 | import './App.css'; 4 | 5 | /** 6 | * Example: Basic implementation of FaceTracker 7 | * 8 | * This demonstrates the simplest way to use the face tracking component 9 | */ 10 | function App() { 11 | return ( 12 |
13 |
14 |

Face Looker Demo

15 |

Move your mouse around the face below!

16 |
17 | 18 |
19 | {/* Basic usage - face in a container */} 20 |
21 | 22 |
23 | 24 |
25 |

Try These:

26 |
    27 |
  • Move your cursor around the screen
  • 28 |
  • Try on mobile with touch
  • 29 |
  • Move quickly vs slowly
  • 30 |
  • Go to the edges and corners
  • 31 |
32 |
33 | 34 | {/* Debug mode enabled */} 35 |
36 |

Debug Mode

37 | 42 |
43 |
44 |
45 | ); 46 | } 47 | 48 | export default App; 49 | 50 | -------------------------------------------------------------------------------- /FaceTracker.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import useGazeTracking from './useGazeTracking'; 3 | import './FaceTracker.css'; // Optional styling 4 | 5 | /** 6 | * FaceTracker Component 7 | * Displays a face that follows mouse/touch movement 8 | */ 9 | export default function FaceTracker({ 10 | className = '', 11 | basePath = '/faces/', 12 | showDebug = false 13 | }) { 14 | const containerRef = useRef(null); 15 | const { currentImage, isLoading, error } = useGazeTracking(containerRef, basePath); 16 | const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); 17 | 18 | const handleMouseMove = (e) => { 19 | if (!containerRef.current) return; 20 | 21 | const rect = containerRef.current.getBoundingClientRect(); 22 | setMousePos({ 23 | x: e.clientX - rect.left, 24 | y: e.clientY - rect.top 25 | }); 26 | }; 27 | 28 | if (error) { 29 | return ( 30 |
31 | Error loading face images: {error.message} 32 |
33 | ); 34 | } 35 | 36 | return ( 37 |
42 | {currentImage && ( 43 | Face following gaze 54 | )} 55 | 56 | {isLoading && ( 57 |
58 | Loading face... 59 |
60 | )} 61 | 62 | {showDebug && ( 63 |
64 |
Mouse: ({Math.round(mousePos.x)}, {Math.round(mousePos.y)})
65 |
Image: {currentImage?.split('/').pop()}
66 |
67 | )} 68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /examples/react-basic/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | min-height: 100vh; 4 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 5 | color: white; 6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; 7 | } 8 | 9 | .App-header { 10 | padding: 2rem; 11 | } 12 | 13 | .App-header h1 { 14 | font-size: 3rem; 15 | margin: 0; 16 | font-weight: 800; 17 | } 18 | 19 | .App-header p { 20 | font-size: 1.2rem; 21 | opacity: 0.9; 22 | } 23 | 24 | .App-main { 25 | padding: 2rem; 26 | max-width: 1200px; 27 | margin: 0 auto; 28 | } 29 | 30 | .face-container { 31 | width: 400px; 32 | height: 400px; 33 | margin: 2rem auto; 34 | border-radius: 50%; 35 | overflow: hidden; 36 | box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); 37 | background: white; 38 | } 39 | 40 | .instructions { 41 | background: rgba(255, 255, 255, 0.1); 42 | border-radius: 20px; 43 | padding: 2rem; 44 | margin: 3rem auto; 45 | max-width: 600px; 46 | backdrop-filter: blur(10px); 47 | } 48 | 49 | .instructions h2 { 50 | margin-top: 0; 51 | } 52 | 53 | .instructions ul { 54 | text-align: left; 55 | list-style: none; 56 | padding: 0; 57 | } 58 | 59 | .instructions li { 60 | padding: 0.5rem 0; 61 | padding-left: 2rem; 62 | position: relative; 63 | } 64 | 65 | .instructions li::before { 66 | content: '👉'; 67 | position: absolute; 68 | left: 0; 69 | } 70 | 71 | .debug-container { 72 | margin-top: 3rem; 73 | } 74 | 75 | .debug-face { 76 | width: 300px; 77 | height: 300px; 78 | margin: 1rem auto; 79 | border-radius: 20px; 80 | overflow: hidden; 81 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); 82 | } 83 | 84 | /* Mobile responsiveness */ 85 | @media (max-width: 768px) { 86 | .App-header h1 { 87 | font-size: 2rem; 88 | } 89 | 90 | .face-container { 91 | width: 300px; 92 | height: 300px; 93 | } 94 | 95 | .debug-face { 96 | width: 250px; 97 | height: 250px; 98 | } 99 | } 100 | 101 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Face Looker Examples 2 | 3 | This directory contains various implementation examples for the Face Looker project. 4 | 5 | ## 📁 Examples 6 | 7 | ### 1. Basic React Implementation (`react-basic/`) 8 | 9 | The simplest way to implement face tracking in your React app. 10 | 11 | **Features:** 12 | - Basic FaceTracker component usage 13 | - Debug mode demonstration 14 | - Responsive design 15 | - Touch support 16 | 17 | **Files:** 18 | - `App.jsx` - Main application component 19 | - `App.css` - Styling 20 | 21 | ### 2. TypeScript Implementation (`typescript/`) 22 | 23 | TypeScript version with full type safety. 24 | 25 | **Features:** 26 | - Full TypeScript types 27 | - Type-safe props 28 | - Better IDE autocomplete 29 | - Production-ready code 30 | 31 | ### 3. Next.js Implementation (`nextjs/`) 32 | 33 | Optimized for Next.js with server-side rendering considerations. 34 | 35 | **Features:** 36 | - Next.js Image optimization (optional) 37 | - SSR-safe implementation 38 | - App Router compatible 39 | - Dynamic imports 40 | 41 | ### 4. Multiple Faces (`multiple-faces/`) 42 | 43 | Example showing multiple face trackers on one page. 44 | 45 | **Features:** 46 | - Multiple independent face trackers 47 | - Different configurations 48 | - Performance optimization 49 | 50 | ### 5. Custom Styling (`custom-styling/`) 51 | 52 | Advanced styling examples. 53 | 54 | **Features:** 55 | - Circular masks 56 | - Border effects 57 | - Animations 58 | - Hover effects 59 | 60 | ## 🚀 Quick Start 61 | 62 | 1. **Generate your faces first:** 63 | ```bash 64 | cd ../ 65 | python main.py --image ./my_face.jpg --out ./out 66 | ``` 67 | 68 | 2. **Copy faces to your React project:** 69 | ```bash 70 | cp -r ./out/faces /path/to/your-react-app/public/ 71 | ``` 72 | 73 | 3. **Copy the example you want:** 74 | ```bash 75 | # For basic React 76 | cp examples/react-basic/* /path/to/your-react-app/src/ 77 | 78 | # For TypeScript 79 | cp examples/typescript/* /path/to/your-react-app/src/ 80 | ``` 81 | 82 | 4. **Install dependencies** (if needed): 83 | ```bash 84 | npm install 85 | # or 86 | yarn install 87 | ``` 88 | 89 | ## 💡 Tips 90 | 91 | - Start with `react-basic` if you're new to the project 92 | - Use `typescript` for production applications 93 | - Check `custom-styling` for inspiration on making it unique 94 | - `multiple-faces` shows how to handle multiple instances 95 | 96 | ## 🔧 Configuration 97 | 98 | All examples assume: 99 | - Face images in `/public/faces/` 100 | - Default grid parameters (`-15` to `15`, step `3`) 101 | - 256×256 image size 102 | 103 | If you changed generation parameters, update the constants in `useGazeTracking.js`. 104 | 105 | ## 📚 Learn More 106 | 107 | See the main [README.md](../README.md) for full documentation. 108 | 109 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Face Looker 2 | 3 | Thanks for your interest in contributing! 🎉 4 | 5 | ## 🐛 Bug Reports 6 | 7 | If you find a bug, please open an issue with: 8 | - Clear description of the problem 9 | - Steps to reproduce 10 | - Expected vs actual behavior 11 | - Python version and OS 12 | - Error messages (if any) 13 | 14 | ## 💡 Feature Requests 15 | 16 | We welcome feature ideas! Please open an issue describing: 17 | - The problem you're trying to solve 18 | - Your proposed solution 19 | - Any alternative solutions you've considered 20 | 21 | ## 🔧 Pull Requests 22 | 23 | ### Setup for Development 24 | 25 | 1. Fork the repository 26 | 2. Clone your fork: 27 | ```bash 28 | git clone https://github.com/YOUR_USERNAME/face_looker.git 29 | cd face_looker 30 | ``` 31 | 32 | 3. Create a virtual environment: 33 | ```bash 34 | python -m venv .venv 35 | source .venv/bin/activate 36 | ``` 37 | 38 | 4. Install dependencies: 39 | ```bash 40 | pip install -r requirements.txt 41 | ``` 42 | 43 | 5. Create a feature branch: 44 | ```bash 45 | git checkout -b feature/your-feature-name 46 | ``` 47 | 48 | ### Code Style 49 | 50 | - Follow PEP 8 for Python code 51 | - Use meaningful variable names 52 | - Add docstrings to functions 53 | - Keep functions focused and small 54 | 55 | ### Testing 56 | 57 | Before submitting a PR: 58 | 1. Test your changes with different parameters 59 | 2. Verify React implementation still works 60 | 3. Check that existing functionality isn't broken 61 | 4. Test on both Python 3.7+ and latest version 62 | 63 | ### Documentation 64 | 65 | - Update README.md if you add features 66 | - Add examples for new functionality 67 | - Include code comments for complex logic 68 | 69 | ### Commit Messages 70 | 71 | Use clear, descriptive commit messages: 72 | ``` 73 | Add support for custom image formats 74 | 75 | - Added PNG and JPEG output options 76 | - Updated save_resized_webp to handle multiple formats 77 | - Added --format argument to CLI 78 | ``` 79 | 80 | ### Submitting 81 | 82 | 1. Push to your fork: 83 | ```bash 84 | git push origin feature/your-feature-name 85 | ``` 86 | 87 | 2. Open a Pull Request with: 88 | - Clear description of changes 89 | - Why the change is needed 90 | - Any breaking changes 91 | - Screenshots (if UI changes) 92 | 93 | ## 📝 Example Contributions 94 | 95 | Good first contributions: 96 | - Add more examples 97 | - Improve documentation 98 | - Fix typos 99 | - Add tests 100 | - Performance improvements 101 | 102 | ## 🤝 Code of Conduct 103 | 104 | - Be respectful and inclusive 105 | - Provide constructive feedback 106 | - Help newcomers 107 | - Focus on the best solution, not winning arguments 108 | 109 | ## ❓ Questions? 110 | 111 | Open an issue with the "question" label or reach out to the maintainers. 112 | 113 | Thank you for contributing! 🙏 114 | 115 | -------------------------------------------------------------------------------- /generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Quick start script for generating face images 3 | 4 | set -e 5 | 6 | echo "🎭 Face Looker - Quick Start Generator" 7 | echo "======================================" 8 | echo "" 9 | 10 | # Check if REPLICATE_API_TOKEN is set 11 | if [ -z "$REPLICATE_API_TOKEN" ]; then 12 | echo "❌ Error: REPLICATE_API_TOKEN not set!" 13 | echo "" 14 | echo "Please set your Replicate API token:" 15 | echo " export REPLICATE_API_TOKEN=your_token_here" 16 | echo "" 17 | echo "Get your token at: https://replicate.com/account/api-tokens" 18 | exit 1 19 | fi 20 | 21 | # Check if virtual environment exists 22 | if [ ! -d ".venv" ]; then 23 | echo "📦 Creating virtual environment..." 24 | python3 -m venv .venv 25 | fi 26 | 27 | # Activate virtual environment 28 | echo "🔌 Activating virtual environment..." 29 | source .venv/bin/activate 30 | 31 | # Install dependencies 32 | echo "📥 Installing dependencies..." 33 | pip install -q -r requirements.txt 34 | 35 | # Check if image is provided 36 | if [ -z "$1" ]; then 37 | echo "❌ Error: No image provided!" 38 | echo "" 39 | echo "Usage: ./generate.sh [output_dir] [step]" 40 | echo "" 41 | echo "Examples:" 42 | echo " ./generate.sh my_face.jpg" 43 | echo " ./generate.sh my_face.jpg ./faces" 44 | echo " ./generate.sh my_face.jpg ./faces 2.5" 45 | exit 1 46 | fi 47 | 48 | IMAGE=$1 49 | OUTPUT=${2:-./out} 50 | STEP=${3:-3} 51 | 52 | # Check if image exists 53 | if [ ! -f "$IMAGE" ]; then 54 | echo "❌ Error: Image not found: $IMAGE" 55 | exit 1 56 | fi 57 | 58 | echo "" 59 | echo "⚙️ Configuration:" 60 | echo " Image: $IMAGE" 61 | echo " Output: $OUTPUT" 62 | echo " Step: $STEP" 63 | echo "" 64 | 65 | # Calculate number of images 66 | IMAGES=$(echo "scale=0; ((15 - (-15)) / $STEP + 1) ^ 2" | bc) 67 | echo "📊 Will generate approximately $IMAGES images" 68 | echo "" 69 | 70 | # Confirm 71 | read -p "Continue? (y/n) " -n 1 -r 72 | echo 73 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 74 | echo "Cancelled." 75 | exit 0 76 | fi 77 | 78 | # Run generation 79 | echo "" 80 | echo "🚀 Starting generation..." 81 | echo "" 82 | 83 | python main.py \ 84 | --image "$IMAGE" \ 85 | --out "$OUTPUT" \ 86 | --step "$STEP" \ 87 | --skip-existing 88 | 89 | echo "" 90 | echo "✅ Done! Images saved to: $OUTPUT" 91 | echo "" 92 | echo "📋 Next steps:" 93 | echo " 1. Copy faces to your React project:" 94 | echo " cp -r $OUTPUT /path/to/your-react-app/public/faces" 95 | echo "" 96 | echo " 2. Copy React components:" 97 | echo " cp useGazeTracking.js /path/to/your-react-app/src/hooks/" 98 | echo " cp FaceTracker.jsx /path/to/your-react-app/src/components/" 99 | echo "" 100 | echo " 3. Use in your app:" 101 | echo " import FaceTracker from './components/FaceTracker'" 102 | echo "" 103 | 104 | -------------------------------------------------------------------------------- /useGazeTracking.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react'; 2 | 3 | // Grid configuration (must match your generation parameters) 4 | const P_MIN = -15; 5 | const P_MAX = 15; 6 | const STEP = 3; 7 | const SIZE = 256; 8 | 9 | /** 10 | * Converts normalized coordinates [-1, 1] to grid coordinates 11 | */ 12 | function quantizeToGrid(val) { 13 | const raw = P_MIN + (val + 1) * (P_MAX - P_MIN) / 2; // [-1,1] -> [-15,15] 14 | const snapped = Math.round(raw / STEP) * STEP; 15 | return Math.max(P_MIN, Math.min(P_MAX, snapped)); 16 | } 17 | 18 | /** 19 | * Converts grid coordinates to filename format 20 | */ 21 | function gridToFilename(px, py) { 22 | const sanitize = (val) => val.toString().replace('-', 'm').replace('.', 'p'); 23 | return `gaze_px${sanitize(px)}_py${sanitize(py)}_${SIZE}.webp`; 24 | } 25 | 26 | /** 27 | * Custom hook for gaze tracking 28 | * @param {React.RefObject} containerRef - Reference to the container element 29 | * @param {string} basePath - Base path to face images (default: '/faces/') 30 | * @returns {Object} { currentImage, isLoading, error } 31 | */ 32 | export function useGazeTracking(containerRef, basePath = '/faces/') { 33 | const [currentImage, setCurrentImage] = useState(null); 34 | const [isLoading, setIsLoading] = useState(false); 35 | const [error, setError] = useState(null); 36 | 37 | const updateGaze = useCallback((clientX, clientY) => { 38 | if (!containerRef.current) return; 39 | 40 | const rect = containerRef.current.getBoundingClientRect(); 41 | const centerX = rect.left + rect.width / 2; 42 | const centerY = rect.top + rect.height / 2; 43 | 44 | // Convert to normalized coordinates [-1, 1] 45 | const nx = (clientX - centerX) / (rect.width / 2); 46 | const ny = (clientY - centerY) / (rect.height / 2); 47 | 48 | // Clamp to [-1, 1] range 49 | const clampedX = Math.max(-1, Math.min(1, nx)); 50 | const clampedY = Math.max(-1, Math.min(1, ny)); 51 | 52 | // Convert to grid coordinates 53 | const px = quantizeToGrid(clampedX); 54 | const py = quantizeToGrid(clampedY); 55 | 56 | // Generate filename 57 | const filename = gridToFilename(px, py); 58 | const imagePath = `${basePath}${filename}`; 59 | 60 | setCurrentImage(imagePath); 61 | }, [basePath]); 62 | 63 | const handleMouseMove = useCallback((e) => { 64 | updateGaze(e.clientX, e.clientY); 65 | }, [updateGaze]); 66 | 67 | const handleTouchMove = useCallback((e) => { 68 | if (e.touches.length > 0) { 69 | const touch = e.touches[0]; 70 | updateGaze(touch.clientX, touch.clientY); 71 | } 72 | }, [updateGaze]); 73 | 74 | useEffect(() => { 75 | const container = containerRef.current; 76 | if (!container) return; 77 | 78 | // Add event listeners 79 | container.addEventListener('mousemove', handleMouseMove); 80 | container.addEventListener('touchmove', handleTouchMove, { passive: true }); 81 | 82 | // Set initial center gaze 83 | const rect = container.getBoundingClientRect(); 84 | const centerX = rect.left + rect.width / 2; 85 | const centerY = rect.top + rect.height / 2; 86 | updateGaze(centerX, centerY); 87 | 88 | return () => { 89 | container.removeEventListener('mousemove', handleMouseMove); 90 | container.removeEventListener('touchmove', handleTouchMove); 91 | }; 92 | }, [handleMouseMove, handleTouchMove, updateGaze]); 93 | 94 | return { currentImage, isLoading, error }; 95 | } 96 | 97 | export default useGazeTracking; 98 | 99 | -------------------------------------------------------------------------------- /QUICKSTART.md: -------------------------------------------------------------------------------- 1 | # ⚡ Quick Start Guide 2 | 3 | Get up and running with Face Looker in 5 minutes! 4 | 5 | ## 🎯 Goal 6 | 7 | Create an interactive face that follows your cursor in a React app. 8 | 9 | ## 📝 Prerequisites 10 | 11 | - [ ] Python 3.7+ installed 12 | - [ ] Node.js installed (for React) 13 | - [ ] Replicate account (free tier works!) 14 | - [ ] A 512×512 photo of a face 15 | 16 | ## 🚀 Steps 17 | 18 | ### 1. Get Your Replicate API Token (2 minutes) 19 | 20 | 1. Go to https://replicate.com/ 21 | 2. Sign up (free) 22 | 3. Go to https://replicate.com/account/api-tokens 23 | 4. Copy your API token 24 | 5. Save it: 25 | 26 | ```bash 27 | export REPLICATE_API_TOKEN=your_token_here 28 | ``` 29 | 30 | ### 2. Clone and Setup (1 minute) 31 | 32 | ```bash 33 | # Clone the repo 34 | git clone https://github.com/yourusername/face_looker.git 35 | cd face_looker 36 | 37 | # Quick setup (auto-installs dependencies) 38 | ./generate.sh my_face.jpg 39 | ``` 40 | 41 | Or manual setup: 42 | 43 | ```bash 44 | python -m venv .venv 45 | source .venv/bin/activate 46 | pip install -r requirements.txt 47 | ``` 48 | 49 | ### 3. Generate Face Images (2-5 minutes) 50 | 51 | **Automatic (recommended):** 52 | ```bash 53 | ./generate.sh my_face.jpg 54 | ``` 55 | 56 | **Manual:** 57 | ```bash 58 | python main.py --image my_face.jpg --out ./out 59 | ``` 60 | 61 | ⏰ This takes 2-5 minutes depending on your settings. Grab a coffee! 62 | 63 | ### 4. Use in React (30 seconds) 64 | 65 | ```bash 66 | # Copy faces to your React project 67 | cp -r ./out /path/to/your-react-app/public/faces 68 | 69 | # Copy React files 70 | cp useGazeTracking.js /path/to/your-react-app/src/hooks/ 71 | cp FaceTracker.jsx /path/to/your-react-app/src/components/ 72 | cp FaceTracker.css /path/to/your-react-app/src/components/ 73 | ``` 74 | 75 | ### 5. Add to Your App (30 seconds) 76 | 77 | ```jsx 78 | import FaceTracker from './components/FaceTracker'; 79 | 80 | function App() { 81 | return ( 82 |
83 | 84 |
85 | ); 86 | } 87 | ``` 88 | 89 | ## ✅ Done! 90 | 91 | Your face should now follow the cursor! 🎉 92 | 93 | ## 🎛️ Customization 94 | 95 | ### Smoother transitions (more images): 96 | ```bash 97 | python main.py --image my_face.jpg --out ./out --step 2.5 98 | ``` 99 | 100 | ### Faster generation (fewer images): 101 | ```bash 102 | python main.py --image my_face.jpg --out ./out --step 5 103 | ``` 104 | 105 | ### Circular face: 106 | ```css 107 | .face-tracker { 108 | border-radius: 50%; 109 | overflow: hidden; 110 | } 111 | ``` 112 | 113 | ## 🐛 Troubleshooting 114 | 115 | ### "REPLICATE_API_TOKEN not set" 116 | ```bash 117 | export REPLICATE_API_TOKEN=your_actual_token 118 | ``` 119 | 120 | ### Images not loading in React 121 | - Check that faces are in `public/faces/` 122 | - Verify `basePath="/faces/"` matches your structure 123 | - Check browser console for 404 errors 124 | 125 | ### Face not following cursor 126 | - Click on the face area first (might need focus) 127 | - Check that configuration matches in `useGazeTracking.js` 128 | - Verify all images generated successfully 129 | 130 | ## 📚 Next Steps 131 | 132 | - Read the full [README.md](./README.md) for advanced usage 133 | - Check [examples/](./examples/) for more implementations 134 | - Customize styling in `FaceTracker.css` 135 | - Try different grid densities 136 | 137 | ## 💰 Cost 138 | 139 | - Default settings: ~$0.01 (121 images) 140 | - Smoother (step 2.5): ~$0.02 (169 images) 141 | - Free tier usually covers initial testing! 142 | 143 | ## 🆘 Need Help? 144 | 145 | - Check the [README.md](./README.md) 146 | - Open an issue on GitHub 147 | - See [CONTRIBUTING.md](./CONTRIBUTING.md) 148 | 149 | --- 150 | 151 | **Estimated total time:** 5-10 minutes 152 | **Cost:** ~$0.01-0.02 153 | **Difficulty:** Easy ⭐ 154 | 155 | Ready? Let's go! 🚀 156 | 157 | -------------------------------------------------------------------------------- /generate_missing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Generate only the missing face images for the portfolio. 4 | This script identifies which images are missing and generates only those. 5 | """ 6 | 7 | import os 8 | import sys 9 | from pathlib import Path 10 | from main import build_gaze_grid, filename_for, run_expression_editor, save_resized_webp, is_url 11 | 12 | def find_missing_images(portfolio_dir, vmin=-15, vmax=15, step=2.5, size=256): 13 | """Find which images are missing from the portfolio directory.""" 14 | portfolio_path = Path(portfolio_dir) 15 | if not portfolio_path.exists(): 16 | print(f"ERROR: Portfolio directory not found: {portfolio_path}") 17 | return [] 18 | 19 | # Generate expected filenames for step=2.5 20 | grid = build_gaze_grid(vmin, vmax, step) 21 | missing_files = [] 22 | 23 | print(f"Checking {len(grid)} expected files in {portfolio_path}") 24 | 25 | for gp in grid: 26 | fname = filename_for(gp.px, gp.py, size) 27 | file_path = portfolio_path / fname 28 | if not file_path.exists(): 29 | missing_files.append((gp.px, gp.py, fname)) 30 | print(f" MISSING: {fname}") 31 | else: 32 | print(f" EXISTS: {fname}") 33 | 34 | return missing_files 35 | 36 | def generate_missing_images(image_path, portfolio_dir, vmin=-15, vmax=15, step=2.5, size=256): 37 | """Generate only the missing images.""" 38 | 39 | # Check for API token 40 | if not os.getenv("REPLICATE_API_TOKEN"): 41 | print("ERROR: REPLICATE_API_TOKEN not set in environment.") 42 | print("Please set your Replicate API token:") 43 | print("export REPLICATE_API_TOKEN=your_token_here") 44 | return False 45 | 46 | # Find missing images 47 | print("🔍 Scanning for missing images...") 48 | missing = find_missing_images(portfolio_dir, vmin, vmax, step, size) 49 | 50 | if not missing: 51 | print("✅ All images are present! No missing files found.") 52 | return True 53 | 54 | print(f"📊 Found {len(missing)} missing images out of {13*13} total expected.") 55 | print("Missing images:") 56 | for _, _, fname in missing: 57 | print(f" - {fname}") 58 | 59 | # Prepare image input for Replicate 60 | if is_url(image_path): 61 | image_input = image_path 62 | else: 63 | p = Path(image_path) 64 | if not p.exists(): 65 | print(f"ERROR: Image path not found: {p}") 66 | return False 67 | image_input = open(p, "rb") 68 | 69 | # Generate missing images 70 | print(f"\n🎨 Generating {len(missing)} missing images...") 71 | success_count = 0 72 | 73 | for i, (px, py, fname) in enumerate(missing, 1): 74 | print(f"[{i}/{len(missing)}] Generating {fname} (px={px}, py={py})...") 75 | 76 | try: 77 | output_files = run_expression_editor(image_input, px, py) 78 | if not output_files: 79 | print(f" ⚠️ WARNING: No output for ({px}, {py}). Skipping.") 80 | continue 81 | 82 | # Save to portfolio directory 83 | target_path = Path(portfolio_dir) / fname 84 | save_resized_webp(output_files[0], target_path, size=size, quality=95) 85 | success_count += 1 86 | print(f" ✅ Saved: {fname}") 87 | 88 | except Exception as e: 89 | print(f" ❌ ERROR generating {fname}: {e}") 90 | continue 91 | 92 | print(f"\n🎉 Generation complete!") 93 | print(f"✅ Successfully generated: {success_count}/{len(missing)} images") 94 | print(f"📁 Images saved to: {portfolio_dir}") 95 | 96 | return success_count == len(missing) 97 | 98 | def main(): 99 | """Main function to run the missing image generation.""" 100 | import argparse 101 | 102 | parser = argparse.ArgumentParser(description="Generate missing face images for portfolio") 103 | parser.add_argument("--image", default="./my_face.jpg", help="Path to source image") 104 | parser.add_argument("--portfolio", default="/Users/kylanoconnor/Desktop/projects/kylan_portfolio_site/public/faces", help="Portfolio faces directory") 105 | parser.add_argument("--min", dest="vmin", type=float, default=-15.0, help="Minimum value for pupil_x/y") 106 | parser.add_argument("--max", dest="vmax", type=float, default=15.0, help="Maximum value for pupil_x/y") 107 | parser.add_argument("--step", type=float, default=2.5, help="Step size for grid sampling") 108 | parser.add_argument("--size", type=int, default=256, help="Resize dimension for outputs") 109 | 110 | args = parser.parse_args() 111 | 112 | print("🚀 Missing Face Image Generator") 113 | print("=" * 50) 114 | print(f"📸 Source image: {args.image}") 115 | print(f"📁 Portfolio dir: {args.portfolio}") 116 | print(f"📐 Grid: {args.vmin} to {args.vmax}, step {args.step}") 117 | print(f"🖼️ Size: {args.size}x{args.size}") 118 | print() 119 | 120 | success = generate_missing_images( 121 | args.image, 122 | args.portfolio, 123 | args.vmin, 124 | args.vmax, 125 | args.step, 126 | args.size 127 | ) 128 | 129 | if success: 130 | print("\n🎯 All missing images generated successfully!") 131 | sys.exit(0) 132 | else: 133 | print("\n⚠️ Some images failed to generate. Check the errors above.") 134 | sys.exit(1) 135 | 136 | if __name__ == "__main__": 137 | main() 138 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env python3 3 | """ 4 | Generate a grid of gaze images using Replicate's fofr/expression-editor model, 5 | varying only pupil_x and pupil_y (range: -15..15), then resize outputs to 256px. 6 | Outputs are saved as WebP files with easy-to-parse filenames and a CSV index. 7 | 8 | Usage examples: 9 | export REPLICATE_API_TOKEN=your_token_here 10 | pip install replicate pillow tqdm 11 | python main.py --image ./me_512.jpg --out ./out --min -15 --max 15 --step 3 12 | 13 | Notes: 14 | - Only pupil_x and pupil_y are changed. All other model inputs use defaults. 15 | - Input image can be a local file path or a URL. 16 | - Each output is resized to 256x256 using Lanczos filter. 17 | - A CSV index (index.csv) mapping filenames to (pupil_x,pupil_y) is created. 18 | """ 19 | 20 | import argparse 21 | import io 22 | import math 23 | import os 24 | import sys 25 | from dataclasses import dataclass 26 | from pathlib import Path 27 | from typing import Iterable, Tuple, List 28 | 29 | import replicate 30 | from PIL import Image 31 | from tqdm import tqdm 32 | 33 | MODEL_VERSION = "bf913bc90e1c44ba288ba3942a538693b72e8cc7df576f3beebe56adc0a92b86" 34 | 35 | @dataclass(frozen=True) 36 | class GazePoint: 37 | px: float 38 | py: float 39 | 40 | def frange(start: float, stop: float, step: float) -> Iterable[float]: 41 | """Like range() but for floats, inclusive of stop when it lands exactly.""" 42 | x = start 43 | # Protect against infinite loops from improper step sign 44 | if step == 0: 45 | raise ValueError("step must be non-zero") 46 | cmp = (lambda a, b: a <= b) if step > 0 else (lambda a, b: a >= b) 47 | while cmp(x, stop + (1e-9 if step > 0 else -1e-9)): 48 | yield round(x, 6) # avoid ugly float strings 49 | x += step 50 | 51 | def clamp(v: float, lo: float, hi: float) -> float: 52 | return max(lo, min(hi, v)) 53 | 54 | def build_gaze_grid(xmin: float, xmax: float, ymin: float, ymax: float, step: float) -> List[GazePoint]: 55 | """Build a grid of (pupil_x, pupil_y) with separate ranges for X and Y.""" 56 | xs = list(frange(xmin, xmax, step)) 57 | ys = list(frange(ymin, ymax, step)) 58 | grid = [GazePoint(px=x, py=y) for y in ys for x in xs] 59 | return grid 60 | 61 | def is_url(s: str) -> bool: 62 | return s.startswith("http://") or s.startswith("https://") or s.startswith("data:") 63 | 64 | def run_expression_editor(image_input, pupil_x: float, pupil_y: float): 65 | """ 66 | Calls Replicate model with pupil_x, pupil_y, rotate_pitch, and rotate_yaw adjusted. 67 | Returns a list of file-like objects per Replicate SDK (we expect 1). 68 | """ 69 | # Clamp pupil values 70 | clamped_px = float(clamp(pupil_x, -15, 15)) 71 | clamped_py = float(clamp(pupil_y, -15, 15)) 72 | 73 | # Add pitch and yaw based on pupil position for more exaggerated look 74 | # Scale factor: convert pupil range (-15 to 15) to rotation range 75 | # Yaw follows horizontal pupil movement (left/right) 76 | rotate_yaw = (clamped_px / 15.0) * 10.0 # Max ±10 degrees 77 | # Pitch follows vertical pupil movement (up/down) - INVERTED 78 | rotate_pitch = -(clamped_py / 15.0) * 10.0 # Max ±10 degrees, inverted 79 | 80 | input_payload = { 81 | "image": image_input, 82 | "pupil_x": clamped_px, 83 | "pupil_y": clamped_py, 84 | "rotate_yaw": float(clamp(rotate_yaw, -20, 20)), 85 | "rotate_pitch": float(clamp(rotate_pitch, -20, 20)), 86 | } 87 | return replicate.run( 88 | f"fofr/expression-editor:{MODEL_VERSION}", 89 | input=input_payload 90 | ) 91 | 92 | def save_resized_webp(file_like, out_path: Path, size: int = 256, quality: int = 95): 93 | """ 94 | Read the webp bytes from Replicate, resize to square `size`, and save as webp. 95 | """ 96 | raw = file_like.read() 97 | img = Image.open(io.BytesIO(raw)).convert("RGBA") 98 | img = img.resize((size, size), Image.Resampling.LANCZOS) 99 | out_path.parent.mkdir(parents=True, exist_ok=True) 100 | img.save(out_path, format="WEBP", quality=quality, method=6) 101 | 102 | def sanitize_val(v: float) -> str: 103 | """ 104 | Make a safe string piece for filename from a float, preserving sign and major decimals. 105 | E.g., -12.5 -> "m12p5"; 3 -> "3"; 0.0 -> "0" 106 | """ 107 | s = f"{v}".replace("-", "m").replace(".", "p") 108 | return s 109 | 110 | def filename_for(px: float, py: float, size: int = 256) -> str: 111 | """Create consistent filename embedding gaze values and size.""" 112 | return f"gaze_px{sanitize_val(px)}_py{sanitize_val(py)}_{size}.webp" 113 | 114 | def write_index(csv_path: Path, rows: List[Tuple[str, float, float]]): 115 | csv_path.parent.mkdir(parents=True, exist_ok=True) 116 | with csv_path.open("w", encoding="utf-8") as f: 117 | f.write("filename,pupil_x,pupil_y\n") 118 | for name, px, py in rows: 119 | f.write(f"{name},{px},{py}\n") 120 | 121 | def main(): 122 | parser = argparse.ArgumentParser(description="Generate gaze images with varying pupil_x/y using Replicate.") 123 | parser.add_argument("--image", required=True, help="Path to local image (512x512 recommended) or URL") 124 | parser.add_argument("--out", default="./out", help="Output directory") 125 | parser.add_argument("--xmin", type=float, default=-15.0, help="Minimum value for pupil_x (left)") 126 | parser.add_argument("--xmax", type=float, default=15.0, help="Maximum value for pupil_x (right)") 127 | parser.add_argument("--ymin", type=float, default=-15.0, help="Minimum value for pupil_y (down)") 128 | parser.add_argument("--ymax", type=float, default=15.0, help="Maximum value for pupil_y (up)") 129 | parser.add_argument("--step", type=float, default=3.0, help="Step size for grid sampling") 130 | parser.add_argument("--size", type=int, default=256, help="Resize dimension (square) for outputs") 131 | parser.add_argument("--skip-existing", action="store_true", help="Skip generation if target file already exists") 132 | args = parser.parse_args() 133 | 134 | # Validate token presence early 135 | if not os.getenv("REPLICATE_API_TOKEN"): 136 | print("ERROR: REPLICATE_API_TOKEN not set in environment.", file=sys.stderr) 137 | sys.exit(1) 138 | 139 | out_dir = Path(args.out) 140 | out_dir.mkdir(parents=True, exist_ok=True) 141 | 142 | # Prepare image input for Replicate 143 | if is_url(args.image): 144 | image_input = args.image 145 | else: 146 | p = Path(args.image) 147 | if not p.exists(): 148 | print(f"ERROR: image path not found: {p}", file=sys.stderr) 149 | sys.exit(1) 150 | image_input = open(p, "rb") 151 | 152 | # Build grid and iterate 153 | grid = build_gaze_grid(args.xmin, args.xmax, args.ymin, args.ymax, args.step) 154 | index_rows: List[Tuple[str, float, float]] = [] 155 | 156 | for gp in tqdm(grid, desc="Generating", unit="img"): 157 | fname = filename_for(gp.px, gp.py, args.size) 158 | target = out_dir / fname 159 | if args.skip_existing and target.exists(): 160 | index_rows.append((fname, gp.px, gp.py)) 161 | continue 162 | 163 | try: 164 | output_files = run_expression_editor(image_input, gp.px, gp.py) 165 | if not output_files: 166 | print(f"WARNING: No output for ({gp.px}, {gp.py}). Skipping.", file=sys.stderr) 167 | continue 168 | 169 | # Replicate returns a list; we take first 170 | save_resized_webp(output_files[0], target, size=args.size, quality=95) 171 | index_rows.append((fname, gp.px, gp.py)) 172 | except Exception as e: 173 | print(f"ERROR generating ({gp.px}, {gp.py}): {e}", file=sys.stderr) 174 | 175 | # Write CSV index 176 | write_index(out_dir / "index.csv", index_rows) 177 | 178 | # Helpful note for selecting nearest gaze later 179 | print(f"Done. Wrote {len(index_rows)} images to {out_dir.resolve()} and index.csv") 180 | 181 | if __name__ == "__main__": 182 | main() 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Face Looker 👁️ 2 | 3 | 4 | 5 | https://github.com/user-attachments/assets/c8684ae4-3009-47cb-8ad4-0e7af65a608f 6 | 7 | 8 | 9 | Generate a grid of face images with different gaze directions using AI, then use them to create an interactive face that follows the user's cursor in real-time. 10 | 11 | ![Demo](demo.gif) 12 | 13 | ## 🎯 Overview 14 | 15 | This project has two parts: 16 | 1. **Generate Face Images** - Create hundreds of face images looking in different directions using either: 17 | - **Replicate Model** (easiest) - Upload your image and get everything you need in one click 18 | - **Python Script** (advanced) - More control over generation parameters 19 | 2. **React Hook** - Makes those images interactive by displaying the right image based on cursor position 20 | 21 | Perfect for creating engaging portfolio headers, interactive avatars, or fun UI elements! 22 | 23 | ## 📋 Prerequisites 24 | 25 | - A 512×512 photo of a face (your photo or any portrait) 26 | - Node.js (for React implementation) 27 | - [Replicate API account](https://replicate.com/) (free tier available) 28 | 29 | ## 🚀 Part 1: Generate Face Images 30 | 31 | You have two options to generate the face images: 32 | 33 | ### Option 1: Use the Replicate Model (Easiest) ✨ 34 | 35 | The easiest way to get started is using the dedicated [Replicate model](https://replicate.com/kylan02/face-looker) created in collaboration with [fofr](https://x.com/fofrAI). Simply upload your face image and it will automatically generate: 36 | 37 | - ✅ All the face images looking in different directions 38 | - ✅ A sprite/grid sheet that combines all faces into a single image 39 | - ✅ A ZIP file containing vanilla HTML, JS and CSS to render the effect 40 | - ✅ A preview video showing how the animation will look 41 | 42 | **To use it:** 43 | 1. Visit [https://replicate.com/kylan02/face-looker](https://replicate.com/kylan02/face-looker) 44 | 2. Upload your 512×512 face image 45 | 3. Download the generated ZIP file 46 | 4. Extract and use the images/code in your project 47 | 48 | The model automatically uses the Expression Editor model to generate all the images you need. See the [model page](https://replicate.com/kylan02/face-looker) for more details. 49 | 50 | ### Option 2: Use the Python Script (Advanced) 🔧 51 | 52 | If you prefer more control over the generation process or want to customize the parameters, you can use the Python script described below. 53 | 54 | --- 55 | 56 | ### Step 1: Setup 57 | 58 | ```bash 59 | # Clone or download this repository 60 | cd face_looker 61 | 62 | # Create a virtual environment (recommended) 63 | python -m venv .venv 64 | source .venv/bin/activate # On Windows: .venv\Scripts\activate 65 | 66 | # Install dependencies 67 | pip install -r requirements.txt 68 | ``` 69 | 70 | ### Step 2: Get Your Replicate API Token 71 | 72 | 1. Sign up at [replicate.com](https://replicate.com/) 73 | 2. Go to your [account settings](https://replicate.com/account/api-tokens) 74 | 3. Copy your API token 75 | 4. Set it as an environment variable: 76 | 77 | ```bash 78 | export REPLICATE_API_TOKEN=your_token_here 79 | ``` 80 | 81 | Or add it to your `.bashrc`/`.zshrc` for permanent use: 82 | 83 | ```bash 84 | echo 'export REPLICATE_API_TOKEN=your_token_here' >> ~/.zshrc 85 | source ~/.zshrc 86 | ``` 87 | 88 | ### Step 3: Prepare Your Face Image 89 | 90 | **Image Requirements:** 91 | - Use a **512×512 pixel** image for best results 92 | - Face should be centered and looking straight ahead 93 | - Good lighting, clear features 94 | - Neutral expression works best 95 | 96 | **Where to put your image:** 97 | 1. **Any filename works!** You can use any name like `me.jpg`, `portrait.png`, `selfie.jpeg`, etc. 98 | 2. **Any location works!** You can put it in the project folder or anywhere on your computer 99 | 3. **Example file structure:** 100 | ``` 101 | face_looker/ 102 | ├── main.py 103 | ├── my_face.jpg ← Your image can go here 104 | ├── requirements.txt 105 | └── README.md 106 | ``` 107 | 108 | **Or anywhere else:** 109 | ``` 110 | /Users/you/Pictures/portrait.png 111 | /path/to/any/folder/selfie.jpg 112 | ``` 113 | 114 | **Before running the script, make sure you have:** 115 | - ✅ Set your `REPLICATE_API_TOKEN` environment variable 116 | - ✅ Activated your virtual environment (if using one) 117 | - ✅ Installed dependencies with `pip install -r requirements.txt` 118 | - ✅ Your face image is in the project directory 119 | 120 | ### Step 4: Generate Your Face Grid 121 | 122 | **📊 How many images will be created?** 123 | 124 | The default settings create **121 images** (11×11 grid), but you can customize this: 125 | 126 | | Step Size | Grid Size | Total Images | Generation Time | Smoothness | 127 | |-----------|-----------|--------------|-----------------|------------| 128 | | `--step 5` | 7×7 | **49 images** | ~2-3 minutes | Basic | 129 | | `--step 3` (default) | 11×11 | **121 images** | ~5-8 minutes | Good | 130 | | `--step 2.5` | 13×13 | **169 images** | ~8-12 minutes | Smooth | 131 | | `--step 2` | 16×16 | **256 images** | ~12-18 minutes | Very smooth | 132 | 133 | **Basic usage:** 134 | ```bash 135 | # Any filename works! 136 | python main.py --image ./my_face.jpg --out ./out 137 | python main.py --image ./portrait.png --out ./out 138 | python main.py --image /Users/you/Pictures/selfie.jpg --out ./out 139 | ``` 140 | 141 | **Custom grid density:** 142 | ```bash 143 | # Smoother transitions (more images, longer generation time) 144 | python main.py --image ./my_face.jpg --out ./out --step 2.5 145 | 146 | # Faster generation (fewer images, less smooth) 147 | python main.py --image ./my_face.jpg --out ./out --step 5 148 | ``` 149 | 150 | **Full options:** 151 | ```bash 152 | python main.py \ 153 | --image ./my_face.jpg \ # Input image path or URL 154 | --out ./out \ # Output directory 155 | --min -15 \ # Minimum gaze value 156 | --max 15 \ # Maximum gaze value 157 | --step 3 \ # Step size (smaller = more images) 158 | --size 256 \ # Output image size (256x256) 159 | --skip-existing # Skip already generated images 160 | ``` 161 | 162 | ### Understanding the Parameters 163 | 164 | | Parameter | Default | Description | 165 | |-----------|---------|-------------| 166 | | `--min` | -15 | Minimum pupil position (left/up) | 167 | | `--max` | 15 | Maximum pupil position (right/down) | 168 | | `--step` | 3 | Grid spacing (smaller = smoother, more images) | 169 | | `--size` | 256 | Output image dimensions (256×256) | 170 | 171 | **Image count formula:** `((max - min) / step + 1)²` 172 | 173 | Examples: 174 | - `step=3` (default): 121 images (11×11 grid) 175 | - `step=2.5`: 169 images (13×13 grid) 176 | - `step=5`: 49 images (7×7 grid) 177 | 178 | ### Step 5: Output 179 | 180 | The script generates: 181 | - **Images:** `gaze_px{X}_py{Y}_256.webp` files 182 | - Example: `gaze_px0_py0_256.webp` (looking at center) 183 | - Example: `gaze_px15_py0_256.webp` (looking right) 184 | - Example: `gaze_px0_pym15_256.webp` (looking up) 185 | - **CSV Index:** `index.csv` mapping filenames to coordinates 186 | 187 | ``` 188 | out/ 189 | ├── gaze_px-15_py-15_256.webp 190 | ├── gaze_px-15_py-12_256.webp 191 | ├── ... 192 | ├── gaze_px15_py15_256.webp 193 | └── index.csv 194 | ``` 195 | 196 | ## 🎨 Part 2: React Implementation 197 | 198 | ### Setup in Your React Project 199 | 200 | 1. **Copy the generated faces to your public folder:** 201 | 202 | ```bash 203 | # Copy all face images to your React public folder 204 | cp -r ./out/faces /path/to/your-react-app/public/faces 205 | ``` 206 | 207 | 2. **Copy the React files to your project:** 208 | 209 | ```bash 210 | # Copy the hook and component 211 | cp useGazeTracking.js /path/to/your-react-app/src/hooks/ 212 | cp FaceTracker.jsx /path/to/your-react-app/src/components/ 213 | cp FaceTracker.css /path/to/your-react-app/src/components/ 214 | ``` 215 | 216 | ### Basic Usage 217 | 218 | ```jsx 219 | import FaceTracker from './components/FaceTracker'; 220 | 221 | function App() { 222 | return ( 223 |
224 |

My Portfolio

225 | 226 | {/* Basic usage */} 227 | 228 | 229 | {/* With custom styling */} 230 | 234 |
235 | ); 236 | } 237 | ``` 238 | 239 | ### Advanced Usage 240 | 241 | ```jsx 242 | import FaceTracker from './components/FaceTracker'; 243 | 244 | function Header() { 245 | return ( 246 |
252 |
253 | 257 |
258 |
259 | ); 260 | } 261 | ``` 262 | 263 | ### Using the Hook Directly 264 | 265 | For more control, use the `useGazeTracking` hook directly: 266 | 267 | ```jsx 268 | import { useRef, useEffect } from 'react'; 269 | import { useGazeTracking } from './hooks/useGazeTracking'; 270 | 271 | function CustomFaceComponent() { 272 | const containerRef = useRef(null); 273 | const { currentImage, isLoading, error } = useGazeTracking( 274 | containerRef, 275 | '/faces/' 276 | ); 277 | 278 | return ( 279 |
283 | {currentImage && ( 284 | Following face 294 | )} 295 | {isLoading &&

Loading...

} 296 | {error &&

Error: {error.message}

} 297 |
298 | ); 299 | } 300 | ``` 301 | 302 | ### Configuration 303 | 304 | If you change the generation parameters, update these constants in `useGazeTracking.js`: 305 | 306 | ```javascript 307 | // Must match your generation parameters! 308 | const P_MIN = -15; // Same as --min 309 | const P_MAX = 15; // Same as --max 310 | const STEP = 3; // Same as --step 311 | const SIZE = 256; // Same as --size 312 | ``` 313 | 314 | ## 🎛️ Customization 315 | 316 | ### Changing Image Directory 317 | 318 | ```jsx 319 | 320 | ``` 321 | 322 | ### Adding Custom Styling 323 | 324 | ```css 325 | /* FaceTracker.css */ 326 | .face-tracker { 327 | width: 100%; 328 | height: 100%; 329 | position: relative; 330 | overflow: hidden; 331 | background: #f0f0f0; 332 | border-radius: 50%; /* Circular face */ 333 | box-shadow: 0 10px 30px rgba(0,0,0,0.2); 334 | } 335 | 336 | .face-image { 337 | user-select: none; 338 | pointer-events: none; 339 | } 340 | 341 | .face-debug { 342 | position: absolute; 343 | top: 10px; 344 | left: 10px; 345 | background: rgba(0, 0, 0, 0.7); 346 | color: white; 347 | padding: 10px; 348 | border-radius: 5px; 349 | font-family: monospace; 350 | font-size: 12px; 351 | } 352 | ``` 353 | 354 | ### Performance Optimization 355 | 356 | 1. **Preload images** (optional): 357 | ```jsx 358 | useEffect(() => { 359 | // Preload all face images 360 | const images = []; 361 | for (let py = -15; py <= 15; py += 3) { 362 | for (let px = -15; px <= 15; px += 3) { 363 | const img = new Image(); 364 | img.src = `/faces/gaze_px${px}_py${py}_256.webp`; 365 | images.push(img); 366 | } 367 | } 368 | }, []); 369 | ``` 370 | 371 | 2. **Use fewer images** - Generate with larger `--step` value (e.g., `--step 5`) 372 | 373 | ## 📱 Mobile Support 374 | 375 | The component automatically supports touch events! The face will follow finger movement on mobile devices. 376 | 377 | ## 🔧 Troubleshooting 378 | 379 | ### Images not generating? 380 | 381 | **Check your API token:** 382 | ```bash 383 | echo $REPLICATE_API_TOKEN 384 | ``` 385 | 386 | **Verify Replicate credits:** 387 | - Check your account at [replicate.com/account](https://replicate.com/account) 388 | - Free tier includes some credits, but may require payment for large batches 389 | 390 | **Resume interrupted generation:** 391 | ```bash 392 | python main.py --image ./my_face.jpg --out ./out --skip-existing 393 | ``` 394 | 395 | ### Face not following cursor in React? 396 | 397 | 1. **Verify images are in the correct location:** 398 | - Check `public/faces/` directory exists 399 | - Confirm images have correct naming pattern 400 | 401 | 2. **Check browser console for errors:** 402 | - Missing images will show 404 errors 403 | - Path issues will show in network tab 404 | 405 | 3. **Verify configuration matches:** 406 | - `P_MIN`, `P_MAX`, `STEP` in `useGazeTracking.js` must match generation parameters 407 | 408 | ### Performance issues? 409 | 410 | - Generate fewer images (use `--step 5` or higher) 411 | - Reduce image size (use `--size 128`) 412 | - Preload images (see Performance Optimization above) 413 | 414 | ## 📊 Cost Estimation 415 | 416 | Replicate charges per second of GPU time, it should only be a couple of cents. 417 | 418 | Check current pricing at [replicate.com/pricing](https://replicate.com/pricing) 419 | 420 | ## 🎯 Use Cases 421 | 422 | - **Portfolio headers** - Make your about page more engaging 423 | - **Interactive avatars** - Add personality to chat interfaces 424 | - **Product demos** - Draw attention to important elements 425 | - **Educational content** - Create attention-grabbing tutorials 426 | - **Games** - Use as character faces or NPCs 427 | 428 | ## 📝 Examples 429 | 430 | See the [examples](./examples) folder for: 431 | - Full Next.js implementation 432 | - TypeScript version 433 | - Circular face mask 434 | - Multiple faces on one page 435 | 436 | ## 🤝 Contributing 437 | 438 | Contributions welcome! Feel free to: 439 | - Add new features 440 | - Improve documentation 441 | - Share your implementations 442 | - Report bugs 443 | 444 | ## 📄 License 445 | 446 | MIT License - feel free to use in personal and commercial projects! 447 | 448 | ## 🙏 Credits 449 | 450 | - Face generation powered by [Replicate](https://replicate.com/) 451 | - Uses [fofr/expression-editor](https://replicate.com/fofr/expression-editor) model 452 | - Created with ❤️ by Kylan 453 | 454 | ## 🔗 Links 455 | 456 | - [Face Looker Replicate Model](https://replicate.com/kylan02/face-looker) - Generate all images with one click 457 | - [Replicate API Docs](https://replicate.com/docs) 458 | - [Expression Editor Model](https://replicate.com/fofr/expression-editor) 459 | - [Live Demo](https://kylanoconnor.com) 460 | 461 | --- 462 | 463 | **Questions?** Open an issue or contact [@kylancodes](https://x.com/kylancodes) on X 464 | 465 | **Like this project?** Give it a ⭐ on GitHub! 466 | --------------------------------------------------------------------------------