diff --git a/demo-ui/README.md b/demo-ui/README.md deleted file mode 100644 index 0f743b1..0000000 --- a/demo-ui/README.md +++ /dev/null @@ -1,111 +0,0 @@ -# Biometric Processor Demo UI - -A professional Next.js demo application showcasing the Biometric Processor API capabilities. - -## Features - -- **Face Enrollment**: Register new faces with quality validation -- **1:1 Verification**: Compare two faces for identity verification -- **1:N Search**: Search for a face across enrolled database -- **Liveness Detection**: Detect presentation attacks -- **Quality Analysis**: Analyze face image quality metrics -- **Demographics Analysis**: Estimate age, gender, and emotions -- **Real-time Proctoring**: Continuous face monitoring with WebSocket -- **Batch Processing**: Process multiple images efficiently - -## Tech Stack - -- **Framework**: Next.js 14 (App Router) -- **Language**: TypeScript -- **Styling**: TailwindCSS + shadcn/ui -- **State Management**: Zustand -- **Data Fetching**: TanStack Query -- **Testing**: Vitest + Playwright - -## Getting Started - -### Prerequisites - -- Node.js 18.17 or later -- npm 9.0 or later -- Biometric Processor API running on localhost:8001 - -### Installation - -```bash -# Install dependencies -npm install - -# Copy environment file -cp .env.example .env.local - -# Start development server -npm run dev -``` - -Open [http://localhost:3000](http://localhost:3000) to view the demo. - -### Environment Variables - -```env -NEXT_PUBLIC_API_URL=http://localhost:8001 -NEXT_PUBLIC_WS_URL=ws://localhost:8001 -``` - -## Development - -```bash -# Run development server -npm run dev - -# Run type checking -npm run type-check - -# Run linting -npm run lint - -# Format code -npm run format - -# Run unit tests -npm run test - -# Run E2E tests -npm run test:e2e -``` - -## Project Structure - -``` -demo-ui/ -├── src/ -│ ├── app/ # Next.js App Router pages -│ ├── components/ # React components -│ │ ├── ui/ # shadcn/ui components -│ │ ├── layout/ # Layout components -│ │ ├── media/ # Camera/upload components -│ │ └── biometric/ # Biometric-specific components -│ ├── lib/ # Utilities and core logic -│ │ ├── api/ # API client -│ │ ├── store/ # Zustand stores -│ │ └── utils/ # Utility functions -│ ├── hooks/ # Custom React hooks -│ ├── types/ # TypeScript types -│ └── locales/ # i18n translations -├── tests/ # Test files -└── public/ # Static assets -``` - -## Building for Production - -```bash -# Build the application -npm run build - -# Start production server -npm run start -``` - -## License - -MIT diff --git a/docs/1-getting-started/DEMO_USAGE.md b/docs/1-getting-started/DEMO_USAGE.md deleted file mode 100644 index 3c9b86b..0000000 --- a/docs/1-getting-started/DEMO_USAGE.md +++ /dev/null @@ -1,357 +0,0 @@ -# Live Camera Testing Guide - -## Why Web-Based Approach is Complex vs C++ + OpenCV - -### C++ + OpenCV Approach (Simple) -```cpp -VideoCapture cap(0); // Open camera -while(true) { - Mat frame; - cap >> frame; // Get frame - process(frame); // Process it - imshow("window", frame); // Show it - waitKey(1); -} -``` -**Advantages:** -- ✅ Direct camera access -- ✅ Simple loop -- ✅ Immediate display -- ✅ No network latency -- ✅ No security restrictions - ---- - -### Web-Based Approach (Complex) - -```javascript -// 1. Request camera permission (can be denied) -navigator.mediaDevices.getUserMedia({video: true}) - -// 2. Convert frame to blob -canvas.toBlob((blob) => { - // 3. Convert blob to base64 - reader.readAsDataURL(blob) - - // 4. Send over WebSocket - ws.send(JSON.stringify({type: 'frame', data: base64})) -}) - -// 5. Receive result -ws.onmessage = (msg) => { - // 6. Parse JSON - const result = JSON.parse(msg.data) - - // 7. Update UI - updateDisplay(result) -} -``` - -**Challenges:** -- ❌ Browser security (HTTPS required, permissions) -- ❌ JavaScript bundling and caching issues -- ❌ WebSocket protocol complexity (JSON, base64 encoding) -- ❌ Network latency (client → server → client) -- ❌ Browser differences (Chrome, Firefox, Safari) -- ❌ CORS, CSP, and other security policies -- ❌ Build system complexity (Next.js, Webpack, etc.) - ---- - -## Solution: Simple Python Client (Like C++ Version!) - -I've created `test_live_camera_simple.py` which works **exactly like your C++ + OpenCV version**! - -### Quick Start - -```bash -# 1. Install dependencies -pip install opencv-python websockets numpy - -# 2. Start the API server (in one terminal) -python -m uvicorn app.main:app --host 0.0.0.0 --port 8001 - -# 3. Run the simple client (in another terminal) -python test_live_camera_simple.py -``` - -**That's it!** A window opens showing your camera with real-time analysis overlay! - ---- - -## Simple Client Features - -### Visual Overlay -- ✅ **FPS counter** (real-time performance) -- ✅ **Frame counters** (sent vs processed) -- ✅ **Quality score** with pass/fail indicator -- ✅ **Face bounding box** with confidence -- ✅ **Liveness detection** (live/spoof) -- ✅ **Demographics** (age, gender, emotion) - optional -- ✅ **Enrollment ready indicator** - optional - -### Keyboard Controls -- `q` - Quit -- `s` - Take screenshot -- `f` - Toggle FPS display - -### Command-Line Options - -```bash -# Use deployed API -python test_live_camera_simple.py \ - --url wss://biometric-api-902542798396.europe-west1.run.app/api/v1/ws/live-analysis - -# Use different camera (e.g., USB webcam) -python test_live_camera_simple.py --camera 1 - -# Change analysis mode -python test_live_camera_simple.py --mode liveness -python test_live_camera_simple.py --mode demographics -python test_live_camera_simple.py --mode enrollment_ready -python test_live_camera_simple.py --mode full -``` - ---- - -## Analysis Modes - -| Mode | What It Shows | -|------|---------------| -| `quality_only` | Face detection + quality score (default, fastest) | -| `liveness` | Quality + liveness detection (live/spoof) | -| `demographics` | Quality + age, gender, emotion (slower) | -| `enrollment_ready` | Quality + enrollment readiness indicator | -| `full` | All features (slowest) | - ---- - -## Why the Simple Client Works Better - -### Direct OpenCV -- ✅ No browser security restrictions -- ✅ No HTTPS required -- ✅ No JavaScript bundling/caching issues -- ✅ Direct camera access -- ✅ Immediate window display - -### Simple Protocol -- ✅ One WebSocket connection -- ✅ Send frame → receive result -- ✅ No complex state management -- ✅ No React re-renders -- ✅ No DOM manipulation - -### Performance -- ✅ Lower latency (no browser overhead) -- ✅ Higher FPS (direct camera access) -- ✅ Less memory (no browser rendering) -- ✅ More control (camera settings, resolution) - ---- - -## Comparison Table - -| Feature | Web Browser | Simple Python Client | C++ + OpenCV | -|---------|-------------|----------------------|--------------| -| **Complexity** | High | Low | Very Low | -| **Setup Time** | Hours (build, deploy) | Seconds | Seconds | -| **Dependencies** | Node.js, npm, Next.js, etc. | Just Python + OpenCV | Just OpenCV | -| **Camera Access** | Requires permission | Direct | Direct | -| **Performance** | Good (10-15 FPS) | Excellent (30+ FPS) | Excellent (30+ FPS) | -| **Debugging** | Hard (browser devtools) | Easy (print statements) | Easy (debugger) | -| **Deployment** | Complex (build, bundle) | Simple (one file) | Simple (compile) | -| **User Experience** | Professional UI | Developer tool | Developer tool | - ---- - -## When to Use Each Approach - -### Use Web Browser When: -- ✅ You need a production user interface -- ✅ Users access remotely (no local software) -- ✅ Cross-platform compatibility is critical -- ✅ You have a design team -- ✅ Security and audit trails are important - -### Use Simple Python Client When: -- ✅ Testing and development -- ✅ Quick prototyping -- ✅ Local debugging -- ✅ Performance testing -- ✅ You want something that "just works" - -### Use C++ + OpenCV When: -- ✅ Maximum performance needed -- ✅ Embedded systems (Raspberry Pi, etc.) -- ✅ Offline processing -- ✅ Integration with existing C++ codebase -- ✅ Direct hardware access needed - ---- - -## Fixing the Web-Based Issue - -The web-based live camera **should** work, but has a browser caching issue. Here's how to fix it: - -### Option 1: Hard Refresh (Easiest) -``` -Chrome/Edge: Ctrl + Shift + R -Firefox: Ctrl + F5 -Safari: Cmd + Option + R -``` - -### Option 2: Incognito Mode -``` -Chrome: Ctrl + Shift + N -Firefox: Ctrl + Shift + P -Safari: Cmd + Shift + N -``` - -### Option 3: Clear Browser Cache -1. Open DevTools (F12) -2. Right-click refresh button -3. Select "Empty Cache and Hard Reload" - -### Option 4: Rebuild Frontend -```bash -cd demo-ui -rm -rf .next node_modules/.cache -npm run build -``` - ---- - -## Architecture Diagram - -### Web-Based (Complex) -``` -┌─────────┐ HTTPS ┌─────────┐ -│ Browser │ ────────────► │ Next.js │ -│ │ │ Server │ -│ Camera │ HTML │ │ -│ ↓ │ ◄──────────── │ (SSR) │ -│ Canvas │ └─────────┘ -│ ↓ │ │ -│ Base64 │ WebSocket │ -│ ↓ │ ◄─────────────────► │ -│ JSON │ ↓ -└─────────┘ ┌──────────┐ - │ FastAPI │ - │ Server │ - └──────────┘ -``` - -### Simple Python Client (Like C++) -``` -┌──────────────┐ -│ Python │ -│ │ -│ OpenCV │ -│ ↓ │ -│ Camera │ WebSocket ┌──────────┐ -│ ↓ │ ◄─────────────────►│ FastAPI │ -│ cv2.imshow() │ │ Server │ -│ ↓ │ └──────────┘ -│ Window │ -└──────────────┘ -``` - ---- - -## Troubleshooting - -### "Camera not found" -```bash -# List available cameras -python -c "import cv2; print([i for i in range(10) if cv2.VideoCapture(i).isOpened()])" - -# Try different camera ID -python test_live_camera_simple.py --camera 1 -``` - -### "Connection refused" -```bash -# Make sure API server is running -curl http://localhost:8001/api/v1/health - -# Check WebSocket endpoint -curl --include \ - --no-buffer \ - --header "Connection: Upgrade" \ - --header "Upgrade: websocket" \ - --header "Sec-WebSocket-Version: 13" \ - --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \ - http://localhost:8001/api/v1/ws/live-analysis -``` - -### "Module not found: websockets" -```bash -pip install websockets -``` - -### "Low FPS" -```bash -# Use faster mode -python test_live_camera_simple.py --mode quality_only - -# Or enable frame skipping (modify code: frame_skip=2) -``` - ---- - -## Performance Benchmarks - -### Expected FPS by Mode - -| Mode | Local (CPU) | Local (GPU) | Deployed | -|------|-------------|-------------|----------| -| `quality_only` | 25-30 FPS | 30+ FPS | 15-20 FPS | -| `liveness` | 15-20 FPS | 25-30 FPS | 10-15 FPS | -| `demographics` | 5-10 FPS | 10-15 FPS | 3-5 FPS | -| `full` | 3-5 FPS | 5-10 FPS | 2-3 FPS | - -### Network Latency Impact - -| Connection | Added Latency | -|------------|---------------| -| Local (localhost) | ~1-2ms | -| LAN (same network) | ~5-10ms | -| Cloud (GCP) | ~50-100ms | -| Cloud (far region) | ~200-500ms | - ---- - -## Next Steps - -1. **Test the simple client** - Just run `python test_live_camera_simple.py` -2. **Compare with web version** - See which you prefer -3. **Customize overlay** - Edit the `draw_overlay()` function -4. **Add features** - Save best quality frames, auto-enroll, etc. -5. **Deploy** - Use the simple client for kiosk applications - ---- - -## Summary - -**The Simple Python Client:** -- ✅ Works like C++ + OpenCV (simple loop) -- ✅ Opens in seconds (no build, no bundle) -- ✅ Direct camera access (no permissions) -- ✅ Real-time visual feedback (OpenCV window) -- ✅ High performance (30+ FPS) -- ✅ Easy to debug (print statements work!) - -**The Web-Based Version:** -- ✅ Professional UI for end users -- ✅ Works remotely (no local software) -- ❌ Complex setup (build, bundle, cache) -- ❌ Security restrictions (HTTPS, permissions) -- ❌ Hard to debug (browser devtools) - -**Recommendation:** Use the simple Python client for testing and development, use the web version for production deployment! - ---- - -*Created: 2025-12-28* -*For questions, see the simple client code: `test_live_camera_simple.py`* diff --git a/docs/1-getting-started/QUICK_START.md b/docs/1-getting-started/QUICK_START.md deleted file mode 100644 index e12b231..0000000 --- a/docs/1-getting-started/QUICK_START.md +++ /dev/null @@ -1,275 +0,0 @@ -# Quick Start Guide - -## Fastest Way to Test - -### Option 1: Browser (Easiest!) -``` -1. Open: http://localhost:8001/docs -2. Click any endpoint -> "Try it out" -3. Upload image and test! -``` - -### Option 2: Python Script -```bash -python test_complete_workflow.py -``` - -### Option 3: cURL -```bash -curl http://localhost:8001/api/v1/health -``` - ---- - -## Prerequisites - -- Python 3.11+ installed -- Virtual environment activated -- Test images with clear, front-facing faces - ---- - -## Start Server - -```bash -# Activate virtual environment -# Linux/macOS -source venv/bin/activate - -# Windows -.\.venv\Scripts\activate - -# Start server -uvicorn app.main:app --reload --host 0.0.0.0 --port 8001 -``` - -Server will be available at: **http://localhost:8001** - ---- - -## API Endpoints - -| Endpoint | Method | Description | -|----------|--------|-------------| -| `/api/v1/health` | GET | Health check | -| `/api/v1/enroll` | POST | Enroll a face | -| `/api/v1/verify` | POST | Verify face (1:1) | -| `/api/v1/search` | POST | Search face (1:N) | -| `/api/v1/liveness` | POST | Liveness detection | -| `/api/v1/batch/enroll` | POST | Batch enrollment | -| `/api/v1/batch/verify` | POST | Batch verification | -| `/api/v1/card-type/detect-live` | POST | Card type detection | - ---- - -## Run Tests - -### Complete Workflow Test (Recommended) -```bash -python test_complete_workflow.py -``` - -**Tests:** -- Health check -- Enroll users -- Verify same person (should match) -- Verify different person (should NOT match) -- Liveness detection -- Error handling - -### Find Good Images -```bash -python find_good_images.py -``` - -### Simple Interactive Test -```bash -python test_api_simple.py -``` - ---- - -## Interactive Testing (Browser) - -### Swagger UI -**URL:** http://localhost:8001/docs - -**How to use:** -1. Expand an endpoint (e.g., `/api/v1/enroll`) -2. Click "Try it out" -3. Fill in parameters: - - `user_id`: "test_user_123" - - `file`: Click "Choose File" and upload image -4. Click "Execute" -5. See response below - -**Recommended order:** -1. GET `/api/v1/health` - Check server -2. POST `/api/v1/enroll` - Enroll a face -3. POST `/api/v1/verify` - Verify the same face -4. POST `/api/v1/liveness` - Check liveness - ---- - -## Example Workflow - -### 1. Health Check -```bash -curl http://localhost:8001/api/v1/health -``` - -### 2. Enroll User -```bash -curl -X POST "http://localhost:8001/api/v1/enroll" \ - -F "user_id=john_doe" \ - -F "file=@/path/to/face.jpg" -``` - -### 3. Verify User -```bash -curl -X POST "http://localhost:8001/api/v1/verify" \ - -F "user_id=john_doe" \ - -F "file=@/path/to/another_face.jpg" -``` - -### 4. Check Liveness -```bash -curl -X POST "http://localhost:8001/api/v1/liveness" \ - -F "file=@/path/to/live_image.jpg" -``` - ---- - -## Expected Results - -### Successful Enrollment -```json -{ - "success": true, - "user_id": "john_doe", - "quality_score": 85.5, - "message": "Face enrolled successfully", - "embedding_dimension": 128 -} -``` - -### Successful Verification (Match) -```json -{ - "verified": true, - "confidence": 0.87, - "distance": 0.13, - "threshold": 0.6, - "message": "Face verified successfully" -} -``` - -### Failed Verification (Different Person) -```json -{ - "verified": false, - "confidence": 0.05, - "distance": 0.95, - "threshold": 0.6, - "message": "Face verification failed" -} -``` - -### Liveness Check Passed -```json -{ - "is_live": true, - "liveness_score": 75.8, - "challenge": "texture_analysis", - "challenge_completed": true, - "message": "Liveness check passed" -} -``` - ---- - -## Common Errors - -### No Face Detected -```json -{ - "error_code": "FACE_NOT_DETECTED", - "message": "No face detected in the image..." -} -``` -**Solution:** Use image with clear, front-facing face - -### Poor Quality -```json -{ - "error_code": "POOR_IMAGE_QUALITY", - "message": "Image quality too low..." -} -``` -**Solution:** Use higher quality image - -### User Not Enrolled -```json -{ - "error_code": "EMBEDDING_NOT_FOUND", - "message": "User not enrolled..." -} -``` -**Solution:** Enroll user first before verification - ---- - -## Troubleshooting - -### Server not responding? -```bash -# Check if server is running -curl http://localhost:8001/api/v1/health - -# If not, start it: -uvicorn app.main:app --reload --port 8001 -``` - -### Image quality too low? -```bash -# Find images that work: -python find_good_images.py -``` - ---- - -## Documentation - -- **API Documentation:** http://localhost:8001/docs -- **ReDoc:** http://localhost:8001/redoc -- **Full Testing Guide:** [MANUAL_TESTING_GUIDE.md](MANUAL_TESTING_GUIDE.md) - ---- - -## Quick Commands - -```bash -# Start server -uvicorn app.main:app --reload --port 8001 - -# Test everything -python test_complete_workflow.py - -# Find good images -python find_good_images.py - -# Health check -curl http://localhost:8001/api/v1/health -``` - ---- - -## Tips for Best Results - -### For Face Images: -- Front-facing face -- Good lighting -- Clear, not blurry -- Only one face in image -- Face size at least 80x80 pixels -- Avoid sunglasses, face masks, extreme angles diff --git a/docs/1-getting-started/QUICK_TEST_GUIDE.md b/docs/1-getting-started/QUICK_TEST_GUIDE.md deleted file mode 100644 index c31df14..0000000 --- a/docs/1-getting-started/QUICK_TEST_GUIDE.md +++ /dev/null @@ -1,279 +0,0 @@ -# Quick Test Guide for Deployed API - -> **Note:** The GCP Cloud Run deployment (`biometric-api-902542798396.europe-west1.run.app`) is no longer active. The biometric-processor now runs locally via WSL2 and is exposed via Cloudflare Tunnel at `https://bio.fivucsas.com`. Replace all URLs below with the Cloudflare Tunnel URL when the tunnel is running, or `http://localhost:8001` for local testing. - -Since you have access to the deployed API at: -**https://bio.fivucsas.com** (or `http://localhost:8001` locally) - -Here are quick tests you can run from your browser or terminal. - ---- - -## Method 1: Browser Testing - -### Test Health Endpoint (Easiest!) - -Just open this URL in your browser: -``` -https://biometric-api-902542798396.europe-west1.run.app/api/v1/health -``` - -**Expected Response:** -```json -{ - "status": "healthy", - "service": "FIVUCSAS Biometric Processor", - "version": "1.0.0", - "model": "Facenet" -} -``` - -### View API Documentation - -Open in browser: -``` -https://biometric-api-902542798396.europe-west1.run.app/docs -``` - -This gives you an **interactive Swagger UI** where you can test all endpoints directly! - ---- - -## Method 2: Terminal Testing (curl) - -### 1. Health Check -```bash -curl https://biometric-api-902542798396.europe-west1.run.app/api/v1/health -``` - -### 2. Quality Analysis (with test image) -```bash -curl -X POST https://biometric-api-902542798396.europe-west1.run.app/api/v1/quality/analyze \ - -F "file=@tests/fixtures/images/afuat/profileImage_1200.jpg" -``` - -### 3. Face Detection -```bash -curl -X POST https://biometric-api-902542798396.europe-west1.run.app/api/v1/face/detect \ - -F "file=@tests/fixtures/images/afuat/profileImage_1200.jpg" -``` - -### 4. Demographics Analysis -```bash -curl -X POST https://biometric-api-902542798396.europe-west1.run.app/api/v1/demographics/analyze \ - -F "file=@tests/fixtures/images/afuat/profileImage_1200.jpg" -``` - -### 5. Liveness Check -```bash -curl -X POST https://biometric-api-902542798396.europe-west1.run.app/api/v1/liveness/check \ - -F "file=@tests/fixtures/images/afuat/profileImage_1200.jpg" -``` - -### 6. Facial Landmarks -```bash -curl -X POST https://biometric-api-902542798396.europe-west1.run.app/api/v1/landmarks/detect \ - -F "file=@tests/fixtures/images/afuat/profileImage_1200.jpg" -``` - -### 7. Enroll a Face -```bash -curl -X POST https://biometric-api-902542798396.europe-west1.run.app/api/v1/enroll \ - -F "file=@tests/fixtures/images/afuat/profileImage_1200.jpg" \ - -F "user_id=test-user-001" \ - -F "tenant_id=test-tenant" -``` - -### 8. Verify a Face -```bash -# First enroll (see above), then verify with different photo -curl -X POST https://biometric-api-902542798396.europe-west1.run.app/api/v1/verify \ - -F "file=@tests/fixtures/images/afuat/3.jpg" \ - -F "user_id=test-user-001" \ - -F "tenant_id=test-tenant" -``` - -### 9. Search for a Face -```bash -curl -X POST https://biometric-api-902542798396.europe-west1.run.app/api/v1/search \ - -F "file=@tests/fixtures/images/afuat/3.jpg" \ - -F "tenant_id=test-tenant" \ - -F "max_results=5" -``` - -### 10. Compare Two Faces -```bash -curl -X POST https://biometric-api-902542798396.europe-west1.run.app/api/v1/compare \ - -F "file1=@tests/fixtures/images/afuat/profileImage_1200.jpg" \ - -F "file2=@tests/fixtures/images/afuat/3.jpg" -``` - ---- - -## Method 3: Comprehensive Automated Testing - -Run the Python test suite (from your machine): - -```bash -# Update the test script to use deployed URL (already done in test_deployed_api.py) -python test_deployed_api.py -``` - -This will: -- Test all 35 test cases -- Generate `DEPLOYED_API_TEST_REPORT.md` -- Show pass/fail for each endpoint -- Measure response times -- Provide detailed error analysis - -**Expected Duration:** 2-3 minutes - ---- - -## Method 4: Interactive Testing (Postman/Insomnia) - -1. Import the OpenAPI spec from: - ``` - https://biometric-api-902542798396.europe-west1.run.app/openapi.json - ``` - -2. Test endpoints interactively with: - - Request history - - Environment variables - - Response validation - ---- - -## What to Look For - -### ✅ Success Indicators - -1. **Health Endpoint**: Returns 200 with `"status": "healthy"` -2. **Quality Analysis**: Returns 0-100 scores (NOT 2000+%) -3. **Demographics**: - - Good images → 200 with age/gender/emotion - - Small images (<224px) → 400 Bad Request (NOT 500!) -4. **Face Detection**: Returns bounding box and confidence -5. **Enrollment**: Returns 200 with embedding_id -6. **Verification**: Returns match boolean and similarity score - -### ⚠️ Issues to Report - -1. **500 Internal Server Error** on any endpoint -2. **Quality scores > 100%** (should be fixed!) -3. **Blur scores > 100%** (should be fixed!) -4. **Demographics returning 500 for small images** (should be 400!) -5. **Any crashes or timeouts** - ---- - -## Test Images Available - -From the repository: - -``` -tests/fixtures/images/ -├── afuat/ -│ ├── profileImage_1200.jpg ← Good quality, large -│ ├── 3.jpg ← Small image (test demographics 400 error) -│ ├── DSC_8681.jpg ← No face (test error handling) -│ └── indir.jpg ← Tiny face -├── aga/ -│ ├── spring21_veda1.png ← Good PNG -│ └── indir.jpg ← Small face -└── ahab/ - ├── foto.jpg ← Different person - └── 1679744618228.jpg ← Same person, different photo -``` - ---- - -## Quick One-Liner Tests - -### Test All Core Endpoints Quickly -```bash -# Health -curl -s https://biometric-api-902542798396.europe-west1.run.app/api/v1/health | jq - -# Quality (normalized scores check) -curl -s -X POST https://biometric-api-902542798396.europe-west1.run.app/api/v1/quality/analyze \ - -F "file=@tests/fixtures/images/afuat/profileImage_1200.jpg" | jq '.overall_score' - -# Demographics (should work with good image) -curl -s -X POST https://biometric-api-902542798396.europe-west1.run.app/api/v1/demographics/analyze \ - -F "file=@tests/fixtures/images/afuat/profileImage_1200.jpg" | jq '.age' - -# Demographics (should return 400 for small image, NOT 500!) -curl -s -X POST https://biometric-api-902542798396.europe-west1.run.app/api/v1/demographics/analyze \ - -F "file=@tests/fixtures/images/afuat/3.jpg" | jq '.error_code' -``` - ---- - -## Expected Response Times - -| Endpoint | Expected Response Time | -|----------|------------------------| -| `/health` | < 100ms | -| `/quality/analyze` | 200-500ms | -| `/demographics/analyze` | 1-3s (ML inference) | -| `/face/detect` | 100-300ms | -| `/landmarks/detect` | 300-800ms | -| `/liveness/check` | 500-1500ms | -| `/enroll` | 300-800ms | -| `/verify` | 400-1000ms | -| `/search` | 500-2000ms | -| `/compare` | 600-1200ms | - ---- - -## Troubleshooting - -### If you get 403 Forbidden -- The proxy might be blocking your IP -- Try from a different network or VPN - -### If you get 500 Internal Server Error -- Check GCP Cloud Run logs: - ```bash - gcloud logging read "resource.type=cloud_run_revision" --limit=50 - ``` - -### If you get 502 Bad Gateway -- Service might be cold-starting (wait 30s and retry) -- Or deployment might have failed - -### If demographics returns 500 for small images -- This is the bug we're verifying! -- Should return 400 with clear error message -- Report this if it still happens - ---- - -## After Testing - -### Report Results - -Create an issue or update the test report with: -1. Which endpoints passed/failed -2. Any unexpected errors (especially 500s) -3. Response times (slow endpoints) -4. Whether bug fixes are working: - - Quality scores normalized (0-100)? - - Demographics returns 400 for small images? - - Live camera working? - -### Next Steps - -Once all tests pass: -1. Update `COMPREHENSIVE_STATUS_AND_TEST_REPORT.md` with results -2. Set deployment confidence to 95/100 -3. Plan production rollout -4. Create monitoring dashboards -5. Document any performance issues - ---- - -**Happy Testing! 🚀** - -*For comprehensive automated testing, use: `python test_deployed_api.py`* diff --git a/docs/1-getting-started/README.md b/docs/1-getting-started/README.md deleted file mode 100644 index c0c5a88..0000000 --- a/docs/1-getting-started/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Getting Started - -Quick guides to get you up and running with the biometric processor! - -## Documents - -- **[DEMO_USAGE.md](DEMO_USAGE.md)** - Complete guide to using demo_local.py (recommended!) -- **[QUICK_START.md](QUICK_START.md)** - Quick start guide -- **[QUICK_TEST_GUIDE.md](QUICK_TEST_GUIDE.md)** - Quick testing guide - -## Recommended Path - -1. **Start with demo_local.py** - The best way to test the system - ```bash - python demo_local.py - ``` - See [DEMO_USAGE.md](DEMO_USAGE.md) for full documentation. - -2. **Read the API Reference** - Understand the REST API - See [../2-api-documentation/API_REFERENCE.md](../2-api-documentation/API_REFERENCE.md) - -3. **Deploy (if needed)** - Deploy to production - See [../3-deployment/](../3-deployment/) - -## Quick Command Reference - -```bash -# Run demo with all features -python demo_local.py - -# Run specific mode -python demo_local.py --mode landmarks -python demo_local.py --mode enroll -python demo_local.py --mode demographics - -# Use different camera -python demo_local.py --camera 1 -``` - -## Performance - -**demo_local.py performance**: 18-30 FPS (CPU), optimized and production-ready! - -See [../5-performance/DEMO_LOCAL_PERFORMANCE_ANALYSIS.md](../5-performance/DEMO_LOCAL_PERFORMANCE_ANALYSIS.md) for detailed analysis. diff --git a/docs/2-api-documentation/API_REFERENCE.md b/docs/2-api-documentation/API_REFERENCE.md deleted file mode 100644 index dcfe107..0000000 --- a/docs/2-api-documentation/API_REFERENCE.md +++ /dev/null @@ -1,628 +0,0 @@ -# Comprehensive API Status and Test Report - -**Generated:** 2025-12-28 -**Project:** FIVUCSAS Biometric Processor API -**Version:** 1.0.0 -**Python:** 3.11+ -**Framework:** FastAPI 0.128+ - ---- - -## Executive Summary - -| Metric | Status | Details | -|--------|--------|---------| -| **Project Completion** | 100% | All MVP features implemented | -| **Production Deployment** | ⏳ PENDING | Cloudflare Tunnel (bio.fivucsas.com) | -| **Recent Testing** | ⚠️ PARTIAL | Local testing completed, deployment inaccessible from this environment | -| **Critical Bugs Fixed** | ✅ 6/6 | All December 27 bugs resolved | -| **Infrastructure** | ✅ COMPLETE | CI/CD, monitoring, database setup | - -### 🎯 Overall Assessment - -**STATUS: PRODUCTION-READY WITH CAVEATS** - -The biometric processor API is **fully functional** based on local testing completed on December 27, 2025. However, **comprehensive endpoint testing** could not be completed in this session due to: -1. Deployed API inaccessible (proxy restrictions: 403 "host_not_allowed") -2. Local server requires full ML dependency installation (~5-10 minutes) - -**Recommendation:** Run comprehensive tests on local development environment or from a machine with direct access to the GCP deployment. - ---- - -## Deployment Information - -> **Note:** The GCP Cloud Run deployment (December 2025) is no longer active. See `DEPLOYMENT_CHALLENGES.md` for historical details. Current deployment target is WSL2/local GPU via Cloudflare Tunnel. - -### Current Deployment Target - -| Resource | Details | -|----------|---------| -| **Service URL** | https://bio.fivucsas.com (Cloudflare Tunnel) | -| **Local URL** | http://localhost:8001 | -| **Status** | ⏳ Pending (tunnel setup required) | -| **Resources** | Local GPU (NVIDIA GTX 1650, 4GB VRAM) | -| **Database** | PostgreSQL 16 + pgvector (local Docker) | -| **Cache** | Redis 7 (local Docker) | - -### Deployment History (Historical) - -- **7 GCP Cloud Run revision attempts** before successful deployment (Dec 2025, now decommissioned) -- **All dependency issues resolved** (NumPy 2.x, Keras 3.x, OpenGL, lightphe, prometheus_client) -- **Database pgvector extension** successfully enabled via Cloud Run Job - ---- - -## API Endpoints Overview - -### Core Biometric Endpoints (15 Total) - -| # | Endpoint | Method | Purpose | Last Test Status | -|---|----------|--------|---------|------------------| -| 1 | `/api/v1/health` | GET | Health check | ✅ PASS (Dec 27) | -| 2 | `/api/v1/quality/analyze` | POST | Image quality analysis | ✅ PASS (Dec 27) | -| 3 | `/api/v1/demographics/analyze` | POST | Age, gender, emotion detection | ⏳ PENDING (fix applied) | -| 4 | `/api/v1/face/detect` | POST | Single face detection | ❓ UNTESTED | -| 5 | `/api/v1/faces/detect-all` | POST | Multi-face detection | ❓ UNTESTED | -| 6 | `/api/v1/landmarks/detect` | POST | 468-point facial landmarks | ❓ UNTESTED | -| 7 | `/api/v1/liveness/check` | POST | Liveness detection | ❓ UNTESTED | -| 8 | `/api/v1/enroll` | POST | Face enrollment | ❓ UNTESTED | -| 9 | `/api/v1/verify` | POST | Face verification (1:1) | ❓ UNTESTED | -| 10 | `/api/v1/search` | POST | Face search (1:N) | ❓ UNTESTED | -| 11 | `/api/v1/compare` | POST | Direct face comparison | ❓ UNTESTED | -| 12 | `/api/v1/batch/enroll` | POST | Batch enrollment | ❓ UNTESTED | -| 13 | `/api/v1/batch/verify` | POST | Batch verification | ❓ UNTESTED | -| 14 | `/api/v1/similarity/matrix` | POST | NxN similarity matrix | ❓ UNTESTED | -| 15 | `/api/v1/ws/live-analysis` | WS | Live camera stream | ⏳ PENDING (browser cache issue) | - -### Additional Features Endpoints - -| Endpoint | Purpose | Status | -|----------|---------|--------| -| `/api/v1/embeddings/export` | Export embeddings | ❓ UNTESTED | -| `/api/v1/embeddings/import` | Import embeddings | ❓ UNTESTED | -| `/api/v1/webhooks/register` | Register webhook | ❓ UNTESTED | -| `/api/v1/webhooks` | List webhooks | ❓ UNTESTED | -| `/api/v1/webhooks/{id}` | Delete webhook | ❓ UNTESTED | -| `/api/v1/webhooks/{id}/test` | Test webhook | ❓ UNTESTED | - ---- - -## Recent Bug Fixes (December 27, 2025) - -### ✅ FIXED - Critical Issues - -| # | Issue | Root Cause | Fix Applied | Status | -|---|-------|------------|-------------|--------| -| 1 | **Blur scores over 100%** (2765%, 1103%) | Raw Laplacian variance not normalized | Normalized to 0-100 scale in `analyze_quality.py:L45` | ✅ VERIFIED (56%, 60%, 100%) | -| 2 | **Face size over 100%** (307%, 262%) | Raw pixel values returned | Normalized based on 200px reference in `analyze_quality.py:L52` | ✅ VERIFIED (47%, 45%, 100%) | -| 3 | **Camera permission denied** | `Permissions-Policy: camera=()` blocked all access | Changed to `camera=(self)` in `security_headers.py:L132` | ✅ FIXED | -| 4 | **WebSocket crash** | Plain "ping" sent, `receive_json()` expected JSON | Modified to use `receive_text()` in `live_analysis.py:L78` | ✅ FIXED | -| 5 | **Demographics 500 error** | `DemographicsError` not mapped to HTTP status | Added to status_code_map in `error_handler.py:L42` | ⏳ PENDING USER VERIFICATION | -| 6 | **Live camera race condition** | Frame capture before WebSocket connection | Moved to `useEffect` watching `isConnected` in `live-camera-stream.tsx:L156` | ⏳ PENDING BROWSER CACHE CLEAR | - -### Files Modified - -``` -Backend (5 files): - app/application/use_cases/analyze_quality.py - app/domain/entities/quality_feedback.py - app/api/middleware/security_headers.py - app/api/routes/live_analysis.py - app/api/middleware/error_handler.py - -Frontend (3 files): - demo-ui/src/lib/utils/format.ts - demo-ui/src/hooks/use-quality-analysis.ts - demo-ui/src/components/media/live-camera-stream.tsx -``` - ---- - -## Test Results Summary - -### Tests Completed (December 27, 2025) - -| Test Category | Tests Run | Passed | Failed | Notes | -|---------------|-----------|--------|--------|-------| -| **Health Check** | 1 | 1 | 0 | ✅ Service responding | -| **Quality Analysis** | 8 | 8 | 0 | ✅ Metrics normalized correctly | -| **Demographics** | 8 | 6 | 2 | ⏳ Small images should return 400, fix pending verification | -| **WebSocket** | 1 | 1 | 0 | ✅ No crashes, frames pending | -| **Camera Permissions** | 1 | 1 | 0 | ✅ Browser access granted | - -### Tests Pending - -| Endpoint Category | Endpoints | Priority | Reason Not Tested | -|-------------------|-----------|----------|-------------------| -| Face Detection | 2 | HIGH | Awaiting local server setup | -| Landmarks | 1 | MEDIUM | Awaiting local server setup | -| Liveness | 1 | HIGH | Awaiting local server setup | -| Enrollment/Verification | 3 | HIGH | Awaiting local server setup | -| Search/Compare | 2 | HIGH | Awaiting local server setup | -| Batch Operations | 2 | MEDIUM | Awaiting local server setup | -| Embeddings Export/Import | 2 | LOW | Awaiting local server setup | -| Webhooks | 4 | LOW | Awaiting local server setup | - ---- - -## Infrastructure Assessment - -### ✅ Deployment Infrastructure - -| Component | Status | Details | -|-----------|--------|---------| -| **Dockerfile** | ✅ COMPLETE | Multi-stage build with health checks | -| **Docker Compose** | ✅ COMPLETE | Full stack (API, Postgres, Redis, Prometheus, Grafana) | -| **CI/CD Pipeline** | ✅ OPERATIONAL | GitHub Actions (lint → test → build → deploy) | -| **PR Validation** | ✅ OPERATIONAL | Automated checks on pull requests | -| **Cloud Monitoring** | ✅ ACTIVE | Uptime checks every 5 minutes (Europe, USA-Iowa, Asia-Pacific) | -| **Alert Policy** | ✅ CONFIGURED | Triggers on uptime check failures | -| **Database Migrations** | ✅ READY | Alembic migrations configured | -| **Rate Limiting** | ✅ IMPLEMENTED | Redis-backed with X-RateLimit headers | -| **API Key Auth** | ✅ IMPLEMENTED | SHA-256 hashed keys with scopes | - -### Database Setup - -| Component | Status | Location | -|-----------|--------|----------| -| PostgreSQL 15 | ✅ RUNNING | Cloud SQL (fivucsas:europe-west1:biometric-db) | -| pgvector Extension | ✅ ENABLED | Successfully installed via Cloud Run Job | -| Schema Migrations | ✅ READY | `migrations/versions/` + `alembic/versions/` | -| Connection Pool | ✅ CONFIGURED | `pool_manager.py` with async connections | - ---- - -## Performance Characteristics - -### Expected Response Times (Based on Local Testing) - -| Endpoint | Avg Response | Notes | -|----------|--------------|-------| -| `/health` | <50ms | No ML processing | -| `/quality/analyze` | 200-500ms | Single face detection + quality metrics | -| `/demographics/analyze` | 1-3s | DeepFace model inference (age, gender, emotion) | -| `/face/detect` | 100-300ms | Single face detection only | -| `/landmarks/detect` | 300-800ms | MediaPipe Face Mesh (468 points) | -| `/liveness/check` | 500-1500ms | Texture analysis + optional active detection | -| `/enroll` | 300-800ms | Face detection + embedding extraction | -| `/verify` | 400-1000ms | Embedding extraction + similarity comparison | -| `/search` | 500-2000ms | Depends on database size (1:N comparison) | -| `/compare` | 600-1200ms | Two face extractions + comparison | - -### Resource Usage - -- **Memory:** 1.5-2GB during ML inference -- **CPU:** Spikes to 80-100% during face processing -- **Disk:** Minimal (<100MB for model caching) - ---- - -## Security Features - -| Feature | Status | Implementation | -|---------|--------|----------------| -| **HTTPS Only** | ✅ | Cloud Run enforces HTTPS | -| **Security Headers** | ✅ | HSTS, XSS Protection, CSP, X-Frame-Options | -| **API Key Authentication** | ✅ | SHA-256 hashed keys with scopes and tiers | -| **Rate Limiting** | ✅ | Sliding window with Redis backend | -| **Input Validation** | ✅ | Pydantic schemas on all endpoints | -| **Error Sanitization** | ✅ | No stack traces in production | -| **CORS Configuration** | ✅ | Explicit origins (no wildcard) | - ---- - -## Known Issues and Limitations - -### Current Limitations - -1. **Browser Cache Issue (Live Camera)** - - **Issue:** JavaScript bundle cached with old code (hash: 2ea546ccdd43d9d7) - - **Impact:** Live camera stream not sending frames - - **Workaround:** Test in incognito mode or clear browser cache - - **Status:** ⏳ PENDING USER VERIFICATION - -2. **Demographics Small Images** - - **Issue:** Images <224x224px should return 400, may still return 500 - - **Impact:** Unclear error messages for users - - **Fix Applied:** Added `DemographicsError` to error_handler.py - - **Status:** ⏳ PENDING USER VERIFICATION - -3. **In-Memory Storage (Dev Mode)** - - **Issue:** Face embeddings stored in memory, lost on restart - - **Impact:** Not suitable for production without database - - **Solution:** Use PostgreSQL + pgvector (already configured) - - **Status:** ✅ AVAILABLE (needs environment variable) - -4. **No Automated E2E Tests** - - **Issue:** Integration tests exist, but no CI/CD E2E tests - - **Impact:** Regressions could slip through - - **Recommendation:** Add Playwright/Cypress tests to CI pipeline - - **Status:** 🔜 FUTURE ENHANCEMENT - -### Technical Debt - -1. **Metric Normalization Documentation** - - All metrics normalized to 0-100 scale (good!) - - API docs should reflect this change - - Need validation to ensure scores stay in range - -2. **Comprehensive Error Handling Audit** - - Should audit ALL exception types - - Ensure every custom exception has HTTP status mapping - - Add integration tests for error scenarios - -3. **WebSocket Protocol Fragility** - - Current heartbeat implementation is basic - - Consider WebSocket built-in ping/pong frames - - Add reconnection logic with exponential backoff - -4. **Test Coverage Gaps** - - Need automated integration tests for ALL endpoints - - Need E2E tests for frontend workflows - - Need performance/load testing suite - ---- - -## Recommended Next Steps - -### Priority 1: Comprehensive Endpoint Testing (CRITICAL) - -**Action:** Run the comprehensive test suite on a local development environment - -**Prerequisites:** -```bash -# Install dependencies -pip install -r requirements.txt - -# Start server -python -m uvicorn app.main:app --host 0.0.0.0 --port 8001 - -# Run comprehensive tests (in another terminal) -python test_all_comprehensive_with_report.py -``` - -**Expected Duration:** 5-10 minutes - -**Test Script Location:** `/home/user/biometric-processor/test_all_comprehensive_with_report.py` - -**What It Tests:** -- ✅ All 15 core endpoints -- ✅ Good quality images (frontal, clear, well-lit) -- ✅ Poor quality images (blurry, dark, small) -- ✅ Edge cases (no face, multiple faces, extreme angles) -- ✅ Error handling (400 vs 500 responses) -- ✅ Response times and performance - -**Output:** Generates `COMPREHENSIVE_API_TEST_REPORT.md` with: -- Executive summary (pass/fail counts) -- Results by endpoint -- Performance metrics (avg/min/max response times) -- Detailed failure analysis -- Actionable recommendations - -### Priority 2: Verify Pending Bug Fixes - -1. **Demographics Error Handling** - ```bash - # Test with small image (<224x224px) - curl -X POST http://localhost:8001/api/v1/demographics/analyze \ - -F "file=@tests/fixtures/images/afuat/3.jpg" - - # Expected: 400 Bad Request with clear error message - # NOT: 500 Internal Server Error - ``` - -2. **Live Camera Stream** - ```bash - # Test in incognito browser window - # Navigate to: http://localhost:8001/unified-demo - # Enable live camera stream - # Verify frames are being processed - ``` - -### Priority 3: Load and Stress Testing - -**Action:** Run load tests to establish performance baselines - -**Tools:** -- Locust (already configured: `tests/load/locustfile.py`) -- Apache Bench -- k6 - -**Test Scenarios:** -1. **Single User Load Test** - - 1 user, sequential requests to all endpoints - - Measure baseline response times - - Verify no memory leaks - -2. **Concurrent Users Test** - - 10 concurrent users - - Random endpoint selection - - Measure throughput and response times - -3. **Stress Test** - - Gradually increase from 1 to 100 users - - Find breaking point - - Measure error rates - -4. **Batch Processing Test** - - Test batch endpoints with 100+ images - - Verify processing efficiency - - Check memory usage - -### Priority 4: Production Readiness Checklist - -- [ ] All endpoint tests passing (Priority 1) -- [ ] Pending bug fixes verified (Priority 2) -- [ ] Load testing complete with documented baselines (Priority 3) -- [ ] Error messages user-friendly and informative -- [ ] API documentation updated (normalized metrics, error codes) -- [ ] Monitoring dashboards created (Grafana) -- [ ] Alert thresholds configured (error rates, latency) -- [ ] Backup/restore procedures documented -- [ ] Incident response plan created -- [ ] Security audit completed -- [ ] Rate limits tested and tuned -- [ ] Database migration strategy defined -- [ ] Rollback plan documented - -### Priority 5: Documentation Updates - -1. **API Documentation** - - Update response schemas to reflect 0-100 scale for quality metrics - - Document all error codes (400, 404, 409, 500) - - Add examples for each endpoint - - Include curl examples - -2. **Deployment Guide** - - Document GCP deployment process - - Include troubleshooting steps - - Add monitoring setup guide - -3. **Developer Guide** - - Local development setup - - Testing procedures - - Contributing guidelines - ---- - -## Test Automation Recommendations - -### Create Automated Test Suite - -```python -# tests/integration/test_all_endpoints_automated.py - -import pytest -from pathlib import Path - -class TestBiometricAPI: - """Automated integration tests for all endpoints.""" - - def test_health_endpoint(self, client): - """Test health endpoint returns 200.""" - response = client.get("/api/v1/health") - assert response.status_code == 200 - assert response.json()["status"] == "healthy" - - def test_quality_analysis_good_image(self, client, good_image): - """Test quality analysis with good quality image.""" - response = client.post( - "/api/v1/quality/analyze", - files={"file": good_image} - ) - assert response.status_code == 200 - data = response.json() - assert 0 <= data["overall_score"] <= 100 - assert data["passed"] is True - - def test_demographics_small_image_returns_400(self, client, small_image): - """Test demographics returns 400 for small images.""" - response = client.post( - "/api/v1/demographics/analyze", - files={"file": small_image} - ) - assert response.status_code == 400 # NOT 500! - assert "error_code" in response.json() - - # ... more tests -``` - -### Add to CI/CD Pipeline - -```yaml -# .github/workflows/ci-cd.yml (add to existing) - - integration-tests: - name: Integration Tests - runs-on: ubuntu-latest - needs: build-and-test - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Start services - run: docker-compose up -d - - - name: Wait for services - run: ./scripts/wait-for-services.sh - - - name: Run integration tests - run: pytest tests/integration/ -v --junitxml=results.xml - - - name: Publish test results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - files: results.xml -``` - ---- - -## Performance Optimization Opportunities - -### Identified Bottlenecks - -1. **DeepFace Model Loading** (1-3s on cold start) - - **Solution:** Pre-load models on server startup - - **Impact:** Reduce first-request latency - -2. **Face Detection on Large Images** (500ms-1s) - - **Solution:** Resize images to max 800px before processing - - **Impact:** 2-3x speedup - -3. **Database Queries for Search** (grows with dataset size) - - **Solution:** Use pgvector with proper indexing - - **Impact:** Constant-time search even with millions of embeddings - -4. **Synchronous Batch Processing** - - **Solution:** Use async processing with Celery - - **Impact:** Handle 100+ images in parallel - -### Optimization Recommendations - -1. **Enable Model Caching** - ```python - # app/core/config.py - DEEPFACE_HOME = "/tmp/.deepface" # Already configured - ``` - -2. **Add Image Preprocessing** - ```python - # app/infrastructure/ml/preprocessing.py - def resize_for_processing(image, max_size=800): - if max(image.shape[:2]) > max_size: - scale = max_size / max(image.shape[:2]) - image = cv2.resize(image, None, fx=scale, fy=scale) - return image - ``` - -3. **Implement Response Caching** - ```python - # app/api/middleware/cache.py - from fastapi_cache import FastAPICache - from fastapi_cache.backends.redis import RedisBackend - - @app.on_event("startup") - async def startup(): - redis = aioredis.from_url("redis://localhost") - FastAPICache.init(RedisBackend(redis), prefix="biometric-cache") - ``` - ---- - -## Conclusion - -### Current State Summary - -The FIVUCSAS Biometric Processor API is **functionally complete** and **deployed to production** on GCP Cloud Run. Recent bug fixes (December 27) have addressed all critical issues identified during testing: - -✅ **Strengths:** -- All core features implemented and working -- Successful GCP deployment with proper infrastructure -- CI/CD pipelines operational -- Monitoring and alerting configured -- Critical bugs fixed (quality normalization, permissions, WebSocket) - -⚠️ **Areas for Improvement:** -- Comprehensive endpoint testing incomplete (due to environment limitations) -- Two pending verifications (demographics error handling, live camera) -- Need automated E2E tests in CI/CD -- Performance optimization opportunities exist - -### Deployment Confidence: **HIGH** (85/100) - -**Ready for:** Staging environment, controlled beta testing -**Not yet ready for:** Public production without comprehensive testing - -### Final Recommendation - -**IMMEDIATE ACTION REQUIRED:** - -1. Run comprehensive test suite on local/staging environment (est. 10 minutes) -2. Verify pending bug fixes work as expected -3. Document test results and performance baselines -4. Create automated integration test suite -5. Run load tests to establish capacity limits - -**Once testing is complete:** -- Update API documentation -- Create runbooks for common issues -- Set up monitoring dashboards -- Define SLAs and alerts -- Plan gradual rollout strategy - -**Timeline to Production-Ready:** 1-2 weeks with dedicated testing effort - ---- - -## Appendix - -### Test Images Available - -``` -tests/fixtures/images/ -├── afuat/ -│ ├── profileImage_1200.jpg (Large, good quality) -│ ├── 3.jpg (Small image) -│ ├── DSC_8681.jpg (No face) -│ ├── indir.jpg (Tiny face) -│ └── ... -├── aga/ -│ ├── spring21_veda1.png (Good PNG) -│ ├── indir.jpg (Small face) -│ └── ... -└── ahab/ - ├── foto.jpg (Different person) - ├── 1679744618228.jpg (Different photo, same person) - └── ... -``` - -### Quick Test Commands - -```bash -# Health check -curl http://localhost:8001/api/v1/health - -# Quality analysis -curl -X POST http://localhost:8001/api/v1/quality/analyze \ - -F "file=@tests/fixtures/images/afuat/profileImage_1200.jpg" - -# Demographics -curl -X POST http://localhost:8001/api/v1/demographics/analyze \ - -F "file=@tests/fixtures/images/afuat/profileImage_1200.jpg" - -# Face detection -curl -X POST http://localhost:8001/api/v1/face/detect \ - -F "file=@tests/fixtures/images/afuat/profileImage_1200.jpg" - -# Liveness check -curl -X POST http://localhost:8001/api/v1/liveness/check \ - -F "file=@tests/fixtures/images/afuat/profileImage_1200.jpg" - -# Enrollment -curl -X POST http://localhost:8001/api/v1/enroll \ - -F "file=@tests/fixtures/images/afuat/profileImage_1200.jpg" \ - -F "user_id=test-user-001" \ - -F "tenant_id=test-tenant" - -# Verification -curl -X POST http://localhost:8001/api/v1/verify \ - -F "file=@tests/fixtures/images/afuat/3.jpg" \ - -F "user_id=test-user-001" \ - -F "tenant_id=test-tenant" -``` - -### Contact & Support - -- **Documentation:** `/docs` (Swagger UI) -- **Repository:** https://github.com/Rollingcat-Software/biometric-processor -- **Issues:** https://github.com/Rollingcat-Software/biometric-processor/issues - ---- - -*Report Generated: 2025-12-28 17:30:00 UTC* -*Author: Claude Code (Automated Analysis)* -*Session ID: claude/check-status-plan-next-mdchj* diff --git a/docs/3-deployment/DATABASE_FIXES_APPLIED.md b/docs/3-deployment/DATABASE_FIXES_APPLIED.md deleted file mode 100644 index 33d01a8..0000000 --- a/docs/3-deployment/DATABASE_FIXES_APPLIED.md +++ /dev/null @@ -1,306 +0,0 @@ -# Database Integration Fixes - Applied ✅ - -**Date:** 2025-12-24 -**Status:** All database integration fixes completed and ready for deployment - ---- - -## What Was Fixed - -### 1. ✅ Schema Mismatch - FIXED - -**Problem:** -- Migration created `face_embeddings` table -- Repository expected `biometric_data` table -- Result: Database operations would fail with "table does not exist" - -**Solution:** -- Renamed table from `face_embeddings` to `biometric_data` in migration -- Added missing columns required by repository: - - `biometric_type` - Support for multiple biometric types (FACE, FINGERPRINT, etc.) - - `embedding_model` - Track which ML model generated the embedding - - `is_primary` - Support multiple embeddings per user - - `deleted_at` - Soft delete for audit trail - -### 2. ✅ Missing Vector Index - FIXED - -**Problem:** -- No vector index created for similarity search -- Would cause extremely slow face search (1:N) operations -- Linear scan instead of indexed search - -**Solution:** -- Added HNSW vector index creation to migration: - ```sql - CREATE INDEX ix_biometric_data_embedding_hnsw - ON biometric_data - USING hnsw (embedding vector_cosine_ops) - WITH (m = 16, ef_construction = 64); - ``` -- Enables sub-second similarity search even with millions of faces -- Alternative IVFFlat index included as comment for very large datasets - -### 3. ✅ Incorrect Column Type - FIXED - -**Problem:** -- Embedding column was `ARRAY(Float)` instead of `vector` type -- Would not work with pgvector similarity operators -- Repository uses `<=>` cosine distance operator - -**Solution:** -- Changed to proper `vector(512)` type -- Supports pgvector operators: `<=>`, `<->`, `<#>` -- Enables vector indexing (HNSW, IVFFlat) - -### 4. ✅ Missing Unique Constraint - FIXED - -**Problem:** -- No constraint preventing duplicate enrollments -- Could enroll same user multiple times -- Confusion about which embedding to use - -**Solution:** -- Added partial unique index: - ```sql - CREATE UNIQUE INDEX ix_biometric_data_user_tenant_type_active - ON biometric_data (user_id, tenant_id, biometric_type) - WHERE deleted_at IS NULL; - ``` -- Prevents duplicate active enrollments -- Allows multiple soft-deleted records (for audit trail) - -### 5. ✅ Optimized Indexes - ADDED - -**Problem:** -- Only basic indexes existed -- Common query patterns not optimized - -**Solution:** -- Added composite index: `(tenant_id, user_id)` - Fast tenant-scoped lookups -- Added biometric_type index - Filter by type (FACE vs other biometrics) -- All indexes optimized for repository query patterns - ---- - -## Migration Changes Summary - -**File:** `alembic/versions/20251212_0001_initial_schema.py` - -### Table Schema (biometric_data) - -| Column | Type | Nullable | Default | Notes | -|--------|------|----------|---------|-------| -| `id` | UUID | No | gen_random_uuid() | Primary key | -| `tenant_id` | UUID | Yes | NULL | Multi-tenancy support | -| `user_id` | VARCHAR(255) | No | - | User identifier | -| `biometric_type` | VARCHAR(50) | No | 'FACE' | FACE, FINGERPRINT, etc. | -| `embedding` | vector(512) | No | - | Face embedding vector | -| `embedding_model` | VARCHAR(100) | No | 'Facenet512' | Model used | -| `quality_score` | FLOAT | No | 0.0 | Enrollment quality (0-1) | -| `is_active` | BOOLEAN | No | TRUE | Active/deactivated | -| `is_primary` | BOOLEAN | No | TRUE | Primary embedding | -| `deleted_at` | TIMESTAMP | Yes | NULL | Soft delete | -| `created_at` | TIMESTAMP | No | now() | Creation time | -| `updated_at` | TIMESTAMP | No | now() | Last update time | - -### Indexes Created - -1. **Primary Key:** `id` (UUID) -2. **Unique Constraint:** `(user_id, tenant_id, biometric_type)` WHERE `deleted_at IS NULL` -3. **Regular Indexes:** - - `ix_biometric_data_tenant_id` - Tenant lookups - - `ix_biometric_data_user_id` - User lookups - - `ix_biometric_data_tenant_user` - Combined tenant+user - - `ix_biometric_data_active` - Active records filter - - `ix_biometric_data_type` - Biometric type filter -4. **Vector Index:** `ix_biometric_data_embedding_hnsw` - Fast similarity search - ---- - -## Compatibility - -### Repository Methods ✅ All Compatible - -| Method | Schema Requirements | Status | -|--------|---------------------|--------| -| `save()` | user_id, tenant_id, embedding, quality_score, biometric_type, embedding_model, is_active, is_primary | ✅ All columns present | -| `find_by_user_id()` | SELECT embedding WHERE user_id, tenant_id, is_active, deleted_at | ✅ Works | -| `find_similar()` | Vector index, cosine distance operator | ✅ HNSW index created | -| `delete()` | UPDATE deleted_at, is_active | ✅ Soft delete supported | -| `exists()` | Check user_id, tenant_id, is_active, deleted_at | ✅ Works | -| `count()` | COUNT where tenant_id, is_active, deleted_at | ✅ Works | - ---- - -## Performance Expectations - -### Query Performance (with HNSW index) - -| Operation | Dataset Size | Expected Time | Notes | -|-----------|-------------|---------------|-------| -| Enroll (save) | Any | < 50ms | Single INSERT | -| Verify (1:1) | Any | < 10ms | Indexed lookup | -| Search (1:N) | 1,000 faces | < 5ms | HNSW approximate | -| Search (1:N) | 10,000 faces | < 10ms | HNSW approximate | -| Search (1:N) | 100,000 faces | < 20ms | HNSW approximate | -| Search (1:N) | 1,000,000 faces | < 50ms | HNSW approximate | - -**Note:** HNSW provides ~95% recall with these parameters (m=16, ef_construction=64) - -### Index Build Time - -| Dataset Size | HNSW Build Time | IVFFlat Build Time | -|--------------|-----------------|-------------------| -| 1,000 faces | ~1 second | ~0.5 seconds | -| 10,000 faces | ~10 seconds | ~2 seconds | -| 100,000 faces | ~2 minutes | ~10 seconds | -| 1,000,000 faces | ~20 minutes | ~1 minute | - -**Recommendation:** Use HNSW for best query performance unless dataset is very large (> 1M faces), then consider IVFFlat. - ---- - -## Deployment Steps - -### 1. Apply Migration - -```bash -# Start PostgreSQL -docker-compose up -d postgres - -# Wait for health check -docker-compose ps postgres - -# Apply migration -alembic upgrade head - -# Verify -alembic current -# Should show: 0001_initial (head) -``` - -### 2. Verify Schema - -```bash -# Connect to database -docker-compose exec postgres psql -U biometric -d biometric - -# Check table structure -\d biometric_data - -# Verify vector index -\di ix_biometric_data_embedding_hnsw - -# Check for unique constraint -\d+ biometric_data -``` - -**Expected output for vector index:** -``` - ix_biometric_data_embedding_hnsw | index | biometric | biometric_data | 512 kB | -``` - -### 3. Enable in Application - -```bash -# Update .env -USE_PGVECTOR=True -DATABASE_URL=postgresql://biometric:biometric@localhost:5432/biometric -EMBEDDING_DIMENSION=512 # Or 128 for Facenet (not Facenet512) - -# Restart API -uvicorn app.main:app --reload --host 0.0.0.0 --port 8001 -``` - -### 4. Verify Integration - -```bash -# Test enrollment -curl -X POST "http://localhost:8001/api/v1/enroll" \ - -F "file=@test_face.jpg" \ - -F "user_id=test_user_001" - -# Check database -docker-compose exec postgres psql -U biometric -d biometric -c \ - "SELECT user_id, biometric_type, embedding_model, quality_score FROM biometric_data;" -``` - ---- - -## Migration Rollback - -If you need to rollback: - -```bash -# Downgrade database -alembic downgrade -1 - -# Or reset completely -alembic downgrade base -``` - -**Warning:** This will **delete all data** in biometric_data table! - ---- - -## What's Next - -With database integration complete: - -1. ✅ **Production deployment** - All database code is production-ready -2. ✅ **Multi-tenancy** - Tenant isolation is built-in -3. ✅ **Scalability** - Vector indexes enable millions of faces -4. ✅ **High availability** - Can use PostgreSQL read replicas -5. ✅ **Backup/restore** - Standard PostgreSQL backup tools work - -### Recommended Next Steps - -1. **Set up automated backups:** - ```bash - # Example: Daily backups with pg_dump - docker-compose exec postgres pg_dump -U biometric biometric > backup_$(date +%Y%m%d).sql - ``` - -2. **Monitor vector index usage:** - ```sql - SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read - FROM pg_stat_user_indexes - WHERE indexname LIKE '%embedding%'; - ``` - -3. **Tune HNSW parameters for your workload:** - - Increase `m` (e.g., 32) for better recall at cost of space - - Increase `ef_construction` (e.g., 128) for better index quality - - Adjust `ef_search` at query time for accuracy vs speed tradeoff - -4. **Set up connection pooling** (already in code, just enable) - -5. **Configure replication** for high availability - ---- - -## Summary - -✅ **All database integration fixes applied** -✅ **Schema matches repository expectations** -✅ **Vector indexes created for performance** -✅ **Ready for production deployment** -✅ **Backward compatible** (can still use in-memory mode) - -**The biometric processor now has production-grade persistent storage!** 🎉 - ---- - -## Files Modified - -1. ✅ `alembic/versions/20251212_0001_initial_schema.py` - Complete rewrite - - Table renamed: `face_embeddings` → `biometric_data` - - Added missing columns: `biometric_type`, `embedding_model`, `is_primary`, `deleted_at` - - Changed embedding type: `ARRAY(Float)` → `vector(512)` - - Added HNSW vector index - - Added unique constraint for active embeddings - - Added optimized indexes for common queries - ---- - -**All database integration issues are now resolved!** ✨ diff --git a/docs/3-deployment/ENABLE_DATABASE.md b/docs/3-deployment/ENABLE_DATABASE.md deleted file mode 100644 index 56412c7..0000000 --- a/docs/3-deployment/ENABLE_DATABASE.md +++ /dev/null @@ -1,302 +0,0 @@ -# Enable PostgreSQL Database - Quick Start Guide - -This guide will help you switch from in-memory to PostgreSQL + pgvector storage. - -## Prerequisites - -- Docker and Docker Compose installed -- Port 5432 available -- ~500MB disk space for PostgreSQL - ---- - -## Step 1: Start PostgreSQL - -```bash -# Start PostgreSQL with pgvector -docker-compose up -d postgres - -# Wait for it to be healthy (30-60 seconds) -docker-compose ps postgres - -# Check logs -docker-compose logs postgres -``` - -**Expected output:** -``` -postgres | PostgreSQL init process complete; ready for start up -postgres | database system is ready to accept connections -``` - ---- - -## Step 2: Fix Schema Mismatch - -The migration creates `face_embeddings` but the repository expects `biometric_data`. - -**Option A: Update migration to create biometric_data (RECOMMENDED)** - -Edit: `alembic/versions/20251212_0001_initial_schema.py` - -Change line 60 from: -```python -op.create_table("face_embeddings", # ❌ OLD -``` - -To: -```python -op.create_table("biometric_data", # ✅ NEW -``` - -And update all references to `face_embeddings` to `biometric_data` in the migration. - -**Option B: Update repository to use face_embeddings** - -Edit: `app/infrastructure/persistence/repositories/pgvector_embedding_repository.py` - -Change all SQL queries from `biometric_data` to `face_embeddings`. - ---- - -## Step 3: Run Database Migrations - -```bash -# Apply schema -alembic upgrade head - -# Verify -alembic current -# Should show: 0001_initial (head) -``` - ---- - -## Step 4: Create Vector Index - -Connect to PostgreSQL and create the vector index: - -```bash -# Connect to database -docker-compose exec postgres psql -U biometric -d biometric - -# Create HNSW index for fast similarity search -CREATE INDEX IF NOT EXISTS idx_biometric_embedding_hnsw -ON biometric_data -USING hnsw (embedding vector_cosine_ops) -WITH (m = 16, ef_construction = 64); - -# Verify index -\di idx_biometric_embedding_hnsw - -# Exit -\q -``` - -**Index Options:** - -- **HNSW** (Hierarchical Navigable Small World): - - Best for: High accuracy, moderate dataset size - - Parameters: `m=16, ef_construction=64` - - Build time: Slower - - Query time: Faster - -- **IVFFlat** (Inverted File Flat): - - Best for: Large datasets, acceptable accuracy - - Parameters: `lists=100` (adjust based on dataset size) - - Build time: Faster - - Query time: Moderate - ---- - -## Step 5: Enable pgvector in Configuration - -```bash -# Create .env file if it doesn't exist -cp .env.example .env - -# Edit .env and set: -USE_PGVECTOR=True -DATABASE_URL=postgresql://biometric:biometric@localhost:5432/biometric -EMBEDDING_DIMENSION=128 # Match your FACE_RECOGNITION_MODEL - -# Model dimensions: -# - Facenet: 128 -# - Facenet512: 512 -# - VGG-Face: 2622 -# - ArcFace: 512 -``` - -**IMPORTANT:** `EMBEDDING_DIMENSION` must match your face recognition model! - ---- - -## Step 6: Restart the API - -```bash -# Stop current server (Ctrl+C) - -# Start with new configuration -uvicorn app.main:app --reload --host 0.0.0.0 --port 8001 -``` - -**Look for these log messages:** -``` -INFO: Creating PostgreSQL connection pool... -INFO: PostgreSQL connection pool created successfully -INFO: PgVectorEmbeddingRepository initialized -``` - -**NOT:** -``` -INFO: InMemoryEmbeddingRepository initialized # ❌ Wrong - still in-memory -``` - ---- - -## Step 7: Verify Database Integration - -### Test 1: Enroll a face - -```bash -curl -X POST "http://localhost:8001/api/v1/enroll" \ - -F "file=@test_face.jpg" \ - -F "user_id=test_user_001" -``` - -### Test 2: Check PostgreSQL - -```bash -docker-compose exec postgres psql -U biometric -d biometric -c \ - "SELECT user_id, quality_score, embedding_dimension FROM biometric_data;" -``` - -**Expected output:** -``` - user_id | quality_score | embedding_dimension -----------------+---------------+-------------------- - test_user_001 | 85.5 | 128 -``` - -### Test 3: Verify persistence - -```bash -# Restart the API -# Ctrl+C to stop -uvicorn app.main:app --reload --host 0.0.0.0 --port 8001 - -# Try to verify the enrolled user (should still exist!) -curl -X POST "http://localhost:8001/api/v1/verify" \ - -F "file=@test_face.jpg" \ - -F "user_id=test_user_001" -``` - -**Expected:** `"verified": true` ✅ - -With in-memory mode, this would fail after restart! - ---- - -## Troubleshooting - -### Issue: "connection refused" - -```bash -# Check if PostgreSQL is running -docker-compose ps postgres - -# Check logs -docker-compose logs postgres - -# Restart PostgreSQL -docker-compose restart postgres -``` - -### Issue: "pgvector extension not found" - -```bash -# Create extension manually -docker-compose exec postgres psql -U biometric -d biometric -c \ - "CREATE EXTENSION IF NOT EXISTS vector;" -``` - -### Issue: "Embedding dimension mismatch" - -Check your model vs configuration: - -```bash -# In .env, match these: -FACE_RECOGNITION_MODEL=Facenet # 128 dimensions -EMBEDDING_DIMENSION=128 # Must match! - -# Or: -FACE_RECOGNITION_MODEL=Facenet512 # 512 dimensions -EMBEDDING_DIMENSION=512 # Must match! -``` - -### Issue: "Table biometric_data does not exist" - -You didn't fix the schema mismatch. Go back to Step 2. - ---- - -## Performance Tuning - -### For large datasets (10K+ faces): - -```sql --- Optimize for large datasets -ALTER TABLE biometric_data SET (autovacuum_vacuum_scale_factor = 0.01); - --- Increase work_mem for index builds -SET work_mem = '256MB'; - --- Rebuild index -REINDEX INDEX idx_biometric_embedding_hnsw; -``` - -### Connection pool tuning: - -```bash -# In .env: -DATABASE_POOL_MIN_SIZE=20 # Increase for high traffic -DATABASE_POOL_MAX_SIZE=50 # Max concurrent connections -``` - ---- - -## Rollback to In-Memory - -If you need to go back to in-memory mode: - -```bash -# Edit .env -USE_PGVECTOR=False - -# Restart API -# Data in PostgreSQL is preserved, just not used -``` - ---- - -## Next Steps - -Once database integration is working: - -1. ✅ **Backup strategy**: Set up automated PostgreSQL backups -2. ✅ **Monitoring**: Add pgvector query performance monitoring -3. ✅ **Scaling**: Configure read replicas for high-traffic scenarios -4. ✅ **Index tuning**: Adjust HNSW parameters based on dataset size -5. ✅ **Multi-tenancy**: Test tenant isolation - ---- - -## Summary - -- ✅ PostgreSQL with pgvector provides persistent, scalable embedding storage -- ✅ Vector indexes enable fast similarity search (< 10ms for 1M faces) -- ✅ Multi-tenancy support for SaaS deployments -- ✅ ACID compliance for data consistency -- ✅ Production-ready with connection pooling - -**Your face embeddings will now persist across restarts!** 🎉 diff --git a/docs/4-testing/test-reports/BATCH_TESTING_GUIDE.md b/docs/4-testing/test-reports/BATCH_TESTING_GUIDE.md deleted file mode 100644 index fdb0450..0000000 --- a/docs/4-testing/test-reports/BATCH_TESTING_GUIDE.md +++ /dev/null @@ -1,178 +0,0 @@ -# Batch Processing Endpoints Testing Guide - -## Quick Fix for Current Issue - -### Problem -Batch endpoints fail with: `invalid input for query argument $3: [...] (expected str, got list)` - -### Solution (2 steps) - -**Step 1: Install pgvector package** -```bash -pip install pgvector>=0.2.4 -``` - -**Step 2: Update `app/infrastructure/persistence/repositories/pgvector_embedding_repository.py`** - -Find the `_setup_connection` method (around line 147) and update it: - -```python -async def _setup_connection(self, conn: asyncpg.Connection) -> None: - """Setup connection configuration for pgvector.""" - from pgvector.asyncpg import register_vector - - # Register vector type for pgvector extension - # This ensures vectors are properly handled by asyncpg - await register_vector(conn) # ADD THIS LINE - await conn.execute("SET statement_timeout = '30s'") - logger.debug(f"Configured connection {id(conn)} for pgvector") -``` - -**Step 3: Restart the API server** -```bash -# Stop the server (Ctrl+C) -# Then restart -uvicorn app.main:app --host 0.0.0.0 --port 8001 -``` - -## Quick Test Commands - -### Test 1: Single Enrollment -```bash -curl -X POST "http://localhost:8001/api/v1/enroll" \ - -F "file=@tests/fixtures/images/afuat/504494494_4335957489965886_7910713263520300979_n.jpg" \ - -F "user_id=test_afuat" -``` - -### Test 2: Batch Enroll (3 images for Afuat) -```bash -curl -X POST "http://localhost:8001/api/v1/batch/enroll" \ - -F "files=@tests/fixtures/images/afuat/profileImage_1200.jpg" \ - -F "files=@tests/fixtures/images/afuat/indir.jpg" \ - -F "files=@tests/fixtures/images/afuat/h02.jpg" \ - -F 'items=[{"user_id":"afuat_batch"},{"user_id":"afuat_batch"},{"user_id":"afuat_batch"}]' \ - -F "skip_duplicates=false" -``` - -### Test 3: Batch Verify -```bash -# First enroll if not already done -curl -X POST "http://localhost:8001/api/v1/enroll" \ - -F "file=@tests/fixtures/images/aga/h03.jpg" \ - -F "user_id=aga_verify_test" - -# Then verify with batch -curl -X POST "http://localhost:8001/api/v1/batch/verify" \ - -F "files=@tests/fixtures/images/aga/DSC_8476.jpg" \ - -F "files=@tests/fixtures/images/aga/indir.jpg" \ - -F 'items=[{"item_id":"v1","user_id":"aga_verify_test"},{"item_id":"v2","user_id":"aga_verify_test"}]' \ - -F "threshold=0.6" -``` - -## Available Test Images - -- **Afuat**: 10 images in `tests/fixtures/images/afuat/` -- **Aga**: 7 images in `tests/fixtures/images/aga/` -- **Ahab**: 2 images in `tests/fixtures/images/ahab/` - -## Endpoint Documentation - -### POST /api/v1/batch/enroll - -Enroll multiple face images in a single request. - -**Parameters:** -- `files` (required): List of image files -- `items` (required): JSON array of `{"user_id": "...", "tenant_id": "..."}` objects -- `skip_duplicates` (optional, default=true): Skip users that already exist - -**Response:** -```json -{ - "total_items": 3, - "successful": 3, - "failed": 0, - "skipped": 0, - "results": [ - { - "item_id": "user1", - "status": "success", - "data": {"enrollment_id": "uuid", "quality_score": 85.5}, - "error": null, - "error_code": null - } - ], - "message": "Batch enrollment completed: 3 successful, 0 failed, 0 skipped" -} -``` - -**Limits:** -- Max 50 images per batch -- Max 50MB total file size - -### POST /api/v1/batch/verify - -Verify multiple faces against enrolled users. - -**Parameters:** -- `files` (required): List of image files -- `items` (required): JSON array of `{"item_id": "...", "user_id": "...", "tenant_id": "..."}` objects -- `threshold` (optional, default=0.6): Similarity threshold (0.0-2.0) - -**Response:** -```json -{ - "total_items": 2, - "successful": 2, - "failed": 0, - "results": [ - { - "item_id": "verify1", - "status": "success", - "data": {"matched": true, "similarity": 0.85, "confidence": 95.2}, - "error": null, - "error_code": null - } - ], - "message": "Batch verification completed: 2 successful, 0 failed" -} -``` - -## Error Codes - -- `NO_FACE_DETECTED`: No face found in image -- `POOR_QUALITY`: Image quality below threshold -- `USER_NOT_FOUND`: User not enrolled (verification only) -- `REPOSITORY_ERROR`: Database operation failed -- `UNKNOWN_ERROR`: Other errors - -## Performance Tips - -1. **Batch vs Individual**: Batch requests save HTTP overhead and can be 30-50% faster -2. **Optimal Batch Size**: 5-20 images per batch for best balance -3. **Image Quality**: Higher quality images = faster processing + better accuracy -4. **Concurrent Batches**: API supports multiple concurrent batch requests - -## Common Issues - -### Issue: "Files count does not match items count" -**Solution**: Ensure array lengths match: `files.length === items.length` - -### Issue: "Invalid items JSON" -**Solution**: Check JSON syntax, use proper quotes: `'items=[{"user_id":"test"}]'` - -### Issue: "Batch size exceeds maximum" -**Solution**: Split into smaller batches (max 50 items) - -### Issue: "No face detected" -**Solution**: Use higher quality images with clear, frontal faces - -## Full Test Suite - -See `test_batch_results.md` for comprehensive test scenarios including: -- Single person batches -- Multi-person batches -- Error handling tests -- Performance comparisons -- Skip duplicates behavior -- Cross-person verification tests diff --git a/docs/4-testing/test-reports/DATABASE_TESTING_GUIDE.md b/docs/4-testing/test-reports/DATABASE_TESTING_GUIDE.md deleted file mode 100644 index 56d34c9..0000000 --- a/docs/4-testing/test-reports/DATABASE_TESTING_GUIDE.md +++ /dev/null @@ -1,524 +0,0 @@ -# Database Integration Testing Guide - -**Date:** 2025-12-24 -**Status:** Ready for Testing - -This guide helps you test the complete PostgreSQL + pgvector database integration. - ---- - -## 🚀 Quick Test (Automated) - -**One command to test everything:** - -```bash -./test_database_integration.sh -``` - -This script will: -1. ✅ Start PostgreSQL -2. ✅ Run migrations -3. ✅ Verify schema -4. ✅ Test CRUD operations -5. ✅ Test vector similarity search -6. ✅ Verify indexes and constraints - -**Expected output:** -``` -================================ -Database Integration Test -================================ - -✅ Docker is installed -✅ PostgreSQL is ready -✅ Migration completed successfully -✅ biometric_data table exists -✅ All required columns exist -✅ HNSW vector index exists -✅ INSERT successful -✅ SELECT successful -✅ Vector similarity search successful -✅ UPDATE successful -✅ Soft DELETE successful - -All database integration tests passed! 🎉 -``` - ---- - -## 📋 Manual Testing Steps - -If you prefer to test manually, follow these steps: - -### **Step 1: Start PostgreSQL** - -```bash -# Start PostgreSQL container -docker compose up -d postgres - -# Wait for it to be ready -docker compose ps postgres - -# Check logs -docker compose logs postgres -``` - -**Expected:** PostgreSQL should be running and healthy. - ---- - -### **Step 2: Verify pgvector Extension** - -```bash -# Connect to PostgreSQL -docker compose exec postgres psql -U biometric -d postgres - -# Check pgvector is available -SELECT * FROM pg_available_extensions WHERE name = 'vector'; - -# Exit -\q -``` - -**Expected:** You should see the `vector` extension listed. - ---- - -### **Step 3: Run Database Migration** - -```bash -# Check current migration status -alembic current - -# Run migration -alembic upgrade head - -# Verify migration applied -alembic current -``` - -**Expected output:** -``` -INFO [alembic.runtime.migration] Context impl PostgresqlImpl. -INFO [alembic.runtime.migration] Will assume transactional DDL. -INFO [alembic.runtime.migration] Running upgrade -> 0001_initial -``` - ---- - -### **Step 4: Verify Schema Created** - -```bash -# Connect to database -docker compose exec postgres psql -U biometric -d biometric - -# List tables -\dt - -# Describe biometric_data table -\d biometric_data - -# List indexes -\di -``` - -**Expected tables:** -- `biometric_data` ✅ -- `proctor_sessions` ✅ -- `proctor_incidents` ✅ -- `incident_evidence` ✅ - -**Expected columns in biometric_data:** -``` - Column | Type | Nullable | Default ------------------+--------------------------+----------+------------------- - id | uuid | not null | gen_random_uuid() - tenant_id | uuid | | - user_id | character varying(255) | not null | - biometric_type | character varying(50) | not null | 'FACE' - embedding | vector(512) | | - embedding_model | character varying(100) | not null | 'Facenet512' - quality_score | double precision | not null | - is_active | boolean | not null | true - is_primary | boolean | not null | true - deleted_at | timestamp with time zone | | - created_at | timestamp with time zone | not null | now() - updated_at | timestamp with time zone | not null | now() -``` - -**Expected indexes:** -- `ix_biometric_data_embedding_hnsw` (HNSW index) ✅ -- `ix_biometric_data_user_tenant_type_active` (Unique constraint) ✅ -- `ix_biometric_data_tenant_id` ✅ -- `ix_biometric_data_user_id` ✅ -- `ix_biometric_data_tenant_user` ✅ -- `ix_biometric_data_active` ✅ -- `ix_biometric_data_type` ✅ - ---- - -### **Step 5: Test CRUD Operations** - -```sql --- Still in psql - --- INSERT test data -INSERT INTO biometric_data ( - id, user_id, tenant_id, biometric_type, embedding_model, - quality_score, is_active, is_primary, embedding -) VALUES ( - gen_random_uuid(), - 'test_user_001', - NULL, - 'FACE', - 'Facenet512', - 0.95, - TRUE, - TRUE, - array_fill(0.5::float, ARRAY[512])::vector(512) -); - --- SELECT test data -SELECT user_id, biometric_type, embedding_model, quality_score -FROM biometric_data -WHERE user_id = 'test_user_001'; - --- Expected output: --- user_id | biometric_type | embedding_model | quality_score --- ---------------+----------------+-----------------+--------------- --- test_user_001 | FACE | Facenet512 | 0.95 - --- UPDATE test -UPDATE biometric_data -SET quality_score = 0.99 -WHERE user_id = 'test_user_001'; - --- Verify update -SELECT user_id, quality_score FROM biometric_data WHERE user_id = 'test_user_001'; - --- Soft DELETE test -UPDATE biometric_data -SET deleted_at = CURRENT_TIMESTAMP, is_active = FALSE -WHERE user_id = 'test_user_001'; - --- Verify soft delete -SELECT user_id, is_active, deleted_at FROM biometric_data WHERE user_id = 'test_user_001'; -``` - ---- - -### **Step 6: Test Vector Similarity Search** - -```sql --- Test cosine distance operator -SELECT - user_id, - embedding <=> array_fill(0.5::float, ARRAY[512])::vector(512) AS distance -FROM biometric_data -WHERE deleted_at IS NULL -ORDER BY distance -LIMIT 5; - --- Expected: Results ordered by similarity (lowest distance = most similar) -``` - ---- - -### **Step 7: Test Vector Index Performance** - -```sql --- Explain query to verify index is used -EXPLAIN ANALYZE -SELECT user_id -FROM biometric_data -WHERE embedding <=> array_fill(0.5::float, ARRAY[512])::vector(512) < 0.5 -ORDER BY embedding <=> array_fill(0.5::float, ARRAY[512])::vector(512) -LIMIT 10; - --- Expected: Query plan should show "Index Scan using ix_biometric_data_embedding_hnsw" -``` - ---- - -### **Step 8: Clean Up Test Data** - -```sql --- Delete test data -DELETE FROM biometric_data WHERE user_id = 'test_user_001'; - --- Verify cleanup -SELECT COUNT(*) FROM biometric_data; - --- Exit psql -\q -``` - ---- - -## 🔧 Testing with the API - -### **Step 1: Enable pgvector** - -```bash -# Create or update .env file -echo "USE_PGVECTOR=True" >> .env -echo "DATABASE_URL=postgresql://biometric:biometric@localhost:5432/biometric" >> .env -echo "EMBEDDING_DIMENSION=512" >> .env # Must match your model -``` - -### **Step 2: Start the API** - -```bash -# Start API server -uvicorn app.main:app --reload --host 0.0.0.0 --port 8001 -``` - -**Look for these log messages:** -``` -INFO: Creating PostgreSQL connection pool... -INFO: PostgreSQL connection pool created successfully -INFO: PgVectorEmbeddingRepository initialized -``` - -**NOT:** -``` -INFO: InMemoryEmbeddingRepository initialized # ❌ Wrong - still using in-memory -``` - ---- - -### **Step 3: Test Enrollment (Save to Database)** - -```bash -# Enroll a test user -curl -X POST "http://localhost:8001/api/v1/enroll" \ - -F "file=@test_face.jpg" \ - -F "user_id=api_test_001" -``` - -**Expected response:** -```json -{ - "user_id": "api_test_001", - "enrolled": true, - "quality_score": 0.85 -} -``` - ---- - -### **Step 4: Verify Data in Database** - -```bash -# Check database -docker compose exec postgres psql -U biometric -d biometric -c \ - "SELECT user_id, biometric_type, embedding_model, quality_score FROM biometric_data WHERE user_id = 'api_test_001';" -``` - -**Expected:** -``` - user_id | biometric_type | embedding_model | quality_score -----------------+----------------+-----------------+--------------- - api_test_001 | FACE | Facenet512 | 0.85 -``` - ---- - -### **Step 5: Test Verification** - -```bash -# Verify the enrolled user -curl -X POST "http://localhost:8001/api/v1/verify" \ - -F "file=@test_face.jpg" \ - -F "user_id=api_test_001" -``` - -**Expected response:** -```json -{ - "verified": true, - "user_id": "api_test_001", - "similarity": 0.95, - "confidence": 0.98 -} -``` - ---- - -### **Step 6: Test Persistence (Critical!)** - -```bash -# Restart the API -# Press Ctrl+C to stop -# Then restart: -uvicorn app.main:app --reload --host 0.0.0.0 --port 8001 - -# Try verification again (should still work!) -curl -X POST "http://localhost:8001/api/v1/verify" \ - -F "file=@test_face.jpg" \ - -F "user_id=api_test_001" -``` - -**Expected:** `"verified": true` ✅ - -**With in-memory mode, this would FAIL after restart!** - ---- - -### **Step 7: Test Face Search (1:N)** - -```bash -# Search for similar faces -curl -X POST "http://localhost:8001/api/v1/search" \ - -F "file=@test_face.jpg" \ - -F "threshold=0.6" \ - -F "limit=5" -``` - -**Expected response:** -```json -{ - "matches": [ - { - "user_id": "api_test_001", - "similarity": 0.95, - "distance": 0.05 - } - ], - "count": 1 -} -``` - ---- - -## 🔍 Troubleshooting - -### **Issue: "connection refused"** - -```bash -# Check PostgreSQL is running -docker compose ps postgres - -# Check logs -docker compose logs postgres - -# Restart -docker compose restart postgres -``` - ---- - -### **Issue: "pgvector extension not found"** - -```bash -# Create extension manually -docker compose exec postgres psql -U biometric -d biometric -c \ - "CREATE EXTENSION IF NOT EXISTS vector;" -``` - ---- - -### **Issue: "Table biometric_data does not exist"** - -```bash -# Check current migration -alembic current - -# If not applied, run migration -alembic upgrade head -``` - ---- - -### **Issue: "Embedding dimension mismatch"** - -```bash -# Check your model configuration -grep FACE_MODEL .env -grep EMBEDDING_DIMENSION .env - -# Models and their dimensions: -# Facenet: 128 -# Facenet512: 512 -# VGG-Face: 2622 -# ArcFace: 512 - -# Make sure they match! -``` - ---- - -### **Issue: "Still using InMemoryEmbeddingRepository"** - -```bash -# Verify .env settings -cat .env | grep USE_PGVECTOR - -# Should show: USE_PGVECTOR=True - -# If not, add it: -echo "USE_PGVECTOR=True" >> .env - -# Restart API -``` - ---- - -## ✅ Success Criteria - -Database integration is successful if: - -1. ✅ PostgreSQL starts without errors -2. ✅ Migration runs successfully (alembic upgrade head) -3. ✅ biometric_data table exists with all columns -4. ✅ HNSW vector index exists -5. ✅ Can INSERT embeddings -6. ✅ Can SELECT embeddings -7. ✅ Vector similarity search works -8. ✅ API shows "PgVectorEmbeddingRepository initialized" -9. ✅ Enrollment saves to database -10. ✅ Verification works after API restart (persistence!) - ---- - -## 📊 Performance Benchmarks - -After successful integration, you should see: - -| Operation | Performance | Notes | -|-----------|-------------|-------| -| Enrollment (INSERT) | < 50ms | Single row insert | -| Verification (1:1) | < 10ms | Indexed lookup by user_id | -| Search (1:N, 1K faces) | < 5ms | HNSW index | -| Search (1:N, 100K faces) | < 20ms | HNSW approximate search | - ---- - -## 🎉 Next Steps - -Once database integration is verified: - -1. ✅ Set up automated backups -2. ✅ Configure monitoring (connection pool stats) -3. ✅ Test with large datasets (1M+ faces) -4. ✅ Tune HNSW parameters if needed -5. ✅ Set up read replicas for high availability - ---- - -## 📚 Additional Resources - -- **Port Standards:** See `PORT_STANDARDS.md` -- **Database Setup:** See `ENABLE_DATABASE.md` -- **Fixes Applied:** See `DATABASE_FIXES_APPLIED.md` -- **Automated Test Script:** `./test_database_integration.sh` - ---- - -**Ready to test? Run:** - -```bash -./test_database_integration.sh -``` - -**Or test manually following the steps above!** 🚀 diff --git a/docs/4-testing/test-reports/MANUAL_TESTING_GUIDE.md b/docs/4-testing/test-reports/MANUAL_TESTING_GUIDE.md deleted file mode 100644 index f064f0e..0000000 --- a/docs/4-testing/test-reports/MANUAL_TESTING_GUIDE.md +++ /dev/null @@ -1,413 +0,0 @@ -# Manual Testing Guide - Biometric Processor API - -## Server Status ✅ -**Server is running on:** http://localhost:8001 - ---- - -## Quick Start Methods - -### 1. **Swagger UI (Easiest!)** -Open in your browser: **http://localhost:8001/docs** - -This gives you a complete interactive interface where you can: -- ✅ See all endpoints -- ✅ Test requests directly -- ✅ Upload files -- ✅ View responses -- ✅ See request/response schemas - -**Steps:** -1. Click on any endpoint to expand it -2. Click "Try it out" -3. Fill in parameters -4. Click "Execute" -5. See the response below - ---- - -## 2. Test with cURL - -### Health Check -```bash -curl http://localhost:8001/api/v1/health -``` - -**Expected Response:** -```json -{ - "status": "healthy", - "version": "1.0.0", - "model": "Facenet", - "detector": "opencv" -} -``` - -### Root Endpoint -```bash -curl http://localhost:8001/ -``` - -### Enroll Face -```bash -curl -X POST "http://localhost:8001/api/v1/enroll" \ - -F "user_id=test_user_123" \ - -F "file=@C:/path/to/your/photo.jpg" -``` - -**Expected Response:** -```json -{ - "success": true, - "user_id": "test_user_123", - "quality_score": 85.5, - "message": "Face enrolled successfully", - "embedding_dimension": 128 -} -``` - -### Verify Face (same person) -```bash -curl -X POST "http://localhost:8001/api/v1/verify" \ - -F "user_id=test_user_123" \ - -F "file=@C:/path/to/verify/photo.jpg" -``` - -**Expected Response:** -```json -{ - "verified": true, - "confidence": 0.87, - "distance": 0.13, - "threshold": 0.6, - "message": "Face verified successfully" -} -``` - -### Check Liveness -```bash -curl -X POST "http://localhost:8001/api/v1/liveness" \ - -F "file=@C:/path/to/live/photo.jpg" -``` - -**Expected Response:** -```json -{ - "is_live": true, - "liveness_score": 75.8, - "challenge": "none", - "challenge_completed": true, - "message": "Liveness check passed" -} -``` - ---- - -## 3. Test with PowerShell Script - -Run the automated test: -```powershell -.\test_api.ps1 -``` - -When prompted, enter a path to a test image with a face. - ---- - -## 4. Test with PowerShell (Manual Commands) - -### Health Check -```powershell -Invoke-RestMethod -Uri "http://localhost:8001/api/v1/health" -Method Get | ConvertTo-Json -``` - -### Root Endpoint -```powershell -Invoke-RestMethod -Uri "http://localhost:8001/" -Method Get | ConvertTo-Json -``` - -### Enroll Face (PowerShell) -```powershell -$imagePath = "C:\path\to\your\photo.jpg" -$userId = "test_user_powershell" - -# Read image -$imageBytes = [System.IO.File]::ReadAllBytes($imagePath) -$imageFileName = Split-Path $imagePath -Leaf - -# Create form data -$boundary = [System.Guid]::NewGuid().ToString() -$LF = "`r`n" - -$bodyLines = @( - "--$boundary", - "Content-Disposition: form-data; name=`"user_id`"$LF", - $userId, - "--$boundary", - "Content-Disposition: form-data; name=`"file`"; filename=`"$imageFileName`"", - "Content-Type: image/jpeg$LF" -) -join $LF - -$bodyLines += $LF -$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($bodyLines) -$bodyBytes += $imageBytes -$bodyBytes += [System.Text.Encoding]::UTF8.GetBytes("$LF--$boundary--$LF") - -# Make request -$response = Invoke-RestMethod ` - -Uri "http://localhost:8001/api/v1/enroll" ` - -Method Post ` - -ContentType "multipart/form-data; boundary=$boundary" ` - -Body $bodyBytes - -$response | ConvertTo-Json -``` - ---- - -## 5. Test with Python - -### Install requests -```bash -pip install requests -``` - -### Test Script (test_manual.py) -```python -import requests -import json - -BASE_URL = "http://localhost:8001/api/v1" - -# 1. Health Check -print("Testing Health Endpoint...") -response = requests.get(f"{BASE_URL}/health") -print(f"Status: {response.status_code}") -print(json.dumps(response.json(), indent=2)) -print() - -# 2. Enroll Face -print("Testing Enrollment...") -with open("path/to/your/photo.jpg", "rb") as f: - files = {"file": f} - data = {"user_id": "test_user_python"} - response = requests.post(f"{BASE_URL}/enroll", files=files, data=data) - print(f"Status: {response.status_code}") - print(json.dumps(response.json(), indent=2)) - print() - -# 3. Verify Face -print("Testing Verification...") -with open("path/to/verify/photo.jpg", "rb") as f: - files = {"file": f} - data = {"user_id": "test_user_python"} - response = requests.post(f"{BASE_URL}/verify", files=files, data=data) - print(f"Status: {response.status_code}") - print(json.dumps(response.json(), indent=2)) - print() - -# 4. Liveness Check -print("Testing Liveness...") -with open("path/to/live/photo.jpg", "rb") as f: - files = {"file": f} - response = requests.post(f"{BASE_URL}/liveness", files=files) - print(f"Status: {response.status_code}") - print(json.dumps(response.json(), indent=2)) -``` - -Run: -```bash -python test_manual.py -``` - ---- - -## 6. Test with JavaScript/Node.js - -### Install axios -```bash -npm install axios form-data -``` - -### Test Script (test_manual.js) -```javascript -const axios = require('axios'); -const FormData = require('form-data'); -const fs = require('fs'); - -const BASE_URL = 'http://localhost:8001/api/v1'; - -async function testAPI() { - // 1. Health Check - console.log('Testing Health Endpoint...'); - const health = await axios.get(`${BASE_URL}/health`); - console.log('Status:', health.status); - console.log(JSON.stringify(health.data, null, 2)); - console.log(); - - // 2. Enroll Face - console.log('Testing Enrollment...'); - const enrollForm = new FormData(); - enrollForm.append('user_id', 'test_user_nodejs'); - enrollForm.append('file', fs.createReadStream('path/to/your/photo.jpg')); - - const enroll = await axios.post(`${BASE_URL}/enroll`, enrollForm, { - headers: enrollForm.getHeaders() - }); - console.log('Status:', enroll.status); - console.log(JSON.stringify(enroll.data, null, 2)); - console.log(); - - // 3. Verify Face - console.log('Testing Verification...'); - const verifyForm = new FormData(); - verifyForm.append('user_id', 'test_user_nodejs'); - verifyForm.append('file', fs.createReadStream('path/to/verify/photo.jpg')); - - const verify = await axios.post(`${BASE_URL}/verify`, verifyForm, { - headers: verifyForm.getHeaders() - }); - console.log('Status:', verify.status); - console.log(JSON.stringify(verify.data, null, 2)); -} - -testAPI().catch(console.error); -``` - -Run: -```bash -node test_manual.js -``` - ---- - -## 7. Test with Browser DevTools - -1. Open **http://localhost:8001/docs** -2. Open Browser DevTools (F12) -3. Go to **Network** tab -4. Execute requests in Swagger UI -5. Inspect HTTP requests/responses in Network tab - ---- - -## Available Endpoints - -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/` | Root service info | -| GET | `/api/v1/health` | Health check | -| POST | `/api/v1/enroll` | Enroll a face | -| POST | `/api/v1/verify` | Verify a face (1:1) | -| POST | `/api/v1/search` | Search for similar faces (1:N) | -| POST | `/api/v1/liveness` | Check liveness | -| POST | `/api/v1/batch/enroll` | Batch enrollment | -| POST | `/api/v1/batch/verify` | Batch verification | -| POST | `/api/v1/card-type/detect-live` | Card type detection | - ---- - -## Expected Status Codes - -| Code | Meaning | -|------|---------| -| 200 | Success | -| 400 | Bad Request (invalid input, no face detected, poor quality) | -| 404 | Not Found (user not enrolled) | -| 422 | Validation Error (missing/invalid parameters) | -| 500 | Internal Server Error | - ---- - -## Common Test Scenarios - -### ✅ Successful Enrollment -1. Use clear photo with one face -2. Good lighting -3. Face clearly visible -4. Returns 200 with embedding info - -### ❌ Face Not Detected -1. Use photo without face -2. Returns 400 with "No face detected" message - -### ❌ Poor Quality -1. Use blurry photo -2. Returns 400 with "Poor image quality" message - -### ❌ Multiple Faces -1. Use photo with multiple faces -2. Returns 400 with "Multiple faces detected" message - -### ✅ Successful Verification -1. Enroll a face first -2. Verify with same person's different photo -3. Returns 200 with verified=true - -### ❌ User Not Enrolled -1. Try to verify without enrolling -2. Returns 404 with "User not enrolled" message - ---- - -## Troubleshooting - -### Server not responding? -- Check if server is running: `curl http://localhost:8001/` -- Check logs in the terminal where server is running - -### 400 Bad Request? -- Check image file is valid JPEG/PNG -- Ensure face is clearly visible -- Check image quality (not too blurry) - -### 422 Validation Error? -- Check required parameters are provided -- Verify parameter names are correct - -### File upload fails? -- Check file path is correct -- Ensure file exists and is readable - ---- - -## Tips for Best Results - -### For Face Images: -- ✅ Front-facing face -- ✅ Good lighting -- ✅ Clear, not blurry -- ✅ Only one face in image -- ✅ Face size at least 80x80 pixels -- ❌ Avoid sunglasses -- ❌ Avoid face masks -- ❌ Avoid extreme angles - ---- - -## Server Controls - -### Start Server -```powershell -.\.venv\Scripts\activate -python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8001 -``` - -### Stop Server -Press `Ctrl+C` in the terminal - -### View Logs -Logs appear in the terminal where server is running - ---- - -## Links - -- **Swagger UI**: http://localhost:8001/docs -- **ReDoc**: http://localhost:8001/redoc -- **OpenAPI JSON**: http://localhost:8001/openapi.json -- **Health Check**: http://localhost:8001/api/v1/health - ---- - -**Happy Testing! 🧪** diff --git a/docs/6-architecture/BATCH_ENDPOINTS_ARCHITECTURE.md b/docs/6-architecture/BATCH_ENDPOINTS_ARCHITECTURE.md deleted file mode 100644 index 7051bb4..0000000 --- a/docs/6-architecture/BATCH_ENDPOINTS_ARCHITECTURE.md +++ /dev/null @@ -1,416 +0,0 @@ -# Batch Processing Endpoints - Architecture & Data Flow - -## System Architecture - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ CLIENT REQUEST │ -│ POST /api/v1/batch/enroll │ -│ - files: [image1.jpg, image2.jpg, ...] │ -│ - items: [{"user_id": "user1"}, {"user_id": "user2"}, ...] │ -│ - skip_duplicates: true/false │ -└───────────────────────────┬─────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ API ROUTE LAYER │ -│ app/api/routes/batch.py │ -│ │ -│ ✓ Validate batch size (max 50) │ -│ ✓ Validate total file size (max 50MB) │ -│ ✓ Parse JSON items │ -│ ✓ Validate file count = items count │ -│ ✓ Save files to temp storage │ -└───────────────────────────┬─────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ USE CASE LAYER │ -│ app/application/use_cases/batch_process.py │ -│ │ -│ BatchEnrollmentUseCase.execute(): │ -│ ┌──────────────────────────────────────────────────┐ │ -│ │ For each item: │ │ -│ │ 1. Detect face (IFaceDetector) │ │ -│ │ 2. Extract embedding (IEmbeddingExtractor) │ │ -│ │ 3. Assess quality (IQualityAssessor) │ │ -│ │ 4. Save to repository (IEmbeddingRepository) │ │ -│ │ 5. Track result (success/failed/skipped) │ │ -│ └──────────────────────────────────────────────────┘ │ -└───────────────────────────┬─────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ REPOSITORY LAYER │ -│ app/infrastructure/persistence/repositories/ │ -│ pgvector_embedding_repository.py │ -│ │ -│ PgVectorEmbeddingRepository.save(): │ -│ ┌──────────────────────────────────────────────────┐ │ -│ │ 1. Convert numpy array to list │ │ -│ │ embedding_list = embedding.tolist() │ │ -│ │ │ │ -│ │ 2. Execute SQL INSERT/UPDATE │ │ -│ │ INSERT INTO face_embeddings │ │ -│ │ VALUES ($1, $2, $3, $4) ◄── ISSUE HERE! │ │ -│ │ │ │ -│ │ $3 = embedding_list (Python list) │ │ -│ │ But asyncpg doesn't know how to convert │ │ -│ │ list → vector(512) without pgvector package │ │ -│ └──────────────────────────────────────────────────┘ │ -└───────────────────────────┬─────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ DATABASE LAYER │ -│ PostgreSQL with pgvector extension │ -│ │ -│ Table: face_embeddings │ -│ ┌────────────┬──────────────┬──────────────┐ │ -│ │ user_id │ embedding │ quality_score│ │ -│ │ (varchar) │ (vector(512))│ (float) │ │ -│ └────────────┴──────────────┴──────────────┘ │ -│ │ -│ ERROR: Cannot accept Python list for vector(512) column │ -│ Needs: pgvector type registration in asyncpg connection │ -└─────────────────────────────────────────────────────────────────┘ -``` - -## The Problem - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ CURRENT STATE (BROKEN) │ -└──────────────────────────────────────────────────────────────────┘ - -Python Code (Repository): - embedding_list = [0.1, 0.2, 0.3, ..., 0.512] # Python list - await conn.execute( - "INSERT INTO face_embeddings (embedding) VALUES ($1)", - embedding_list # asyncpg doesn't know this is a vector type! - ) - -asyncpg: - ❌ ERROR: "expected str, got list" - (Doesn't know how to convert Python list → PostgreSQL vector) - -PostgreSQL: - ❌ Never receives the data - (Type conversion fails at driver level) -``` - -## The Solution - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ FIXED STATE │ -└──────────────────────────────────────────────────────────────────┘ - -Step 1: Install pgvector package - pip install pgvector>=0.2.4 - -Step 2: Register vector type in connection setup - async def _setup_connection(self, conn: asyncpg.Connection): - from pgvector.asyncpg import register_vector - await register_vector(conn) # ← This registers the type handler - ... - -Step 3: Now asyncpg knows how to convert Python list → vector - embedding_list = [0.1, 0.2, ..., 0.512] - await conn.execute( - "INSERT INTO face_embeddings (embedding) VALUES ($1)", - embedding_list # asyncpg converts this to vector(512) ✓ - ) - -asyncpg: - ✓ Converts Python list → pgvector format - ✓ Sends to PostgreSQL as proper vector type - -PostgreSQL: - ✓ Receives data in correct format - ✓ Stores in vector(512) column - ✓ Can use vector indexes for similarity search -``` - -## Data Flow (After Fix) - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Batch Enrollment Request (3 images) │ -└──────────────────┬──────────────────────────────────────────────┘ - │ - ▼ - ┌──────────────────────────────────────────────┐ - │ Route: Validate & Save to temp storage │ - │ Time: ~100ms │ - └──────────────┬───────────────────────────────┘ - │ - ▼ - ┌──────────────────────────────────────────────┐ - │ Use Case: Process each image in parallel │ - │ ┌────────────┐ ┌────────────┐ ┌───────────┐│ - │ │ Image 1 │ │ Image 2 │ │ Image 3 ││ - │ │ │ │ │ │ ││ - │ │ Detect │ │ Detect │ │ Detect ││ - │ │ ~200ms │ │ ~200ms │ │ ~200ms ││ - │ │ │ │ │ │ ││ - │ │ Extract │ │ Extract │ │ Extract ││ - │ │ ~300ms │ │ ~300ms │ │ ~300ms ││ - │ │ │ │ │ │ ││ - │ │ Quality │ │ Quality │ │ Quality ││ - │ │ ~50ms │ │ ~50ms │ │ ~50ms ││ - │ │ │ │ │ │ ││ - │ │ Save DB │ │ Save DB │ │ Save DB ││ - │ │ ~10ms │ │ ~10ms │ │ ~10ms ││ - │ └────┬───────┘ └─────┬──────┘ └─────┬─────┘│ - │ │ │ │ │ - │ └───────────────┴──────────────┘ │ - │ Total: ~560ms (parallel) vs ~1680ms (seq) │ - └──────────────┬───────────────────────────────┘ - │ - ▼ - ┌──────────────────────────────────────────────┐ - │ Response: Aggregated results │ - │ { │ - │ "total_items": 3, │ - │ "successful": 3, │ - │ "failed": 0, │ - │ "results": [...] │ - │ } │ - └───────────────────────────────────────────────┘ -``` - -## Performance Comparison - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ INDIVIDUAL REQUESTS (3 enrollments) │ -└──────────────────────────────────────────────────────────────────┘ - -Request 1: POST /enroll (image1) -├─ HTTP overhead: 50ms -├─ Processing: 560ms -└─ Total: 610ms - -Request 2: POST /enroll (image2) -├─ HTTP overhead: 50ms -├─ Processing: 560ms -└─ Total: 610ms - -Request 3: POST /enroll (image3) -├─ HTTP overhead: 50ms -├─ Processing: 560ms -└─ Total: 610ms - -TOTAL TIME: 1830ms (sequential) - -┌──────────────────────────────────────────────────────────────────┐ -│ BATCH REQUEST (3 enrollments) │ -└──────────────────────────────────────────────────────────────────┘ - -Single Request: POST /batch/enroll (3 images) -├─ HTTP overhead: 50ms -├─ Processing (parallel): 560ms -└─ Total: 610ms - -TOTAL TIME: 610ms - -IMPROVEMENT: 67% faster (1220ms saved) -``` - -## Request/Response Format - -### Batch Enrollment Request -``` -POST /api/v1/batch/enroll -Content-Type: multipart/form-data - ---boundary -Content-Disposition: form-data; name="files"; filename="image1.jpg" -Content-Type: image/jpeg - - ---boundary -Content-Disposition: form-data; name="files"; filename="image2.jpg" -Content-Type: image/jpeg - - ---boundary -Content-Disposition: form-data; name="items" - -[ - {"user_id": "user1", "tenant_id": "tenant1"}, - {"user_id": "user2", "tenant_id": "tenant1"} -] ---boundary -Content-Disposition: form-data; name="skip_duplicates" - -false ---boundary-- -``` - -### Batch Enrollment Response -```json -{ - "total_items": 2, - "successful": 2, - "failed": 0, - "skipped": 0, - "results": [ - { - "item_id": "user1", - "status": "success", - "data": { - "enrollment_id": "uuid-1234", - "quality_score": 87.3, - "face_detected": true - }, - "error": null, - "error_code": null - }, - { - "item_id": "user2", - "status": "success", - "data": { - "enrollment_id": "uuid-5678", - "quality_score": 92.1, - "face_detected": true - }, - "error": null, - "error_code": null - } - ], - "message": "Batch enrollment completed: 2 successful, 0 failed, 0 skipped" -} -``` - -### Batch Verification Request -``` -POST /api/v1/batch/verify -Content-Type: multipart/form-data - -[Similar to enrollment, with additional item_id field] -items: [ - {"item_id": "verify1", "user_id": "user1"}, - {"item_id": "verify2", "user_id": "user2"} -] -threshold: 0.6 -``` - -### Batch Verification Response -```json -{ - "total_items": 2, - "successful": 2, - "failed": 0, - "results": [ - { - "item_id": "verify1", - "status": "success", - "data": { - "matched": true, - "similarity": 0.85, - "confidence": 95.2, - "user_id": "user1" - }, - "error": null, - "error_code": null - }, - { - "item_id": "verify2", - "status": "success", - "data": { - "matched": false, - "similarity": 0.42, - "confidence": 12.5, - "user_id": "user2" - }, - "error": null, - "error_code": null - } - ], - "message": "Batch verification completed: 2 successful, 0 failed" -} -``` - -## Error Scenarios - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Scenario: Mismatched file/items count │ -├─────────────────────────────────────────────────────────────────┤ -│ Request: 3 files, 2 items │ -│ Response: { │ -│ "total_items": 0, │ -│ "successful": 0, │ -│ "failed": 0, │ -│ "message": "Files count (3) does not match items count (2)" │ -│ } │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ Scenario: Partial failures in batch │ -├─────────────────────────────────────────────────────────────────┤ -│ Request: 3 images (1 no face, 1 poor quality, 1 good) │ -│ Response: { │ -│ "total_items": 3, │ -│ "successful": 1, │ -│ "failed": 2, │ -│ "results": [ │ -│ { │ -│ "status": "failed", │ -│ "error": "No face detected", │ -│ "error_code": "NO_FACE_DETECTED" │ -│ }, │ -│ { │ -│ "status": "failed", │ -│ "error": "Quality check failed: score=65.2", │ -│ "error_code": "POOR_QUALITY" │ -│ }, │ -│ { │ -│ "status": "success", │ -│ "data": {...} │ -│ } │ -│ ] │ -│ } │ -└─────────────────────────────────────────────────────────────────┘ -``` - -## Security Features - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ SECURITY LAYERS │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ 1. DoS Protection │ -│ ✓ Max batch size: 50 items │ -│ ✓ Max total file size: 50MB │ -│ ✓ Request timeout: 30s per operation │ -│ │ -│ 2. Input Validation │ -│ ✓ File type validation (images only) │ -│ ✓ JSON schema validation (Pydantic) │ -│ ✓ File count = items count enforcement │ -│ │ -│ 3. SQL Injection Prevention │ -│ ✓ Parameterized queries only │ -│ ✓ No string concatenation in SQL │ -│ ✓ Type-safe database operations │ -│ │ -│ 4. Resource Management │ -│ ✓ Automatic temp file cleanup (finally blocks) │ -│ ✓ Connection pooling (prevents exhaustion) │ -│ ✓ Async operations (non-blocking) │ -│ │ -│ 5. Error Handling │ -│ ✓ No sensitive data in error messages │ -│ ✓ Machine-readable error codes │ -│ ✓ Graceful degradation (partial success) │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -**Note:** All diagrams reflect the system state after applying the pgvector fix. diff --git a/docs/6-architecture/DESIGN_ANALYSIS_AND_IMPROVEMENTS.md b/docs/6-architecture/DESIGN_ANALYSIS_AND_IMPROVEMENTS.md deleted file mode 100644 index b122f05..0000000 --- a/docs/6-architecture/DESIGN_ANALYSIS_AND_IMPROVEMENTS.md +++ /dev/null @@ -1,2173 +0,0 @@ -# Biometric Processor - Professional Design Analysis & Improvements - -**Document Version**: 2.0 -**Created**: 2025-11-17 -**Analysis Type**: SOLID, Design Patterns, Software Engineering Principles -**Status**: ✅ COMPREHENSIVE REVIEW COMPLETE - ---- - -## 📋 Table of Contents - -1. [Executive Summary](#executive-summary) -2. [Current Design Analysis](#current-design-analysis) -3. [SOLID Principles Analysis](#solid-principles-analysis) -4. [Design Patterns Analysis](#design-patterns-analysis) -5. [Software Engineering Principles](#software-engineering-principles) -6. [Architectural Issues & Improvements](#architectural-issues--improvements) -7. [Improved Architecture Design](#improved-architecture-design) -8. [Implementation Plan](#implementation-plan) -9. [Migration Strategy](#migration-strategy) - ---- - -## 🎯 Executive Summary - -### Current State -- ✅ **Working MVP**: Basic face enrollment and verification functional -- ⚠️ **Architecture**: Violates multiple SOLID principles -- ⚠️ **Design Patterns**: Missing critical patterns for scalability -- ⚠️ **Code Quality**: Tight coupling, no dependency injection, poor abstraction - -### Critical Issues Identified - -| Category | Issue | Severity | Impact | -|----------|-------|----------|--------| -| **SOLID** | Tight coupling between API and ML models | 🔴 CRITICAL | Hard to test, maintain, extend | -| **SOLID** | Service layer doing too many responsibilities | 🔴 CRITICAL | Violates SRP | -| **Architecture** | No abstraction layer for ML models | 🔴 CRITICAL | Can't swap models | -| **Architecture** | Missing models/schemas module | 🔴 CRITICAL | Import errors | -| **Patterns** | No dependency injection | 🟠 HIGH | Tight coupling | -| **Patterns** | No repository pattern | 🟠 HIGH | No data access abstraction | -| **Patterns** | No factory pattern for models | 🟠 HIGH | Hard-coded dependencies | -| **Security** | Allow all CORS origins | 🔴 CRITICAL | Security vulnerability | -| **Error Handling** | Inconsistent error handling | 🟠 HIGH | Poor user experience | -| **Observability** | Basic logging only | 🟡 MEDIUM | Hard to debug production | - -### Recommendations - -1. **Immediate Actions** (Sprint 1): - - Implement proper layered architecture - - Add dependency injection container - - Create abstraction interfaces for ML models - - Fix missing models/schemas module - - Implement proper CORS configuration - -2. **Short-term** (Sprint 2-3): - - Implement repository pattern - - Add factory pattern for ML models - - Implement comprehensive error handling - - Add structured logging and metrics - -3. **Long-term** (Sprint 4+): - - Add async processing with Celery - - Implement caching strategy - - Add comprehensive testing - - Performance optimization - ---- - -## 📊 Current Design Analysis - -### Current Project Structure -``` -app/ -├── __init__.py -├── main.py # FastAPI app initialization -├── api/ -│ ├── __init__.py -│ └── endpoints/ -│ ├── __init__.py -│ └── face.py # API endpoints -├── core/ -│ ├── __init__.py -│ └── config.py # Configuration -└── services/ - ├── __init__.py - └── face_recognition.py # Business logic + ML -``` - -### Current Code Issues - -#### ❌ Issue 1: Tight Coupling -**Location**: `app/api/endpoints/face.py:9` -```python -from app.services.face_recognition import face_recognition_service -``` -- API directly depends on concrete service implementation -- Singleton pattern used incorrectly (global instance) -- Impossible to mock for testing -- Violates Dependency Inversion Principle - -#### ❌ Issue 2: Single Responsibility Violation -**Location**: `app/services/face_recognition.py` -```python -class FaceRecognitionService: - def extract_embedding(...) # ML operation - def verify_faces(...) # ML operation + business logic - def validate_image(...) # Validation logic - def _calculate_cosine_distance(...) # Math operation -``` -- One class doing: ML operations, validation, calculations, business logic -- Should be split into multiple focused classes - -#### ❌ Issue 3: No Abstraction Layer -**Location**: `app/services/face_recognition.py:24` -```python -DeepFace.build_model(settings.FACE_RECOGNITION_MODEL) -``` -- Direct dependency on DeepFace library -- Can't swap to different ML framework -- Violates Open/Closed Principle - -#### ❌ Issue 4: Missing Models/Schemas -**Location**: `app/api/endpoints/face.py:8` -```python -from app.models.schemas import FaceEnrollResponse, FaceVerificationResponse -``` -- Module doesn't exist -- Will cause import error -- No Pydantic models defined - -#### ❌ Issue 5: Security Vulnerability -**Location**: `app/main.py:24` -```python -allow_origins=["*"] -``` -- Allows requests from any origin -- CSRF vulnerability -- Production security risk - -#### ❌ Issue 6: No Error Handling Strategy -- Generic `Exception` catching everywhere -- No custom exception types -- No error codes or standardized responses -- HTTPException with string messages - -#### ❌ Issue 7: No Dependency Injection -- Services instantiated as singletons -- No IoC container -- Hard to test -- Hard to manage dependencies - ---- - -## 🏛️ SOLID Principles Analysis - -### S - Single Responsibility Principle - -#### ❌ VIOLATIONS - -**1. FaceRecognitionService (app/services/face_recognition.py)** -```python -class FaceRecognitionService: - # Responsibility 1: ML Model Management - def __init__(self): - DeepFace.build_model(...) - - # Responsibility 2: Embedding Extraction - def extract_embedding(self, image_path: str): - ... - - # Responsibility 3: Face Verification - def verify_faces(self, image_path: str, stored_embedding_json: str): - ... - - # Responsibility 4: Image Validation - def validate_image(self, image_path: str): - ... - - # Responsibility 5: Distance Calculation - def _calculate_cosine_distance(self, emb1, emb2): - ... -``` - -**Issues**: -- 5 different responsibilities in one class -- Mixing infrastructure (ML models) with business logic -- Mixing validation with processing - -**✅ SOLUTION**: -```python -# Split into focused classes -class IFaceDetector(Protocol): - def detect(self, image: np.ndarray) -> FaceDetectionResult: ... - -class IEmbeddingExtractor(Protocol): - def extract(self, face_image: np.ndarray) -> np.ndarray: ... - -class IImageValidator(Protocol): - def validate(self, image_path: str) -> ValidationResult: ... - -class ISimilarityCalculator(Protocol): - def calculate(self, emb1: np.ndarray, emb2: np.ndarray) -> float: ... - -class FaceEnrollmentService: - """Business logic for enrollment - single responsibility""" - def __init__( - self, - detector: IFaceDetector, - extractor: IEmbeddingExtractor, - validator: IImageValidator - ): - self._detector = detector - self._extractor = extractor - self._validator = validator -``` - -**2. API Endpoints (app/api/endpoints/face.py)** -```python -@router.post("/face/enroll") -async def enroll_face(file: UploadFile = File(...)): - # Responsibility 1: File handling - with open(temp_file_path, "wb") as buffer: - ... - - # Responsibility 2: Validation - is_valid, error_msg = face_recognition_service.validate_image(...) - - # Responsibility 3: Business logic - success, embedding_json, error = face_recognition_service.extract_embedding(...) - - # Responsibility 4: Error handling - if not success: - raise HTTPException(...) - - # Responsibility 5: Cleanup - os.remove(temp_file_path) -``` - -**Issues**: -- Endpoint doing file I/O, validation, business logic, cleanup -- Should only orchestrate, not implement - -**✅ SOLUTION**: -```python -@router.post("/face/enroll") -async def enroll_face( - file: UploadFile = File(...), - enrollment_service: FaceEnrollmentService = Depends(get_enrollment_service), - file_storage: IFileStorage = Depends(get_file_storage) -): - # Only orchestration - delegate responsibilities - image_path = await file_storage.save_temp(file) - try: - result = await enrollment_service.enroll(image_path) - return FaceEnrollResponse.from_domain(result) - finally: - await file_storage.cleanup(image_path) -``` - -### O - Open/Closed Principle - -#### ❌ VIOLATIONS - -**Hard-coded DeepFace dependency**: -```python -# app/services/face_recognition.py:24 -DeepFace.build_model(settings.FACE_RECOGNITION_MODEL) -``` - -**Issues**: -- Can't extend to use different ML frameworks (FaceNet, ArcFace, Dlib) -- Must modify source code to change models -- Closed for extension - -**✅ SOLUTION**: -```python -# Abstract interface -class IFaceRecognitionModel(Protocol): - def detect_face(self, image: np.ndarray) -> FaceDetectionResult: ... - def extract_embedding(self, face: np.ndarray) -> np.ndarray: ... - -# Concrete implementations -class DeepFaceModel(IFaceRecognitionModel): - def detect_face(self, image: np.ndarray) -> FaceDetectionResult: - return DeepFace.extract_faces(...) - - def extract_embedding(self, face: np.ndarray) -> np.ndarray: - return DeepFace.represent(...) - -class FaceNetModel(IFaceRecognitionModel): - def detect_face(self, image: np.ndarray) -> FaceDetectionResult: - # Different implementation - ... - - def extract_embedding(self, face: np.ndarray) -> np.ndarray: - # Different implementation - ... - -# Factory for creation -class FaceModelFactory: - @staticmethod - def create(model_type: str) -> IFaceRecognitionModel: - if model_type == "deepface": - return DeepFaceModel() - elif model_type == "facenet": - return FaceNetModel() - else: - raise ValueError(f"Unknown model: {model_type}") -``` - -Now you can add new models without modifying existing code! - -### L - Liskov Substitution Principle - -#### ✅ CURRENTLY NOT VIOLATED -- No inheritance hierarchy exists yet -- No polymorphic behavior - -#### ⚠️ POTENTIAL ISSUES -When we add model abstractions, ensure: -```python -# Bad - violates LSP -class BaseDetector: - def detect(self, image: np.ndarray) -> List[Face]: - pass - -class MTCNNDetector(BaseDetector): - def detect(self, image: np.ndarray) -> List[Face]: - # Returns different structure - VIOLATION - return {"faces": [...], "landmarks": [...]} # ❌ - -# Good - maintains contract -class MTCNNDetector(BaseDetector): - def detect(self, image: np.ndarray) -> List[Face]: - # Returns same structure - return [Face(...), Face(...)] # ✅ -``` - -### I - Interface Segregation Principle - -#### ❌ VIOLATIONS - -**Fat interface potential**: -```python -# Current monolithic service -class FaceRecognitionService: - def extract_embedding(...) - def verify_faces(...) - def validate_image(...) - def _calculate_cosine_distance(...) -``` - -**Issues**: -- Clients that only need embedding extraction get verification methods too -- Clients that only need validation get ML methods too - -**✅ SOLUTION**: -```python -# Split into focused interfaces -class IImageValidator(Protocol): - def validate(self, image_path: str) -> ValidationResult: ... - -class IEmbeddingExtractor(Protocol): - def extract(self, image: np.ndarray) -> np.ndarray: ... - -class IFaceVerifier(Protocol): - def verify(self, embedding1: np.ndarray, embedding2: np.ndarray) -> VerificationResult: ... - -# Clients depend only on what they need -class EnrollmentEndpoint: - def __init__(self, validator: IImageValidator, extractor: IEmbeddingExtractor): - # Only gets what it needs - ... - -class VerificationEndpoint: - def __init__(self, validator: IImageValidator, verifier: IFaceVerifier): - # Only gets what it needs - ... -``` - -### D - Dependency Inversion Principle - -#### ❌ VIOLATIONS - -**High-level module depends on low-level module**: -```python -# app/api/endpoints/face.py -from app.services.face_recognition import face_recognition_service - -@router.post("/face/enroll") -async def enroll_face(...): - # High-level (API) depends on low-level (concrete service) - success, embedding_json, error = face_recognition_service.extract_embedding(...) -``` - -**Issues**: -- API tightly coupled to concrete service implementation -- Can't test API without full service -- Can't swap implementations - -**✅ SOLUTION**: -```python -# Define abstraction -class IEnrollmentService(Protocol): - async def enroll(self, image_path: str) -> EnrollmentResult: ... - -# High-level depends on abstraction -@router.post("/face/enroll") -async def enroll_face( - file: UploadFile = File(...), - service: IEnrollmentService = Depends(get_enrollment_service) # Injection -): - result = await service.enroll(image_path) - return FaceEnrollResponse.from_domain(result) - -# Dependency injection container provides concrete implementation -def get_enrollment_service() -> IEnrollmentService: - return FaceEnrollmentService( - detector=get_face_detector(), - extractor=get_embedding_extractor(), - validator=get_image_validator() - ) -``` - ---- - -## 🎨 Design Patterns Analysis - -### Missing Critical Patterns - -#### 1. ❌ REPOSITORY PATTERN (Missing) - -**Current Problem**: -```python -# No database yet, but will need it -# Future code will likely do: -embedding = db.query(Embedding).filter_by(user_id=user_id).first() -``` - -**Issues**: -- Direct database access in services -- Business logic mixed with data access -- Hard to test -- Hard to switch databases - -**✅ SOLUTION**: -```python -class IEmbeddingRepository(Protocol): - """Abstract repository interface""" - async def save(self, embedding: EmbeddingEntity) -> None: ... - async def find_by_user_id(self, user_id: str) -> Optional[EmbeddingEntity]: ... - async def find_similar(self, embedding: np.ndarray, threshold: float, limit: int) -> List[Match]: ... - -class PostgresEmbeddingRepository(IEmbeddingRepository): - """Concrete implementation for PostgreSQL with pgvector""" - def __init__(self, db: AsyncSession): - self._db = db - - async def save(self, embedding: EmbeddingEntity) -> None: - self._db.add(embedding) - await self._db.commit() - - async def find_by_user_id(self, user_id: str) -> Optional[EmbeddingEntity]: - result = await self._db.execute( - select(EmbeddingEntity).where(EmbeddingEntity.user_id == user_id) - ) - return result.scalar_one_or_none() - - async def find_similar(self, embedding: np.ndarray, threshold: float, limit: int) -> List[Match]: - # pgvector similarity search - ... - -class InMemoryEmbeddingRepository(IEmbeddingRepository): - """In-memory implementation for testing""" - def __init__(self): - self._embeddings: Dict[str, EmbeddingEntity] = {} - - async def save(self, embedding: EmbeddingEntity) -> None: - self._embeddings[embedding.user_id] = embedding - - # ... simplified for tests -``` - -#### 2. ❌ FACTORY PATTERN (Missing) - -**Current Problem**: -```python -# Hard-coded model instantiation -DeepFace.build_model(settings.FACE_RECOGNITION_MODEL) -``` - -**✅ SOLUTION**: -```python -class FaceDetectorFactory: - """Factory for creating face detectors""" - - @staticmethod - def create(detector_type: str) -> IFaceDetector: - if detector_type == "mtcnn": - return MTCNNDetector() - elif detector_type == "mediapipe": - return MediaPipeDetector() - elif detector_type == "retinaface": - return RetinaFaceDetector() - else: - raise ValueError(f"Unknown detector type: {detector_type}") - -class EmbeddingExtractorFactory: - """Factory for creating embedding extractors""" - - @staticmethod - def create(model_type: str) -> IEmbeddingExtractor: - if model_type == "facenet": - return FaceNetExtractor() - elif model_type == "arcface": - return ArcFaceExtractor() - elif model_type == "deepface": - return DeepFaceExtractor() - else: - raise ValueError(f"Unknown model type: {model_type}") - -# Usage in dependency injection -def get_face_detector(settings: Settings = Depends(get_settings)) -> IFaceDetector: - return FaceDetectorFactory.create(settings.FACE_DETECTION_BACKEND) - -def get_embedding_extractor(settings: Settings = Depends(get_settings)) -> IEmbeddingExtractor: - return EmbeddingExtractorFactory.create(settings.FACE_RECOGNITION_MODEL) -``` - -#### 3. ❌ STRATEGY PATTERN (Missing) - -**Use Case**: Different similarity calculation strategies - -**✅ SOLUTION**: -```python -class ISimilarityStrategy(Protocol): - """Strategy interface for similarity calculation""" - def calculate(self, emb1: np.ndarray, emb2: np.ndarray) -> float: ... - def get_threshold(self) -> float: ... - -class CosineSimilarityStrategy(ISimilarityStrategy): - def calculate(self, emb1: np.ndarray, emb2: np.ndarray) -> float: - # L2 normalize - emb1_norm = emb1 / np.linalg.norm(emb1) - emb2_norm = emb2 / np.linalg.norm(emb2) - # Cosine similarity - similarity = np.dot(emb1_norm, emb2_norm) - return 1.0 - similarity # Convert to distance - - def get_threshold(self) -> float: - return 0.6 - -class EuclideanDistanceStrategy(ISimilarityStrategy): - def calculate(self, emb1: np.ndarray, emb2: np.ndarray) -> float: - return np.linalg.norm(emb1 - emb2) - - def get_threshold(self) -> float: - return 1.0 - -class FaceVerificationService: - def __init__( - self, - similarity_strategy: ISimilarityStrategy - ): - self._similarity_strategy = similarity_strategy - - async def verify( - self, - embedding1: np.ndarray, - embedding2: np.ndarray - ) -> VerificationResult: - distance = self._similarity_strategy.calculate(embedding1, embedding2) - threshold = self._similarity_strategy.get_threshold() - verified = distance < threshold - - return VerificationResult( - verified=verified, - distance=distance, - confidence=1.0 - distance - ) -``` - -#### 4. ❌ DEPENDENCY INJECTION CONTAINER (Missing) - -**Current Problem**: Manual singleton instantiation -```python -# app/services/face_recognition.py:176 -face_recognition_service = FaceRecognitionService() -``` - -**✅ SOLUTION**: Use dependency-injector library -```python -from dependency_injector import containers, providers -from dependency_injector.wiring import inject, Provide - -class Container(containers.DeclarativeContainer): - """Dependency injection container""" - - # Configuration - config = providers.Configuration() - - # Factories - face_detector_factory = providers.Factory( - FaceDetectorFactory.create, - detector_type=config.face_detection_backend - ) - - embedding_extractor_factory = providers.Factory( - EmbeddingExtractorFactory.create, - model_type=config.face_recognition_model - ) - - # Strategies - similarity_strategy = providers.Singleton( - CosineSimilarityStrategy - ) - - # Validators - image_validator = providers.Singleton( - ImageValidator, - max_file_size=config.max_file_size - ) - - # Services - face_enrollment_service = providers.Singleton( - FaceEnrollmentService, - detector=face_detector_factory, - extractor=embedding_extractor_factory, - validator=image_validator - ) - - face_verification_service = providers.Singleton( - FaceVerificationService, - extractor=embedding_extractor_factory, - similarity_strategy=similarity_strategy - ) - -# FastAPI integration -@router.post("/face/enroll") -async def enroll_face( - file: UploadFile = File(...), - service: FaceEnrollmentService = Depends(Provide[Container.face_enrollment_service]) -): - ... -``` - -#### 5. ❌ FACADE PATTERN (Missing) - -**Use Case**: Simplify complex ML pipeline - -**✅ SOLUTION**: -```python -class FaceProcessingFacade: - """Facade to simplify complex face processing pipeline""" - - def __init__( - self, - detector: IFaceDetector, - quality_assessor: IQualityAssessor, - aligner: IFaceAligner, - extractor: IEmbeddingExtractor - ): - self._detector = detector - self._quality_assessor = quality_assessor - self._aligner = aligner - self._extractor = extractor - - async def process_image(self, image: np.ndarray) -> ProcessingResult: - """ - Single method that orchestrates entire pipeline: - 1. Detect face - 2. Assess quality - 3. Align face - 4. Extract embedding - """ - # Step 1: Detect - detection = await self._detector.detect(image) - if not detection.found: - return ProcessingResult.no_face_detected() - - # Step 2: Quality assessment - quality = await self._quality_assessor.assess(detection.face_region) - if quality.score < 70: - return ProcessingResult.poor_quality(quality) - - # Step 3: Alignment - aligned_face = await self._aligner.align(detection.face_region, detection.landmarks) - - # Step 4: Embedding - embedding = await self._extractor.extract(aligned_face) - - return ProcessingResult.success( - embedding=embedding, - quality=quality, - detection=detection - ) -``` - -#### 6. ❌ CHAIN OF RESPONSIBILITY (Missing) - -**Use Case**: Image preprocessing pipeline - -**✅ SOLUTION**: -```python -class IImageProcessor(Protocol): - """Base handler interface""" - def set_next(self, processor: 'IImageProcessor') -> 'IImageProcessor': ... - async def process(self, image: np.ndarray) -> ProcessorResult: ... - -class BaseImageProcessor(IImageProcessor): - """Base implementation with chaining logic""" - - def __init__(self): - self._next: Optional[IImageProcessor] = None - - def set_next(self, processor: IImageProcessor) -> IImageProcessor: - self._next = processor - return processor - - async def process(self, image: np.ndarray) -> ProcessorResult: - result = await self._process_impl(image) - - if not result.success: - return result - - if self._next: - return await self._next.process(result.image) - - return result - - async def _process_impl(self, image: np.ndarray) -> ProcessorResult: - raise NotImplementedError - -class ImageSizeValidator(BaseImageProcessor): - async def _process_impl(self, image: np.ndarray) -> ProcessorResult: - h, w = image.shape[:2] - if w < 100 or h < 100: - return ProcessorResult.error("Image too small") - return ProcessorResult.success(image) - -class BlurDetector(BaseImageProcessor): - async def _process_impl(self, image: np.ndarray) -> ProcessorResult: - gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) - laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var() - if laplacian_var < 100: - return ProcessorResult.error("Image too blurry") - return ProcessorResult.success(image) - -class ImageNormalizer(BaseImageProcessor): - async def _process_impl(self, image: np.ndarray) -> ProcessorResult: - normalized = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX) - return ProcessorResult.success(normalized) - -# Build chain -def create_preprocessing_chain() -> IImageProcessor: - validator = ImageSizeValidator() - blur_detector = BlurDetector() - normalizer = ImageNormalizer() - - validator.set_next(blur_detector).set_next(normalizer) - return validator - -# Usage -pipeline = create_preprocessing_chain() -result = await pipeline.process(image) -``` - -#### 7. ❌ OBSERVER PATTERN (Missing) - -**Use Case**: Webhook notifications - -**✅ SOLUTION**: -```python -class IEnrollmentObserver(Protocol): - """Observer interface for enrollment events""" - async def on_enrollment_completed(self, event: EnrollmentCompletedEvent) -> None: ... - async def on_enrollment_failed(self, event: EnrollmentFailedEvent) -> None: ... - -class WebhookNotifier(IEnrollmentObserver): - """Concrete observer that sends webhooks""" - - def __init__(self, webhook_client: IWebhookClient): - self._client = webhook_client - - async def on_enrollment_completed(self, event: EnrollmentCompletedEvent) -> None: - await self._client.send( - url=event.callback_url, - payload={ - "status": "completed", - "user_id": event.user_id, - "quality": event.quality_score - } - ) - - async def on_enrollment_failed(self, event: EnrollmentFailedEvent) -> None: - await self._client.send( - url=event.callback_url, - payload={ - "status": "failed", - "user_id": event.user_id, - "error": event.error_message - } - ) - -class EmailNotifier(IEnrollmentObserver): - """Concrete observer that sends emails""" - - async def on_enrollment_completed(self, event: EnrollmentCompletedEvent) -> None: - await send_email( - to=event.user_email, - subject="Enrollment Successful", - body=f"Your face has been enrolled successfully." - ) - -class FaceEnrollmentService: - """Subject that notifies observers""" - - def __init__(self): - self._observers: List[IEnrollmentObserver] = [] - - def add_observer(self, observer: IEnrollmentObserver) -> None: - self._observers.append(observer) - - async def enroll(self, image_path: str, user_id: str) -> EnrollmentResult: - try: - # ... enrollment logic ... - - event = EnrollmentCompletedEvent( - user_id=user_id, - quality_score=quality, - callback_url=callback_url - ) - - # Notify all observers - for observer in self._observers: - await observer.on_enrollment_completed(event) - - return EnrollmentResult.success() - - except Exception as e: - event = EnrollmentFailedEvent( - user_id=user_id, - error_message=str(e), - callback_url=callback_url - ) - - for observer in self._observers: - await observer.on_enrollment_failed(event) - - raise -``` - ---- - -## 🔧 Software Engineering Principles - -### YAGNI (You Aren't Gonna Need It) - -#### ⚠️ VIOLATIONS IN ORIGINAL PLAN - -**Over-engineered features**: -1. ❌ **WebSocket for liveness detection** - Start with POST endpoint first -2. ❌ **1:N Identification** - Might not need initially, start with 1:1 -3. ❌ **MinIO/S3 for temp storage** - Local filesystem sufficient initially -4. ❌ **Multiple liveness challenges** - Start with one (smile or blink) -5. ❌ **Celery for background jobs** - Start synchronous, add async later if needed - -**✅ RECOMMENDATIONS**: -1. ✅ Start with 1:1 verification only -2. ✅ Use POST /liveness with video upload (not WebSocket) -3. ✅ Use local temp folder for images -4. ✅ Implement one liveness challenge (smile) -5. ✅ Synchronous processing initially -6. ✅ Add complexity only when proven necessary - -### DRY (Don't Repeat Yourself) - -#### ❌ VIOLATIONS - -**1. Duplicate file handling code**: -```python -# app/api/endpoints/face.py:31-37 (enroll) -file_extension = os.path.splitext(file.filename)[1] -temp_filename = f"{uuid.uuid4()}{file_extension}" -temp_file_path = os.path.join(settings.UPLOAD_FOLDER, temp_filename) - -with open(temp_file_path, "wb") as buffer: - content = await file.read() - buffer.write(content) - -# app/api/endpoints/face.py:94-100 (verify) - DUPLICATE -file_extension = os.path.splitext(file.filename)[1] -temp_filename = f"{uuid.uuid4()}{file_extension}" -temp_file_path = os.path.join(settings.UPLOAD_FOLDER, temp_filename) - -with open(temp_file_path, "wb") as buffer: - content = await file.read() - buffer.write(content) -``` - -**✅ SOLUTION**: -```python -class FileStorageService: - """DRY - single implementation of file handling""" - - async def save_upload(self, file: UploadFile) -> str: - file_extension = os.path.splitext(file.filename)[1] - temp_filename = f"{uuid.uuid4()}{file_extension}" - temp_file_path = os.path.join(settings.UPLOAD_FOLDER, temp_filename) - - with open(temp_file_path, "wb") as buffer: - content = await file.read() - buffer.write(content) - - return temp_file_path - - async def cleanup(self, file_path: str) -> None: - if os.path.exists(file_path): - os.remove(file_path) - -# Usage -@router.post("/face/enroll") -async def enroll_face( - file: UploadFile = File(...), - storage: FileStorageService = Depends(get_file_storage) -): - image_path = await storage.save_upload(file) - try: - # ... process ... - finally: - await storage.cleanup(image_path) -``` - -**2. Duplicate cleanup code**: -```python -# Both endpoints have identical cleanup -finally: - if temp_file_path and os.path.exists(temp_file_path): - try: - os.remove(temp_file_path) - logger.info(f"Temporary file deleted: {temp_file_path}") - except Exception as e: - logger.warning(f"Failed to delete temporary file: {e}") -``` - -Already solved by FileStorageService above. - -**3. Duplicate validation**: -```python -# Both endpoints validate file type -if not file.content_type.startswith("image/"): - raise HTTPException(status_code=400, detail="File must be an image") -``` - -**✅ SOLUTION**: -```python -# Create dependency -async def validate_image_upload(file: UploadFile = File(...)) -> UploadFile: - if not file.content_type.startswith("image/"): - raise HTTPException(status_code=400, detail="File must be an image") - return file - -# Usage -@router.post("/face/enroll") -async def enroll_face(file: UploadFile = Depends(validate_image_upload)): - # File already validated - ... -``` - -### KISS (Keep It Simple, Stupid) - -#### ⚠️ OVER-COMPLEXITY IN ORIGINAL PLAN - -**1. Too many model options**: -```python -# Config has too many options -FACE_DETECTION_BACKEND: str = "opencv" # opencv, ssd, dlib, mtcnn, retinaface -FACE_RECOGNITION_MODEL: str = "VGG-Face" # VGG-Face, Facenet, OpenFace, DeepFace, DeepID, ArcFace -``` - -**✅ SOLUTION**: -- Pick ONE face detector: MTCNN (good balance) -- Pick ONE recognition model: FaceNet (proven, good performance) -- Add others only if needed - -**2. Complex initial architecture**: -- Original plan: FastAPI + Celery + Redis + PostgreSQL + MinIO + WebSocket - -**✅ SOLUTION - Simplified MVP**: -``` -Phase 1 MVP: -- FastAPI only -- Synchronous processing -- Local file storage -- In-memory embedding storage (for testing) -- Simple POST endpoints - -Phase 2 (if needed): -- Add PostgreSQL with pgvector -- Persistent storage - -Phase 3 (if needed): -- Add Celery for async -- Add Redis - -Phase 4 (if needed): -- Add MinIO/S3 -- Add WebSocket -``` - -### Separation of Concerns - -#### ❌ VIOLATIONS - -**Mixed concerns in service**: -```python -class FaceRecognitionService: - def extract_embedding(self, image_path: str): # File I/O + ML + JSON serialization - # Opens file - if not os.path.exists(image_path): - return False, None, "Image file not found" - - # ML operation - embedding_objs = DeepFace.represent(...) - - # JSON serialization - embedding_json = json.dumps(embedding) -``` - -**✅ SOLUTION**: -```python -# Separate concerns into layers - -# 1. Domain Layer - Pure business logic -class FaceEmbedding: - def __init__(self, vector: np.ndarray): - self.vector = vector - - def to_list(self) -> List[float]: - return self.vector.tolist() - -# 2. ML Layer - Pure ML operations -class FaceNetExtractor: - def extract(self, image: np.ndarray) -> np.ndarray: - # Only ML, no I/O, no serialization - return self._model.predict(image) - -# 3. Infrastructure Layer - I/O operations -class ImageLoader: - def load(self, path: str) -> np.ndarray: - # Only I/O - return cv2.imread(path) - -# 4. Application Layer - Orchestration -class FaceEnrollmentService: - def __init__( - self, - image_loader: ImageLoader, - extractor: FaceNetExtractor - ): - self._loader = image_loader - self._extractor = extractor - - async def enroll(self, image_path: str) -> FaceEmbedding: - # Orchestrate without implementing details - image = self._loader.load(image_path) - vector = self._extractor.extract(image) - return FaceEmbedding(vector) -``` - ---- - -## 🏗️ Architectural Issues & Improvements - -### Issue 1: No Clear Layered Architecture - -#### ❌ CURRENT STATE -``` -API Endpoints → Service (everything mixed) -``` - -#### ✅ PROPER LAYERED ARCHITECTURE - -``` -┌─────────────────────────────────────────────────────────────┐ -│ PRESENTATION LAYER (API) │ -│ - FastAPI routes │ -│ - Request/Response DTOs │ -│ - Input validation │ -│ - HTTP error handling │ -└─────────────────────────────────────────────────────────────┘ - ↓ Depends on -┌─────────────────────────────────────────────────────────────┐ -│ APPLICATION LAYER (Services) │ -│ - Business logic orchestration │ -│ - Use cases (EnrollFaceUseCase, VerifyFaceUseCase) │ -│ - Transaction management │ -│ - Event publishing │ -└─────────────────────────────────────────────────────────────┘ - ↓ Depends on -┌─────────────────────────────────────────────────────────────┐ -│ DOMAIN LAYER (Business Logic) │ -│ - Domain entities (FaceEmbedding, User, etc.) │ -│ - Domain services (SimilarityCalculator) │ -│ - Domain interfaces (IFaceDetector, IEmbeddingExtractor) │ -│ - Business rules │ -└─────────────────────────────────────────────────────────────┘ - ↑ Implemented by -┌─────────────────────────────────────────────────────────────┐ -│ INFRASTRUCTURE LAYER (Technical Details) │ -│ - ML models (MTCNNDetector, FaceNetExtractor) │ -│ - Database repositories (PostgresEmbeddingRepository) │ -│ - External services (WebhookClient, EmailService) │ -│ - File storage (LocalFileStorage, S3FileStorage) │ -└─────────────────────────────────────────────────────────────┘ -``` - -**Dependency Flow**: Presentation → Application → Domain ← Infrastructure - -**Key Principle**: Domain layer has NO dependencies on infrastructure! - -### Issue 2: No Error Handling Strategy - -#### ❌ CURRENT STATE -```python -# Generic exception handling -except Exception as e: - logger.error(f"Error: {e}") - raise HTTPException(status_code=500, detail=f"Failed: {str(e)}") -``` - -**Problems**: -- Leaks internal errors to API responses -- No error codes -- No structured error responses -- No retry logic - -#### ✅ SOLUTION: Domain Exception Hierarchy - -```python -# Domain exceptions -class BiometricProcessorError(Exception): - """Base exception for all domain errors""" - - def __init__(self, message: str, error_code: str): - self.message = message - self.error_code = error_code - super().__init__(message) - -class FaceNotDetectedError(BiometricProcessorError): - def __init__(self): - super().__init__( - message="No face detected in image", - error_code="FACE_NOT_DETECTED" - ) - -class MultipleFacesError(BiometricProcessorError): - def __init__(self, count: int): - super().__init__( - message=f"Multiple faces detected ({count}). Please provide image with single face.", - error_code="MULTIPLE_FACES" - ) - -class PoorImageQualityError(BiometricProcessorError): - def __init__(self, quality_score: float): - super().__init__( - message=f"Image quality too low (score: {quality_score:.2f}). Please provide clearer image.", - error_code="POOR_IMAGE_QUALITY" - ) - -class LivenessCheckFailedError(BiometricProcessorError): - def __init__(self, liveness_score: float): - super().__init__( - message=f"Liveness check failed (score: {liveness_score:.2f}). Please ensure you're a real person.", - error_code="LIVENESS_CHECK_FAILED" - ) - -class EmbeddingNotFoundError(BiometricProcessorError): - def __init__(self, user_id: str): - super().__init__( - message=f"No face embedding found for user {user_id}", - error_code="EMBEDDING_NOT_FOUND" - ) - -class FaceVerificationFailedError(BiometricProcessorError): - def __init__(self, confidence: float): - super().__init__( - message=f"Face verification failed (confidence: {confidence:.2f})", - error_code="VERIFICATION_FAILED" - ) - -# Infrastructure exceptions -class ModelLoadError(BiometricProcessorError): - def __init__(self, model_name: str, reason: str): - super().__init__( - message=f"Failed to load model '{model_name}': {reason}", - error_code="MODEL_LOAD_ERROR" - ) - -class DatabaseError(BiometricProcessorError): - def __init__(self, operation: str): - super().__init__( - message=f"Database operation failed: {operation}", - error_code="DATABASE_ERROR" - ) - -# API error responses -from pydantic import BaseModel - -class ErrorResponse(BaseModel): - error_code: str - message: str - details: Optional[Dict[str, Any]] = None - -# Global exception handler -@app.exception_handler(BiometricProcessorError) -async def biometric_error_handler(request: Request, exc: BiometricProcessorError): - """Convert domain exceptions to HTTP responses""" - - # Map exceptions to HTTP status codes - status_map = { - FaceNotDetectedError: 400, - MultipleFacesError: 400, - PoorImageQualityError: 400, - LivenessCheckFailedError: 400, - EmbeddingNotFoundError: 404, - FaceVerificationFailedError: 401, - ModelLoadError: 500, - DatabaseError: 500, - } - - status_code = status_map.get(type(exc), 500) - - return JSONResponse( - status_code=status_code, - content=ErrorResponse( - error_code=exc.error_code, - message=exc.message - ).dict() - ) - -# Usage in service -async def detect_face(self, image: np.ndarray) -> FaceDetectionResult: - faces = self._detector.detect(image) - - if len(faces) == 0: - raise FaceNotDetectedError() - - if len(faces) > 1: - raise MultipleFacesError(count=len(faces)) - - return faces[0] -``` - -### Issue 3: No Logging/Observability Strategy - -#### ❌ CURRENT STATE -```python -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) - -logger.info("Face enrolled successfully") -``` - -**Problems**: -- No structured logging -- No correlation IDs -- No metrics -- No distributed tracing - -#### ✅ SOLUTION: Structured Logging + Metrics - -```python -import structlog -from opentelemetry import trace, metrics - -# Configure structured logging -structlog.configure( - processors=[ - structlog.stdlib.filter_by_level, - structlog.stdlib.add_logger_name, - structlog.stdlib.add_log_level, - structlog.stdlib.PositionalArgumentsFormatter(), - structlog.processors.TimeStamper(fmt="iso"), - structlog.processors.StackInfoRenderer(), - structlog.processors.format_exc_info, - structlog.processors.UnicodeDecoder(), - structlog.processors.JSONRenderer() - ], - logger_factory=structlog.stdlib.LoggerFactory(), - wrapper_class=structlog.stdlib.BoundLogger, - cache_logger_on_first_use=True, -) - -# Get structured logger -logger = structlog.get_logger() - -# Middleware for correlation ID -@app.middleware("http") -async def add_correlation_id(request: Request, call_next): - correlation_id = request.headers.get("X-Correlation-ID", str(uuid.uuid4())) - - # Add to context for logging - structlog.contextvars.bind_contextvars( - correlation_id=correlation_id, - path=request.url.path, - method=request.method - ) - - response = await call_next(request) - response.headers["X-Correlation-ID"] = correlation_id - - return response - -# Usage in service -async def enroll(self, image_path: str, user_id: str) -> EnrollmentResult: - logger.info( - "enrollment_started", - user_id=user_id, - image_path=image_path - ) - - try: - result = await self._process_enrollment(image_path, user_id) - - logger.info( - "enrollment_completed", - user_id=user_id, - quality_score=result.quality, - duration_ms=result.duration_ms - ) - - # Increment metric - enrollment_counter.add(1, {"status": "success"}) - enrollment_duration.record(result.duration_ms) - - return result - - except Exception as e: - logger.error( - "enrollment_failed", - user_id=user_id, - error=str(e), - exc_info=True - ) - - enrollment_counter.add(1, {"status": "failed"}) - raise - -# Metrics -from opentelemetry.metrics import get_meter - -meter = get_meter(__name__) - -enrollment_counter = meter.create_counter( - name="face_enrollment_total", - description="Total number of face enrollments", - unit="1" -) - -enrollment_duration = meter.create_histogram( - name="face_enrollment_duration", - description="Face enrollment duration", - unit="ms" -) - -verification_counter = meter.create_counter( - name="face_verification_total", - description="Total number of face verifications", - unit="1" -) - -# Tracing -tracer = trace.get_tracer(__name__) - -async def enroll(self, image_path: str, user_id: str) -> EnrollmentResult: - with tracer.start_as_current_span("face_enrollment") as span: - span.set_attribute("user_id", user_id) - - with tracer.start_as_current_span("face_detection"): - detection = await self._detector.detect(image) - - with tracer.start_as_current_span("quality_assessment"): - quality = await self._quality_assessor.assess(detection.face) - - with tracer.start_as_current_span("embedding_extraction"): - embedding = await self._extractor.extract(detection.face) - - span.set_attribute("quality_score", quality.score) - - return EnrollmentResult(...) -``` - -### Issue 4: Missing Configuration Validation - -#### ❌ CURRENT STATE -```python -class Settings(BaseSettings): - VERIFICATION_THRESHOLD: float = 0.40 -``` - -No validation, could be set to invalid values. - -#### ✅ SOLUTION: Pydantic Validation - -```python -from pydantic import BaseSettings, Field, validator -from typing import Literal - -class Settings(BaseSettings): - # App settings - APP_NAME: str = Field(default="FIVUCSAS Biometric Processor") - VERSION: str = Field(default="1.0.0") - ENVIRONMENT: Literal["development", "staging", "production"] = Field(default="development") - DEBUG: bool = Field(default=False) - - # API settings - API_HOST: str = Field(default="0.0.0.0") - API_PORT: int = Field(default=8001, ge=1024, le=65535) # STANDARD PORT - API_WORKERS: int = Field(default=4, ge=1, le=32) - - # CORS settings - CORS_ORIGINS: List[str] = Field(default=[]) - - @validator("CORS_ORIGINS", pre=True) - def parse_cors_origins(cls, v): - if isinstance(v, str): - return [origin.strip() for origin in v.split(",")] - return v - - # File upload settings - UPLOAD_FOLDER: str = Field(default="./temp_uploads") - MAX_FILE_SIZE: int = Field(default=10 * 1024 * 1024, ge=1024, le=50 * 1024 * 1024) # 1KB to 50MB - ALLOWED_IMAGE_FORMATS: List[str] = Field(default=["jpg", "jpeg", "png"]) - - # ML Model settings - FACE_DETECTION_MODEL: Literal["mtcnn", "mediapipe", "retinaface"] = Field(default="mtcnn") - FACE_RECOGNITION_MODEL: Literal["facenet", "arcface", "vggface"] = Field(default="facenet") - MODEL_DEVICE: Literal["cpu", "cuda"] = Field(default="cpu") - - # Thresholds - VERIFICATION_THRESHOLD: float = Field(default=0.6, ge=0.0, le=1.0) - LIVENESS_THRESHOLD: int = Field(default=80, ge=0, le=100) - QUALITY_THRESHOLD: int = Field(default=70, ge=0, le=100) - - # Quality assessment - MIN_IMAGE_SIZE: int = Field(default=100, ge=50, le=1000) - MAX_IMAGE_SIZE: int = Field(default=4000, ge=1000, le=10000) - MIN_FACE_SIZE: int = Field(default=80, ge=40, le=500) - BLUR_THRESHOLD: float = Field(default=100.0, ge=0.0) - - # Database (for future) - DATABASE_URL: Optional[str] = Field(default=None) - DATABASE_POOL_SIZE: int = Field(default=10, ge=1, le=100) - - # Redis (for future) - REDIS_URL: Optional[str] = Field(default=None) - REDIS_MAX_CONNECTIONS: int = Field(default=10, ge=1, le=100) - - # Webhook - WEBHOOK_TIMEOUT: int = Field(default=10, ge=1, le=60) - WEBHOOK_MAX_RETRIES: int = Field(default=3, ge=0, le=10) - - # Logging - LOG_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(default="INFO") - LOG_FORMAT: Literal["json", "text"] = Field(default="json") - - class Config: - env_file = ".env" - case_sensitive = True - - @validator("ENVIRONMENT") - def validate_environment(cls, v): - if v == "production": - # Additional validations for production - ... - return v - - def get_model_config(self) -> Dict[str, Any]: - """Get ML model configuration""" - return { - "detection_model": self.FACE_DETECTION_MODEL, - "recognition_model": self.FACE_RECOGNITION_MODEL, - "device": self.MODEL_DEVICE, - "thresholds": { - "verification": self.VERIFICATION_THRESHOLD, - "liveness": self.LIVENESS_THRESHOLD, - "quality": self.QUALITY_THRESHOLD - } - } - -# Usage -settings = Settings() - -# Validate on startup -@app.on_event("startup") -async def validate_configuration(): - logger.info("Validating configuration", environment=settings.ENVIRONMENT) - - # Check required settings for production - if settings.ENVIRONMENT == "production": - if not settings.DATABASE_URL: - raise ValueError("DATABASE_URL required in production") - - if settings.DEBUG: - raise ValueError("DEBUG must be False in production") - - if "*" in settings.CORS_ORIGINS: - raise ValueError("CORS wildcard not allowed in production") - - # Create upload folder - os.makedirs(settings.UPLOAD_FOLDER, exist_ok=True) - - logger.info("Configuration validated successfully") -``` - ---- - -## 🎯 Improved Architecture Design - -### New Project Structure - -``` -biometric-processor/ -│ -├── app/ -│ ├── __init__.py -│ ├── main.py # FastAPI app + DI setup -│ │ -│ ├── api/ # PRESENTATION LAYER -│ │ ├── __init__.py -│ │ ├── dependencies.py # FastAPI dependencies -│ │ ├── middleware/ -│ │ │ ├── __init__.py -│ │ │ ├── correlation_id.py # Correlation ID middleware -│ │ │ ├── error_handler.py # Global error handling -│ │ │ └── logging.py # Request logging -│ │ ├── routes/ -│ │ │ ├── __init__.py -│ │ │ ├── v1/ -│ │ │ │ ├── __init__.py -│ │ │ │ ├── enrollment.py # POST /api/v1/enroll -│ │ │ │ ├── verification.py # POST /api/v1/verify -│ │ │ │ ├── liveness.py # POST /api/v1/liveness -│ │ │ │ └── health.py # GET /api/v1/health -│ │ └── schemas/ # Request/Response DTOs -│ │ ├── __init__.py -│ │ ├── enrollment.py # EnrollmentRequest, EnrollmentResponse -│ │ ├── verification.py # VerificationRequest, VerificationResponse -│ │ ├── liveness.py # LivenessRequest, LivenessResponse -│ │ └── common.py # Common DTOs (ErrorResponse, etc.) -│ │ -│ ├── application/ # APPLICATION LAYER -│ │ ├── __init__.py -│ │ ├── use_cases/ # Use case implementations -│ │ │ ├── __init__.py -│ │ │ ├── enroll_face.py # EnrollFaceUseCase -│ │ │ ├── verify_face.py # VerifyFaceUseCase -│ │ │ └── check_liveness.py # CheckLivenessUseCase -│ │ └── services/ # Application services -│ │ ├── __init__.py -│ │ ├── face_processing.py # Orchestrates face processing pipeline -│ │ └── notification.py # Handles webhooks/notifications -│ │ -│ ├── domain/ # DOMAIN LAYER -│ │ ├── __init__.py -│ │ ├── entities/ # Domain entities -│ │ │ ├── __init__.py -│ │ │ ├── face_embedding.py # FaceEmbedding -│ │ │ ├── face_detection.py # FaceDetection -│ │ │ └── quality_assessment.py # QualityAssessment -│ │ ├── value_objects/ # Immutable value objects -│ │ │ ├── __init__.py -│ │ │ ├── embedding_vector.py # EmbeddingVector -│ │ │ ├── confidence_score.py # ConfidenceScore -│ │ │ └── bounding_box.py # BoundingBox -│ │ ├── interfaces/ # Domain interfaces (protocols) -│ │ │ ├── __init__.py -│ │ │ ├── face_detector.py # IFaceDetector protocol -│ │ │ ├── embedding_extractor.py # IEmbeddingExtractor protocol -│ │ │ ├── quality_assessor.py # IQualityAssessor protocol -│ │ │ ├── liveness_detector.py # ILivenessDetector protocol -│ │ │ ├── similarity_calculator.py # ISimilarityCalculator protocol -│ │ │ └── embedding_repository.py # IEmbeddingRepository protocol -│ │ ├── services/ # Domain services -│ │ │ ├── __init__.py -│ │ │ └── face_matching.py # FaceMatchingService -│ │ └── exceptions/ # Domain exceptions -│ │ ├── __init__.py -│ │ ├── base.py # BiometricProcessorError -│ │ ├── face_errors.py # Face-related errors -│ │ └── validation_errors.py # Validation errors -│ │ -│ ├── infrastructure/ # INFRASTRUCTURE LAYER -│ │ ├── __init__.py -│ │ ├── ml/ # ML model implementations -│ │ │ ├── __init__.py -│ │ │ ├── factories/ -│ │ │ │ ├── __init__.py -│ │ │ │ ├── detector_factory.py # FaceDetectorFactory -│ │ │ │ └── extractor_factory.py # EmbeddingExtractorFactory -│ │ │ ├── detectors/ -│ │ │ │ ├── __init__.py -│ │ │ │ ├── mtcnn_detector.py # MTCNNDetector -│ │ │ │ └── mediapipe_detector.py# MediaPipeDetector -│ │ │ ├── extractors/ -│ │ │ │ ├── __init__.py -│ │ │ │ ├── facenet_extractor.py # FaceNetExtractor -│ │ │ │ └── arcface_extractor.py # ArcFaceExtractor -│ │ │ ├── quality/ -│ │ │ │ ├── __init__.py -│ │ │ │ └── quality_assessor.py # QualityAssessor -│ │ │ └── liveness/ -│ │ │ ├── __init__.py -│ │ │ └── smile_detector.py # SmileLivenessDetector -│ │ ├── persistence/ # Data persistence -│ │ │ ├── __init__.py -│ │ │ ├── database.py # Database connection -│ │ │ ├── models/ # SQLAlchemy models -│ │ │ │ ├── __init__.py -│ │ │ │ └── embedding.py # EmbeddingModel -│ │ │ └── repositories/ -│ │ │ ├── __init__.py -│ │ │ ├── postgres_embedding.py# PostgresEmbeddingRepository -│ │ │ └── memory_embedding.py # InMemoryEmbeddingRepository (testing) -│ │ ├── storage/ # File storage -│ │ │ ├── __init__.py -│ │ │ ├── local_storage.py # LocalFileStorage -│ │ │ └── s3_storage.py # S3FileStorage (future) -│ │ ├── http/ # HTTP clients -│ │ │ ├── __init__.py -│ │ │ └── webhook_client.py # WebhookClient -│ │ └── cache/ # Caching -│ │ ├── __init__.py -│ │ └── redis_cache.py # RedisCache (future) -│ │ -│ ├── core/ # Core/Shared -│ │ ├── __init__.py -│ │ ├── config.py # Settings (Pydantic) -│ │ ├── container.py # Dependency injection container -│ │ ├── logging.py # Logging configuration -│ │ └── security.py # Security utilities -│ │ -│ └── shared/ # Shared utilities -│ ├── __init__.py -│ ├── image_utils.py # Image processing utilities -│ ├── math_utils.py # Math utilities -│ └── validators.py # Common validators -│ -├── tests/ -│ ├── __init__.py -│ ├── unit/ # Unit tests -│ │ ├── domain/ -│ │ ├── application/ -│ │ └── infrastructure/ -│ ├── integration/ # Integration tests -│ │ ├── api/ -│ │ └── ml/ -│ ├── e2e/ # End-to-end tests -│ └── fixtures/ # Test fixtures (sample images, etc.) -│ -├── migrations/ # Database migrations (Alembic) -│ └── versions/ -│ -├── scripts/ # Utility scripts -│ ├── download_models.py # Download ML models -│ └── seed_database.py # Seed test data -│ -├── docker/ -│ ├── Dockerfile.api -│ └── Dockerfile.worker -│ -├── .env.example # Example environment variables -├── .gitignore -├── docker-compose.yml -├── pyproject.toml # Poetry dependencies -├── requirements.txt # Pip dependencies -├── README.md -├── biometric-processor-MODULE_PLAN.md # Original plan -└── DESIGN_ANALYSIS_AND_IMPROVEMENTS.md # This document -``` - -### Key Improvements - -1. **Clear Layer Separation**: API → Application → Domain ← Infrastructure -2. **Dependency Inversion**: Domain defines interfaces, infrastructure implements -3. **Use Cases**: Each feature is a single use case class -4. **Domain-Driven Design**: Entities, Value Objects, Domain Services -5. **Testability**: Each layer can be tested independently -6. **Extensibility**: Easy to add new models, detectors, storage backends - ---- - -## 📋 Implementation Plan - -### Sprint 1: Foundation & Architecture (Week 1-2) - -#### Goals -- ✅ Establish proper architecture -- ✅ Fix critical issues -- ✅ Enable testing - -#### Tasks - -**1.1 Create Domain Layer** -```bash -Priority: 🔴 CRITICAL -Estimated: 2 days -``` - -- [ ] Define domain interfaces (protocols) - - `IFaceDetector` - - `IEmbeddingExtractor` - - `IQualityAssessor` - - `ISimilarityCalculator` - - `IEmbeddingRepository` - - `IFileStorage` - -- [ ] Create domain entities - - `FaceEmbedding` - - `FaceDetection` - - `QualityAssessment` - -- [ ] Create value objects - - `EmbeddingVector` - - `ConfidenceScore` - - `BoundingBox` - -- [ ] Define domain exceptions - - `BiometricProcessorError` (base) - - `FaceNotDetectedError` - - `MultipleFacesError` - - `PoorImageQualityError` - - `EmbeddingNotFoundError` - -**Files to create**: -- `app/domain/interfaces/*.py` -- `app/domain/entities/*.py` -- `app/domain/value_objects/*.py` -- `app/domain/exceptions/*.py` - -**1.2 Create Infrastructure Implementations** -```bash -Priority: 🔴 CRITICAL -Estimated: 3 days -``` - -- [ ] Refactor existing `FaceRecognitionService` into: - - `MTCNNDetector` (implements `IFaceDetector`) - - `DeepFaceExtractor` (implements `IEmbeddingExtractor`) - - `QualityAssessor` (implements `IQualityAssessor`) - - `CosineSimilarityCalculator` (implements `ISimilarityCalculator`) - -- [ ] Create factories - - `FaceDetectorFactory` - - `EmbeddingExtractorFactory` - -- [ ] Create storage implementations - - `LocalFileStorage` (implements `IFileStorage`) - -- [ ] Create repository implementations - - `InMemoryEmbeddingRepository` (for MVP) - -**Files to create**: -- `app/infrastructure/ml/detectors/mtcnn_detector.py` -- `app/infrastructure/ml/extractors/deepface_extractor.py` -- `app/infrastructure/ml/quality/quality_assessor.py` -- `app/infrastructure/ml/factories/*.py` -- `app/infrastructure/storage/local_storage.py` -- `app/infrastructure/persistence/repositories/memory_embedding.py` - -**1.3 Create Application Layer** -```bash -Priority: 🔴 CRITICAL -Estimated: 2 days -``` - -- [ ] Create use cases - - `EnrollFaceUseCase` - - `VerifyFaceUseCase` - - `CheckLivenessUseCase` (stub for now) - -- [ ] Each use case should: - - Accept dependencies via constructor - - Have single `execute()` method - - Return domain entities - - Throw domain exceptions - -**Files to create**: -- `app/application/use_cases/enroll_face.py` -- `app/application/use_cases/verify_face.py` -- `app/application/use_cases/check_liveness.py` - -**1.4 Setup Dependency Injection** -```bash -Priority: 🔴 CRITICAL -Estimated: 1 day -``` - -- [ ] Install `dependency-injector` -- [ ] Create DI container -- [ ] Wire up all dependencies -- [ ] Integrate with FastAPI - -**Files to create**: -- `app/core/container.py` - -**1.5 Create API Layer with DTOs** -```bash -Priority: 🔴 CRITICAL -Estimated: 2 days -``` - -- [ ] Create Pydantic schemas - - `EnrollmentRequest`, `EnrollmentResponse` - - `VerificationRequest`, `VerificationResponse` - - `ErrorResponse` - -- [ ] Refactor API endpoints to use: - - Dependency injection - - Use cases - - Proper DTOs - - Domain exceptions - -**Files to create**: -- `app/api/schemas/*.py` -- Refactor: `app/api/routes/v1/*.py` - -**1.6 Implement Error Handling** -```bash -Priority: 🟠 HIGH -Estimated: 1 day -``` - -- [ ] Create exception handlers -- [ ] Add middleware for error handling -- [ ] Structured error responses - -**Files to create**: -- `app/api/middleware/error_handler.py` - -**1.7 Fix Security Issues** -```bash -Priority: 🔴 CRITICAL -Estimated: 0.5 day -``` - -- [ ] Fix CORS configuration (remove wildcard) -- [ ] Add rate limiting -- [ ] Add request validation - -**Files to modify**: -- `app/main.py` -- `app/core/config.py` - -**1.8 Add Structured Logging** -```bash -Priority: 🟠 HIGH -Estimated: 1 day -``` - -- [ ] Install `structlog` -- [ ] Configure structured logging -- [ ] Add correlation ID middleware -- [ ] Add logging to all layers - -**Files to create**: -- `app/core/logging.py` -- `app/api/middleware/correlation_id.py` - ---- - -### Sprint 2: Testing & Quality (Week 3) - -#### Goals -- ✅ Comprehensive test coverage -- ✅ Code quality assurance - -#### Tasks - -**2.1 Unit Tests** -```bash -Priority: 🟠 HIGH -Estimated: 3 days -``` - -- [ ] Test domain entities -- [ ] Test domain services -- [ ] Test use cases (with mocks) -- [ ] Test infrastructure components -- [ ] Target: 80%+ coverage - -**2.2 Integration Tests** -```bash -Priority: 🟠 HIGH -Estimated: 2 days -``` - -- [ ] Test API endpoints -- [ ] Test ML pipeline -- [ ] Test error handling - -**2.3 Code Quality** -```bash -Priority: 🟡 MEDIUM -Estimated: 1 day -``` - -- [ ] Setup `black` (formatting) -- [ ] Setup `isort` (imports) -- [ ] Setup `mypy` (type checking) -- [ ] Setup `pylint` (linting) -- [ ] Setup pre-commit hooks - ---- - -### Sprint 3: Liveness Detection (Week 4-5) - -#### Goals -- ✅ Implement liveness detection -- ✅ MVP complete - -#### Tasks - -**3.1 Smile-based Liveness** -```bash -Priority: 🟠 HIGH -Estimated: 3 days -``` - -- [ ] Implement smile detection using facial landmarks -- [ ] Create `SmileLivenessDetector` -- [ ] Add tests -- [ ] Integrate with API - -**3.2 Liveness API** -```bash -Priority: 🟠 HIGH -Estimated: 1 day -``` - -- [ ] Create `POST /api/v1/liveness` -- [ ] Accept image or video -- [ ] Return liveness score -- [ ] Add tests - ---- - -### Sprint 4: Database Integration (Week 6) - -#### Goals -- ✅ Persistent storage -- ✅ Production-ready - -#### Tasks - -**4.1 PostgreSQL with pgvector** -```bash -Priority: 🟠 HIGH -Estimated: 2 days -``` - -- [ ] Setup PostgreSQL with pgvector -- [ ] Create SQLAlchemy models -- [ ] Setup Alembic migrations -- [ ] Create tables - -**4.2 Repository Implementation** -```bash -Priority: 🟠 HIGH -Estimated: 2 days -``` - -- [ ] Implement `PostgresEmbeddingRepository` -- [ ] Support CRUD operations -- [ ] Support similarity search -- [ ] Add tests - -**4.3 Migration from In-Memory** -```bash -Priority: 🟡 MEDIUM -Estimated: 1 day -``` - -- [ ] Update DI container to use Postgres repository -- [ ] Test integration -- [ ] Update documentation - ---- - -### Sprint 5: Optimization & Production Readiness (Week 7-8) - -#### Tasks - -**5.1 Performance Optimization** -```bash -Priority: 🟡 MEDIUM -Estimated: 2 days -``` - -- [ ] Profile slow operations -- [ ] Optimize image preprocessing -- [ ] Add caching where appropriate -- [ ] Load testing - -**5.2 Observability** -```bash -Priority: 🟡 MEDIUM -Estimated: 2 days -``` - -- [ ] Add metrics (Prometheus) -- [ ] Add health checks -- [ ] Add distributed tracing - -**5.3 Documentation** -```bash -Priority: 🟡 MEDIUM -Estimated: 1 day -``` - -- [ ] API documentation (OpenAPI) -- [ ] Architecture documentation -- [ ] Deployment guide - -**5.4 Docker & Deployment** -```bash -Priority: 🟠 HIGH -Estimated: 2 days -``` - -- [ ] Create optimized Dockerfile -- [ ] Docker Compose setup -- [ ] Kubernetes manifests (if needed) -- [ ] CI/CD pipeline - ---- - -## 🔄 Migration Strategy - -### Phase 1: Add New Code (Non-Breaking) - -**Week 1-2**: Build new architecture alongside existing code - -1. Create new directory structure -2. Implement domain layer -3. Implement infrastructure layer -4. Implement application layer -5. Do NOT touch existing code yet - -**Result**: New code exists but isn't used yet - -### Phase 2: Switch API Layer (Breaking Change) - -**Week 3**: Switch API endpoints to use new architecture - -1. Create new API routes in `app/api/routes/v1/` -2. Update `app/main.py` to use new routes -3. Keep old code for rollback -4. Test thoroughly - -**Result**: API uses new architecture - -### Phase 3: Remove Old Code - -**Week 4**: Clean up - -1. Remove old service layer -2. Remove unused dependencies -3. Update tests - -**Result**: Clean codebase with new architecture - ---- - -## ✅ Success Criteria - -### Architecture -- ✅ Clear layer separation -- ✅ SOLID principles followed -- ✅ Design patterns implemented -- ✅ Testable code - -### Code Quality -- ✅ 80%+ test coverage -- ✅ Type hints everywhere -- ✅ No linting errors -- ✅ Formatted code - -### Performance -- ✅ Enrollment: <2s (p95) -- ✅ Verification: <500ms (p95) - -### Security -- ✅ No CORS wildcard -- ✅ Input validation -- ✅ Rate limiting -- ✅ Error messages don't leak internals - -### Observability -- ✅ Structured logging -- ✅ Metrics -- ✅ Health checks -- ✅ Distributed tracing - ---- - -## 📊 Comparison: Before vs After - -| Aspect | Before | After | -|--------|--------|-------| -| **Architecture** | Monolithic service | Layered architecture | -| **SOLID** | Multiple violations | All principles followed | -| **Design Patterns** | None | 7+ patterns | -| **Testability** | Hard to test | Easy to test (DI) | -| **Coupling** | Tight coupling | Loose coupling | -| **Extensibility** | Hard to extend | Easy to add models | -| **Error Handling** | Generic exceptions | Domain exceptions | -| **Logging** | Basic logging | Structured logging | -| **Security** | CORS wildcard | Proper CORS | -| **Dependencies** | Hard-coded | Injected | -| **Code Duplication** | High (DRY violations) | Low (DRY applied) | - ---- - -## 🎓 Summary - -### Critical Issues Fixed -1. ✅ SOLID principles violations -2. ✅ Missing design patterns -3. ✅ No layered architecture -4. ✅ Tight coupling -5. ✅ Security vulnerabilities -6. ✅ Poor error handling -7. ✅ No testability - -### Architecture Improvements -1. ✅ Clean Architecture (Layered) -2. ✅ Dependency Injection -3. ✅ Repository Pattern -4. ✅ Factory Pattern -5. ✅ Strategy Pattern -6. ✅ Facade Pattern -7. ✅ Observer Pattern - -### Implementation Plan -- **Sprint 1-2**: Foundation (Architecture + DI) -- **Sprint 3**: Testing -- **Sprint 4-5**: Liveness Detection -- **Sprint 6**: Database Integration -- **Sprint 7-8**: Production Readiness - -### Timeline -- **Total**: 8 weeks (~2 months) -- **MVP**: 3 weeks (Foundation + Testing + Liveness) -- **Production**: 8 weeks (Full features + Optimization) - ---- - -**Next Steps**: -1. Review and approve this design -2. Start Sprint 1 implementation -3. Weekly progress reviews -4. Iterative improvements - ---- - -**Document Status**: ✅ READY FOR REVIEW -**Requires Approval**: YES -**Implementation Ready**: YES diff --git a/docs/6-architecture/DESIGN_VALIDATION_CHECKLIST.md b/docs/6-architecture/DESIGN_VALIDATION_CHECKLIST.md deleted file mode 100644 index 8f2122b..0000000 --- a/docs/6-architecture/DESIGN_VALIDATION_CHECKLIST.md +++ /dev/null @@ -1,419 +0,0 @@ -# Design Validation Checklist - Biometric Processor - -**Validation Date**: 2025-11-17 -**Design Version**: 2.0 -**Validated Against**: Software Engineer's Essential Checklist -**Status**: ✅ VALIDATED - ALL CRITERIA MET - ---- - -## ✅ Core Design Principles - -### SOLID Principles - -| Principle | Status | Evidence | Location | -|-----------|--------|----------|----------| -| **Single Responsibility** | ✅ PASS | Each class has one clear responsibility. Split `FaceRecognitionService` into: `MTCNNDetector`, `FaceNetExtractor`, `QualityAssessor`, `CosineSimilarityCalculator` | DESIGN_ANALYSIS: Lines 72-185 | -| **Open/Closed** | ✅ PASS | Factory pattern enables adding new models without modifying code. `FaceDetectorFactory` and `EmbeddingExtractorFactory` | DESIGN_ANALYSIS: Lines 187-243 | -| **Liskov Substitution** | ✅ PASS | All implementations properly substitute their interfaces. Protocol-based design ensures contracts are maintained | DESIGN_ANALYSIS: Lines 245-268 | -| **Interface Segregation** | ✅ PASS | Focused interfaces: `IFaceDetector`, `IEmbeddingExtractor`, `IQualityAssessor`, `ISimilarityCalculator`, `IFileStorage` - clients depend only on what they need | DESIGN_ANALYSIS: Lines 270-313 | -| **Dependency Inversion** | ✅ PASS | High-level modules (use cases) depend on abstractions (protocols). DI container provides concrete implementations | DESIGN_ANALYSIS: Lines 315-372 | - -**SOLID Score**: 5/5 ✅ - ---- - -### DRY, KISS, YAGNI - -| Principle | Status | Evidence | -|-----------|--------|----------| -| **DRY** | ✅ PASS | Eliminated duplicate file handling code with `FileStorageService`. Removed duplicate validation and cleanup code | DESIGN_ANALYSIS: Lines 599-655 | -| **KISS** | ✅ PASS | Simplified from complex initial plan: Removed WebSocket (use POST), MinIO (use local), multiple liveness challenges (use one), Celery initially (add later if needed) | DESIGN_ANALYSIS: Lines 657-708 | -| **YAGNI** | ✅ PASS | Identified over-engineered features and deferred: 1:N identification, WebSocket, MinIO, multiple challenges, Celery. Focus on MVP first | DESIGN_ANALYSIS: Lines 579-597 | - -**Code Quality Score**: 3/3 ✅ - ---- - -### Separation of Concerns - -| Layer | Status | Responsibility | Evidence | -|-------|--------|----------------|----------| -| **Presentation** | ✅ PASS | API routes, DTOs, HTTP handling | Clean separation in `app/api/` | -| **Application** | ✅ PASS | Business orchestration, use cases | `app/application/use_cases/` | -| **Domain** | ✅ PASS | Business logic, entities, interfaces | `app/domain/` with no infrastructure deps | -| **Infrastructure** | ✅ PASS | ML models, database, external services | `app/infrastructure/` implements domain interfaces | - -**Architecture Score**: ✅ CLEAN ARCHITECTURE APPLIED - ---- - -### Composition Over Inheritance - -| Item | Status | Evidence | -|------|--------|----------| -| **Composition Used** | ✅ PASS | Use cases composed of injected dependencies, not inherited | IMPLEMENTATION_PLAN: Lines 198-286 | -| **Minimal Inheritance** | ✅ PASS | Only `BiometricProcessorError` base exception. No deep hierarchies | DESIGN_ANALYSIS: Lines 759-819 | -| **Interface-Based** | ✅ PASS | Protocol-based design using composition | Throughout domain layer | - -**Design Score**: ✅ COMPOSITION FAVORED - ---- - -## ✅ Design Patterns - -### Creational Patterns - -| Pattern | Status | Implementation | Location | -|---------|--------|----------------|----------| -| **Singleton** | ✅ IMPLEMENTED | DI container provides singletons for ML models (expensive to create) | IMPLEMENTATION_PLAN: Lines 287-339 | -| **Factory Method** | ✅ IMPLEMENTED | `FaceDetectorFactory`, `EmbeddingExtractorFactory` create models based on config | DESIGN_ANALYSIS: Lines 422-468 | -| **Builder** | ⚪ NOT NEEDED | No complex object construction required in MVP | N/A | -| **Prototype** | ⚪ NOT NEEDED | Not applicable to this domain | N/A | - -**Creational Patterns**: 2/2 needed ✅ - ---- - -### Structural Patterns - -| Pattern | Status | Implementation | Location | -|---------|--------|----------------|----------| -| **Adapter** | ⚪ FUTURE | Can adapt different ML libraries if needed | Extensible via interfaces | -| **Facade** | ✅ IMPLEMENTED | `FaceProcessingFacade` simplifies complex ML pipeline | DESIGN_ANALYSIS: Lines 541-589 | -| **Proxy** | ⚪ NOT NEEDED | No proxy requirements in MVP | N/A | -| **Decorator** | ⚪ FUTURE | Could decorate detectors with caching | Possible extension | - -**Structural Patterns**: 1/1 needed ✅ - ---- - -### Behavioral Patterns - -| Pattern | Status | Implementation | Location | -|---------|--------|----------------|----------| -| **Observer** | ✅ IMPLEMENTED | Webhook notifications use observer pattern | DESIGN_ANALYSIS: Lines 630-709 | -| **Strategy** | ✅ IMPLEMENTED | `CosineSimilarityStrategy`, `EuclideanDistanceStrategy` for different similarity calculations | DESIGN_ANALYSIS: Lines 470-539 | -| **Command** | ⚪ NOT NEEDED | Use cases are command-like but don't need full pattern | N/A | -| **Chain of Responsibility** | ✅ IMPLEMENTED | Image preprocessing pipeline | DESIGN_ANALYSIS: Lines 591-628 | -| **Template Method** | ⚪ NOT NEEDED | Not needed with composition approach | N/A | - -**Behavioral Patterns**: 3/3 needed ✅ - ---- - -### Additional Patterns - -| Pattern | Status | Implementation | Location | -|---------|--------|----------------|----------| -| **Repository** | ✅ IMPLEMENTED | `IEmbeddingRepository` with `PostgresEmbeddingRepository` and `InMemoryEmbeddingRepository` | DESIGN_ANALYSIS: Lines 374-420 | -| **Dependency Injection** | ✅ IMPLEMENTED | Full DI container with `dependency-injector` library | DESIGN_ANALYSIS: Lines 497-539 | - -**Total Design Patterns**: 7 implemented ✅ - ---- - -## ✅ Anti-Patterns Avoided - -### Code Smells - ELIMINATED - -| Smell | Found | Fixed | Evidence | -|-------|-------|-------|----------| -| **God Object** | ✅ YES | ✅ FIXED | `FaceRecognitionService` split into 5 focused classes | DESIGN_ANALYSIS: Lines 72-140 | -| **Spaghetti Code** | ✅ YES | ✅ FIXED | Clear layered architecture implemented | DESIGN_ANALYSIS: Lines 710-757 | -| **Magic Numbers** | ✅ YES | ✅ FIXED | All thresholds in config with validation | DESIGN_ANALYSIS: Lines 911-1006 | -| **Dead Code** | ⚪ N/A | N/A | Will be removed during migration | IMPLEMENTATION_PLAN: Sprint 1 | -| **Shotgun Surgery** | ✅ YES | ✅ FIXED | DI eliminates need to modify multiple files | DESIGN_ANALYSIS: Lines 315-372 | -| **Feature Envy** | ✅ YES | ✅ FIXED | API endpoints now delegate to use cases | IMPLEMENTATION_PLAN: Lines 340-378 | -| **Long Methods** | ✅ YES | ✅ FIXED | Endpoints simplified to ~10 lines | IMPLEMENTATION_PLAN: Lines 340-378 | -| **Large Classes** | ✅ YES | ✅ FIXED | `FaceRecognitionService` (173 lines) → multiple focused classes | DESIGN_ANALYSIS: Lines 72-140 | - -**Code Smells**: 8/8 addressed ✅ - ---- - -### Architecture Anti-Patterns - ELIMINATED - -| Anti-Pattern | Found | Fixed | Evidence | -|--------------|-------|-------|----------| -| **Big Ball of Mud** | ✅ YES | ✅ FIXED | Clean Architecture with clear layers | DESIGN_ANALYSIS: Lines 710-831 | -| **Golden Hammer** | ⚪ NO | N/A | Using appropriate tools for each problem | Throughout design | -| **Lava Flow** | ⚪ NO | N/A | Clean codebase, will remove old code | Migration plan | -| **Vendor Lock-in** | ✅ YES | ✅ FIXED | Abstraction layer over ML libraries | DESIGN_ANALYSIS: Lines 187-243 | -| **Premature Optimization** | ✅ YES | ✅ AVOIDED | Optimization deferred to Sprint 5 after profiling | IMPLEMENTATION_PLAN: Sprint 5 | - -**Architecture Anti-Patterns**: 5/5 addressed ✅ - ---- - -### Development Anti-Patterns - ELIMINATED - -| Anti-Pattern | Found | Fixed | Evidence | -|--------------|-------|-------|----------| -| **Copy-Paste Programming** | ✅ YES | ✅ FIXED | DRY violations eliminated with `FileStorageService` | DESIGN_ANALYSIS: Lines 599-641 | -| **Hard Coding** | ✅ YES | ✅ FIXED | All config externalized with Pydantic Settings | DESIGN_ANALYSIS: Lines 911-1006 | -| **Not Invented Here** | ⚪ NO | N/A | Using established libraries (DeepFace, FastAPI) | Throughout | -| **Reinventing the Wheel** | ⚪ NO | N/A | Using standard patterns and libraries | Throughout | - -**Development Anti-Patterns**: 4/4 addressed ✅ - ---- - -## ✅ Code Quality Principles - -### Clean Code - -| Principle | Status | Evidence | -|-----------|--------|----------| -| **Meaningful Names** | ✅ PASS | `EnrollFaceUseCase`, `FaceDetectionResult`, `CosineSimilarityCalculator` - all descriptive | Throughout code examples | -| **Small Functions** | ✅ PASS | Use case methods ~20-40 lines, endpoints ~10 lines | IMPLEMENTATION_PLAN: Lines 198-378 | -| **Few Arguments** | ✅ PASS | Dependencies injected via constructor, execute methods have 1-2 args | IMPLEMENTATION_PLAN: Lines 198-286 | -| **Self-Documenting** | ✅ PASS | Clear naming, type hints make code self-explanatory | All code examples | -| **Comments Explain Why** | ✅ PASS | Comments explain business rules, not obvious code | Code examples include docstrings | -| **Consistent Style** | ✅ PASS | Black formatter, isort, pylint configured | IMPLEMENTATION_PLAN: Lines 627-682 | - -**Clean Code Score**: 6/6 ✅ - ---- - -### Error Handling - -| Principle | Status | Implementation | -|-----------|--------|----------------| -| **Use Exceptions** | ✅ PASS | Domain exceptions instead of error codes | DESIGN_ANALYSIS: Lines 759-819 | -| **Provide Context** | ✅ PASS | Exceptions include relevant data (user_id, quality_score, etc.) | DESIGN_ANALYSIS: Lines 771-819 | -| **Don't Return Null** | ✅ PASS | Using `Optional[T]` and raising exceptions | IMPLEMENTATION_PLAN: Lines 198-286 | -| **Fail Fast** | ✅ PASS | Validation at entry points, immediate exception on errors | IMPLEMENTATION_PLAN: Lines 198-286 | -| **Appropriate Level** | ✅ PASS | Domain exceptions at domain layer, HTTP errors at API layer | DESIGN_ANALYSIS: Lines 821-896 | - -**Error Handling Score**: 5/5 ✅ - ---- - -### Testing - -| Principle | Status | Implementation | -|-----------|--------|----------------| -| **Unit Tests** | ✅ PLANNED | All business logic tested | IMPLEMENTATION_PLAN: Lines 383-570 | -| **High Coverage** | ✅ PLANNED | Target 80%+ | IMPLEMENTATION_PLAN: Line 544 | -| **Test Behavior** | ✅ PASS | Tests verify outcomes, not implementation | IMPLEMENTATION_PLAN: Lines 422-536 | -| **AAA Pattern** | ✅ PASS | Arrange, Act, Assert in all test examples | IMPLEMENTATION_PLAN: Lines 422-536 | -| **Independent Tests** | ✅ PASS | Each test isolated with fixtures | IMPLEMENTATION_PLAN: Lines 422-469 | -| **Edge Cases** | ✅ PLANNED | Tests for no face, multiple faces, poor quality | IMPLEMENTATION_PLAN: Lines 470-536 | - -**Testing Score**: 6/6 ✅ - ---- - -## ✅ Architecture Principles - -### Modularity & Coupling - -| Principle | Status | Evidence | -|-----------|--------|----------| -| **High Cohesion** | ✅ PASS | Each module has related functionality | Domain layer groups related concepts | -| **Loose Coupling** | ✅ PASS | Layers coupled via interfaces only | Infrastructure implements domain interfaces | -| **Clear Boundaries** | ✅ PASS | 4 distinct layers with clear responsibilities | DESIGN_ANALYSIS: Lines 710-757 | -| **Minimal Dependencies** | ✅ PASS | Domain has zero infrastructure dependencies | Clean Architecture dependency rule | - -**Modularity Score**: 4/4 ✅ - ---- - -### Scalability Considerations - -| Consideration | Status | Implementation | -|---------------|--------|----------------| -| **Horizontal Scaling** | ✅ DESIGNED | Stateless API design | Application layer | -| **Stateless Services** | ✅ PASS | No session state, all state in DB | Use cases don't hold state | -| **Strategic Caching** | ✅ PLANNED | Caching strategy in Sprint 5 | IMPLEMENTATION_PLAN: Sprint 5 | -| **Async Processing** | ✅ PLANNED | Celery for long operations in Sprint 4+ | DESIGN_ANALYSIS: Lines 94-103 | -| **Database Scaling** | ✅ DESIGNED | pgvector for efficient similarity search | IMPLEMENTATION_PLAN: Sprint 4 | - -**Scalability Score**: 5/5 ✅ - ---- - -### Security First - -| Security Practice | Status | Implementation | -|-------------------|--------|----------------| -| **Input Validation** | ✅ PASS | Pydantic models validate all input | IMPLEMENTATION_PLAN: Lines 340-378 | -| **SQL Injection Prevention** | ✅ PASS | SQLAlchemy ORM, parameterized queries | IMPLEMENTATION_PLAN: Sprint 4 | -| **Authentication/Authorization** | ⚪ FUTURE | Delegated to identity-core-api | Integration point | -| **Encrypted Storage** | ✅ PLANNED | Database encryption at rest configurable | DESIGN_ANALYSIS: Line 578 | -| **Least Privilege** | ✅ DESIGNED | Service accounts with minimal permissions | Deployment strategy | -| **Updated Dependencies** | ✅ PLANNED | Dependency scanning in CI/CD | IMPLEMENTATION_PLAN: Sprint 5 | -| **No Secrets in Code** | ✅ PASS | All config in environment variables | DESIGN_ANALYSIS: Lines 752-783 | -| **CORS Configured** | ✅ FIXED | Removed wildcard, proper origins | DESIGN_ANALYSIS: Lines 141-159 | -| **Rate Limiting** | ✅ PLANNED | slowapi integration | IMPLEMENTATION_PLAN: Lines 415-434 | - -**Security Score**: 9/9 ✅ - ---- - -## ✅ Performance Best Practices - -| Practice | Status | Implementation | -|----------|--------|----------------| -| **Profile Before Optimize** | ✅ PLANNED | Profiling in Sprint 5 before optimization | IMPLEMENTATION_PLAN: Sprint 5 | -| **Algorithm Optimization** | ✅ DESIGNED | Efficient similarity search with pgvector | IMPLEMENTATION_PLAN: Sprint 4 | -| **Big O Awareness** | ✅ PASS | O(log n) similarity search with indexing | pgvector implementation | -| **Appropriate Data Structures** | ✅ PASS | numpy arrays for embeddings, vectors in DB | Throughout | -| **Lazy Loading** | ✅ DESIGNED | Models loaded on startup (singleton) | DI container | -| **Smart Caching** | ✅ PLANNED | Cache expensive ML operations | Sprint 5 | -| **Minimize DB Queries** | ✅ DESIGNED | Repository pattern with efficient queries | IMPLEMENTATION_PLAN: Sprint 4 | -| **Database Indexing** | ✅ PLANNED | Indexes on user_id, tenant_id, vector | IMPLEMENTATION_PLAN: Lines 788-820 | - -**Performance Score**: 8/8 ✅ - ---- - -## ✅ Version Control Best Practices - -| Practice | Status | Evidence | -|----------|--------|----------| -| **Clear Commit Messages** | ✅ PASS | Detailed commit with context and bullet points | Git commit created | -| **Small Logical Changes** | ✅ PASS | This commit only adds design documents | Single focused commit | -| **Atomic Commits** | ✅ PASS | Related changes in single commit | 2 design files together | -| **Feature Branches** | ✅ PASS | Working on `claude/check-the-m-*` branch | Branch created | -| **Code Review Ready** | ✅ PASS | Documents ready for team review | Comprehensive documentation | -| **No Secrets** | ✅ PASS | No credentials in repo, .env.example only | .env.example pattern | - -**Version Control Score**: 6/6 ✅ - ---- - -## ✅ Documentation - -| Documentation Type | Status | Location | -|--------------------|--------|----------| -| **README** | ✅ EXISTS | README.md (to be updated) | -| **API Contracts** | ✅ PLANNED | OpenAPI/Swagger in Sprint 5 | IMPLEMENTATION_PLAN: Sprint 5 | -| **Architecture Diagrams** | ✅ CREATED | ASCII diagrams in DESIGN_ANALYSIS.md | Lines 106-155, 710-757 | -| **Deployment Procedures** | ✅ PLANNED | Sprint 5 deployment guide | IMPLEMENTATION_PLAN: Sprint 5 | -| **Known Limitations** | ✅ DOCUMENTED | Trade-offs noted in design docs | DESIGN_ANALYSIS throughout | -| **Complex Business Logic** | ✅ DOCUMENTED | Liveness detection, quality assessment explained | MODULE_PLAN: Lines 37-80 | -| **API Documentation** | ✅ PLANNED | Swagger UI, OpenAPI spec | IMPLEMENTATION_PLAN: Sprint 5 | -| **Code Examples** | ✅ EXTENSIVE | Complete implementations in IMPLEMENTATION_PLAN | Throughout IMPLEMENTATION_PLAN | - -**Documentation Score**: 8/8 ✅ - ---- - -## ✅ Collaboration & Communication - -| Practice | Status | Evidence | -|----------|--------|----------| -| **Code for Humans** | ✅ PASS | Descriptive names, clear structure, extensive comments | All code examples | -| **Code Review Ready** | ✅ PASS | Clear documentation makes review easy | 2 comprehensive docs | -| **Constructive Content** | ✅ PASS | Analysis identifies issues and provides solutions | DESIGN_ANALYSIS throughout | -| **Knowledge Sharing** | ✅ PASS | Detailed explanations of patterns and principles | Educational content included | - -**Collaboration Score**: 4/4 ✅ - ---- - -## ✅ Before Committing Code - Final Checklist - -| Question | Answer | Evidence | -|----------|--------|----------| -| **Does it work?** | ⏸️ NOT YET IMPLEMENTED | Design phase, implementation in Sprint 1-5 | -| **Is it readable?** | ✅ YES | Clear names, structure, extensive documentation | -| **Is it maintainable?** | ✅ YES | Clean architecture, SOLID principles, design patterns | -| **Follows conventions?** | ✅ YES | Python best practices, FastAPI patterns | -| **Are there tests?** | ⏸️ PLANNED | Comprehensive test strategy in Sprint 2 | -| **Properly documented?** | ✅ YES | 2 extensive design documents created | -| **Errors handled?** | ✅ YES | Domain exception hierarchy designed | -| **Could this be simpler?** | ✅ YES | Applied KISS, YAGNI - simplified from original | -| **Security vulnerabilities?** | ✅ NONE | Fixed CORS, added rate limiting, validation | -| **Performance adequate?** | ⏸️ TO BE MEASURED | Performance testing in Sprint 5 | - -**Pre-Commit Score**: 7/7 ready, 3/3 pending implementation ✅ - ---- - -## 📊 Overall Validation Summary - -### Scores by Category - -| Category | Items | Passed | Score | Status | -|----------|-------|--------|-------|--------| -| **SOLID Principles** | 5 | 5 | 100% | ✅ | -| **DRY, KISS, YAGNI** | 3 | 3 | 100% | ✅ | -| **Design Patterns** | 7 | 7 | 100% | ✅ | -| **Anti-Patterns Avoided** | 17 | 17 | 100% | ✅ | -| **Clean Code** | 6 | 6 | 100% | ✅ | -| **Error Handling** | 5 | 5 | 100% | ✅ | -| **Testing** | 6 | 6 | 100% | ✅ | -| **Architecture** | 4 | 4 | 100% | ✅ | -| **Scalability** | 5 | 5 | 100% | ✅ | -| **Security** | 9 | 9 | 100% | ✅ | -| **Performance** | 8 | 8 | 100% | ✅ | -| **Version Control** | 6 | 6 | 100% | ✅ | -| **Documentation** | 8 | 8 | 100% | ✅ | - -**Total Items Validated**: 89 -**Items Passed**: 89 -**Pass Rate**: 100% ✅ - ---- - -## 🎯 Critical Findings - -### ✅ Strengths - -1. **Complete SOLID Compliance**: All 5 principles properly applied -2. **Comprehensive Pattern Usage**: 7 design patterns implemented appropriately -3. **All Anti-Patterns Eliminated**: 17 anti-patterns identified and fixed -4. **Security Hardened**: Fixed critical CORS vulnerability, added rate limiting -5. **Clean Architecture**: Proper layer separation with dependency inversion -6. **High Testability**: 80%+ coverage target with proper test strategy -7. **Professional Error Handling**: Domain exception hierarchy with error codes -8. **Scalability Designed**: Stateless design, efficient DB, async processing -9. **Well Documented**: Extensive documentation with code examples - -### ⚠️ Items Pending Implementation - -1. **Tests**: Comprehensive test suite (Sprint 2) -2. **Performance Metrics**: Actual performance to be measured (Sprint 5) -3. **Working Code**: Implementation follows in Sprint 1-5 - -### 🎓 Validation Conclusion - -**DESIGN APPROVED** ✅ - -The design fully complies with all software engineering best practices. All SOLID principles are followed, design patterns are appropriately applied, anti-patterns are avoided, and the architecture is clean, scalable, and maintainable. - -The design represents **professional, production-grade software engineering** and is ready for implementation. - ---- - -## 📋 Recommendations - -### Immediate Actions (Before Sprint 1) -1. ✅ Team review of design documents -2. ✅ Approval from tech lead/architect -3. ✅ Setup development environment - -### During Implementation -1. Follow implementation plan strictly -2. Don't skip tests (Sprint 2) -3. Code review every pull request -4. Measure performance in Sprint 5 -5. Keep documentation updated - -### After Implementation -1. Conduct architecture review -2. Security audit -3. Performance benchmarking -4. Gather team feedback - ---- - -**Validation Status**: ✅ DESIGN APPROVED - READY FOR IMPLEMENTATION -**Confidence Level**: VERY HIGH (100% checklist compliance) -**Risk Level**: LOW (all best practices followed) - -**Validated By**: Design Analysis Against Software Engineering Essential Checklist -**Date**: 2025-11-17 diff --git a/docs/6-architecture/FEATURE_DESIGN.md b/docs/6-architecture/FEATURE_DESIGN.md deleted file mode 100644 index 6fff1ea..0000000 --- a/docs/6-architecture/FEATURE_DESIGN.md +++ /dev/null @@ -1,1889 +0,0 @@ -# New Features Design Document - -**Date**: December 11, 2025 -**Version**: 1.1 -**Status**: Design Phase (SE Checklist Compliant) - ---- - -## Overview - -This document outlines the design for 11 new features to be added to the Biometric Processor API. All features follow Clean Architecture principles, SOLID principles, and existing patterns. - -**Compliance**: Validated against SE Checklist (DESIGN_VALIDATION_CHECKLIST.md) - ---- - -## Feature Summary - -| # | Feature | Priority | Effort | Sprint | -|---|---------|----------|--------|--------| -| 1 | Face Quality Feedback | High | 1 day | 6.1 | -| 2 | Multi-face Detection | High | 1 day | 6.1 | -| 3 | Image Preprocessing | Medium | 2 days | 6.1 | -| 4 | Age/Gender Estimation | High | 2 days | 6.2 | -| 5 | Face Landmark Detection | Medium | 2 days | 6.2 | -| 6 | Anti-spoofing Report | High | 1 day | 6.2 | -| 7 | Similarity Matrix | Medium | 2 days | 6.3 | -| 8 | Face Comparison API | High | 2 days | 6.3 | -| 9 | Embedding Export/Import | Medium | 3 days | 6.3 | -| 10 | Webhook Notifications | Low | 3 days | 6.4 | -| 11 | Rate Limiting per Tenant | Low | 2 days | 6.4 | - ---- - -## Architecture Overview - -### New Directory Structure - -``` -app/ -├── domain/ -│ ├── entities/ -│ │ ├── quality_feedback.py # NEW -│ │ ├── multi_face_result.py # NEW -│ │ ├── demographics.py # NEW -│ │ ├── face_landmarks.py # NEW -│ │ ├── liveness_report.py # NEW -│ │ ├── similarity_matrix.py # NEW -│ │ ├── face_comparison.py # NEW -│ │ ├── preprocess_result.py # NEW -│ │ └── webhook_event.py # NEW -│ ├── interfaces/ -│ │ ├── demographics_analyzer.py # NEW -│ │ ├── landmark_detector.py # NEW -│ │ ├── image_preprocessor.py # NEW -│ │ ├── webhook_sender.py # NEW -│ │ └── rate_limit_storage.py # NEW (SE Compliance) -│ └── exceptions/ -│ └── feature_errors.py # NEW (SE Compliance - all feature exceptions) -├── application/ -│ └── use_cases/ -│ ├── analyze_quality.py # NEW -│ ├── detect_multi_face.py # NEW -│ ├── analyze_demographics.py # NEW -│ ├── detect_landmarks.py # NEW -│ ├── compare_faces.py # NEW -│ ├── compute_similarity_matrix.py # NEW -│ ├── export_embeddings.py # NEW -│ ├── import_embeddings.py # NEW -│ └── send_webhook.py # NEW -├── infrastructure/ -│ ├── ml/ -│ │ ├── factories/ -│ │ │ ├── demographics_factory.py # NEW (SE Compliance - Open/Closed) -│ │ │ ├── landmark_factory.py # NEW (SE Compliance - Open/Closed) -│ │ │ └── preprocessor_factory.py # NEW (SE Compliance - Open/Closed) -│ │ ├── demographics/ -│ │ │ └── deepface_demographics.py # NEW -│ │ ├── landmarks/ -│ │ │ ├── mediapipe_landmarks.py # NEW -│ │ │ └── dlib_landmarks.py # NEW (alternative implementation) -│ │ └── preprocessing/ -│ │ └── image_preprocessor.py # NEW -│ ├── webhooks/ -│ │ ├── webhook_factory.py # NEW (SE Compliance - Open/Closed) -│ │ ├── http_webhook_sender.py # NEW -│ │ └── mock_webhook_sender.py # NEW (for testing) -│ └── rate_limit/ -│ ├── storage_factory.py # NEW (SE Compliance - Open/Closed) -│ ├── memory_storage.py # NEW -│ └── redis_storage.py # NEW (optional Redis backend) -├── api/ -│ ├── routes/ -│ │ ├── quality.py # NEW -│ │ ├── multi_face.py # NEW -│ │ ├── demographics.py # NEW -│ │ ├── landmarks.py # NEW -│ │ ├── comparison.py # NEW -│ │ ├── similarity_matrix.py # NEW -│ │ ├── embeddings_io.py # NEW -│ │ └── webhooks.py # NEW -│ ├── schemas/ -│ │ ├── quality.py # NEW -│ │ ├── demographics.py # NEW -│ │ ├── landmarks.py # NEW -│ │ ├── comparison.py # NEW -│ │ └── webhooks.py # NEW -│ └── middleware/ -│ └── rate_limiter.py # UPDATE -└── core/ - ├── config.py # UPDATE - └── container.py # UPDATE (new factory functions) -``` - ---- - -## Factory Classes (Open/Closed Principle) - -Following the existing pattern (`FaceDetectorFactory`, `EmbeddingExtractorFactory`, `LivenessDetectorFactory`), all new ML components require factory classes. - -### DemographicsAnalyzerFactory - -```python -# app/infrastructure/ml/factories/demographics_factory.py -from typing import Literal - -from app.domain.interfaces.demographics_analyzer import IDemographicsAnalyzer - -DemographicsBackend = Literal["deepface"] - - -class DemographicsAnalyzerFactory: - """Factory for creating demographics analyzer instances. - - Follows Open/Closed Principle: Add new backends without modifying existing code. - """ - - @staticmethod - def create( - backend: DemographicsBackend = "deepface", - include_race: bool = False, - include_emotion: bool = True, - ) -> IDemographicsAnalyzer: - """Create demographics analyzer instance. - - Args: - backend: Backend to use for analysis - include_race: Whether to include race estimation - include_emotion: Whether to include emotion analysis - - Returns: - IDemographicsAnalyzer implementation - """ - if backend == "deepface": - from app.infrastructure.ml.demographics.deepface_demographics import ( - DeepFaceDemographicsAnalyzer, - ) - return DeepFaceDemographicsAnalyzer( - include_race=include_race, - include_emotion=include_emotion, - ) - raise ValueError(f"Unknown demographics backend: {backend}") -``` - -### LandmarkDetectorFactory - -```python -# app/infrastructure/ml/factories/landmark_factory.py -from typing import Literal - -from app.domain.interfaces.landmark_detector import ILandmarkDetector - -LandmarkModel = Literal["mediapipe_468", "dlib_68"] - - -class LandmarkDetectorFactory: - """Factory for creating landmark detector instances.""" - - @staticmethod - def create( - model: LandmarkModel = "mediapipe_468", - ) -> ILandmarkDetector: - """Create landmark detector instance. - - Args: - model: Landmark model to use - - Returns: - ILandmarkDetector implementation - """ - if model == "mediapipe_468": - from app.infrastructure.ml.landmarks.mediapipe_landmarks import ( - MediaPipeLandmarkDetector, - ) - return MediaPipeLandmarkDetector() - elif model == "dlib_68": - from app.infrastructure.ml.landmarks.dlib_landmarks import ( - DlibLandmarkDetector, - ) - return DlibLandmarkDetector() - raise ValueError(f"Unknown landmark model: {model}") -``` - -### ImagePreprocessorFactory - -```python -# app/infrastructure/ml/factories/preprocessor_factory.py -from app.domain.interfaces.image_preprocessor import IImagePreprocessor - - -class ImagePreprocessorFactory: - """Factory for creating image preprocessor instances.""" - - @staticmethod - def create( - auto_rotate: bool = True, - max_size: int = 1920, - normalize: bool = True, - ) -> IImagePreprocessor: - """Create image preprocessor instance.""" - from app.infrastructure.ml.preprocessing.image_preprocessor import ( - OpenCVImagePreprocessor, - ) - return OpenCVImagePreprocessor( - auto_rotate=auto_rotate, - max_size=max_size, - normalize=normalize, - ) -``` - -### WebhookSenderFactory - -```python -# app/infrastructure/webhooks/webhook_factory.py -from typing import Literal - -from app.domain.interfaces.webhook_sender import IWebhookSender - -WebhookTransport = Literal["http", "mock"] - - -class WebhookSenderFactory: - """Factory for creating webhook sender instances.""" - - @staticmethod - def create( - transport: WebhookTransport = "http", - timeout: int = 10, - retry_count: int = 3, - ) -> IWebhookSender: - """Create webhook sender instance.""" - if transport == "http": - from app.infrastructure.webhooks.http_webhook_sender import ( - HttpWebhookSender, - ) - return HttpWebhookSender(timeout=timeout, retry_count=retry_count) - elif transport == "mock": - from app.infrastructure.webhooks.mock_webhook_sender import ( - MockWebhookSender, - ) - return MockWebhookSender() - raise ValueError(f"Unknown webhook transport: {transport}") -``` - -### RateLimitStorageFactory - -```python -# app/infrastructure/rate_limit/storage_factory.py -from typing import Literal - -from app.domain.interfaces.rate_limit_storage import IRateLimitStorage - -StorageBackend = Literal["memory", "redis"] - - -class RateLimitStorageFactory: - """Factory for creating rate limit storage instances.""" - - @staticmethod - def create( - backend: StorageBackend = "memory", - redis_url: str | None = None, - ) -> IRateLimitStorage: - """Create rate limit storage instance.""" - if backend == "memory": - from app.infrastructure.rate_limit.memory_storage import ( - InMemoryRateLimitStorage, - ) - return InMemoryRateLimitStorage() - elif backend == "redis": - from app.infrastructure.rate_limit.redis_storage import ( - RedisRateLimitStorage, - ) - return RedisRateLimitStorage(redis_url=redis_url) - raise ValueError(f"Unknown storage backend: {backend}") -``` - ---- - -## Exception Classes & Error Codes - -Following existing pattern (`BiometricProcessorError` hierarchy). - -### New Exception Classes - -```python -# app/domain/exceptions/feature_errors.py -from app.domain.exceptions.base import BiometricProcessorError - - -class DemographicsError(BiometricProcessorError): - """Demographics analysis error.""" - error_code = "DEMOGRAPHICS_ERROR" - - -class DemographicsModelError(DemographicsError): - """Demographics model loading/inference error.""" - error_code = "DEMOGRAPHICS_MODEL_ERROR" - - -class LandmarkError(BiometricProcessorError): - """Landmark detection error.""" - error_code = "LANDMARK_ERROR" - - -class LandmarkModelError(LandmarkError): - """Landmark model error.""" - error_code = "LANDMARK_MODEL_ERROR" - - -class PreprocessingError(BiometricProcessorError): - """Image preprocessing error.""" - error_code = "PREPROCESSING_ERROR" - - -class WebhookError(BiometricProcessorError): - """Base webhook error.""" - error_code = "WEBHOOK_ERROR" - - -class WebhookDeliveryError(WebhookError): - """Webhook delivery failed after retries.""" - error_code = "WEBHOOK_DELIVERY_FAILED" - - -class WebhookTimeoutError(WebhookError): - """Webhook request timed out.""" - error_code = "WEBHOOK_TIMEOUT" - - -class WebhookConfigError(WebhookError): - """Webhook configuration error.""" - error_code = "WEBHOOK_CONFIG_ERROR" - - -class ExportError(BiometricProcessorError): - """Embedding export error.""" - error_code = "EXPORT_ERROR" - - -class ImportError(BiometricProcessorError): - """Embedding import error.""" - error_code = "IMPORT_ERROR" - - -class ImportValidationError(ImportError): - """Import file validation failed.""" - error_code = "IMPORT_VALIDATION_ERROR" - - -class RateLimitError(BiometricProcessorError): - """Rate limit exceeded.""" - error_code = "RATE_LIMIT_EXCEEDED" -``` - -### Error Code Table - -| Code | Exception | HTTP Status | Description | -|------|-----------|-------------|-------------| -| `DEMOGRAPHICS_ERROR` | DemographicsError | 500 | General demographics error | -| `DEMOGRAPHICS_MODEL_ERROR` | DemographicsModelError | 500 | Model loading failed | -| `LANDMARK_ERROR` | LandmarkError | 500 | General landmark error | -| `LANDMARK_MODEL_ERROR` | LandmarkModelError | 500 | Model loading failed | -| `PREPROCESSING_ERROR` | PreprocessingError | 400 | Image preprocessing failed | -| `WEBHOOK_ERROR` | WebhookError | 500 | General webhook error | -| `WEBHOOK_DELIVERY_FAILED` | WebhookDeliveryError | 500 | Delivery failed after retries | -| `WEBHOOK_TIMEOUT` | WebhookTimeoutError | 504 | Webhook timed out | -| `WEBHOOK_CONFIG_ERROR` | WebhookConfigError | 400 | Invalid configuration | -| `EXPORT_ERROR` | ExportError | 500 | Export failed | -| `IMPORT_ERROR` | ImportError | 500 | Import failed | -| `IMPORT_VALIDATION_ERROR` | ImportValidationError | 400 | Invalid import file | -| `RATE_LIMIT_EXCEEDED` | RateLimitError | 429 | Too many requests | - ---- - -## Container.py Updates (Dependency Injection) - -### New Factory Functions - -```python -# app/core/container.py additions - -from app.domain.interfaces.demographics_analyzer import IDemographicsAnalyzer -from app.domain.interfaces.landmark_detector import ILandmarkDetector -from app.domain.interfaces.image_preprocessor import IImagePreprocessor -from app.domain.interfaces.webhook_sender import IWebhookSender -from app.domain.interfaces.rate_limit_storage import IRateLimitStorage - -from app.infrastructure.ml.factories.demographics_factory import DemographicsAnalyzerFactory -from app.infrastructure.ml.factories.landmark_factory import LandmarkDetectorFactory -from app.infrastructure.ml.factories.preprocessor_factory import ImagePreprocessorFactory -from app.infrastructure.webhooks.webhook_factory import WebhookSenderFactory -from app.infrastructure.rate_limit.storage_factory import RateLimitStorageFactory - - -@lru_cache() -def get_demographics_analyzer() -> IDemographicsAnalyzer: - """Get demographics analyzer instance (singleton).""" - logger.info("Creating demographics analyzer") - return DemographicsAnalyzerFactory.create( - backend="deepface", - include_race=settings.DEMOGRAPHICS_INCLUDE_RACE, - include_emotion=settings.DEMOGRAPHICS_INCLUDE_EMOTION, - ) - - -@lru_cache() -def get_landmark_detector() -> ILandmarkDetector: - """Get landmark detector instance (singleton).""" - logger.info(f"Creating landmark detector: {settings.LANDMARK_MODEL}") - return LandmarkDetectorFactory.create(model=settings.LANDMARK_MODEL) - - -@lru_cache() -def get_image_preprocessor() -> IImagePreprocessor: - """Get image preprocessor instance (singleton).""" - logger.info("Creating image preprocessor") - return ImagePreprocessorFactory.create( - auto_rotate=settings.PREPROCESS_AUTO_ROTATE, - max_size=settings.PREPROCESS_MAX_SIZE, - normalize=settings.PREPROCESS_NORMALIZE, - ) - - -@lru_cache() -def get_webhook_sender() -> IWebhookSender: - """Get webhook sender instance (singleton).""" - logger.info("Creating webhook sender") - return WebhookSenderFactory.create( - transport="http", - timeout=settings.WEBHOOK_TIMEOUT, - retry_count=settings.WEBHOOK_RETRY_COUNT, - ) - - -@lru_cache() -def get_rate_limit_storage() -> IRateLimitStorage: - """Get rate limit storage instance (singleton).""" - logger.info(f"Creating rate limit storage: {settings.RATE_LIMIT_STORAGE}") - return RateLimitStorageFactory.create( - backend=settings.RATE_LIMIT_STORAGE, - redis_url=settings.REDIS_URL if settings.RATE_LIMIT_STORAGE == "redis" else None, - ) - - -# Use Case Factory Functions - -def get_analyze_quality_use_case() -> AnalyzeQualityUseCase: - """Get analyze quality use case instance.""" - return AnalyzeQualityUseCase( - detector=get_face_detector(), - quality_assessor=get_quality_assessor(), - ) - - -def get_detect_multi_face_use_case() -> DetectMultiFaceUseCase: - """Get detect multi-face use case instance.""" - return DetectMultiFaceUseCase( - detector=get_face_detector(), - quality_assessor=get_quality_assessor(), - ) - - -def get_analyze_demographics_use_case() -> AnalyzeDemographicsUseCase: - """Get analyze demographics use case instance.""" - return AnalyzeDemographicsUseCase( - detector=get_face_detector(), - demographics_analyzer=get_demographics_analyzer(), - ) - - -def get_detect_landmarks_use_case() -> DetectLandmarksUseCase: - """Get detect landmarks use case instance.""" - return DetectLandmarksUseCase( - detector=get_face_detector(), - landmark_detector=get_landmark_detector(), - ) - - -def get_compare_faces_use_case() -> CompareFacesUseCase: - """Get compare faces use case instance.""" - return CompareFacesUseCase( - detector=get_face_detector(), - extractor=get_embedding_extractor(), - similarity_calculator=get_similarity_calculator(), - quality_assessor=get_quality_assessor(), - ) - - -def get_compute_similarity_matrix_use_case() -> ComputeSimilarityMatrixUseCase: - """Get compute similarity matrix use case instance.""" - return ComputeSimilarityMatrixUseCase( - detector=get_face_detector(), - extractor=get_embedding_extractor(), - similarity_calculator=get_similarity_calculator(), - ) - - -def get_export_embeddings_use_case() -> ExportEmbeddingsUseCase: - """Get export embeddings use case instance.""" - return ExportEmbeddingsUseCase( - repository=get_embedding_repository(), - ) - - -def get_import_embeddings_use_case() -> ImportEmbeddingsUseCase: - """Get import embeddings use case instance.""" - return ImportEmbeddingsUseCase( - repository=get_embedding_repository(), - ) - - -def get_send_webhook_use_case() -> SendWebhookUseCase: - """Get send webhook use case instance.""" - return SendWebhookUseCase( - webhook_sender=get_webhook_sender(), - ) -``` - ---- - -## Missing Interface: IRateLimitStorage - -```python -# app/domain/interfaces/rate_limit_storage.py -from typing import Protocol -from dataclasses import dataclass - - -@dataclass -class RateLimitInfo: - """Rate limit information for a key.""" - limit: int - remaining: int - reset_at: int # Unix timestamp - tier: str - - -class IRateLimitStorage(Protocol): - """Interface for rate limit storage.""" - - async def increment(self, key: str, limit: int, window_seconds: int) -> RateLimitInfo: - """Increment request count and return current limit info. - - Args: - key: Unique key (tenant_id or API key) - limit: Maximum requests in window - window_seconds: Time window in seconds - - Returns: - Current rate limit information - """ - ... - - async def get(self, key: str) -> RateLimitInfo | None: - """Get current rate limit info without incrementing. - - Args: - key: Unique key - - Returns: - Rate limit info or None if not found - """ - ... - - async def reset(self, key: str) -> None: - """Reset rate limit for a key. - - Args: - key: Unique key to reset - """ - ... -``` - ---- - -## Feature 1: Face Quality Feedback - -### Purpose -Return specific, actionable reasons why an image failed quality checks. - -### API Endpoint - -``` -POST /api/v1/quality/analyze -``` - -### Request -```json -{ - "file": "" -} -``` - -### Response -```json -{ - "overall_score": 72.5, - "passed": false, - "issues": [ - { - "code": "BLUR_DETECTED", - "severity": "high", - "message": "Image is too blurry", - "value": 45.2, - "threshold": 100.0, - "suggestion": "Use a stable camera or better lighting" - }, - { - "code": "FACE_TOO_SMALL", - "severity": "medium", - "message": "Face is too small in frame", - "value": 65, - "threshold": 80, - "suggestion": "Move closer to the camera" - } - ], - "metrics": { - "blur_score": 45.2, - "brightness": 0.65, - "face_size": 65, - "face_angle": 12.5, - "occlusion": 0.0 - } -} -``` - -### Domain Entity - -```python -# app/domain/entities/quality_feedback.py -@dataclass -class QualityIssue: - code: str # BLUR_DETECTED, FACE_TOO_SMALL, etc. - severity: str # high, medium, low - message: str - value: float - threshold: float - suggestion: str - -@dataclass -class QualityMetrics: - blur_score: float - brightness: float - face_size: int - face_angle: float - occlusion: float - -@dataclass -class QualityFeedback: - overall_score: float - passed: bool - issues: List[QualityIssue] - metrics: QualityMetrics -``` - -### Issue Codes - -| Code | Description | Threshold | -|------|-------------|-----------| -| `BLUR_DETECTED` | Image is blurry | blur_score < 100 | -| `LOW_BRIGHTNESS` | Image too dark | brightness < 0.3 | -| `HIGH_BRIGHTNESS` | Image too bright | brightness > 0.9 | -| `FACE_TOO_SMALL` | Face too small | face_size < 80px | -| `FACE_TOO_LARGE` | Face too close | face_size > 80% of image | -| `FACE_ANGLE` | Face not frontal | angle > 30° | -| `OCCLUSION` | Face partially covered | occlusion > 0.2 | -| `MULTIPLE_FACES` | Multiple faces detected | count > 1 | -| `NO_FACE` | No face detected | count = 0 | - ---- - -## Feature 2: Multi-face Detection - -### Purpose -Detect and return information about all faces in an image. - -### API Endpoint - -``` -POST /api/v1/faces/detect-all -``` - -### Response -```json -{ - "face_count": 3, - "faces": [ - { - "face_id": 0, - "bounding_box": { - "x": 100, "y": 50, "width": 150, "height": 180 - }, - "confidence": 0.98, - "quality_score": 85.5, - "landmarks": { - "left_eye": [125, 90], - "right_eye": [175, 88], - "nose": [150, 130], - "mouth_left": [130, 160], - "mouth_right": [170, 158] - } - }, - // ... more faces - ], - "image_dimensions": { - "width": 1920, - "height": 1080 - } -} -``` - -### Domain Entity - -```python -# app/domain/entities/multi_face_result.py -@dataclass -class BoundingBox: - x: int - y: int - width: int - height: int - -@dataclass -class BasicLandmarks: - left_eye: Tuple[int, int] - right_eye: Tuple[int, int] - nose: Tuple[int, int] - mouth_left: Tuple[int, int] - mouth_right: Tuple[int, int] - -@dataclass -class DetectedFace: - face_id: int - bounding_box: BoundingBox - confidence: float - quality_score: float - landmarks: BasicLandmarks - -@dataclass -class MultiFaceResult: - face_count: int - faces: List[DetectedFace] - image_width: int - image_height: int -``` - ---- - -## Feature 3: Image Preprocessing - -### Purpose -Automatically preprocess images before face operations. - -### Interface - -```python -# app/domain/interfaces/image_preprocessor.py -class IImagePreprocessor(Protocol): - def preprocess( - self, - image: np.ndarray, - options: PreprocessOptions - ) -> PreprocessResult: - """Preprocess image for face recognition.""" - ... -``` - -### Preprocessing Steps - -| Step | Description | Configurable | -|------|-------------|--------------| -| Auto-rotate | Fix EXIF orientation | Yes | -| Resize | Limit max dimension | Yes (max 1920px) | -| Normalize | Histogram equalization | Yes | -| Denoise | Remove noise | Yes | -| Color correction | White balance | Yes | - -### Domain Entity - -```python -# app/domain/entities/preprocess_result.py -@dataclass -class PreprocessOptions: - auto_rotate: bool = True - max_size: int = 1920 - normalize: bool = True - denoise: bool = False - color_correct: bool = False - -@dataclass -class PreprocessResult: - image: np.ndarray - original_size: Tuple[int, int] - new_size: Tuple[int, int] - was_rotated: bool - rotation_angle: int - operations_applied: List[str] -``` - ---- - -## Feature 4: Age/Gender Estimation - -### Purpose -Estimate age and gender from face image. - -### API Endpoint - -``` -POST /api/v1/demographics/analyze -``` - -### Response -```json -{ - "age": { - "value": 28, - "range": [25, 32], - "confidence": 0.85 - }, - "gender": { - "value": "female", - "confidence": 0.92 - }, - "race": { - "dominant": "asian", - "confidence": 0.78, - "all": { - "asian": 0.78, - "white": 0.15, - "black": 0.04, - "indian": 0.02, - "arab": 0.01 - } - }, - "emotion": { - "dominant": "happy", - "confidence": 0.88, - "all": { - "happy": 0.88, - "neutral": 0.08, - "surprise": 0.02, - "sad": 0.01, - "angry": 0.01, - "fear": 0.00, - "disgust": 0.00 - } - } -} -``` - -### Interface - -```python -# app/domain/interfaces/demographics_analyzer.py -class IDemographicsAnalyzer(Protocol): - def analyze(self, image: np.ndarray) -> DemographicsResult: - """Analyze demographics from face image.""" - ... - - def get_supported_attributes(self) -> List[str]: - """Get list of analyzable attributes.""" - ... -``` - -### Domain Entity - -```python -# app/domain/entities/demographics.py -@dataclass -class AgeEstimate: - value: int - range: Tuple[int, int] - confidence: float - -@dataclass -class GenderEstimate: - value: str # "male" or "female" - confidence: float - -@dataclass -class RaceEstimate: - dominant: str - confidence: float - all_probabilities: Dict[str, float] - -@dataclass -class EmotionEstimate: - dominant: str - confidence: float - all_probabilities: Dict[str, float] - -@dataclass -class DemographicsResult: - age: AgeEstimate - gender: GenderEstimate - race: Optional[RaceEstimate] - emotion: Optional[EmotionEstimate] -``` - -### Implementation - -Uses DeepFace's built-in `analyze()` function with actions=['age', 'gender', 'race', 'emotion']. - ---- - -## Feature 5: Face Landmark Detection - -### Purpose -Return detailed facial landmarks (68 or 468 points). - -### API Endpoint - -``` -POST /api/v1/landmarks/detect -``` - -### Query Parameters -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `model` | string | "mediapipe" | Model: "dlib_68", "mediapipe_468" | -| `include_3d` | bool | false | Include 3D coordinates | - -### Response -```json -{ - "model": "mediapipe_468", - "landmark_count": 468, - "landmarks": [ - {"id": 0, "x": 123, "y": 456, "z": 0.5}, - {"id": 1, "x": 125, "y": 458, "z": 0.48}, - // ... 468 points - ], - "regions": { - "left_eye": [33, 133, 160, 159, 158, 144, 145, 153], - "right_eye": [362, 263, 387, 386, 385, 373, 374, 380], - "nose": [1, 2, 3, 4, 5, 6, 168, 197, 195, 5], - "mouth": [61, 185, 40, 39, 37, 0, 267, 269, 270, 409], - "left_eyebrow": [70, 63, 105, 66, 107, 55, 65], - "right_eyebrow": [300, 293, 334, 296, 336, 285, 295], - "face_oval": [10, 338, 297, 332, 284, 251, 389, 356, ...] - }, - "face_mesh_connections": [...], // For visualization - "head_pose": { - "pitch": 5.2, - "yaw": -3.1, - "roll": 1.8 - } -} -``` - -### Interface - -```python -# app/domain/interfaces/landmark_detector.py -class ILandmarkDetector(Protocol): - def detect( - self, - image: np.ndarray, - include_3d: bool = False - ) -> LandmarkResult: - """Detect facial landmarks.""" - ... - - def get_landmark_count(self) -> int: - """Get number of landmarks this detector provides.""" - ... -``` - -### Domain Entity - -```python -# app/domain/entities/face_landmarks.py -@dataclass -class Landmark: - id: int - x: int - y: int - z: Optional[float] = None - -@dataclass -class HeadPose: - pitch: float # Up/down - yaw: float # Left/right - roll: float # Tilt - -@dataclass -class LandmarkResult: - model: str - landmark_count: int - landmarks: List[Landmark] - regions: Dict[str, List[int]] - head_pose: Optional[HeadPose] -``` - ---- - -## Feature 6: Anti-spoofing Report - -### Purpose -Provide detailed breakdown of liveness detection analysis. - -### API Endpoint - -``` -POST /api/v1/liveness/report -``` - -### Response -```json -{ - "is_live": true, - "overall_score": 78.5, - "threshold": 70.0, - "detection_mode": "combined", - "analysis": { - "passive": { - "score": 72.3, - "passed": true, - "components": { - "texture_analysis": { - "score": 85.2, - "weight": 0.35, - "details": { - "laplacian_variance": 245.6, - "expected_range": [100, 500], - "verdict": "Natural texture detected" - } - }, - "color_distribution": { - "score": 68.4, - "weight": 0.25, - "details": { - "hsv_variance": 0.42, - "skin_tone_valid": true, - "verdict": "Natural color distribution" - } - }, - "frequency_analysis": { - "score": 71.0, - "weight": 0.25, - "details": { - "high_freq_ratio": 0.38, - "pattern_detected": false, - "verdict": "No printing patterns" - } - }, - "moire_detection": { - "score": 65.0, - "weight": 0.15, - "details": { - "moire_score": 0.12, - "screen_detected": false, - "verdict": "No screen patterns" - } - } - } - }, - "active": { - "score": 82.0, - "passed": true, - "components": { - "eye_analysis": { - "score": 90.0, - "details": { - "ear_value": 0.28, - "ear_threshold": 0.25, - "eyes_open": true, - "blink_detected": false - } - }, - "mouth_analysis": { - "score": 75.0, - "details": { - "mar_value": 0.45, - "mar_threshold": 0.6, - "smile_detected": false - } - } - } - } - }, - "recommendations": [ - "Consider enabling active liveness for higher security", - "Face angle is slightly off-center" - ], - "risk_level": "low" -} -``` - -### Domain Entity - -```python -# app/domain/entities/liveness_report.py -@dataclass -class ComponentAnalysis: - score: float - weight: float - details: Dict[str, Any] - verdict: str - -@dataclass -class PassiveAnalysis: - score: float - passed: bool - texture: ComponentAnalysis - color: ComponentAnalysis - frequency: ComponentAnalysis - moire: ComponentAnalysis - -@dataclass -class ActiveAnalysis: - score: float - passed: bool - eye_analysis: ComponentAnalysis - mouth_analysis: ComponentAnalysis - -@dataclass -class LivenessReport: - is_live: bool - overall_score: float - threshold: float - detection_mode: str - passive: Optional[PassiveAnalysis] - active: Optional[ActiveAnalysis] - recommendations: List[str] - risk_level: str # low, medium, high -``` - ---- - -## Feature 7: Similarity Matrix - -### Purpose -Compare multiple faces and return NxN similarity matrix. - -### API Endpoint - -``` -POST /api/v1/similarity/matrix -``` - -### Request -```json -{ - "files": ["", "", "", ...], - "labels": ["person_a", "person_b", "person_c", ...] // Optional -} -``` - -### Response -```json -{ - "size": 3, - "labels": ["person_a", "person_b", "person_c"], - "matrix": [ - [1.00, 0.25, 0.18], - [0.25, 1.00, 0.82], - [0.18, 0.82, 1.00] - ], - "clusters": [ - {"cluster_id": 0, "members": ["person_a"]}, - {"cluster_id": 1, "members": ["person_b", "person_c"]} - ], - "pairs": [ - {"a": "person_a", "b": "person_b", "similarity": 0.25, "match": false}, - {"a": "person_a", "b": "person_c", "similarity": 0.18, "match": false}, - {"a": "person_b", "b": "person_c", "similarity": 0.82, "match": true} - ], - "threshold": 0.6, - "computation_time_ms": 1250 -} -``` - -### Domain Entity - -```python -# app/domain/entities/similarity_matrix.py -@dataclass -class SimilarityPair: - a: str - b: str - similarity: float - match: bool - -@dataclass -class Cluster: - cluster_id: int - members: List[str] - -@dataclass -class SimilarityMatrixResult: - size: int - labels: List[str] - matrix: List[List[float]] - clusters: List[Cluster] - pairs: List[SimilarityPair] - threshold: float - computation_time_ms: int -``` - ---- - -## Feature 8: Face Comparison API - -### Purpose -Compare two face images directly without enrollment. - -### API Endpoint - -``` -POST /api/v1/compare -``` - -### Request -``` -Content-Type: multipart/form-data -- file1: -- file2: -- threshold: 0.6 (optional) -``` - -### Response -```json -{ - "match": true, - "similarity": 0.87, - "distance": 0.13, - "threshold": 0.6, - "confidence": "high", - "face1": { - "detected": true, - "quality_score": 85.2, - "bounding_box": {"x": 100, "y": 50, "width": 150, "height": 180} - }, - "face2": { - "detected": true, - "quality_score": 82.8, - "bounding_box": {"x": 120, "y": 60, "width": 140, "height": 170} - }, - "message": "Faces match with high confidence" -} -``` - -### Domain Entity - -```python -# app/domain/entities/face_comparison.py -@dataclass -class FaceInfo: - detected: bool - quality_score: float - bounding_box: BoundingBox - -@dataclass -class FaceComparisonResult: - match: bool - similarity: float - distance: float - threshold: float - confidence: str # high, medium, low - face1: FaceInfo - face2: FaceInfo - message: str -``` - ---- - -## Feature 9: Embedding Export/Import - -### Purpose -Export and import face embeddings for backup/migration. - -### API Endpoints - -``` -GET /api/v1/embeddings/export?tenant_id=default&format=json -POST /api/v1/embeddings/import -``` - -### Export Response -```json -{ - "version": "1.0", - "export_date": "2025-12-11T10:30:00Z", - "tenant_id": "default", - "model": "Facenet", - "embedding_dimension": 128, - "count": 150, - "embeddings": [ - { - "user_id": "user_001", - "embedding": [0.123, -0.456, ...], // 128 floats - "created_at": "2025-12-01T08:00:00Z", - "metadata": {"name": "John Doe"} - }, - // ... - ], - "checksum": "sha256:abc123..." -} -``` - -### Import Request -```json -{ - "file": "", - "mode": "merge", // merge, replace, skip_existing - "tenant_id": "default" -} -``` - -### Import Response -```json -{ - "success": true, - "imported": 145, - "skipped": 5, - "errors": 0, - "details": [ - {"user_id": "user_001", "status": "imported"}, - {"user_id": "user_002", "status": "skipped", "reason": "already_exists"} - ] -} -``` - ---- - -## Feature 10: Webhook Notifications - -### Purpose -Send async notifications on biometric events. - -### Configuration - -```python -# app/core/config.py additions -WEBHOOK_ENABLED: bool = False -WEBHOOK_URL: Optional[str] = None -WEBHOOK_SECRET: Optional[str] = None -WEBHOOK_EVENTS: List[str] = ["enrollment", "verification", "liveness"] -WEBHOOK_RETRY_COUNT: int = 3 -WEBHOOK_TIMEOUT: int = 10 -``` - -### API Endpoints - -``` -POST /api/v1/webhooks/register -GET /api/v1/webhooks -DELETE /api/v1/webhooks/{webhook_id} -POST /api/v1/webhooks/{webhook_id}/test -``` - -### Webhook Payload - -```json -{ - "event_id": "evt_abc123", - "event_type": "enrollment.success", - "timestamp": "2025-12-11T10:30:00Z", - "tenant_id": "default", - "data": { - "user_id": "user_001", - "quality_score": 85.5, - "embedding_dimension": 128 - }, - "signature": "sha256=..." -} -``` - -### Event Types - -| Event | Trigger | -|-------|---------| -| `enrollment.success` | Face enrolled successfully | -| `enrollment.failed` | Enrollment failed | -| `verification.match` | Face verified (match) | -| `verification.mismatch` | Face not matched | -| `liveness.pass` | Liveness check passed | -| `liveness.fail` | Liveness check failed | -| `search.found` | Face found in search | -| `search.not_found` | Face not found | - -### Interface - -```python -# app/domain/interfaces/webhook_sender.py -class IWebhookSender(Protocol): - async def send( - self, - url: str, - event: WebhookEvent, - secret: Optional[str] = None - ) -> WebhookResult: - """Send webhook notification.""" - ... -``` - -### Domain Entity - -```python -# app/domain/entities/webhook_event.py -@dataclass -class WebhookEvent: - event_id: str - event_type: str - timestamp: datetime - tenant_id: str - data: Dict[str, Any] - -@dataclass -class WebhookResult: - success: bool - status_code: Optional[int] - response_time_ms: int - error: Optional[str] - retry_count: int -``` - ---- - -## Feature 11: Rate Limiting per Tenant - -### Purpose -Apply different rate limits based on tenant/API key. - -### Configuration - -```python -# app/core/config.py additions -RATE_LIMIT_DEFAULT: int = 60 # requests per minute -RATE_LIMIT_PREMIUM: int = 300 -RATE_LIMIT_STORAGE: str = "memory" # memory, redis -``` - -### Rate Limit Tiers - -| Tier | Requests/min | Burst | -|------|--------------|-------| -| Free | 60 | 10 | -| Standard | 120 | 20 | -| Premium | 300 | 50 | -| Unlimited | ∞ | ∞ | - -### Response Headers - -``` -X-RateLimit-Limit: 60 -X-RateLimit-Remaining: 45 -X-RateLimit-Reset: 1702296600 -X-RateLimit-Tier: standard -``` - -### Rate Limit Exceeded Response - -```json -{ - "error_code": "RATE_LIMIT_EXCEEDED", - "message": "Rate limit exceeded. Try again in 45 seconds.", - "retry_after": 45, - "limit": 60, - "tier": "standard" -} -``` - -### Implementation - -```python -# app/api/middleware/rate_limiter.py -class TenantRateLimiter: - def __init__(self, storage: IRateLimitStorage): - self._storage = storage - self._tiers = { - "free": RateLimit(60, 10), - "standard": RateLimit(120, 20), - "premium": RateLimit(300, 50), - "unlimited": RateLimit(float('inf'), float('inf')) - } - - async def check(self, tenant_id: str, tier: str) -> RateLimitResult: - """Check if request is within rate limit.""" - ... -``` - ---- - -## New API Endpoints Summary - -| Endpoint | Method | Feature | -|----------|--------|---------| -| `/api/v1/quality/analyze` | POST | Quality Feedback | -| `/api/v1/faces/detect-all` | POST | Multi-face Detection | -| `/api/v1/demographics/analyze` | POST | Age/Gender | -| `/api/v1/landmarks/detect` | POST | Landmarks | -| `/api/v1/liveness/report` | POST | Anti-spoofing Report | -| `/api/v1/similarity/matrix` | POST | Similarity Matrix | -| `/api/v1/compare` | POST | Face Comparison | -| `/api/v1/embeddings/export` | GET | Export | -| `/api/v1/embeddings/import` | POST | Import | -| `/api/v1/webhooks` | GET/POST/DELETE | Webhooks | - ---- - -## Configuration Additions - -```python -# app/core/config.py additions - -# Demographics -DEMOGRAPHICS_ENABLED: bool = True -DEMOGRAPHICS_INCLUDE_RACE: bool = False # Privacy consideration -DEMOGRAPHICS_INCLUDE_EMOTION: bool = True - -# Landmarks -LANDMARK_MODEL: Literal["dlib_68", "mediapipe_468"] = "mediapipe_468" - -# Preprocessing -PREPROCESS_AUTO_ROTATE: bool = True -PREPROCESS_MAX_SIZE: int = 1920 -PREPROCESS_NORMALIZE: bool = True - -# Webhooks -WEBHOOK_ENABLED: bool = False -WEBHOOK_URL: Optional[str] = None -WEBHOOK_SECRET: Optional[str] = None -WEBHOOK_EVENTS: List[str] = ["enrollment", "verification", "liveness"] - -# Rate Limiting -RATE_LIMIT_ENABLED: bool = True -RATE_LIMIT_DEFAULT: int = 60 -RATE_LIMIT_STORAGE: Literal["memory", "redis"] = "memory" - -# Export/Import -EXPORT_FORMAT: Literal["json", "msgpack"] = "json" -EXPORT_INCLUDE_METADATA: bool = True -``` - ---- - -## Implementation Order - -### Sprint 6.1 (Week 1) -1. Face Quality Feedback -2. Multi-face Detection -3. Image Preprocessing - -### Sprint 6.2 (Week 2) -4. Age/Gender Estimation -5. Face Landmark Detection -6. Anti-spoofing Report - -### Sprint 6.3 (Week 3) -7. Similarity Matrix -8. Face Comparison API -9. Embedding Export/Import - -### Sprint 6.4 (Week 4) -10. Webhook Notifications -11. Rate Limiting per Tenant - ---- - -## Dependencies to Add - -``` -# requirements.txt additions -scipy>=1.11.0 # For clustering in similarity matrix -httpx>=0.25.0 # For async webhook calls -msgpack>=1.0.0 # For binary export format (optional) -``` - ---- - -## Testing Strategy - -Each feature requires: -1. Unit tests for domain entities -2. Unit tests for use cases -3. Integration tests for API endpoints -4. E2E workflow tests - -Minimum coverage: 80% - -### Test Examples - -#### Unit Test: Demographics Analyzer - -```python -# tests/unit/use_cases/test_analyze_demographics.py -import pytest -import numpy as np -from unittest.mock import Mock, AsyncMock - -from app.application.use_cases.analyze_demographics import AnalyzeDemographicsUseCase -from app.domain.entities.demographics import ( - DemographicsResult, AgeEstimate, GenderEstimate, EmotionEstimate -) -from app.domain.entities.face_detection_result import FaceDetectionResult - - -class TestAnalyzeDemographicsUseCase: - """Test suite for demographics analysis use case.""" - - @pytest.fixture - def mock_face_detector(self): - """Create mock face detector.""" - detector = Mock() - detector.detect.return_value = FaceDetectionResult( - face_detected=True, - face_coordinates=(100, 50, 150, 180), - confidence=0.98, - ) - return detector - - @pytest.fixture - def mock_demographics_analyzer(self): - """Create mock demographics analyzer.""" - analyzer = Mock() - analyzer.analyze.return_value = DemographicsResult( - age=AgeEstimate(value=28, range=(25, 32), confidence=0.85), - gender=GenderEstimate(value="female", confidence=0.92), - race=None, - emotion=EmotionEstimate( - dominant="happy", - confidence=0.88, - all_probabilities={"happy": 0.88, "neutral": 0.08} - ), - ) - return analyzer - - @pytest.fixture - def use_case(self, mock_face_detector, mock_demographics_analyzer): - """Create use case with mocked dependencies.""" - return AnalyzeDemographicsUseCase( - detector=mock_face_detector, - demographics_analyzer=mock_demographics_analyzer, - ) - - @pytest.mark.asyncio - async def test_analyze_demographics_success(self, use_case, mock_face_detector): - """Test successful demographics analysis.""" - # Arrange - image = np.zeros((480, 640, 3), dtype=np.uint8) - - # Act - result = await use_case.execute(image) - - # Assert - assert result.age.value == 28 - assert result.gender.value == "female" - assert result.emotion.dominant == "happy" - mock_face_detector.detect.assert_called_once() - - @pytest.mark.asyncio - async def test_analyze_demographics_no_face(self, mock_demographics_analyzer): - """Test demographics analysis when no face detected.""" - # Arrange - mock_detector = Mock() - mock_detector.detect.return_value = FaceDetectionResult(face_detected=False) - use_case = AnalyzeDemographicsUseCase( - detector=mock_detector, - demographics_analyzer=mock_demographics_analyzer, - ) - image = np.zeros((480, 640, 3), dtype=np.uint8) - - # Act & Assert - with pytest.raises(FaceNotFoundError): - await use_case.execute(image) -``` - -#### Integration Test: Compare Faces API - -```python -# tests/integration/api/test_comparison_api.py -import pytest -from httpx import AsyncClient -from pathlib import Path - -from app.main import app - - -class TestComparisonAPI: - """Integration tests for face comparison API.""" - - @pytest.fixture - def sample_images(self): - """Load test images.""" - return { - "same_person_1": Path("tests/fixtures/person_a_1.jpg"), - "same_person_2": Path("tests/fixtures/person_a_2.jpg"), - "different_person": Path("tests/fixtures/person_b.jpg"), - } - - @pytest.mark.asyncio - async def test_compare_same_person(self, sample_images): - """Test comparing images of the same person.""" - async with AsyncClient(app=app, base_url="http://test") as client: - # Arrange - files = { - "file1": open(sample_images["same_person_1"], "rb"), - "file2": open(sample_images["same_person_2"], "rb"), - } - - # Act - response = await client.post("/api/v1/compare", files=files) - - # Assert - assert response.status_code == 200 - data = response.json() - assert data["match"] is True - assert data["similarity"] > 0.6 - assert data["confidence"] in ["high", "medium"] - - @pytest.mark.asyncio - async def test_compare_different_persons(self, sample_images): - """Test comparing images of different persons.""" - async with AsyncClient(app=app, base_url="http://test") as client: - files = { - "file1": open(sample_images["same_person_1"], "rb"), - "file2": open(sample_images["different_person"], "rb"), - } - - response = await client.post("/api/v1/compare", files=files) - - assert response.status_code == 200 - data = response.json() - assert data["match"] is False - assert data["similarity"] < 0.6 - - @pytest.mark.asyncio - async def test_compare_missing_file(self): - """Test error when file is missing.""" - async with AsyncClient(app=app, base_url="http://test") as client: - files = {"file1": open("tests/fixtures/person_a_1.jpg", "rb")} - - response = await client.post("/api/v1/compare", files=files) - - assert response.status_code == 422 # Validation error -``` - -#### Unit Test: Webhook Sender - -```python -# tests/unit/infrastructure/test_webhook_sender.py -import pytest -from unittest.mock import AsyncMock, patch -from datetime import datetime - -from app.infrastructure.webhooks.http_webhook_sender import HttpWebhookSender -from app.domain.entities.webhook_event import WebhookEvent, WebhookResult - - -class TestHttpWebhookSender: - """Tests for HTTP webhook sender.""" - - @pytest.fixture - def sender(self): - """Create webhook sender instance.""" - return HttpWebhookSender(timeout=10, retry_count=3) - - @pytest.fixture - def sample_event(self): - """Create sample webhook event.""" - return WebhookEvent( - event_id="evt_123", - event_type="enrollment.success", - timestamp=datetime.utcnow(), - tenant_id="default", - data={"user_id": "user_001", "quality_score": 85.5}, - ) - - @pytest.mark.asyncio - async def test_send_webhook_success(self, sender, sample_event): - """Test successful webhook delivery.""" - with patch("httpx.AsyncClient.post") as mock_post: - mock_post.return_value = AsyncMock(status_code=200) - - result = await sender.send( - url="https://example.com/webhook", - event=sample_event, - secret="test_secret", - ) - - assert result.success is True - assert result.status_code == 200 - assert result.retry_count == 0 - - @pytest.mark.asyncio - async def test_send_webhook_retry_on_failure(self, sender, sample_event): - """Test webhook retry on temporary failure.""" - with patch("httpx.AsyncClient.post") as mock_post: - # Fail twice, succeed on third attempt - mock_post.side_effect = [ - AsyncMock(status_code=503), - AsyncMock(status_code=503), - AsyncMock(status_code=200), - ] - - result = await sender.send( - url="https://example.com/webhook", - event=sample_event, - ) - - assert result.success is True - assert result.retry_count == 2 - - @pytest.mark.asyncio - async def test_send_webhook_signature(self, sender, sample_event): - """Test webhook signature generation.""" - with patch("httpx.AsyncClient.post") as mock_post: - mock_post.return_value = AsyncMock(status_code=200) - - await sender.send( - url="https://example.com/webhook", - event=sample_event, - secret="test_secret", - ) - - # Verify signature header was included - call_args = mock_post.call_args - headers = call_args.kwargs.get("headers", {}) - assert "X-Webhook-Signature" in headers -``` - -### Edge Cases to Test - -| Feature | Edge Case | Expected Behavior | -|---------|-----------|-------------------| -| Multi-face | No faces in image | Return empty faces array | -| Multi-face | >10 faces | Return up to max limit | -| Demographics | Face occluded | Reduce confidence score | -| Landmarks | Profile view | Return partial landmarks | -| Comparison | Same image twice | Return 100% similarity | -| Similarity Matrix | Single image | Return 1x1 matrix | -| Export | Empty database | Return empty embeddings array | -| Import | Duplicate user_id | Handle based on mode (merge/skip) | -| Webhook | Invalid URL | Fail with WebhookConfigError | -| Rate Limit | First request | Set initial count to 1 | - ---- - -## Documentation Updates - -After implementation: -1. Update README.md with new endpoints -2. Update API documentation (Swagger) -3. Add examples for each new feature -4. Update QUICK_START.md - ---- - -## SE Checklist Compliance Summary - -| Category | Status | Notes | -|----------|--------|-------| -| SOLID Principles | ✅ PASS | All principles followed | -| Factory Pattern | ✅ PASS | 5 factories defined | -| Error Handling | ✅ PASS | 13 exception classes with error codes | -| Dependency Injection | ✅ PASS | Container.py updates specified | -| Interface Segregation | ✅ PASS | All new interfaces defined | -| Testing | ✅ PASS | Unit and integration examples provided | -| API Consistency | ✅ PASS | Uses /api/v1 prefix | - ---- - -**Design Status**: COMPLETE - SE CHECKLIST COMPLIANT -**Ready for Implementation**: YES -**Estimated Total Effort**: 4 weeks (1 sprint per week) diff --git a/docs/6-architecture/MODULE_PLAN.md b/docs/6-architecture/MODULE_PLAN.md deleted file mode 100644 index bcc57bc..0000000 --- a/docs/6-architecture/MODULE_PLAN.md +++ /dev/null @@ -1,898 +0,0 @@ -# Biometric Processor - Module Implementation Plan - -**Module Name**: biometric-processor -**Repository**: https://github.com/Rollingcat-Software/biometric-processor -**Technology**: Python 3.10+ / FastAPI -**Purpose**: AI/ML microservice for face recognition and liveness detection -**Status**: ❌ NOT STARTED - Full Implementation Required -**Priority**: 🟡 MEDIUM - Can develop in parallel after basic integration - ---- - -## 📋 Table of Contents - -1. [Module Overview](#module-overview) -2. [Current Status](#current-status) -3. [Architecture](#architecture) -4. [ML Models & Algorithms](#ml-models--algorithms) -5. [API Endpoints](#api-endpoints) -6. [Implementation Tasks](#implementation-tasks) -7. [Testing Requirements](#testing-requirements) -8. [Deployment](#deployment) -9. [Integration Points](#integration-points) - ---- - -## 🎯 Module Overview - -### Purpose -The Biometric Processor is the core AI/ML service responsible for: -- Face detection and quality assessment -- **Active liveness detection** (KEY INNOVATION - Biometric puzzle) -- Face encoding/embedding generation -- Face matching (1:1 verification and 1:N identification) -- Async job processing for enrollments and verifications -- Integration with identity-core-api via webhooks - -### Key Innovation: Active Liveness Detection -Unlike passive liveness (analyzing single photo), FIVUCSAS uses **active liveness**: -1. Generate random challenge: "Smile", "Blink", "Turn left", "Turn right" -2. Capture video stream during challenge -3. Verify user performed correct action in real-time -4. Calculate liveness score (0-100) -5. Prevent spoofing attacks (photos, videos, deepfakes) - -### Key Responsibilities -1. **Face Detection**: Detect faces in images/video frames -2. **Quality Assessment**: Evaluate image quality (lighting, blur, angle) -3. **Liveness Detection**: Active challenge-response verification -4. **Face Encoding**: Generate 128-D or 512-D embeddings -5. **Face Matching**: Compare embeddings for verification/identification -6. **Job Processing**: Async processing queue for long-running tasks -7. **Storage**: Store biometric templates securely (encrypted) - ---- - -## 📊 Current Status - -### ❌ Not Implemented (From Scratch) - -This is a **completely new implementation**. The repository may have placeholder files, but no functional ML models or endpoints exist. - -#### What Needs to Be Built - -1. **Project Setup** - - Python project structure - - FastAPI application - - Docker containerization - - Dependencies (TensorFlow/PyTorch, OpenCV, etc.) - -2. **Face Detection Pipeline** - - Face detection model integration (MTCNN, Haar Cascade, or YOLO) - - Face alignment and normalization - - Multi-face handling - -3. **Liveness Detection** - - Challenge generation engine - - Video frame analysis - - Action verification (smile detection, eye blink, head movement) - - Liveness score calculation - - Anti-spoofing algorithms - -4. **Face Recognition** - - Face embedding model (FaceNet, ArcFace, or similar) - - Embedding storage (pgvector) - - Similarity matching algorithm - - Threshold configuration - -5. **API Layer** - - RESTful endpoints - - WebSocket for real-time liveness challenges - - Job status tracking - - Webhook callbacks to identity-core-api - -6. **Background Processing** - - Celery or RQ for job queue - - Redis for task broker - - Worker processes for parallel processing - -7. **Storage & Caching** - - Redis for temporary data and job status - - PostgreSQL for biometric templates - - S3 or local storage for images (temporary) - ---- - -## 🏗️ Architecture - -### Technology Stack -```yaml -Framework: FastAPI (Python 3.10+) -ML Libraries: - - TensorFlow 2.x or PyTorch 2.x - - OpenCV 4.x (image processing) - - face_recognition (simple option) or FaceNet/ArcFace (advanced) - - MediaPipe (alternative for face detection) -Task Queue: Celery + Redis -Database: PostgreSQL 16 with pgvector extension -Cache: Redis 7 -Object Storage: MinIO or AWS S3 (for temporary images) -Deployment: Docker + Kubernetes -``` - -### System Architecture -``` -┌─────────────────────────────────────────────────────┐ -│ Biometric Processor Service │ -├─────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────┐ ┌────────────────────┐ │ -│ │ FastAPI App │ │ Celery Workers │ │ -│ │ (REST API) │ │ (Background Jobs) │ │ -│ └────────┬────────┘ └────────┬───────────┘ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌─────────────────┐ ┌────────────────────┐ │ -│ │ ML Models │ │ Redis Queue │ │ -│ │ - Face Detect │ │ (Job Broker) │ │ -│ │ - Liveness │ └────────────────────┘ │ -│ │ - FaceNet │ │ -│ └─────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────┐ ┌────────────────────┐ │ -│ │ PostgreSQL │ │ MinIO/S3 │ │ -│ │ (pgvector) │ │ (Image Storage) │ │ -│ └─────────────────┘ └────────────────────┘ │ -│ │ -│ External Integration: │ -│ ┌─────────────────────────────────────────┐ │ -│ │ Webhook → identity-core-api │ │ -│ │ POST /api/v1/biometric/callback │ │ -│ └─────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────┘ -``` - -### Processing Flow - -#### Enrollment Flow -``` -1. Client → POST /enroll - - Submit photo + user info - -2. FastAPI → Create job → Redis queue - -3. Celery Worker: - a. Detect face in image - b. Assess quality (blur, lighting, angle) - c. Perform liveness detection (if video provided) - d. Generate face embedding (128-D or 512-D vector) - e. Store embedding in PostgreSQL (pgvector) - f. Update job status: COMPLETED or FAILED - -4. Worker → Webhook callback to identity-core-api - POST /api/v1/biometric/callback - { jobId, status, quality, liveness, error } -``` - -#### Verification Flow -``` -1. Client → POST /verify - - Submit photo + userId - -2. FastAPI → Immediate processing (real-time) - -3. Processing: - a. Detect face - b. Generate embedding - c. Retrieve stored embedding from DB - d. Calculate similarity (cosine similarity) - e. Compare against threshold (e.g., 0.6) - f. Return: MATCH or NO_MATCH + confidence score -``` - -#### Liveness Detection Flow -``` -1. Client → WebSocket /liveness - -2. Server → Generate random challenge - - Challenge: "Please smile" - -3. Client → Stream video frames - -4. Server → Analyze frames in real-time - - Detect smile using facial landmarks - - Calculate liveness score - - Detect spoofing attempts - -5. Server → Return result - - Liveness score: 0-100 - - Status: LIVE or SPOOF -``` - ---- - -## 🤖 ML Models & Algorithms - -### 1. Face Detection -**Options**: -- **MTCNN** (Multi-task Cascaded CNN) - Fast and accurate -- **Haar Cascade** - Simple but less accurate -- **MediaPipe Face Detection** - Google's solution, very fast -- **YOLO Face** - Real-time detection - -**Recommended**: MTCNN or MediaPipe - -**Output**: Bounding box coordinates, facial landmarks - ---- - -### 2. Face Quality Assessment -**Metrics**: -- **Blur Detection**: Laplacian variance -- **Lighting**: Mean brightness, contrast -- **Face Angle**: Pitch, yaw, roll (using facial landmarks) -- **Face Size**: Pixel dimensions -- **Occlusion**: Detect sunglasses, masks - -**Quality Score**: 0-100 -- 0-40: Poor (reject) -- 41-70: Fair (warn user) -- 71-100: Good (accept) - ---- - -### 3. Active Liveness Detection -**Challenges**: -1. **Smile Detection** - - Use facial landmarks (mouth corners) - - Detect upward movement - - Threshold: Mouth aspect ratio > 0.5 - -2. **Eye Blink Detection** - - Eye aspect ratio (EAR) - - Detect closure and reopening - - Threshold: EAR < 0.2 for 2-3 frames - -3. **Head Movement** - - Detect pitch (up/down) - - Detect yaw (left/right) - - Measure angle change - -**Anti-Spoofing**: -- Texture analysis (detect print patterns) -- Depth analysis (monocular depth estimation) -- Micro-movement detection -- Challenge randomization - -**Liveness Score**: 0-100 -- 0-50: Likely spoof -- 51-80: Uncertain -- 81-100: Live person - ---- - -### 4. Face Embedding Generation -**Options**: -- **FaceNet** (Google) - 128-D embeddings, proven accuracy -- **ArcFace** (InsightFace) - 512-D embeddings, state-of-the-art -- **VGGFace** - 2048-D embeddings, older -- **face_recognition** library - Simple Python wrapper around dlib - -**Recommended**: FaceNet (for balance) or ArcFace (for best accuracy) - -**Process**: -1. Align face (normalize rotation and scale) -2. Pass through neural network -3. Extract embedding vector (128-D or 512-D) -4. L2 normalize the vector - ---- - -### 5. Face Matching -**Similarity Metric**: Cosine Similarity -```python -def cosine_similarity(embedding1, embedding2): - return np.dot(embedding1, embedding2) / ( - np.linalg.norm(embedding1) * np.linalg.norm(embedding2) - ) -``` - -**Thresholds**: -- **High Security**: 0.7 (1% False Acceptance Rate) -- **Balanced**: 0.6 (0.1% FAR, default) -- **Low Security**: 0.5 (10% FAR) - -**1:N Identification**: -- Use pgvector for efficient similarity search -- Query: `SELECT * FROM embeddings ORDER BY embedding <=> query_vector LIMIT 5` -- Return top N matches with confidence scores - ---- - -## 🔌 API Endpoints - -### REST Endpoints - -#### 1. Health Check -``` -GET /health -Response: { "status": "ok", "version": "1.0.0" } -``` - -#### 2. Enroll Face -``` -POST /enroll -Request: - - multipart/form-data - - image: File (JPEG/PNG) - - userId: string - - tenantId: string - - requireLiveness: boolean (optional) - -Response: -{ - "jobId": "uuid", - "status": "PENDING", - "estimatedTime": 5 // seconds -} -``` - -#### 3. Verify Face (1:1) -``` -POST /verify -Request: - - multipart/form-data - - image: File - - userId: string - -Response: -{ - "match": true, - "confidence": 0.87, - "liveness": 95, - "quality": 88 -} -``` - -#### 4. Identify Face (1:N) -``` -POST /identify -Request: - - multipart/form-data - - image: File - - tenantId: string - - topN: int (default: 5) - -Response: -{ - "matches": [ - { "userId": "123", "confidence": 0.92 }, - { "userId": "456", "confidence": 0.78 } - ] -} -``` - -#### 5. Get Job Status -``` -GET /jobs/{jobId} -Response: -{ - "jobId": "uuid", - "status": "COMPLETED", // PENDING, PROCESSING, COMPLETED, FAILED - "result": { - "quality": 88, - "liveness": 95, - "enrolled": true - }, - "error": null, - "createdAt": "2025-11-17T12:00:00Z", - "completedAt": "2025-11-17T12:00:05Z" -} -``` - -#### 6. Standalone Liveness Check -``` -POST /liveness -Request: - - multipart/form-data - - video: File (MP4, WebM) or image sequence - -Response: -{ - "liveness": 92, - "isLive": true, - "challenge": "smile", - "challengeCompleted": true -} -``` - -### WebSocket Endpoint - -#### Real-time Liveness Detection -``` -WebSocket /ws/liveness - -Client → Server: -{ - "action": "start", - "userId": "123" -} - -Server → Client: -{ - "challenge": "smile", - "instruction": "Please smile for the camera" -} - -Client → Server: (video frames as base64) -{ - "frame": "base64_encoded_image" -} - -Server → Client: (real-time feedback) -{ - "liveness": 75, - "challengeProgress": 0.8 -} - -Server → Client: (final result) -{ - "status": "completed", - "liveness": 95, - "isLive": true -} -``` - ---- - -## 📝 Implementation Tasks - -### Phase 1: Project Setup (1 week) -**Priority**: 🔴 CRITICAL - -#### Task 1.1: Project Structure -- [ ] Create Python project with Poetry or pip -- [ ] Setup FastAPI application -- [ ] Configure project structure (routers, services, models, ml) -- [ ] Setup environment variables (.env) -- [ ] Create requirements.txt or pyproject.toml - -#### Task 1.2: Database Setup -- [ ] PostgreSQL with pgvector extension -- [ ] Create migrations (Alembic) -- [ ] Tables: enrollments, embeddings, jobs -- [ ] Indexes for fast similarity search - -#### Task 1.3: Redis Setup -- [ ] Configure Redis connection -- [ ] Setup Celery with Redis broker -- [ ] Create worker configuration - -#### Task 1.4: Docker Setup -- [ ] Create Dockerfile for FastAPI app -- [ ] Create Dockerfile for Celery worker -- [ ] Create docker-compose.yml (app + worker + redis + postgres) - -**Acceptance Criteria**: -- ✅ FastAPI app runs on port 8001 (STANDARD PORT) -- ✅ Can connect to PostgreSQL and Redis -- ✅ Celery worker starts successfully -- ✅ Health endpoint returns 200 OK - ---- - -### Phase 2: Face Detection (1 week) -**Priority**: 🔴 CRITICAL - -#### Task 2.1: Integrate Face Detection Model -- [ ] Choose model: MTCNN or MediaPipe -- [ ] Install dependencies -- [ ] Create FaceDetector class -- [ ] Method: `detect_face(image) -> BoundingBox` -- [ ] Handle no face detected -- [ ] Handle multiple faces (use largest or reject) - -#### Task 2.2: Face Alignment -- [ ] Detect facial landmarks (eyes, nose, mouth) -- [ ] Align face to canonical position -- [ ] Normalize rotation -- [ ] Crop to standard size (e.g., 160x160) - -#### Task 2.3: Quality Assessment -- [ ] Implement blur detection (Laplacian variance) -- [ ] Implement lighting assessment -- [ ] Implement face angle estimation -- [ ] Calculate overall quality score (0-100) - -**Acceptance Criteria**: -- ✅ Can detect faces in photos -- ✅ Returns bounding box coordinates -- ✅ Quality score accurate for various conditions -- ✅ Rejects blurry or poorly lit images - ---- - -### Phase 3: Liveness Detection (2 weeks) -**Priority**: 🟠 HIGH - -#### Task 3.1: Challenge Generation -- [ ] Random challenge selection (smile, blink, turn left, turn right) -- [ ] Challenge instructions generator -- [ ] Prevent predictable patterns - -#### Task 3.2: Smile Detection -- [ ] Use facial landmarks (mouth corners) -- [ ] Calculate mouth aspect ratio -- [ ] Detect smile vs neutral -- [ ] Threshold calibration - -#### Task 3.3: Blink Detection -- [ ] Use facial landmarks (eyes) -- [ ] Calculate Eye Aspect Ratio (EAR) -- [ ] Detect eye closure -- [ ] Count blinks - -#### Task 3.4: Head Movement Detection -- [ ] Estimate head pose (pitch, yaw, roll) -- [ ] Detect left/right turn -- [ ] Detect up/down movement -- [ ] Validate movement amplitude - -#### Task 3.5: Anti-Spoofing -- [ ] Texture analysis for print detection -- [ ] Micro-movement detection -- [ ] Challenge randomization -- [ ] Liveness score calculation - -#### Task 3.6: WebSocket Integration -- [ ] Implement WebSocket endpoint -- [ ] Real-time frame processing -- [ ] Stream liveness feedback to client -- [ ] Handle connection errors - -**Acceptance Criteria**: -- ✅ Can detect smile with >95% accuracy -- ✅ Can detect blinks reliably -- ✅ Can detect head movement -- ✅ Rejects photos and videos (spoofing) -- ✅ WebSocket provides real-time feedback - ---- - -### Phase 4: Face Recognition (2 weeks) -**Priority**: 🔴 CRITICAL - -#### Task 4.1: Integrate Face Embedding Model -- [ ] Choose model: FaceNet or ArcFace -- [ ] Download pre-trained weights -- [ ] Create FaceEncoder class -- [ ] Method: `generate_embedding(face_image) -> np.ndarray` -- [ ] L2 normalization - -#### Task 4.2: Embedding Storage -- [ ] Create embeddings table in PostgreSQL -- [ ] Store embeddings as vectors (pgvector) -- [ ] Index for similarity search -- [ ] Encryption at rest (optional) - -#### Task 4.3: Face Matching (1:1 Verification) -- [ ] Implement cosine similarity -- [ ] Configure thresholds -- [ ] Create verification service -- [ ] Return match + confidence score - -#### Task 4.4: Face Matching (1:N Identification) -- [ ] Implement pgvector similarity search -- [ ] Query top N matches -- [ ] Filter by tenant ID -- [ ] Return ranked results - -**Acceptance Criteria**: -- ✅ Face embeddings are consistent (same face → similar embeddings) -- ✅ 1:1 verification accuracy >99% -- ✅ 1:N identification returns correct match in top 5 -- ✅ Search performance <200ms for 10,000 embeddings - ---- - -### Phase 5: API Layer (1 week) -**Priority**: 🟠 HIGH - -#### Task 5.1: Implement Enrollment Endpoint -- [ ] POST /enroll endpoint -- [ ] Accept image upload -- [ ] Create background job -- [ ] Return job ID -- [ ] Webhook callback on completion - -#### Task 5.2: Implement Verification Endpoint -- [ ] POST /verify endpoint -- [ ] Real-time processing (no queue) -- [ ] Return match result -- [ ] Include quality and liveness scores - -#### Task 5.3: Implement Identification Endpoint -- [ ] POST /identify endpoint -- [ ] Search all embeddings in tenant -- [ ] Return top N matches -- [ ] Include confidence scores - -#### Task 5.4: Implement Job Status Endpoint -- [ ] GET /jobs/{jobId} -- [ ] Query Redis for job status -- [ ] Return progress and result - -#### Task 5.5: Implement Liveness Endpoint -- [ ] POST /liveness for static check -- [ ] WebSocket /ws/liveness for real-time - -**Acceptance Criteria**: -- ✅ All endpoints return proper HTTP status codes -- ✅ Request validation works -- ✅ Error messages are clear -- ✅ API documentation (OpenAPI/Swagger) - ---- - -### Phase 6: Background Processing (1 week) -**Priority**: 🟠 HIGH - -#### Task 6.1: Celery Task Implementation -- [ ] Create enrollment task -- [ ] Create batch processing task -- [ ] Configure task timeouts -- [ ] Configure retry logic - -#### Task 6.2: Job Status Tracking -- [ ] Store job status in Redis -- [ ] Update status during processing -- [ ] Expire old jobs after 24 hours - -#### Task 6.3: Webhook Integration -- [ ] Implement webhook callback to identity-core-api -- [ ] POST /api/v1/biometric/callback -- [ ] Retry logic for failed webhooks -- [ ] Webhook authentication - -**Acceptance Criteria**: -- ✅ Enrollment jobs process in background -- ✅ Job status updates in real-time -- ✅ Webhook successfully notifies identity-core-api -- ✅ Failed jobs are retried - ---- - -### Phase 7: Testing & Optimization (1 week) -**Priority**: 🟡 MEDIUM - -#### Task 7.1: Unit Tests -- [ ] Test face detection -- [ ] Test liveness detection -- [ ] Test embedding generation -- [ ] Test similarity matching -- [ ] 80%+ code coverage - -#### Task 7.2: Integration Tests -- [ ] Test full enrollment flow -- [ ] Test verification flow -- [ ] Test identification flow -- [ ] Test webhook callbacks - -#### Task 7.3: Performance Optimization -- [ ] Profile slow operations -- [ ] Optimize image preprocessing -- [ ] Batch embedding generation -- [ ] Use GPU if available (CUDA) -- [ ] Implement caching for repeated queries - -#### Task 7.4: Load Testing -- [ ] Test with 100 concurrent enrollments -- [ ] Test with 1000 verifications per second -- [ ] Identify bottlenecks -- [ ] Scale workers as needed - -**Acceptance Criteria**: -- ✅ 80%+ test coverage -- ✅ All critical paths tested -- ✅ Enrollment: <2s (p95) -- ✅ Verification: <500ms (p95) -- ✅ Can handle 1000+ concurrent requests - ---- - -## 🧪 Testing Requirements - -### Unit Tests -```python -# test_face_detection.py -def test_detect_face_single_face() -def test_detect_face_no_face() -def test_detect_face_multiple_faces() -def test_quality_assessment_good() -def test_quality_assessment_blurry() - -# test_liveness.py -def test_smile_detection() -def test_blink_detection() -def test_head_movement() -def test_anti_spoofing() - -# test_face_recognition.py -def test_generate_embedding_consistency() -def test_verify_same_person() -def test_verify_different_people() -def test_identify_correct_match() -``` - -### Integration Tests -```python -# test_enrollment_flow.py -async def test_full_enrollment_flow() -async def test_enrollment_webhook_callback() - -# test_verification_flow.py -async def test_verification_success() -async def test_verification_failure() -``` - -### Performance Tests -```bash -# Load testing with Locust or k6 -POST /verify - 1000 req/s - p95 < 500ms -POST /enroll - 100 req/s - p95 < 2s -POST /identify - 100 req/s - p95 < 1s -``` - ---- - -## 🚀 Deployment - -### Environment Variables -```bash -# Application -APP_NAME=biometric-processor -APP_VERSION=1.0.0 -APP_PORT=8001 # STANDARD PORT -LOG_LEVEL=INFO - -# Database -DATABASE_URL=postgresql://user:pass@localhost:5432/biometric_db - -# Redis -REDIS_URL=redis://localhost:6379/0 - -# Celery -CELERY_BROKER_URL=redis://localhost:6379/0 -CELERY_RESULT_BACKEND=redis://localhost:6379/1 - -# ML Models -FACE_DETECTION_MODEL=mtcnn -FACE_RECOGNITION_MODEL=facenet -MODEL_DEVICE=cuda # or cpu - -# Thresholds -FACE_MATCH_THRESHOLD=0.6 -LIVENESS_THRESHOLD=80 -QUALITY_THRESHOLD=70 - -# External Services -IDENTITY_API_URL=http://identity-core-api:8080/api/v1 -WEBHOOK_SECRET=your_secret_key -``` - -### Docker Compose -```yaml -version: '3.8' - -services: - biometric-api: - build: . - ports: - - "8001:8001" # STANDARD PORT - environment: - - DATABASE_URL=postgresql://postgres:postgres@db:5432/biometric - - REDIS_URL=redis://redis:6379/0 - depends_on: - - db - - redis - - biometric-worker: - build: . - command: celery -A app.worker worker --loglevel=info - environment: - - DATABASE_URL=postgresql://postgres:postgres@db:5432/biometric - - REDIS_URL=redis://redis:6379/0 - depends_on: - - db - - redis - - db: - image: pgvector/pgvector:pg16 - environment: - - POSTGRES_DB=biometric - - POSTGRES_PASSWORD=postgres - volumes: - - pgdata:/var/lib/postgresql/data - - redis: - image: redis:7-alpine - volumes: - - redisdata:/data - -volumes: - pgdata: - redisdata: -``` - ---- - -## 🔗 Integration Points - -### Identity Core API Integration -```python -# Webhook callback after enrollment -async def send_webhook(job_id: str, result: dict): - webhook_url = f"{IDENTITY_API_URL}/biometric/callback" - headers = {"Authorization": f"Bearer {WEBHOOK_SECRET}"} - payload = { - "jobId": job_id, - "status": result["status"], - "quality": result.get("quality"), - "liveness": result.get("liveness"), - "error": result.get("error") - } - await http_client.post(webhook_url, json=payload, headers=headers) -``` - ---- - -## 📈 Success Criteria - -### Functionality -- ✅ Face detection works on various image qualities -- ✅ Liveness detection prevents spoofing attacks -- ✅ Face matching accuracy >99% -- ✅ All API endpoints functional - -### Performance -- ✅ Enrollment: <2s (p95) -- ✅ Verification: <500ms (p95) -- ✅ Identification: <1s (p95) for 10,000 faces -- ✅ Supports 1000+ concurrent requests - -### Accuracy -- ✅ Face detection: >98% -- ✅ Liveness detection: >98% (reject spoofs) -- ✅ Face verification FAR: <0.1% -- ✅ Face verification FRR: <1% - -### Security -- ✅ Biometric templates encrypted -- ✅ Webhook authentication -- ✅ Input validation -- ✅ Rate limiting - ---- - -## 📅 Implementation Timeline - -| Phase | Tasks | Estimated Time | Priority | -|-------|-------|----------------|----------| -| **Phase 1** | Project Setup | 1 week | 🔴 CRITICAL | -| **Phase 2** | Face Detection | 1 week | 🔴 CRITICAL | -| **Phase 3** | Liveness Detection | 2 weeks | 🟠 HIGH | -| **Phase 4** | Face Recognition | 2 weeks | 🔴 CRITICAL | -| **Phase 5** | API Layer | 1 week | 🟠 HIGH | -| **Phase 6** | Background Processing | 1 week | 🟠 HIGH | -| **Phase 7** | Testing & Optimization | 1 week | 🟡 MEDIUM | -| **Total** | | **9 weeks** | **~2-3 months** | - ---- - -**Document Version**: 1.0 -**Created**: 2025-11-17 -**Last Updated**: 2025-11-17 -**Owner**: ML/AI Team -**Review Date**: Weekly during implementation diff --git a/docs/6-architecture/MODULE_QUALITY_ASSURANCE_STRATEGY.md b/docs/6-architecture/MODULE_QUALITY_ASSURANCE_STRATEGY.md deleted file mode 100644 index 94a555f..0000000 --- a/docs/6-architecture/MODULE_QUALITY_ASSURANCE_STRATEGY.md +++ /dev/null @@ -1,966 +0,0 @@ -# Module Quality Assurance Strategy - Biometric Processor -## Architectural Quality Assessment & Improvement Plan - -**Author**: Software Architect -**Date**: 2025-12-25 -**Module**: biometric-processor v1.0.0 -**Purpose**: Comprehensive quality assurance strategy for production readiness - ---- - -## Executive Summary - -### Current Quality Status: 🟡 **GOOD ARCHITECTURE, CRITICAL GAPS** - -**Strengths**: -- ✅ Excellent architectural foundation (Clean Architecture/Hexagonal) -- ✅ 323 Python files with clear separation of concerns -- ✅ Comprehensive documentation (20+ design documents) -- ✅ Modern tech stack (FastAPI, PostgreSQL+pgvector, Redis) -- ✅ Quality tooling configured (Black, isort, mypy, Ruff, Bandit) - -**Critical Gaps**: -- 🔴 **3 Critical Issues** blocking production (race conditions, security, resource leaks) -- 🟠 **12 High-Priority Issues** requiring immediate attention -- 🟡 **Incomplete test coverage** (33 test files vs 323 source files) -- 🟡 **Quality gates not enforced** in CI/CD pipeline - -### Overall Quality Score: **6.5/10** - -**Recommendation**: Implement comprehensive quality assurance program before production deployment. - ---- - -## 1. Quality Dimensions Framework - -### 1.1 Code Quality (Current: 7/10) - -**What We Measure**: -- Static analysis compliance -- Type safety coverage -- Code complexity metrics -- Code duplication -- Security vulnerabilities -- Technical debt ratio - -**Current State**: -```yaml -✅ Configured: Black, isort, mypy, Ruff, Bandit -✅ Standards: 100-char line length, strict mypy -⚠️ Gaps: - - No complexity analysis (cyclomatic/cognitive) - - No duplication detection - - No SonarQube/quality gate enforcement - - Pre-commit hooks not mandatory in CI -``` - -**Target State**: -```yaml -✅ All linting enforced in CI (fail on any violation) -✅ Complexity limits: cyclomatic < 10, cognitive < 15 -✅ Duplication: < 3% across codebase -✅ Security: Zero high/critical Bandit findings -✅ Type coverage: > 95% with strict mypy -✅ Quality gate: SonarQube quality gate PASS -``` - -**Action Items**: -1. **Enforce linting in CI** - Make CI fail on Ruff/Black violations -2. **Add complexity analysis** - Use radon/mccabe for complexity metrics -3. **Integrate SonarQube** - Set quality gates for maintainability -4. **Add pre-commit CI check** - Ensure hooks are run -5. **Track technical debt** - Use SonarQube debt ratio (target < 5%) - ---- - -### 1.2 Test Quality (Current: 5/10) - -**What We Measure**: -- Test coverage (line, branch, mutation) -- Test pyramid balance (unit:integration:e2e = 70:20:10) -- Test reliability (flakiness rate) -- Test execution time -- Edge case coverage - -**Current State**: -```yaml -Test Files: 33 test files -Coverage Target: 80% (configured in pytest.ini) -Actual Coverage: Unknown (not measured in recent runs) - -Test Distribution: - Unit Tests: Present (tests/unit/) - Integration Tests: Minimal (tests/integration/ - only 2 files) - E2E Tests: Present (tests/e2e/) - Load Tests: Present (tests/load/) - Benchmarks: Present (tests/benchmarks/) - -❌ Coverage Gaps: - - No coverage report in latest CI runs - - Unit tests don't cover all 23 use cases - - Missing integration tests for: - * PostgreSQL+pgvector operations - * Event bus (Redis) workflows - * Webhook delivery - * Cache behavior under load - - Missing concurrent scenario tests - - Missing mutation testing -``` - -**Target State**: -```yaml -Coverage: - Line Coverage: > 85% - Branch Coverage: > 80% - Mutation Coverage: > 75% - -Test Pyramid: - Unit Tests: 70% (~200+ test cases) - Integration Tests: 20% (~60 test cases) - E2E Tests: 10% (~30 test cases) - -Performance: - Unit Test Suite: < 60 seconds - Full Test Suite: < 5 minutes - Flakiness Rate: < 0.1% - -Critical Coverage: - ✅ All 23 use cases: 100% coverage - ✅ All domain entities: 100% coverage - ✅ All API endpoints: 100% coverage - ✅ Error paths: > 90% coverage - ✅ Concurrent scenarios: Covered -``` - -**Action Items**: -1. **Measure current coverage** - Run pytest with coverage, generate baseline report -2. **Fill unit test gaps** - Achieve 100% use case coverage -3. **Expand integration tests** - Test all external integrations -4. **Add concurrent tests** - Test race conditions (issue #1 from review) -5. **Implement mutation testing** - Use mutmut to find weak tests -6. **Add contract tests** - Test API contracts with Pact/Schemathesis -7. **Create test coverage dashboard** - Visualize coverage trends over time - ---- - -### 1.3 Security Quality (Current: 6/10) - -**What We Measure**: -- OWASP Top 10 compliance -- Dependency vulnerabilities -- Secret scanning -- Security test coverage -- Penetration test results - -**Current State**: -```yaml -✅ Configured: - - Bandit security scanning - - pip-audit for dependency vulnerabilities - - Input validation on API endpoints - -⚠️ Identified Issues: - - CRITICAL: Correlation ID not validated (XSS, log injection) - - HIGH: Content-Type spoofing possible - - HIGH: No file size limits (DoS) - - MEDIUM: No rate limiting on enrollment endpoints - -❌ Missing: - - SAST (Static Application Security Testing) integration - - DAST (Dynamic Application Security Testing) - - Secret scanning in git history - - Security regression tests - - Regular penetration testing - - OWASP ZAP/Burp Suite scans -``` - -**Target State**: -```yaml -✅ Zero critical/high vulnerabilities -✅ OWASP Top 10 compliance: 100% -✅ Dependency scan: Daily automated scans -✅ Secret scanning: Pre-commit + CI enforcement -✅ Security tests: - - Injection attacks: Covered - - Authentication bypass: Covered - - XSS/CSRF: Covered - - DoS: Covered -✅ Penetration test: Quarterly + before major releases -``` - -**Action Items**: -1. **Fix critical security issues** - Address issues #3, #7 from review -2. **Integrate SAST** - Add Semgrep/CodeQL to CI -3. **Add DAST** - Automated OWASP ZAP scans -4. **Implement secret scanning** - GitGuardian/TruffleHog -5. **Create security test suite** - Test common attack vectors -6. **Add rate limiting** - Protect all endpoints (issue #12) -7. **Security documentation** - Security best practices guide -8. **Regular audits** - Schedule quarterly security reviews - ---- - -### 1.4 Performance Quality (Current: 6/10) - -**What We Measure**: -- Response time percentiles (p50, p95, p99) -- Throughput (requests/second) -- Resource utilization (CPU, memory, GPU) -- Scalability limits -- Performance regression - -**Current State**: -```yaml -Benchmarks: Present (tests/benchmarks/) -Load Tests: Present (tests/load/) using Locust - -❌ Gaps: - - No baseline performance metrics documented - - No performance regression tests in CI - - No memory leak detection - - No profiling for optimization opportunities - - No capacity planning data - -⚠️ Known Issues: - - Sync file I/O in async context (logging) - - cv2.imread blocks event loop - - No timeout on ML operations (can hang) - - Cache stampede possible (thundering herd) -``` - -**Target State**: -```yaml -Performance SLOs: - Enrollment (single image): p95 < 2s, p99 < 5s - Verification (1:1): p95 < 500ms, p99 < 1s - Search (1:N, 1000 users): p95 < 2s, p99 < 5s - Liveness detection: p95 < 1s, p99 < 2s - -Throughput: - Concurrent requests: Support 100 concurrent - Enrollment rate: > 50 enrollments/minute - Verification rate: > 200 verifications/minute - -Resource Limits: - Memory per request: < 500MB - CPU per request: < 2 cores - Memory leak rate: 0% over 24h - -Scalability: - Database: 1M+ users with < 1s search - Horizontal scaling: Linear to 10 pods -``` - -**Action Items**: -1. **Establish baselines** - Run comprehensive benchmarks, document results -2. **Add performance tests to CI** - Fail if regression > 20% -3. **Fix async blocking** - Resolve issues #4, #9 from review -4. **Add timeouts** - All ML operations (issue #5) -5. **Memory leak detection** - Use memory_profiler in tests -6. **Implement caching optimizations** - Fix stampede (issue #6) -7. **Capacity planning** - Document scaling characteristics -8. **APM integration** - Add Datadog/New Relic for production - ---- - -### 1.5 Reliability Quality (Current: 6/10) - -**What We Measure**: -- Error rate (target < 0.1%) -- Mean Time Between Failures (MTBF) -- Mean Time To Recovery (MTTR) -- Fault tolerance -- Data consistency - -**Current State**: -```yaml -✅ Implemented: - - Structured logging (JSON) - - Health check endpoints - - Correlation IDs for tracing - - Exception hierarchy - -⚠️ Issues: - - Race condition in cache (data corruption) - - No partial state cleanup (memory leaks) - - No circuit breakers actually used - - No retry logic for transient failures - - No graceful degradation - -❌ Missing: - - Chaos engineering tests - - Disaster recovery tests - - Backup/restore procedures - - Data consistency validation - - Idempotency guarantees -``` - -**Target State**: -```yaml -Availability: 99.9% uptime (< 43 minutes/month downtime) -Error Budget: 0.1% error rate -MTBF: > 720 hours (30 days) -MTTR: < 5 minutes - -Fault Tolerance: - ✅ Database failure: Graceful degradation to read-only - ✅ Redis failure: Continue without cache - ✅ ML model failure: Circuit breaker, fallback - ✅ Partial failure recovery: Automatic cleanup - -Data Integrity: - ✅ Idempotent operations: All writes - ✅ Transaction support: Atomic enrollment - ✅ Consistency checks: Automated validation -``` - -**Action Items**: -1. **Fix race conditions** - Issue #1 (critical) -2. **Add partial state cleanup** - Issue #2 (critical) -3. **Integrate circuit breakers** - Issue #10 (high) -4. **Implement retry logic** - Exponential backoff for transient failures -5. **Add idempotency keys** - Issue #8 (high) -6. **Chaos testing** - Use Chaos Monkey/Litmus -7. **Disaster recovery plan** - Document and test -8. **Implement health probes** - Liveness, readiness, startup - ---- - -### 1.6 Maintainability Quality (Current: 8/10) - -**What We Measure**: -- Documentation coverage -- Code complexity -- Modularity metrics (coupling, cohesion) -- Dependency management -- Ease of onboarding - -**Current State**: -```yaml -✅ Strengths: - - Excellent architecture (Clean/Hexagonal) - - Comprehensive documentation (20+ docs) - - Clear module boundaries - - Strong type hints - - Dependency injection - - SOLID principles applied - -⚠️ Minor Issues: - - No API versioning strategy documented - - No deprecation policy - - No architectural decision records (ADRs) -``` - -**Target State**: -```yaml -Documentation: - ✅ API documentation: 100% endpoints documented - ✅ Architecture diagrams: Current and up-to-date - ✅ ADRs: All major decisions recorded - ✅ Onboarding guide: < 1 day to first contribution - -Code Maintainability: - ✅ Complexity: All functions < 10 cyclomatic complexity - ✅ Modularity: Low coupling, high cohesion - ✅ Dependencies: Regular updates, no deprecated packages - -Change Impact: - ✅ Time to add new feature: < 1 day - ✅ Time to fix bug: < 4 hours - ✅ Code review time: < 2 hours -``` - -**Action Items**: -1. **Create ADR repository** - Document architectural decisions -2. **Add API versioning** - Strategy for backwards compatibility -3. **Generate architecture diagrams** - C4 model diagrams -4. **Create developer guide** - Onboarding checklist -5. **Dependency update policy** - Monthly dependency updates -6. **Deprecation policy** - Document API lifecycle - ---- - -## 2. Quality Gates - -### 2.1 Commit-Level Gates (Pre-commit Hooks) - -**Enforced**: -```yaml -✅ Code Formatting: Black (line-length: 100) -✅ Import Sorting: isort (Black-compatible) -✅ Type Checking: mypy (strict mode, disallow untyped defs) -✅ Linting: Ruff (fast linter) -✅ File Checks: - - Trailing whitespace - - End-of-file fixer - - Large files (< 1MB) - - Merge conflicts - - Debug statements - - YAML/TOML syntax -``` - -**Should Add**: -```yaml -🔧 Security: Secret scanning (detect-secrets) -🔧 Complexity: Reject functions > 10 complexity -🔧 TODOs: Require issue links for TODOs -🔧 Commit Message: Conventional commits format -``` - ---- - -### 2.2 CI/CD Quality Gates - -**Current CI Pipeline** (`.github/workflows/ci.yml`): -```yaml -Stage 1: Lint & Type Check - - Ruff linter - - Ruff formatter - ⚠️ Not failing on violations - -Stage 2: Unit Tests - - Run tests/unit/ - - Coverage report (XML) - - Upload to Codecov - ⚠️ Coverage not enforced (--cov-fail-under not in CI) - -Stage 3: Integration Tests - - Redis service - - Run tests/integration/ - ⚠️ No database service for pgvector tests - -Stage 4: Docker Build - - Build Docker image - - Cache layers - ✅ Working correctly - -Stage 5: Security Scan - - Bandit security scan - - pip-audit - ⚠️ Not failing build (|| true) -``` - -**Recommended CI Pipeline**: -```yaml -Stage 1: Fast Feedback (< 2 min) - ✅ Pre-commit hooks validation - ✅ Ruff linting (FAIL on violations) - ✅ Black formatting check (FAIL on violations) - ✅ mypy type checking (FAIL on errors) - -Stage 2: Security Scan (< 3 min) - ✅ Secret scanning (FAIL on secrets found) - ✅ Bandit security scan (FAIL on HIGH/CRITICAL) - ✅ pip-audit (FAIL on critical vulnerabilities) - ✅ Semgrep SAST (FAIL on security issues) - -Stage 3: Unit Tests (< 5 min) - ✅ Run tests/unit/ (FAIL on any failure) - ✅ Coverage: FAIL if < 85% line, < 80% branch - ✅ Upload coverage to Codecov - ✅ Upload coverage to SonarQube - -Stage 4: Integration Tests (< 10 min) - ✅ Services: PostgreSQL+pgvector, Redis - ✅ Run tests/integration/ (FAIL on any failure) - ✅ Test all external integrations - -Stage 5: E2E Tests (< 10 min) - ✅ Full API testing - ✅ Run tests/e2e/ (FAIL on any failure) - -Stage 6: Performance Tests (< 10 min) - ✅ Run critical benchmarks - ✅ FAIL if regression > 20% - ✅ Store benchmark results - -Stage 7: Quality Gate (< 2 min) - ✅ SonarQube quality gate (PASS required) - ✅ Complexity check (FAIL if any function > 10) - ✅ Duplication check (FAIL if > 3%) - -Stage 8: Build & Push (< 5 min) - ✅ Docker build - ✅ Push to registry (tagged) - ✅ Sign image (Sigstore) - -Stage 9: Deploy to Staging (manual approval) - ✅ Deploy to staging environment - ✅ Run smoke tests - ✅ DAST security scan (OWASP ZAP) -``` - -**Total CI Time**: ~30-40 minutes for full pipeline - ---- - -### 2.3 Release Quality Gates - -**Before Production Release**: - -```yaml -Required: - ✅ All CI stages PASSED - ✅ Code review: 2+ approvals (1 senior engineer) - ✅ All critical/high security issues resolved - ✅ Test coverage: > 85% - ✅ Performance benchmarks: No regression - ✅ Load testing: Passed (1000 concurrent users) - ✅ Security scan: No critical/high vulnerabilities - ✅ Documentation: Updated (API docs, changelog) - ✅ Database migrations: Tested and reversible - ✅ Monitoring: Alerts configured - ✅ Rollback plan: Documented and tested - ✅ Feature flags: Configured for gradual rollout - -Recommended: - 🔧 Penetration test: Completed within 30 days - 🔧 Chaos engineering: Basic resilience tested - 🔧 Disaster recovery: Tested within 90 days - 🔧 Compliance: SOC2/GDPR requirements met -``` - ---- - -## 3. Quality Assurance Tools & Infrastructure - -### 3.1 Static Analysis Tools - -| Tool | Purpose | Integration | Priority | -|------|---------|-------------|----------| -| **Ruff** | Fast Python linter | ✅ CI, pre-commit | CRITICAL | -| **Black** | Code formatter | ✅ CI, pre-commit | CRITICAL | -| **isort** | Import sorting | ✅ CI, pre-commit | HIGH | -| **mypy** | Type checking | ✅ CI, pre-commit | CRITICAL | -| **Bandit** | Security scanning | ✅ CI | CRITICAL | -| **Semgrep** | SAST security | 🔧 Add to CI | HIGH | -| **Radon** | Complexity metrics | 🔧 Add to CI | MEDIUM | -| **Pylint** | Comprehensive linting | ✅ Configured, not in CI | LOW | -| **SonarQube** | Quality platform | 🔧 Add | HIGH | - ---- - -### 3.2 Dynamic Analysis Tools - -| Tool | Purpose | Integration | Priority | -|------|---------|-------------|----------| -| **pytest** | Test framework | ✅ CI | CRITICAL | -| **pytest-cov** | Coverage reporting | ✅ CI | CRITICAL | -| **mutmut** | Mutation testing | 🔧 Add to CI | MEDIUM | -| **Locust** | Load testing | ✅ Available | HIGH | -| **memory_profiler** | Memory leak detection | 🔧 Add to tests | HIGH | -| **py-spy** | Profiling | 🔧 Add for optimization | MEDIUM | -| **OWASP ZAP** | DAST security | 🔧 Add to staging | HIGH | - ---- - -### 3.3 Monitoring & Observability - -**Required in Production**: - -```yaml -Logging: - ✅ Structured JSON logging (current) - ✅ Correlation IDs (current) - 🔧 Log aggregation: ELK/Datadog/CloudWatch - 🔧 Log retention: 90 days - -Metrics: - ✅ Prometheus metrics (current) - 🔧 Custom metrics: - - enrollment_duration_seconds (histogram) - - verification_duration_seconds (histogram) - - ml_model_inference_time (histogram) - - cache_hit_rate (gauge) - - error_rate_by_type (counter) - - active_requests (gauge) - - memory_usage_bytes (gauge) - 🔧 Grafana dashboards (configured but not complete) - -Tracing: - 🔧 Distributed tracing: Jaeger/Tempo - 🔧 APM: Datadog/New Relic/Dynatrace - -Alerting: - 🔧 Critical alerts: - - Error rate > 1% - - Response time p95 > 5s - - Memory usage > 80% - - Database connection pool exhausted - - ML model timeout rate > 5% - 🔧 On-call rotation configured - 🔧 Incident response runbook -``` - ---- - -## 4. Implementation Roadmap - -### Phase 1: Critical Fixes (Week 1-2) - **BLOCKER RESOLUTION** - -**Objective**: Resolve 3 critical issues preventing production deployment - -```yaml -Week 1: - 🔴 Issue #1: Fix cache race condition - - Add asyncio.Lock to CachedEmbeddingRepository - - Add concurrent test cases - - Verify no data corruption under load - Estimate: 2 days - - 🔴 Issue #3: Validate correlation IDs - - Add regex validation - - Add security tests - - Update middleware - Estimate: 1 day - - 🔴 Issue #5: Add ML operation timeouts - - Add timeout configuration - - Wrap all ML calls with asyncio.wait_for() - - Add timeout tests - Estimate: 2 days - -Week 2: - 🟠 Issue #2: Fix partial state cleanup - - Add cleanup in exception handlers - - Add memory leak tests - - Verify cleanup works - Estimate: 2 days - - 🟠 Issue #10: Integrate circuit breakers - - Wire circuit breakers into ML calls - - Add circuit breaker tests - - Document configuration - Estimate: 2 days - - ✅ Verification: - - All critical tests passing - - Load test with 100 concurrent users - - Memory leak test (24h stability) - Estimate: 1 day -``` - -**Success Criteria**: All critical issues resolved, verified under load - ---- - -### Phase 2: Quality Foundation (Week 3-4) - **TESTING & SECURITY** - -**Objective**: Establish comprehensive quality infrastructure - -```yaml -Week 3: Testing Infrastructure - 🔧 Test coverage expansion: - - Write missing unit tests (target 85%) - - Add integration tests for all use cases - - Add concurrent scenario tests - Estimate: 3 days - - 🔧 Add mutation testing: - - Configure mutmut - - Run mutation tests - - Fix weak tests - Estimate: 2 days - -Week 4: Security Hardening - 🔧 Fix high-priority security issues: - - Issue #7: File type validation - - Issue #8: Idempotency keys - - Issue #12: Rate limiting - Estimate: 3 days - - 🔧 Add security scanning: - - Integrate Semgrep - - Add secret scanning - - Create security test suite - Estimate: 2 days -``` - -**Success Criteria**: Test coverage > 85%, zero critical security issues - ---- - -### Phase 3: Quality Automation (Week 5-6) - **CI/CD ENHANCEMENT** - -**Objective**: Automate all quality gates - -```yaml -Week 5: CI/CD Pipeline Enhancement - 🔧 Update CI pipeline: - - Add all quality gates - - Enforce coverage thresholds - - Add performance regression tests - Estimate: 3 days - - 🔧 Integrate SonarQube: - - Set up SonarQube server - - Configure quality gates - - Add to CI pipeline - Estimate: 2 days - -Week 6: Monitoring & Observability - 🔧 Production monitoring: - - Complete Grafana dashboards - - Configure alerts - - Set up log aggregation - Estimate: 3 days - - 🔧 Documentation: - - Update all documentation - - Create ADRs - - Write operational runbook - Estimate: 2 days -``` - -**Success Criteria**: Fully automated quality gates, production-ready monitoring - ---- - -### Phase 4: Validation & Launch (Week 7-8) - **PRODUCTION READINESS** - -**Objective**: Validate production readiness - -```yaml -Week 7: Comprehensive Testing - 🔧 Load testing: - - 1000 concurrent users - - 24-hour stability test - - Memory leak verification - Estimate: 2 days - - 🔧 Chaos engineering: - - Database failure scenarios - - Redis failure scenarios - - ML model failure scenarios - Estimate: 2 days - - 🔧 Security testing: - - OWASP ZAP scan - - Penetration test - - Vulnerability assessment - Estimate: 1 day - -Week 8: Staging & Launch - 🔧 Staging deployment: - - Deploy to staging - - Run smoke tests - - Performance validation - Estimate: 2 days - - 🔧 Production launch: - - Gradual rollout (10% → 50% → 100%) - - Monitor metrics - - Validate success criteria - Estimate: 3 days -``` - -**Success Criteria**: All quality gates passed, successful production deployment - ---- - -## 5. Quality Metrics Dashboard - -### 5.1 Key Performance Indicators (KPIs) - -**Code Quality Metrics**: -```yaml -Maintainability Index: > 65 (current: measure baseline) -Cyclomatic Complexity: Average < 5, Max < 10 -Code Duplication: < 3% -Technical Debt Ratio: < 5% -Type Coverage: > 95% -``` - -**Test Quality Metrics**: -```yaml -Line Coverage: > 85% -Branch Coverage: > 80% -Mutation Coverage: > 75% -Test Execution Time: < 5 minutes (full suite) -Flakiness Rate: < 0.1% -``` - -**Security Metrics**: -```yaml -Critical Vulnerabilities: 0 -High Vulnerabilities: 0 -Medium Vulnerabilities: < 5 -Dependency Vulnerabilities: 0 critical/high -Secret Leaks: 0 -``` - -**Performance Metrics**: -```yaml -Response Time (p95): < 2s (enrollment), < 500ms (verification) -Error Rate: < 0.1% -Throughput: > 100 concurrent requests -Memory Usage: < 2GB per pod -``` - -**Reliability Metrics**: -```yaml -Uptime: > 99.9% -MTBF: > 720 hours -MTTR: < 5 minutes -Failed Deployments: < 1% -``` - ---- - -### 5.2 Quality Trends Tracking - -**Weekly Quality Report**: -```yaml -Metrics to Track: - - Test coverage trend - - Security vulnerabilities found/fixed - - Performance regression incidents - - Production incidents - - Code complexity trend - - Technical debt trend - - Deployment frequency - - Lead time for changes -``` - ---- - -## 6. Continuous Improvement - -### 6.1 Regular Quality Reviews - -**Daily**: -- Monitor CI/CD pipeline health -- Review failed builds/tests -- Track security scan results - -**Weekly**: -- Review quality metrics dashboard -- Triage new technical debt -- Review production incidents - -**Monthly**: -- Comprehensive quality review meeting -- Update quality standards -- Review and prioritize technical debt -- Security vulnerability review - -**Quarterly**: -- Architecture review -- Penetration testing -- Disaster recovery testing -- Dependency updates and upgrades -- Quality process retrospective - ---- - -### 6.2 Quality Culture - -**Practices to Adopt**: -```yaml -✅ Code Review Standards: - - All code requires review - - Minimum 1 approval (2 for critical changes) - - Review checklist enforced - -✅ Definition of Done: - - All tests passing - - Coverage maintained/improved - - Documentation updated - - Security scan passed - - Performance benchmarks passed - -✅ Blameless Postmortems: - - Document all production incidents - - Focus on process improvement - - Share learnings across team - -✅ Quality Champions: - - Rotate quality champion role - - Responsible for quality advocacy - - Lead quality improvement initiatives -``` - ---- - -## 7. Success Criteria - -### 7.1 Immediate (8 Weeks) - -```yaml -✅ All 3 critical issues resolved -✅ All 12 high-priority issues resolved -✅ Test coverage > 85% -✅ Zero critical/high security vulnerabilities -✅ CI/CD pipeline with full quality gates -✅ Load test passed (1000 concurrent users) -✅ Staging deployment successful -✅ Production monitoring configured -``` - -### 7.2 Short-term (3 Months) - -```yaml -✅ Production deployment with 99.9% uptime -✅ Quality metrics tracked and improving -✅ All quality gates enforced -✅ Security testing integrated -✅ Performance benchmarks stable -✅ Technical debt ratio < 5% -✅ Team trained on quality practices -``` - -### 7.3 Long-term (6 Months) - -```yaml -✅ Continuous quality improvement culture -✅ Automated quality enforcement -✅ Zero production incidents from quality issues -✅ Fast feedback loops (< 10 min CI) -✅ High team satisfaction with quality -✅ Industry-leading quality metrics -``` - ---- - -## 8. Appendix - -### 8.1 Quality Tools Budget - -**One-time Setup Costs**: -- SonarQube Cloud: $400/year -- Security scanning tools: $100/month -- APM (Datadog/New Relic): $300/month -- Load testing infrastructure: $200/month - -**Total Estimated Cost**: ~$1,200/month (~$14,400/year) - -**ROI**: Prevention of single production incident > annual cost - ---- - -### 8.2 References - -- SENIOR_ENGINEER_CODE_REVIEW.md - Detailed code review findings -- DESIGN_VALIDATION_CHECKLIST.md - Architecture validation -- .github/workflows/ci.yml - Current CI pipeline -- pytest.ini - Test configuration -- pyproject.toml - Tool configuration - ---- - -## Conclusion - -This biometric-processor module has a **solid architectural foundation** but requires **immediate attention to critical quality issues** before production deployment. The proposed 8-week quality assurance program will: - -1. **Resolve all blocking issues** (Weeks 1-2) -2. **Establish quality foundation** (Weeks 3-4) -3. **Automate quality enforcement** (Weeks 5-6) -4. **Validate production readiness** (Weeks 7-8) - -**Recommendation**: Commit to this quality program before production launch to ensure long-term success and maintainability. - ---- - -**Prepared by**: Software Architect -**Next Review**: After Phase 1 completion -**Status**: 📋 **AWAITING APPROVAL** diff --git a/docs/6-architecture/PHASE2_DESIGN.md b/docs/6-architecture/PHASE2_DESIGN.md deleted file mode 100644 index 1c07ad4..0000000 --- a/docs/6-architecture/PHASE2_DESIGN.md +++ /dev/null @@ -1,595 +0,0 @@ -# Phase 2: Production Readiness Features - -## Overview - -This document describes the design for production-ready features including Redis-backed rate limiting, API key authentication, metrics/monitoring, and structured logging. - -## Features - -### 1. Redis Rate Limiting - -**Purpose**: Replace in-memory rate limiting with Redis for distributed deployments. - -#### Domain Layer - -**Interface** (`app/domain/interfaces/rate_limit_storage.py`): -```python -# Already exists - IRateLimitStorage Protocol -# No changes needed - Redis implementation will implement this interface -``` - -#### Infrastructure Layer - -**Implementation** (`app/infrastructure/rate_limit/redis_storage.py`): -```python -class RedisRateLimitStorage(IRateLimitStorage): - """Redis-backed rate limit storage with sliding window.""" - - def __init__( - self, - redis_url: str, - window_seconds: int = 60, - default_limit: int = 60, - ): - self._redis = redis.from_url(redis_url) - self._window = window_seconds - self._default_limit = default_limit - - def increment(self, key: str) -> int: - """Increment counter using Redis INCR with expiry.""" - pipe = self._redis.pipeline() - pipe.incr(key) - pipe.expire(key, self._window) - result = pipe.execute() - return result[0] - - def get_info(self, key: str) -> RateLimitInfo: - """Get rate limit info from Redis.""" - count = self._redis.get(key) or 0 - ttl = self._redis.ttl(key) - return RateLimitInfo( - count=int(count), - limit=self._default_limit, - remaining=max(0, self._default_limit - int(count)), - reset_at=datetime.utcnow() + timedelta(seconds=max(0, ttl)), - ) - - def is_rate_limited(self, key: str) -> bool: - """Check if key exceeds rate limit.""" - count = self._redis.get(key) or 0 - return int(count) >= self._default_limit - - def reset(self, key: str) -> None: - """Delete key from Redis.""" - self._redis.delete(key) -``` - -**Factory Update** (`app/infrastructure/rate_limit/storage_factory.py`): -```python -@staticmethod -def create(storage_type: str, **kwargs) -> IRateLimitStorage: - if storage_type == "memory": - return InMemoryRateLimitStorage(**kwargs) - elif storage_type == "redis": - return RedisRateLimitStorage( - redis_url=kwargs.get("redis_url", "redis://localhost:6379/0"), - window_seconds=kwargs.get("window_seconds", 60), - default_limit=kwargs.get("default_limit", 60), - ) - else: - raise ValueError(f"Unknown storage type: {storage_type}") -``` - -#### Configuration - -**Config Updates** (`app/core/config.py`): -```python -# Redis Configuration -REDIS_URL: Optional[str] = Field(default="redis://localhost:6379/0") -REDIS_ENABLED: bool = Field(default=False) - -# Rate Limiting -RATE_LIMIT_STORAGE: Literal["memory", "redis"] = Field(default="memory") -RATE_LIMIT_WINDOW_SECONDS: int = Field(default=60, ge=1, le=3600) -``` - ---- - -### 2. Rate Limit Middleware - -**Purpose**: Apply rate limiting to all API endpoints automatically. - -#### API Layer - -**Middleware** (`app/api/middleware/rate_limit.py`): -```python -from fastapi import Request, HTTPException -from starlette.middleware.base import BaseHTTPMiddleware - -class RateLimitMiddleware(BaseHTTPMiddleware): - """Rate limiting middleware using sliding window.""" - - def __init__(self, app, storage: IRateLimitStorage, limit: int = 60): - super().__init__(app) - self._storage = storage - self._limit = limit - - async def dispatch(self, request: Request, call_next): - # Extract client identifier - client_id = self._get_client_id(request) - - # Check rate limit - if self._storage.is_rate_limited(client_id): - info = self._storage.get_info(client_id) - raise HTTPException( - status_code=429, - detail="Rate limit exceeded", - headers={ - "X-RateLimit-Limit": str(info.limit), - "X-RateLimit-Remaining": "0", - "X-RateLimit-Reset": info.reset_at.isoformat(), - "Retry-After": str(int((info.reset_at - datetime.utcnow()).total_seconds())), - }, - ) - - # Increment counter - self._storage.increment(client_id) - - # Process request - response = await call_next(request) - - # Add rate limit headers - info = self._storage.get_info(client_id) - response.headers["X-RateLimit-Limit"] = str(info.limit) - response.headers["X-RateLimit-Remaining"] = str(info.remaining) - response.headers["X-RateLimit-Reset"] = info.reset_at.isoformat() - - return response - - def _get_client_id(self, request: Request) -> str: - """Extract client identifier from request.""" - # Priority: API key > X-Forwarded-For > client IP - api_key = request.headers.get("X-API-Key") - if api_key: - return f"api:{api_key}" - - forwarded = request.headers.get("X-Forwarded-For") - if forwarded: - return f"ip:{forwarded.split(',')[0].strip()}" - - return f"ip:{request.client.host}" -``` - ---- - -### 3. API Key Authentication - -**Purpose**: Secure API access with API key authentication. - -#### Domain Layer - -**Entity** (`app/domain/entities/api_key.py`): -```python -@dataclass -class APIKey: - """API key entity.""" - key_id: str - key_hash: str # SHA-256 hash of the actual key - name: str - tenant_id: str - scopes: List[str] # ["read", "write", "admin"] - rate_limit: int # Custom rate limit for this key - is_active: bool - created_at: datetime - expires_at: Optional[datetime] - last_used_at: Optional[datetime] -``` - -**Interface** (`app/domain/interfaces/api_key_repository.py`): -```python -from typing import Protocol, Optional - -class IAPIKeyRepository(Protocol): - """Repository interface for API keys.""" - - async def find_by_key_hash(self, key_hash: str) -> Optional[APIKey]: - """Find API key by hash.""" - ... - - async def save(self, api_key: APIKey) -> None: - """Save API key.""" - ... - - async def update_last_used(self, key_id: str) -> None: - """Update last used timestamp.""" - ... -``` - -#### Infrastructure Layer - -**In-Memory Implementation** (`app/infrastructure/persistence/repositories/memory_api_key_repository.py`): -```python -class InMemoryAPIKeyRepository(IAPIKeyRepository): - """In-memory API key repository for development.""" - - def __init__(self): - self._keys: Dict[str, APIKey] = {} - - async def find_by_key_hash(self, key_hash: str) -> Optional[APIKey]: - return self._keys.get(key_hash) - - async def save(self, api_key: APIKey) -> None: - self._keys[api_key.key_hash] = api_key - - async def update_last_used(self, key_id: str) -> None: - for key in self._keys.values(): - if key.key_id == key_id: - key.last_used_at = datetime.utcnow() - break -``` - -#### API Layer - -**Dependency** (`app/api/dependencies/auth.py`): -```python -import hashlib -from fastapi import Depends, HTTPException, Security -from fastapi.security import APIKeyHeader - -api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) - -async def verify_api_key( - api_key: Optional[str] = Security(api_key_header), - repository: IAPIKeyRepository = Depends(get_api_key_repository), -) -> Optional[APIKey]: - """Verify API key and return key info.""" - if not api_key: - if settings.API_KEY_REQUIRED: - raise HTTPException(status_code=401, detail="API key required") - return None - - # Hash the key for lookup - key_hash = hashlib.sha256(api_key.encode()).hexdigest() - - # Find in repository - key_info = await repository.find_by_key_hash(key_hash) - - if not key_info: - raise HTTPException(status_code=401, detail="Invalid API key") - - if not key_info.is_active: - raise HTTPException(status_code=401, detail="API key disabled") - - if key_info.expires_at and key_info.expires_at < datetime.utcnow(): - raise HTTPException(status_code=401, detail="API key expired") - - # Update last used - await repository.update_last_used(key_info.key_id) - - return key_info - -def require_scope(scope: str): - """Dependency to require specific scope.""" - async def check_scope( - api_key: Optional[APIKey] = Depends(verify_api_key), - ): - if api_key and scope not in api_key.scopes: - raise HTTPException(status_code=403, detail=f"Scope '{scope}' required") - return api_key - return check_scope -``` - -#### Configuration - -```python -# API Key Authentication -API_KEY_REQUIRED: bool = Field(default=False) -API_KEY_HEADER: str = Field(default="X-API-Key") -``` - ---- - -### 4. Prometheus Metrics - -**Purpose**: Expose application metrics for monitoring. - -#### Infrastructure Layer - -**Metrics Module** (`app/infrastructure/monitoring/metrics.py`): -```python -from prometheus_client import Counter, Histogram, Gauge, Info -import time - -# Application info -APP_INFO = Info("biometric_processor", "Application information") - -# Request metrics -REQUEST_COUNT = Counter( - "http_requests_total", - "Total HTTP requests", - ["method", "endpoint", "status"], -) - -REQUEST_LATENCY = Histogram( - "http_request_duration_seconds", - "HTTP request latency", - ["method", "endpoint"], - buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0], -) - -# Business metrics -ENROLLMENT_COUNT = Counter( - "face_enrollments_total", - "Total face enrollments", - ["tenant_id", "status"], -) - -VERIFICATION_COUNT = Counter( - "face_verifications_total", - "Total face verifications", - ["tenant_id", "result"], -) - -LIVENESS_CHECK_COUNT = Counter( - "liveness_checks_total", - "Total liveness checks", - ["mode", "result"], -) - -# ML metrics -FACE_DETECTION_LATENCY = Histogram( - "face_detection_seconds", - "Face detection latency", - ["backend"], - buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0], -) - -EMBEDDING_EXTRACTION_LATENCY = Histogram( - "embedding_extraction_seconds", - "Embedding extraction latency", - ["model"], - buckets=[0.05, 0.1, 0.25, 0.5, 1.0, 2.0], -) - -# System metrics -ACTIVE_REQUESTS = Gauge( - "active_requests", - "Number of active requests", -) - -EMBEDDINGS_COUNT = Gauge( - "embeddings_stored_total", - "Total embeddings stored", - ["tenant_id"], -) -``` - -**Metrics Middleware** (`app/api/middleware/metrics.py`): -```python -from starlette.middleware.base import BaseHTTPMiddleware -import time - -class MetricsMiddleware(BaseHTTPMiddleware): - """Middleware to collect request metrics.""" - - async def dispatch(self, request: Request, call_next): - ACTIVE_REQUESTS.inc() - - start_time = time.time() - - try: - response = await call_next(request) - - # Record metrics - REQUEST_COUNT.labels( - method=request.method, - endpoint=request.url.path, - status=response.status_code, - ).inc() - - REQUEST_LATENCY.labels( - method=request.method, - endpoint=request.url.path, - ).observe(time.time() - start_time) - - return response - finally: - ACTIVE_REQUESTS.dec() -``` - -#### API Layer - -**Metrics Endpoint** (`app/api/routes/metrics.py`): -```python -from fastapi import APIRouter -from fastapi.responses import PlainTextResponse -from prometheus_client import generate_latest, CONTENT_TYPE_LATEST - -router = APIRouter(prefix="/metrics", tags=["Monitoring"]) - -@router.get("", response_class=PlainTextResponse) -async def get_metrics(): - """Expose Prometheus metrics.""" - return PlainTextResponse( - content=generate_latest(), - media_type=CONTENT_TYPE_LATEST, - ) -``` - ---- - -### 5. Structured Logging - -**Purpose**: JSON-formatted logs for log aggregation systems. - -#### Infrastructure Layer - -**Logging Setup** (`app/infrastructure/logging/structured.py`): -```python -import logging -import json -import sys -from datetime import datetime -from typing import Any, Dict - -class StructuredFormatter(logging.Formatter): - """JSON log formatter.""" - - def format(self, record: logging.LogRecord) -> str: - log_data: Dict[str, Any] = { - "timestamp": datetime.utcnow().isoformat() + "Z", - "level": record.levelname, - "logger": record.name, - "message": record.getMessage(), - "module": record.module, - "function": record.funcName, - "line": record.lineno, - } - - # Add exception info if present - if record.exc_info: - log_data["exception"] = self.formatException(record.exc_info) - - # Add extra fields - if hasattr(record, "request_id"): - log_data["request_id"] = record.request_id - if hasattr(record, "tenant_id"): - log_data["tenant_id"] = record.tenant_id - if hasattr(record, "user_id"): - log_data["user_id"] = record.user_id - if hasattr(record, "duration_ms"): - log_data["duration_ms"] = record.duration_ms - - return json.dumps(log_data) - -def setup_structured_logging(level: str = "INFO"): - """Configure structured logging.""" - root_logger = logging.getLogger() - root_logger.setLevel(getattr(logging, level)) - - # Remove existing handlers - root_logger.handlers.clear() - - # Add structured handler - handler = logging.StreamHandler(sys.stdout) - handler.setFormatter(StructuredFormatter()) - root_logger.addHandler(handler) -``` - -**Request Context** (`app/api/middleware/request_context.py`): -```python -import uuid -from contextvars import ContextVar -from starlette.middleware.base import BaseHTTPMiddleware - -request_id_var: ContextVar[str] = ContextVar("request_id", default="") - -class RequestContextMiddleware(BaseHTTPMiddleware): - """Add request context for logging.""" - - async def dispatch(self, request: Request, call_next): - # Generate request ID - request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())) - request_id_var.set(request_id) - - # Process request - response = await call_next(request) - - # Add request ID to response - response.headers["X-Request-ID"] = request_id - - return response -``` - ---- - -## Configuration Summary - -**New Environment Variables**: - -| Variable | Default | Description | -|----------|---------|-------------| -| `REDIS_URL` | redis://localhost:6379/0 | Redis connection URL | -| `REDIS_ENABLED` | false | Enable Redis integration | -| `RATE_LIMIT_STORAGE` | memory | Rate limit backend (memory/redis) | -| `RATE_LIMIT_WINDOW_SECONDS` | 60 | Sliding window duration | -| `API_KEY_REQUIRED` | false | Require API key for all requests | -| `LOG_FORMAT` | json | Log format (json/text) | -| `METRICS_ENABLED` | true | Enable Prometheus metrics | - ---- - -## File Structure - -``` -app/ -├── api/ -│ ├── dependencies/ -│ │ └── auth.py # NEW: API key auth dependency -│ ├── middleware/ -│ │ ├── rate_limit.py # NEW: Rate limit middleware -│ │ ├── metrics.py # NEW: Metrics middleware -│ │ └── request_context.py # NEW: Request context middleware -│ └── routes/ -│ └── metrics.py # NEW: Prometheus endpoint -├── domain/ -│ ├── entities/ -│ │ └── api_key.py # NEW: API key entity -│ └── interfaces/ -│ └── api_key_repository.py # NEW: API key repository interface -├── infrastructure/ -│ ├── logging/ -│ │ └── structured.py # NEW: Structured logging -│ ├── monitoring/ -│ │ └── metrics.py # NEW: Prometheus metrics -│ ├── persistence/ -│ │ └── repositories/ -│ │ └── memory_api_key_repository.py # NEW -│ └── rate_limit/ -│ └── redis_storage.py # NEW: Redis rate limiter -└── core/ - └── config.py # UPDATE: New settings -``` - ---- - -## Dependencies - -**New Requirements** (`requirements.txt` additions): -``` -redis>=5.0.0 -prometheus-client>=0.19.0 -``` - ---- - -## SE Checklist Compliance - -| Requirement | Status | Implementation | -|-------------|--------|----------------| -| Clean Architecture | ✅ | Domain interfaces, infrastructure implementations | -| Dependency Inversion | ✅ | IRateLimitStorage, IAPIKeyRepository | -| Open/Closed Principle | ✅ | Factory patterns for storage backends | -| Single Responsibility | ✅ | Separate middleware for each concern | -| Interface Segregation | ✅ | Focused interfaces (storage, repository) | -| Factory Pattern | ✅ | RateLimitStorageFactory extended | -| Configuration | ✅ | Environment-based settings | -| Error Handling | ✅ | HTTP exceptions with proper codes | -| Logging | ✅ | Structured JSON logging | -| Monitoring | ✅ | Prometheus metrics | -| Security | ✅ | API key auth, rate limiting | -| Testability | ✅ | In-memory implementations for testing | - ---- - -## Implementation Order - -1. **Redis Rate Limiting** - Infrastructure + factory update -2. **Rate Limit Middleware** - Apply to all routes -3. **API Key Authentication** - Entity + repository + dependency -4. **Prometheus Metrics** - Metrics module + middleware + endpoint -5. **Structured Logging** - Formatter + context middleware -6. **Configuration** - Update config.py -7. **Main.py Integration** - Wire up middlewares -8. **Tests** - Unit + integration tests -9. **Documentation** - Update README diff --git a/docs/6-architecture/PORT_CONFIGURATION_SUMMARY.md b/docs/6-architecture/PORT_CONFIGURATION_SUMMARY.md deleted file mode 100644 index 3f59cdf..0000000 --- a/docs/6-architecture/PORT_CONFIGURATION_SUMMARY.md +++ /dev/null @@ -1,404 +0,0 @@ -# 🎯 PORT CONFIGURATION - CRITICAL REFERENCE - -**Last Updated:** 2025-12-24 -**Status:** ✅ ALL PORTS STANDARDIZED -**Action Required:** Read this FIRST before starting any service - ---- - -## ⚠️ CRITICAL - READ THIS FIRST - -**THE ONLY OFFICIAL PORTS TO USE:** - -``` -┌─────────────────────────────────────────────────────────┐ -│ SERVICE │ PORT │ URL │ -├─────────────────────────────────────────────────────────┤ -│ Biometric API │ 8001 │ http://localhost:8001 │ -│ Demo UI │ 3000 │ http://localhost:3000 │ -│ PostgreSQL │ 5432 │ localhost:5432 │ -│ Redis │ 6379 │ localhost:6379 │ -│ Prometheus │ 9090 │ http://localhost:9090 │ -└─────────────────────────────────────────────────────────┘ -``` - -**🚫 NEVER USE PORT 8000 - IT IS DEPRECATED!** - ---- - -## 📋 QUICK START CHECKLIST - -Before starting development: - -- [ ] 1. Verify `.env` has `API_PORT=8001` -- [ ] 2. Verify `demo-ui/.env.local` has `NEXT_PUBLIC_API_URL=http://localhost:8001` -- [ ] 3. Check no other service is using port 8001 -- [ ] 4. Start services in this order: - ```bash - # 1. Database & Cache - docker-compose up -d postgres redis - - # 2. API (choose ONE method): - # Method A: Using venv - .venv/Scripts/python.exe -m uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload - - # Method B: Using Docker - docker-compose up -d biometric-api - - # 3. Demo UI - cd demo-ui && npm run dev - ``` -- [ ] 5. Verify API is accessible: http://localhost:8001/api/v1/health -- [ ] 6. Verify UI is accessible: http://localhost:3000 - ---- - -## 🔍 FILES UPDATED (2025-12-24) - -### ✅ Configuration Files Fixed - -| File | Change Made | Status | -|------|------------|--------| -| `.env` | `API_PORT=8000` → `API_PORT=8001` | ✅ FIXED | -| `demo-ui/.env.local` | `localhost:8000` → `localhost:8001` | ✅ FIXED | -| `demo-ui/.env.example` | `localhost:8000` → `localhost:8001` | ✅ FIXED | -| `.env.example` | Already correct (8001) | ✅ OK | -| `docker-compose.yml` | Already correct (8001) | ✅ OK | -| `app/core/config.py` | Already correct (default=8001) | ✅ OK | - -### ✅ Documentation Files Verified - -| File | Port Referenced | Status | -|------|----------------|--------| -| `PORT_STANDARDS.md` | 8001 (Official Standard) | ✅ OK | -| `README.md` | 8001 | ✅ OK | -| `QUICK_START.md` | 8001 | ✅ OK | -| `docs/api/API_DOCUMENTATION.md` | 8001 | ✅ OK | - -### ✅ Test/Demo Files Verified - -| File | Port Referenced | Status | -|------|----------------|--------| -| `demo/utils/config.py` | 8001 | ✅ OK | -| `demo/utils/api_client.py` | 8001 | ✅ OK | -| `tests/load/locustfile.py` | 8001 | ✅ OK | - ---- - -## 🚨 COMMON MISTAKES TO AVOID - -### ❌ WRONG - Using Port 8000 - -```bash -# DON'T DO THIS: -uvicorn app.main:app --port 8000 # ❌ WRONG PORT! -NEXT_PUBLIC_API_URL=http://localhost:8000 # ❌ WRONG! -``` - -### ✅ CORRECT - Using Port 8001 - -```bash -# DO THIS INSTEAD: -uvicorn app.main:app --port 8001 # ✅ CORRECT -NEXT_PUBLIC_API_URL=http://localhost:8001 # ✅ CORRECT -``` - ---- - -## 🔧 ENVIRONMENT VARIABLES - COMPLETE REFERENCE - -### API Server Configuration - -```bash -# .env file (ROOT directory) -API_PORT=8001 # ✅ MUST be 8001 -HOST=0.0.0.0 # Listen on all interfaces -DATABASE_URL=postgresql+asyncpg://biometric:biometric@localhost:5432/biometric -REDIS_HOST=localhost -REDIS_PORT=6379 # Standard Redis port -``` - -### Demo UI Configuration - -```bash -# demo-ui/.env.local file -NEXT_PUBLIC_API_URL=http://localhost:8001 # ✅ MUST be 8001 -NEXT_PUBLIC_WS_URL=ws://localhost:8001 # ✅ MUST be 8001 -``` - ---- - -## 🐳 DOCKER COMPOSE - PORT MAPPING - -```yaml -# docker-compose.yml (already correct) -services: - biometric-api: - ports: - - "8001:8001" # ✅ External:Internal both 8001 - environment: - - API_PORT=8001 # ✅ Internal port - - demo-ui: - ports: - - "3000:3000" # ✅ Standard Next.js port - - postgres: - ports: - - "5432:5432" # ✅ Standard PostgreSQL port - - redis: - ports: - - "6379:6379" # ✅ Standard Redis port -``` - ---- - -## 🧪 TESTING PORT CONFIGURATION - -### 1. Test API Port - -```bash -# Should show "healthy" on port 8001 -curl http://localhost:8001/api/v1/health - -# Expected response: -{ - "status": "healthy", - "version": "1.0.0", - "model": "Facenet", - "detector": "opencv" -} -``` - -### 2. Test UI Port - -```bash -# Should return HTML (Next.js app) -curl http://localhost:3000 -``` - -### 3. Test Database Port - -```bash -# Should connect successfully -docker exec biometric-postgres psql -U biometric -d biometric -c "SELECT 1;" -``` - -### 4. Test Redis Port - -```bash -# Should return "PONG" -docker exec biometric-redis redis-cli ping -``` - ---- - -## 🔍 FINDING PORT CONFLICTS - -### Check if Port 8001 is Already in Use - -```bash -# Windows: -netstat -ano | findstr ":8001" - -# Linux/Mac: -lsof -i :8001 -# Or: netstat -tulpn | grep 8001 -``` - -### If Port 8001 is Busy - -```bash -# Option 1: Kill the process using the port -# Windows: taskkill /PID /F -# Linux/Mac: kill -9 - -# Option 2: Find what's using it and stop it properly -# Example: Stop another development server -``` - -**⚠️ DO NOT change the standard port. Fix the conflict instead.** - ---- - -## 📚 WHY THESE SPECIFIC PORTS? - -### Port 8001 (API) -- ✅ Avoids conflicts with common dev servers (8000, 8080) -- ✅ Clear separation from Identity Core API (8080) -- ✅ Standard microservice port range (8000-8999) -- ✅ Less common than 8000, fewer conflicts - -### Port 3000 (UI) -- ✅ Next.js default port -- ✅ Industry standard for React/Next.js apps -- ✅ Widely recognized by developers - -### Port 5432 (PostgreSQL) -- ✅ Official PostgreSQL default port -- ✅ Standard across all PostgreSQL installations - -### Port 6379 (Redis) -- ✅ Official Redis default port -- ✅ Universal Redis standard - ---- - -## 🎓 DEVELOPER GUIDELINES - -### When Starting New Development - -1. **ALWAYS** check `PORT_STANDARDS.md` first -2. **NEVER** hardcode port 8000 anywhere -3. **ALWAYS** use environment variables for ports -4. **VERIFY** configuration before starting services - -### When Adding New Services - -1. Add the port to `PORT_STANDARDS.md` -2. Update this summary document -3. Document in `README.md` -4. Update `docker-compose.yml` -5. Add to environment variable examples - -### When Updating Documentation - -1. Search for `8000` - should only appear in: - - `PORT_STANDARDS.md` (in "deprecated" section) - - `RELEASE_NOTES.md` (in changelog) - - This file (in "wrong" examples) -2. Replace any other occurrences with `8001` - ---- - -## 🔐 PRODUCTION CONSIDERATIONS - -### Port Mapping (Production) - -```bash -# Production typically uses reverse proxy -# External (Internet) → Internal (Container) - -HTTPS/443 → 8001 (API) -HTTPS/443 → 3000 (UI) - -# Database & Redis should NOT be exposed externally -# They only communicate internally within Docker network -``` - -### Environment-Specific Ports - -```bash -# Development -API: http://localhost:8001 -UI: http://localhost:3000 - -# Staging -API: https://api-staging.example.com (→ 8001) -UI: https://app-staging.example.com (→ 3000) - -# Production -API: https://api.example.com (→ 8001) -UI: https://app.example.com (→ 3000) -``` - ---- - -## ✅ VERIFICATION COMMANDS - -Run these commands to verify all ports are correct: - -```bash -# 1. Check for deprecated port 8000 references -# (Should return empty or only in PORT_STANDARDS.md/RELEASE_NOTES) -grep -r "8000" . --exclude-dir={.git,node_modules,.venv,__pycache__} \ - --exclude="*.{log,pyc}" | grep -v "PORT_STANDARDS.md" | grep -v "RELEASE_NOTES" - -# 2. Verify .env file -grep "API_PORT" .env -# Should show: API_PORT=8001 - -# 3. Verify demo-ui config -grep "NEXT_PUBLIC_API_URL" demo-ui/.env.local -# Should show: NEXT_PUBLIC_API_URL=http://localhost:8001 - -# 4. Verify docker-compose -grep -A 1 "biometric-api:" docker-compose.yml | grep ports -# Should show: - "8001:8001" - -# 5. Verify Python config -grep "API_PORT.*Field.*default" app/core/config.py -# Should show: API_PORT: int = Field(default=8001...) -``` - ---- - -## 📞 TROUBLESHOOTING - -### "Connection Refused" Error - -**Symptom:** UI shows "Failed to load resource: net::ERR_CONNECTION_REFUSED" - -**Cause:** UI is trying to connect to wrong port - -**Solution:** -1. Check `demo-ui/.env.local` has `NEXT_PUBLIC_API_URL=http://localhost:8001` -2. Restart the UI: `cd demo-ui && npm run dev` -3. Hard refresh browser: Ctrl+Shift+R - -### "Port Already in Use" Error - -**Symptom:** `Error: listen EADDRINUSE: address already in use :::8001` - -**Cause:** Another process is using port 8001 - -**Solution:** -```bash -# Find what's using the port -netstat -ano | findstr ":8001" - -# Kill the process (replace PID with actual PID) -taskkill /PID /F -``` - -### API Works on 8000 but Not 8001 - -**Cause:** `.env` file has wrong port - -**Solution:** -1. Open `.env` file -2. Change `API_PORT=8000` to `API_PORT=8001` -3. Restart the API server - ---- - -## 📝 REFERENCES - -- **Official Standard:** See `PORT_STANDARDS.md` -- **API Documentation:** See `docs/api/API_DOCUMENTATION.md` -- **Quick Start Guide:** See `QUICK_START.md` -- **Docker Setup:** See `docker-compose.yml` - ---- - -## ⚡ SUMMARY - -**REMEMBER THESE THREE NUMBERS:** - -``` -API: 8001 ← NOT 8000! -UI: 3000 -DB: 5432 -``` - -**IF IN DOUBT:** Check `PORT_STANDARDS.md` or this file. - -**NEVER COMMIT:** Configuration with port 8000 for the API. - ---- - -**Last Verified:** 2025-12-24 -**Next Review:** When adding new services -**Maintained By:** Development Team -**Status:** ✅ ENFORCED - ALL CONFIGURATIONS STANDARDIZED diff --git a/docs/6-architecture/PORT_STANDARDS.md b/docs/6-architecture/PORT_STANDARDS.md deleted file mode 100644 index 11ed660..0000000 --- a/docs/6-architecture/PORT_STANDARDS.md +++ /dev/null @@ -1,350 +0,0 @@ -# Port Standardization - Biometric Processor - -**Date:** 2025-12-24 -**Status:** Official Port Standard - ---- - -## 🎯 **Official Port Allocation** - -This document defines the **official port standards** for all services in the biometric processor ecosystem. All documentation, configuration, and deployment files MUST use these ports. - -### **Production Port Standards** - -| Service | Port | Protocol | Purpose | Status | -|---------|------|----------|---------|--------| -| **Biometric API** | **8001** | HTTP/HTTPS | Main API service | ✅ STANDARD | -| **Demo UI (Next.js)** | **3000** | HTTP/HTTPS | Web interface | ✅ STANDARD | -| **PostgreSQL** | **5432** | TCP | Database (pgvector) | ✅ STANDARD | -| **Redis** | **6379** | TCP | Cache & rate limiting | ✅ STANDARD | -| **Prometheus** | **9090** | HTTP | Metrics collection | ✅ STANDARD | -| **Admin Panel** | **8080** | HTTP | Identity Core API | 📝 FUTURE | - ---- - -## 🚫 **DEPRECATED Ports** - -**DO NOT USE:** -- ❌ **8000** - Old API port (replaced by 8001) -- ❌ **3001** - Alternative UI port (use 3000 only) -- ❌ **8080** - Conflicted with admin (reserved for Identity Core) - -**Why 8001 instead of 8000?** -- Avoids common conflicts with development servers -- Clear separation from Identity Core API (8080) -- Consistent with microservice port allocation - ---- - -## 📋 **Port Allocation Rules** - -### **Development Environment** - -```bash -# Biometric Processor API -http://localhost:8001 - -# Demo UI (Next.js) -http://localhost:3000 - -# PostgreSQL -postgresql://localhost:5432/biometric - -# Redis -redis://localhost:6379 - -# Prometheus -http://localhost:9090 -``` - -### **Docker/Container Environment** - -```yaml -services: - biometric-api: - ports: - - "8001:8001" # External:Internal - - demo-ui: - ports: - - "3000:3000" - - postgres: - ports: - - "5432:5432" - - redis: - ports: - - "6379:6379" - - prometheus: - ports: - - "9090:9090" -``` - -### **Kubernetes Environment** - -```yaml -# Service ports (ClusterIP) -- biometric-api: 8001 -- demo-ui: 3000 -- postgres: 5432 (internal only) -- redis: 6379 (internal only) -- prometheus: 9090 (internal only) - -# Ingress (external access) -- https://api.example.com -> biometric-api:8001 -- https://app.example.com -> demo-ui:3000 -``` - ---- - -## 🔧 **Configuration Updates Required** - -### **1. Environment Variables** - -```bash -# .env -API_PORT=8001 # ✅ CORRECT -# API_PORT=8000 # ❌ WRONG - -# demo-ui/.env -NEXT_PUBLIC_API_URL=http://localhost:8001 # ✅ CORRECT -# NEXT_PUBLIC_API_URL=http://localhost:8000 # ❌ WRONG -``` - -### **2. Docker Compose** - -```yaml -# docker-compose.yml -biometric-api: - ports: - - "8001:8001" # ✅ CORRECT - # - "8000:8000" # ❌ WRONG - environment: - - API_PORT=8001 # ✅ CORRECT -``` - -### **3. Application Code** - -```python -# app/core/config.py -API_PORT: int = Field(default=8001) # ✅ CORRECT -# API_PORT: int = Field(default=8000) # ❌ WRONG -``` - -### **4. Documentation** - -All documentation MUST use: -- API examples: `http://localhost:8001` -- UI examples: `http://localhost:3000` -- Never reference port 8000 or 3001 - ---- - -## 📝 **Files to Update** - -### **Critical Files (MUST Update)** - -1. **Configuration:** - - ✅ `.env.example` - - ✅ `app/core/config.py` - - ✅ `demo-ui/.env.example` - -2. **Deployment:** - - ✅ `docker-compose.yml` - - ✅ `k8s/base/deployment.yaml` - - ✅ `k8s/base/service.yaml` - -3. **Documentation:** - - ✅ `README.md` - - ✅ `QUICK_START.md` - - ✅ `docs/api/API_DOCUMENTATION.md` - - All `*.md` files with port references - -4. **Test Files:** - - ✅ `tests/load/locustfile.py` - - ✅ `demo/utils/config.py` - - ✅ Test scripts (`test_*.py`) - ---- - -## ✅ **Verification Checklist** - -After updating ports, verify: - -```bash -# 1. Search for deprecated ports -grep -r "8000\|3001" . --exclude-dir={.git,node_modules} | grep -v "PORT_STANDARDS.md" -# Should return: No results (except in history/changelog) - -# 2. Verify API starts on correct port -uvicorn app.main:app --reload -# Should show: Uvicorn running on http://0.0.0.0:8001 - -# 3. Verify Docker Compose -docker-compose up -# biometric-api should expose 8001 -# demo-ui should expose 3000 - -# 4. Test API access -curl http://localhost:8001/api/v1/health -# Should return: 200 OK - -# 5. Test UI access -curl http://localhost:3000 -# Should return: HTML (Next.js app) -``` - ---- - -## 🔄 **Migration Guide** - -### **If Currently Using Port 8000:** - -1. **Stop all services:** - ```bash - docker-compose down - # Or: Ctrl+C if running locally - ``` - -2. **Update configuration:** - ```bash - # Update .env - sed -i 's/8000/8001/g' .env - - # Update demo-ui/.env - sed -i 's/8000/8001/g' demo-ui/.env - ``` - -3. **Restart services:** - ```bash - docker-compose up -d - # Or: uvicorn app.main:app --reload - ``` - -4. **Update bookmarks/scripts:** - - Change browser bookmarks from :8000 to :8001 - - Update any custom scripts or automation - ---- - -## 🌐 **URL Standards** - -### **Development URLs** - -``` -API Base URL: http://localhost:8001 -API Docs: http://localhost:8001/docs -API Health: http://localhost:8001/api/v1/health - -Demo UI: http://localhost:3000 -Metrics: http://localhost:9090 -``` - -### **Production URLs** (example) - -``` -API: https://api.biometric.example.com -Demo UI: https://app.biometric.example.com -Admin: https://admin.identity.example.com -``` - ---- - -## 🔒 **Security Considerations** - -### **Firewall Rules** - -```bash -# Allow only necessary ports -ufw allow 8001/tcp # API -ufw allow 3000/tcp # UI (if public) -ufw deny 5432/tcp # PostgreSQL (internal only) -ufw deny 6379/tcp # Redis (internal only) -``` - -### **Docker Network Isolation** - -```yaml -# docker-compose.yml -networks: - backend: # Internal only - Postgres, Redis - frontend: # Public - API, UI -``` - -**Rules:** -- PostgreSQL & Redis: Backend network only (no external exposure) -- API: Both networks (frontend for clients, backend for DB/Redis) -- UI: Frontend network only - ---- - -## 📊 **Port Conflict Resolution** - -### **Common Conflicts** - -| Port | Common Conflicts | Resolution | -|------|-----------------|------------| -| 8001 | Rarely conflicts | ✅ Safe choice | -| 3000 | React, Remix dev servers | Stop other dev servers first | -| 5432 | Other PostgreSQL | Use Docker or change to 5433 | -| 6379 | Other Redis | Use Docker or change to 6380 | - -### **If Port 8001 is Already in Use** - -```bash -# Find what's using the port -lsof -i :8001 -# Or: netstat -tulpn | grep 8001 - -# Option 1: Kill the process -kill -9 - -# Option 2: Use alternative port temporarily -API_PORT=8002 uvicorn app.main:app --reload -``` - -**Note:** Do NOT change the standard. Fix the conflict instead. - ---- - -## 📚 **References** - -- **Why these ports?** - - 8001: Common microservice port, avoids conflicts - - 3000: Next.js default, widely adopted - - 5432: PostgreSQL standard - - 6379: Redis standard - - 9090: Prometheus standard - -- **Industry Standards:** - - 80/443: HTTP/HTTPS (production with reverse proxy) - - 8000-8999: Application servers - - 3000-3999: Frontend frameworks - - 5000-5999: Databases - - 6000-6999: Caches - - 9000-9999: Monitoring/metrics - ---- - -## ✨ **Summary** - -**Official Ports:** -- 🔵 API: **8001** -- 🟢 UI: **3000** -- 🟣 PostgreSQL: **5432** -- 🔴 Redis: **6379** -- 🟠 Prometheus: **9090** - -**Deprecated:** -- ❌ 8000 (old API) -- ❌ 3001 (alternative UI) - -**All systems MUST use these standardized ports for consistency.** - ---- - -**Last Updated:** 2025-12-24 -**Approved By:** Development Team -**Status:** **MANDATORY - ENFORCE IN ALL ENVIRONMENTS** diff --git a/docs/FIVUCSAS_Biometric_Processor_Paper.md b/docs/FIVUCSAS_Biometric_Processor_Paper.md deleted file mode 100644 index 7acfdf7..0000000 --- a/docs/FIVUCSAS_Biometric_Processor_Paper.md +++ /dev/null @@ -1,690 +0,0 @@ -# FIVUCSAS: A Multi-Modal Challenge-Response Framework with Cross-Modal Binding for Presentation Attack Detection in Facial Verification Systems - -**Authors:** FIVUCSAS Research Team -**Affiliation:** Rollingcat Software, Marmara University CSE -**Date:** May 2026 - ---- - -## Abstract - -We present FIVUCSAS, a facial verification pipeline integrating a multi-modal Presentation Attack Detection (PAD) framework that combines passive texture/frequency analysis with a dual-modality active challenge system spanning facial and hand gestures. Our primary contributions are: (1) a *cross-modal spatial binding* mechanism that geometrically ties face and hand landmarks into a single coherent 3D scene, preventing confederate split-screen attacks; (2) a four-category hand gesture challenge subsystem (finger counting, arithmetic cognitive proofs, z-validated finger touch, DTW-verified shape tracing) that complements conventional EAR/MAR facial challenges; and (3) a quality-aware weighted fusion function combining six passive channels with dual-modality active scores. We evaluate the system on OULU-NPU (Protocols 1–4), Replay-Attack, and an internal Screen Replay corpus, reporting ACER of 2.8% on OULU-NPU Protocol 1 and BPCER₁₀ of 3.1% under the constrained protocol. Ablation studies demonstrate that cross-modal binding reduces SAR against confederate attacks from 73.2% to 4.6%, and the cognitive MATH challenge contributes a 12.4 percentage-point SAR reduction independent of passive channels. The system operates within a hexagonal (Ports & Adapters) architecture backed by pgvector HNSW-indexed embedding storage. We explicitly scope the threat model to software-based RGB-only defenses and acknowledge residual vulnerabilities to 3D silicone masks, real-time deepfake synthesis, and client-side frame injection on rooted devices. - -**Keywords:** Presentation Attack Detection, Cross-Modal Binding, Cognitive Liveness, Hand Gesture Recognition, Dynamic Time Warping, Face Anti-Spoofing, pgvector - ---- - -## 1. Introduction - -### 1.1 Background - -Facial verification underpins remote identity proofing in financial services (KYC/AML), examination proctoring, and access control. The assumption that biometric samples originate from a live, physically present individual is challenged by Presentation Attack Instruments (PAIs) categorized under ISO/IEC 30107-3 [1]. High-resolution displays, printed photographs, and increasingly sophisticated deepfakes threaten system integrity. - -Active Liveness approaches prompting facial gestures (blink, smile) provide necessary but insufficient defense: screen replay of pre-recorded video naturally satisfies Eye Aspect Ratio (EAR) and Mouth Aspect Ratio (MAR) thresholds [6]. Passive methods (texture analysis, frequency-domain Moiré detection, deep classifiers) strengthen defense but remain vulnerable to high-quality replay media on OLED panels with minimal Moiré artifacts [3]. - -### 1.2 Threat Model and Scope - -We define the following PAI species within scope: - -| PAI Species | Description | In Scope | -|-------------|-------------|----------| -| Screen Replay (SR) | Pre-recorded video on LCD/OLED display | Yes | -| Photo Print (PP) | High-resolution printed photograph | Yes | -| Cutout Attack (CA) | Printed photo with eye/mouth holes | Yes | -| Confederate Split-Screen (CSS) | Tablet showing face + attacker's real hands | Yes | -| Digital Injection (DI) | Frame injection via virtual camera / rooted device | Partial* | -| 3D Silicone Mask (3DM) | Custom prosthetic mask | No | -| Real-Time Deepfake (RTD) | Live neural face swap adapting to challenges | No | -| Adversarial Perturbation (AP) | Gradient-based attacks on MiniFASNet | No | - -*Digital Injection is partially addressed via session nonce binding (Section 5.4) but requires hardware attestation for full mitigation — outside the scope of a software-only RGB defense. - -**Explicit out-of-scope acknowledgment:** This work does not claim to defeat 3D silicone masks (requiring IR/ToF hardware), real-time deepfake generators with feedback loops (requiring temporal inconsistency analysis beyond our current architecture), or adversarial ML attacks against the passive classifier (requiring adversarial training and input certification). These represent active research frontiers. - -### 1.3 Contributions - -1. **Cross-modal spatial binding** — a geometric consistency check requiring face and hand landmarks to occupy physically plausible 3D positions within a single scene, defeating confederate split-screen attacks where face and hands originate from separate sources (Section 4.2). -2. **Four-category hand gesture challenge subsystem** — finger counting, arithmetic cognitive proofs, z-validated finger touch, and DTW-verified shape tracing, integrated into a unified session orchestrator alongside facial EAR/MAR (Section 3.4–3.8). -3. **Quality-aware weighted fusion** — combining six passive channels (FFT, LBP, color, blur, MiniFASNet, pseudo-depth) with dual-modality active scores via a formally defined quality function (Section 5). -4. **Empirical evaluation** on OULU-NPU, Replay-Attack, and an internal corpus, with per-channel ablation and cross-attack SAR analysis (Section 6). -5. **Hexagonal-architecture biometric pipeline** with pgvector HNSW-indexed embedding storage and factory-pattern detector substitution (Section 3.1–3.3). - -### 1.4 Paper Organization - -Section 2 surveys related work. Section 3 describes the system architecture and biometric pipeline. Section 4 presents the hybrid anti-spoofing framework with cross-modal binding. Section 5 formalizes decision fusion. Section 6 reports experimental evaluation. Section 7 addresses privacy/compliance. Section 8 concludes. - ---- - -## 2. Related Work - -### 2.1 Passive Presentation Attack Detection - -**Texture-based methods.** Boulkenafet et al. [3] demonstrated LBP histogram variance as a discriminator between live and print/replay presentations on CASIA-FASD (EER 2.9%). Chingovska et al. [13] extended this with LPQ and BSIF descriptors on Replay-Attack, achieving HTER 6.1%. De Souza et al. [14] combined LBP with SVM classifiers, reporting ACER 4.2% on OULU-NPU Protocol 1. These methods are lightweight but brittle against high-quality OLED replay where texture degradation is minimal. - -**Frequency-domain methods.** Li et al. [15] exploited Moiré patterns in the power spectral density of screen-captured faces, demonstrating characteristic peaks at display sub-pixel pitch frequencies. Patel et al. [16] applied 2D FFT with radial spectral partitioning, achieving EER 1.5% on MSU-MFSD. However, OLED displays with irregular sub-pixel arrangements (PenTile) produce weaker Moiré signatures, limiting generalization. - -**Deep learning classifiers.** Yu et al. [4] proposed CDCN (Central Difference Convolutional Networks) achieving ACER 1.0% on OULU-NPU Protocol 1, introducing central difference operations for fine-grained texture capture. The same group later proposed MiniFASNet [5] with competitive accuracy at reduced computational cost (ACER 1.8%, 2.3M parameters). Wang et al. [17] (FAS-SGTD) introduced spatio-temporal gradient analysis. Liu et al. [18] proposed auxiliary depth supervision, and George et al. [19] (FaceBagNet) demonstrated patch-based deep ensemble methods achieving ACER 2.2% on Protocol 4 (cross-dataset). Recent survey by Yu et al. [20] (IEEE TPAMI, 2023) comprehensively covers the evolution from handcrafted to deep PAD. - -### 2.2 Active Liveness Detection - -Soukupova and Cech [6] formalized EAR-based blink detection with 68-landmark models. Challenge-response systems have been deployed commercially (Apple FaceID attention detection, iProov Genuine Presence Assurance) but remain vulnerable to video replay. Tang et al. [21] proposed randomized multi-challenge sequences to increase attack difficulty, reporting spoofed success rates below 3% on internal datasets. - -### 2.3 Hand Gesture-Based Liveness - -Hand gesture verification for liveness is less explored. Kowalski et al. [22] demonstrated finger counting challenges via CNN-based hand pose estimation, achieving 94% legitimate acceptance with 8% spoof acceptance on a small internal corpus. Chen et al. [23] proposed sign-language gesture challenges for accessibility-inclusive liveness, but did not address cross-modal binding between face and hand regions. Tirunagari et al. [24] used hand motion signatures for liveliness assessment but focused on single-hand wave patterns without cognitive challenge integration. - -### 2.4 Cross-Modal Consistency - -The concept of cross-modal binding for anti-spoofing is nascent. Li et al. [25] proposed audio-visual synchronization as a liveness signal (lip-sync consistency), achieving 96.3% accuracy against replay but only for video-with-audio attacks. Zhang et al. [26] demonstrated body-face geometric consistency for full-body presentation attack detection. To our knowledge, no prior work has formalized geometric binding between facial landmarks and hand landmarks as an explicit anti-spoofing mechanism for the confederate split-screen threat model. - -### 2.5 Positioning - -FIVUCSAS is distinguished from prior work by: (a) the explicit treatment of confederate split-screen attacks via cross-modal binding — a threat model not addressed by pure-passive or face-only-active systems; (b) the integration of cognitive challenges (arithmetic) as a non-physical liveness signal orthogonal to both texture analysis and motor gesture verification; and (c) the evaluation across multiple PAI species with per-channel ablation demonstrating marginal contribution of each component. - ---- - -## 3. System Architecture & Biometric Pipeline - -### 3.1 Hexagonal Architecture - -The FIVUCSAS Biometric Processor follows the Ports & Adapters pattern (Cockburn, 2005 [27]) to decouple domain logic from infrastructure concerns. The architecture is motivated by a specific problem in biometric pipelines: ML model backends evolve rapidly (new detection models, updated MediaPipe versions, alternative embedding networks), while business rules (enrollment policies, liveness thresholds, tenant isolation) should remain stable. The hexagonal pattern enforces this separation through explicit interface boundaries. - -**Domain Layer (Ports).** Defines 23 Protocol interfaces representing driving ports (inbound operations) and driven ports (outbound dependencies): - -- Driving ports: `IEnrollFace`, `IVerifyFace`, `ICheckLiveness`, `IStartActiveSession` -- Driven ports: `IFaceDetector`, `ILandmarkDetector`, `ILivenessDetector`, `IEmbeddingRepository`, `IGestureValidator` - -**Application Layer.** Use case orchestrators (`EnrollFaceUseCase`, `VerifyFaceUseCase`, `StartActiveLivenessUseCase`) compose driven port calls without knowledge of concrete implementations. - -**Infrastructure Layer (Adapters).** Concrete implementations bound at boot time via dependency injection container (`app/core/container.py`): - -``` -IFaceDetector → DeepFaceDetector(backend="mtcnn") -ILivenessDetector → EnhancedLivenessDetector | UniFaceLivenessDetector -IEmbeddingRepository → PgVectorEmbeddingRepository -IGestureValidator → MediaPipeGestureValidator -``` - -**Adapter swap demonstration.** Switching from `EnhancedLivenessDetector` (handcrafted texture+frequency) to `UniFaceLivenessDetector` (deep MiniFASNet) requires only environment variable change (`LIVENESS_BACKEND=uniface`). No application or domain layer code is modified — the factory resolves the interface to the new adapter. This was exercised in production when we migrated from texture-only to MiniFASNet-primary detection. - -### 3.2 Facial Detection & Alignment - -Face detection uses the `DeepFaceDetector` adapter wrapping DeepFace v0.0.98+ [28] with configurable backends: - -| Backend | Latency (P50) | Precision | Deployment | -|---------|---------------|-----------|------------| -| OpenCV DNN | 15ms | 92.1% | CPU default | -| MTCNN [8] | 45ms | 96.7% | Enrollment | -| RetinaFace | 80ms | 99.2% | GPU-only | -| MediaPipe [9] | 20ms | 94.3% | Real-time | - -A GPU guard (`ALLOW_HEAVY_ML=false`) blocks GPU-dependent backends at initialization when CUDA is unavailable, failing fast rather than silently degrading. - -Facial landmark extraction uses MediaPipe Face Mesh [29] with `refine_landmarks=True`, producing 468 canonical landmarks + 10 iris landmarks (478 total). These landmarks drive quality assessment (frontalness, occlusion) and liveness subsystems. - -### 3.3 Embedding Generation & Vector Storage - -Face embeddings are extracted via FaceNet512 [7], producing 512-dimensional L2-normalized vectors stored in PostgreSQL with the pgvector extension [30]. - -**Similarity search.** Verification computes cosine distance: - -$$d_{\cos}(\mathbf{e}_p, \mathbf{e}_r) = 1 - \frac{\mathbf{e}_p \cdot \mathbf{e}_r}{\|\mathbf{e}_p\| \|\mathbf{e}_r\|}$$ - -**Operating point selection.** The verification threshold $\tau_{\text{match}}$ is tuned per deployment use case: - -| Use Case | $\tau_{\text{match}}$ | Measured FAR | Measured FRR | -|----------|----------------------|--------------|--------------| -| Convenience unlock | 0.40 | 1.2×10⁻³ | 2.1% | -| Standard verification | 0.32 | 4.7×10⁻⁵ | 4.8% | -| KYC/AML (production default) | 0.28 | 8.3×10⁻⁶ | 7.2% | - -FAR/FRR measured on an internal test set of 12,400 genuine pairs and 1.2M impostor pairs. DET curves are reported in Section 6.4. - -**Index architecture.** HNSW indexes with `ef_construction=200`, `M=16` provide sub-linear search. For multi-tenant deployments, per-tenant HNSW indexes are created via PostgreSQL partitioning on `tenant_id`, ensuring graph traversal never touches cross-tenant vectors. Row Level Security (RLS) policies provide defense-in-depth: - -```sql -CREATE POLICY tenant_isolation ON face_embeddings - USING (tenant_id = current_setting('app.current_tenant')::uuid); -``` - -### 3.4 The Biometric Puzzle Mechanism - -The `ActiveLivenessSession` orchestrates five randomized challenges per session from four categories: - -| Category | Description | Random Parameters | -|----------|-------------|-------------------| -| GESTURE | Display finger count (0–10) | Target count | -| MATH | Arithmetic → finger answer | Operation, operands | -| FINGER_TOUCH | Bring fingertip pairs into contact | Touch command | -| SHAPE_TRACE | Trace geometric shape | Shape template | - -**Session flow.** (1) Server generates a random challenge sequence selecting 5 challenges with at least one from each of the 4 categories + 1 random repeat. (2) Client presents each challenge in sequence with per-challenge countdown. (3) Per-challenge quality score $q_i \in [0, 1]$ is recorded. (4) Final active score: - -$$S_{\text{active}} = \frac{1}{5} \sum_{i=1}^{5} q_i$$ - -Verification requires $S_{\text{active}} \geq 0.67$ AND at least one pass from every category present. - -**Role of EAR/MAR.** Facial EAR/MAR operates as a *continuous background detector* during the hand gesture session, not as a discrete challenge category. Its function is to verify that the face region remains animate (not a static photograph) while the subject performs hand challenges. This provides temporal binding: the face must be demonstrably live *during* hand gesture performance, not merely present in a separate video stream. See Section 4.2 for the cross-modal binding that formalizes this relationship. - -### 3.5 Hand Gesture Recognition Pipeline - -Built on MediaPipe HandLandmarker [11] (21 landmarks per hand, dual-hand tracking): - -**Preprocessing.** CLAHE (Contrast Limited Adaptive Histogram Equalization) on LAB lightness channel improves second-hand detection in shadowed regions. - -**Handedness.** Assigned by wrist X-coordinate screen position (deterministic), bypassing MediaPipe's inconsistent model-output handedness label. - -**Finger state detection.** Distance-difference ratio normalized by hand scale: - -$$r_f = \frac{d(\text{Wrist}, \text{Tip}_f) - d(\text{Wrist}, \text{PIP}_f)}{d(\text{Wrist}, \text{MCP}_{\text{middle}})}$$ - -Thumb: $r_{\text{thumb}} = d(\text{ThumbTip}, \text{PinkyMCP}) / s_{\text{hand}}$ - -Hysteresis thresholds prevent oscillation: $\tau_{\text{open}} = 0.20$, $\tau_{\text{close}} = 0.12$ (thumb: 0.75 / 0.60). Four-layer stabilization: (1) adaptive hand-scale normalization, (2) hysteresis dual-thresholding, (3) EWMA smoothing (α = 0.35), (4) 5-frame moving median filter. - -**Threshold derivation.** Hysteresis values were determined via grid search on a calibration set of 40 subjects (20 sessions each, 800 total), optimizing for F1 on finger-count correctness. Sensitivity analysis in Section 6.5. - -### 3.6 Arithmetic Challenge as Cognitive Liveness Proof - -The MATH challenge requires real-time human cognition — a property orthogonal to physical texture analysis and motor gesture verification. Problems are constrained to answers in [0, 10]: - -- Addition: $a + b = ?$, $a, b \geq 0$, $a + b \leq 10$ -- Subtraction: $a - b = ?$, $a \geq b$, $a \leq 10$ -- Multiplication: pre-computed pairs with $a \times b \leq 10$ -- Division: $a \div b = ?$, $b \geq 1$, $a \bmod b = 0$, $a/b \leq 10$ - -**Blind evaluation protocol.** No real-time correctness feedback is provided during the 10-second countdown. At $T = 0$, the system evaluates the mode finger count over the final 0.5-second stability window. - -**Security note.** The cognitive challenge is not intended as a standalone anti-spoofing mechanism. Its contribution is evaluated empirically in Section 6.3 (ablation). Its primary value is against automated replay rigs that lack real-time cognitive feedback loops. - -### 3.7 Shape Tracing with DTW Verification - -The subject traces a geometric path (circle, square, triangle, S-curve) with their index fingertip: - -**Template library.** Circle (48 control points, r=0.18), Square (5 waypoints), Triangle (4 waypoints, isosceles), S-Curve (18 control points). - -**Verification algorithm:** - -1. Resample both paths to $N = 50$ arc-length-equidistant points. -2. Centroid-normalize: $\hat{p}_i = (p_i - \bar{p}) / \max_j \|p_j - \bar{p}\|$ (translation + scale invariance). -3. DTW cost matrix [12]: - -$$D(i, j) = \|\hat{p}^{\text{trace}}_i - \hat{p}^{\text{template}}_j\|_2 + \min\{D(i{-}1, j),\ D(i, j{-}1),\ D(i{-}1, j{-}1)\}$$ - -4. Normalized cost: $C = D(N, N) / N$ -5. Accept if $C \leq \tau_{\text{DTW}} = 0.25$ - -**Threshold derivation.** $\tau_{\text{DTW}}$ was selected at the EER point on the calibration set (40 subjects, 4 shapes × 5 repetitions = 800 traces). Genuine trace distribution: $C \sim \mathcal{N}(0.12, 0.04)$; random/adversarial distribution: $C \sim \mathcal{N}(0.38, 0.09)$. - -### 3.8 Z-Axis Depth-Validated Finger Touch - -Leverages MediaPipe's per-landmark z-coordinate (monocular relative depth estimate — see Section 4.5 for limitations): - -**5-layer verification gate:** - -1. **Fist gate:** Rejects if all fingers curled -2. **Bystander finger:** ≥1 non-touching finger extended (ratio ≥ 0.05) -3. **Normalized distance:** $d_{\text{norm}} = \|p_a - p_b\| / D_{\text{bbox}} \leq 0.28$ -4. **Relative depth consistency:** $|z_a - z_b| \leq 0.04$ — prevents 2D overlap spoofing -5. **Temporal hold:** 10 consecutive frames satisfying all conditions - -Touch commands: THUMB_TO_INDEX (landmarks 4↔8), THUMB_TO_MIDDLE (4↔12), THUMB_TO_RING (4↔16), THUMB_TO_PINKY (4↔20), DOUBLE_THUMB_TOUCH (L4↔R4). - ---- - -## 4. Hybrid Anti-Spoofing Framework - -### 4.1 Passive Liveness Channels - -#### 4.1.1 Moiré Pattern Detection via DFT - -Grayscale face region → Hanning window → 2D DFT → log-scaled magnitude spectrum → radial frequency partitioning: - -$$R_{\text{HF}} = \frac{\sum_{r_{\text{low}} \leq \|(u,v)\| < r_{\text{high}}} S(u,v)^2}{\sum_{\|(u,v)\| < r_{\text{high}}} S(u,v)^2}$$ - -Augmented with Gabor filter bank (orientations: 0°, 45°, 90°, 135°). Screen-captured images exhibit elevated $R_{\text{HF}}$ from periodic sub-pixel grid interference. - -**Limitation.** OLED panels with PenTile/diamond sub-pixel layouts produce weaker Moiré signatures than LCD stripe patterns. This channel alone is insufficient for OLED replay detection. - -#### 4.1.2 LBP Texture Discrimination - -Uniform rotation-invariant LBP (scikit-image [31], P=8, R=1): - -$$\text{LBP}_{P,R}(x_c, y_c) = \sum_{p=0}^{P-1} s(g_p - g_c) \cdot 2^p$$ - -Discriminative feature: histogram variance $\sigma^2_{\text{LBP}}$. Live skin exhibits higher variance from pores, wrinkles, and sub-dermal vascular patterns. Computed at 50% resolution (128×128) for latency optimization: ~25ms at half-resolution vs. 50–100ms at full (256×256). - -#### 4.1.3 Color Naturalness - -HSV saturation profiling (live skin: unimodal in [0.15, 0.70]). YCrCb skin masking ($133 \leq Cr \leq 173$, $77 \leq Cb \leq 127$) → skin coverage ratio. - -#### 4.1.4 Laplacian Variance - -$$\sigma^2_{\text{Lap}} = \text{Var}(\nabla^2 I_{\text{face}})$$ - -Correlates with optical path characteristics. Flat presentation surfaces (screens, photos) produce lower variance due to uniform focal distance across the face region. - -#### 4.1.5 MiniFASNet Deep Classification - -UniFace/MiniFASNet [5] via ONNX Runtime: multi-scale feature extraction (3×3, 5×5, 7×7 branches), Central Difference Convolution layers, binary sigmoid output. Input: 80×80 RGB, ~15ms CPU inference. Trained on CASIA-FASD + Replay-Attack; reports ACER 1.8% on OULU-NPU Protocol 1 in isolation (our measured reproduction: 2.1%). - -#### 4.1.6 Monocular Relative Depth Estimate - -MediaPipe Face Mesh produces per-landmark z-coordinates from a single RGB image via a learned depth head. **This is not metric depth** — it is a relative, monocular estimate subject to scale ambiguity and adversarial fragility. We use it as a *soft signal* contributing to the fusion, not as a hard discriminator: - -- Depth variance across 468 landmarks -- Nose protrusion ratio: $\rho_{\text{nose}} = (z_{\text{nose}} - \bar{z}_{\text{ears}}) / \|p_{\text{L,ear}} - p_{\text{R,ear}}\|$ - -**Known limitations:** (1) Curved displays (foldable phones) may produce non-zero depth variance. (2) Photos with strong perspective can fool monocular estimators. (3) Adversarial perturbations against the depth head are unexplored. The fusion weight (0.10) reflects this uncertainty. - -### 4.2 Cross-Modal Spatial Binding (Novel) - -The core vulnerability of dual-modality systems without binding: an attacker positions a tablet showing the target's face video, then uses their own live hands below the tablet. Both face and hand pipelines pass independently. - -**Defense: Geometric consistency enforcement.** - -The cross-modal binding mechanism requires that face and hand landmarks occupy physically plausible relative positions within a single coherent scene: - -**Constraint 1: Vertical ordering.** Hand wrist landmarks must appear below the face chin landmark in screen coordinates: - -$$y_{\text{wrist}} > y_{\text{chin}} + \delta_{\text{min}}$$ - -where $\delta_{\text{min}}$ is calibrated to the face bounding box height (default: 0.1 × face height). - -**Constraint 2: Scale consistency.** The ratio of face bounding box size to hand bounding box size must remain within physiologically plausible bounds: - -$$\frac{s_{\text{face}}}{s_{\text{hand}}} \in [1.2, 4.5]$$ - -Extreme ratios indicate separate focal planes (face on a screen at different distance than hands). - -**Constraint 3: Parallax binding via nose-touch challenge.** One of the five challenges is designated as a *binding challenge* (randomly selected from: "touch your nose with your right index finger", "cover your left eye with your left hand", "place your palm next to your left ear"). This challenge requires the hand to physically occlude or adjacently contact a facial landmark, verified by: - -$$\|p_{\text{fingertip}} - p_{\text{facial\_target}}\|_2 / D_{\text{face}} \leq \tau_{\text{bind}} = 0.15$$ - -AND co-occurrence of face landmark displacement (the face mesh landmarks shift when a hand physically contacts the face — absent when face and hands are in separate planes). - -**Constraint 4: Illumination consistency.** Mean pixel intensity in the face bounding box and hand bounding box are compared. Significant deviation (> 2σ from calibrated ratio) suggests separate light sources: - -$$\left|\frac{\bar{I}_{\text{face}}}{\bar{I}_{\text{hand}}} - R_{\text{calib}}\right| > 2\sigma_R$$ - -**Empirical validation.** Cross-modal binding reduces SAR against confederate split-screen attacks from 73.2% (without binding) to 4.6% (with binding), measured on our internal CSS corpus (Section 6.3). - -### 4.3 Hand-Side Anti-Spoofing - -**Static-hand variance detector.** Monitors wrist position standard deviation over a 30-frame sliding window: - -$$\sigma_{\text{wrist}} = \text{Std}\left(\{p_{\text{wrist}}^{(t)}\}_{t=k-29}^{k}\right)$$ - -Flagged when $\sigma_{\text{wrist}} < 3 \times 10^{-4}$ persists for > 90 frames (3 seconds at 30fps), indicating an unnaturally static hand characteristic of a photograph. - -**Clarification on framing:** This is a positional variance detector, not a spectral tremor analyzer. True physiological tremor detection (8–12 Hz) would require ≥24 Hz sampling (Nyquist) with spectral analysis (FFT on the position time-series). Our 30fps camera is at the Nyquist limit for 12 Hz with no margin for jitter. The detector instead measures *absence of natural positional variance* — a simpler but empirically effective discriminator (see Section 6.3 ablation). - -**Brightness variance monitor.** Pixel intensity variance in the hand bounding box over 60 frames; flags if variance < 0.05. - -### 4.4 Active Facial Liveness (Background) - -EAR and MAR operate as continuous background validators during the hand gesture session: - -$$\text{EAR} = \frac{\|p_2 - p_6\| + \|p_3 - p_5\|}{2 \|p_1 - p_4\|}$$ - -Blink detection: $\text{EAR} < \tau_{\text{blink}} = 0.21$. At least one natural blink must be detected during the session window (independent of challenge prompts). This verifies the face is animate, not a static photograph, during hand challenge performance. - -$$\text{MAR} = \frac{\|p_{\text{upper}} - p_{\text{lower}}\|}{\|p_{\text{left}} - p_{\text{right}}\|}$$ - -**Role clarification.** EAR/MAR do not defend against video replay of the face (as established in Section 1.2). Their role is exclusively to provide temporal liveness evidence for the *binding* mechanism: the face must exhibit spontaneous biological motion (blinks) during the same temporal window as hand challenge performance. - -### 4.5 Limitations of the Active Subsystem - -The hand gesture subsystem provides strong defense against passive screen replay but has bounded efficacy against: - -- **Confederate attack with binding bypass:** If an attacker can position a second person's face close enough to their hands to satisfy Constraint 3 (nose-touch), binding is defeated. This requires physical proximity and coordination between two parties — significantly harder than screen replay but not impossible. -- **Real-time relay:** If challenges are relayed to the target subject (who performs them in real-time at a remote location while their video is streamed), all active challenges are defeated. Defense against relay attacks requires environmental binding (e.g., geo-verification, ambient audio matching) — outside our scope. -- **Motor-impaired users:** See Section 7.2 for accessibility considerations. - ---- - -## 5. Decision Logic & Fusion - -### 5.1 Quality Function Definition - -The quality score $q \in [0, 1]$ is a composite of four image quality metrics: - -$$q = 0.35 \cdot q_{\text{blur}} + 0.25 \cdot q_{\text{exposure}} + 0.20 \cdot q_{\text{size}} + 0.20 \cdot q_{\text{frontal}}$$ - -where: - -- $q_{\text{blur}} = \min(1, \sigma^2_{\text{Lap}} / \sigma^2_{\text{ref}})$ — Laplacian variance normalized against a reference threshold ($\sigma^2_{\text{ref}} = 100$) -- $q_{\text{exposure}} = 1 - |I_{\text{mean}} - 130| / 130$ — deviation from ideal mean intensity (130) -- $q_{\text{size}} = \min(1, A_{\text{face}} / A_{\text{min}})$ — face area relative to minimum acceptable area ($A_{\text{min}} = 64 \times 64$ pixels) -- $q_{\text{frontal}} = 1 - \max(|\theta_{\text{yaw}}|, |\theta_{\text{pitch}}|) / 45°$ — head pose deviation from frontal - -Weights were determined via logistic regression on a development set (N=2000 images) predicting binary "quality adequate for liveness analysis." - -### 5.2 Score Normalization - -All passive channel outputs are normalized to [0, 100]: - -| Channel | Raw Signal | Normalization | -|---------|-----------|---------------| -| FFT Moiré | $R_{\text{HF}} \in [0, 1]$ | $(1 - R_{\text{HF}}) \times 100$ | -| LBP texture | $\sigma^2_{\text{LBP}}$ | Sigmoid: $100 / (1 + e^{-k(\sigma^2 - \mu)})$ | -| Color | HSV/YCrCb composite | Linear scaling | -| Laplacian | $\sigma^2_{\text{Lap}}$ | $\min(100, \sigma^2_{\text{Lap}} / \sigma^2_{\text{sat}} \times 100)$ | -| MiniFASNet | $s_{\text{deep}} \in [0, 1]$ | $s_{\text{deep}} \times 100$ | -| Pseudo-depth | $\rho_{\text{nose}}$ | Linear mapping | - -### 5.3 Weighted Fusion - -$$L = \alpha(q) \cdot L_{\text{passive}} + (1 - \alpha(q)) \cdot L_{\text{active}}$$ - -$$\alpha(q) = \min(0.75, \ 0.35 + 0.40 \cdot q)$$ - -Passive sub-score: - -$$L_{\text{passive}} = \sum_{c \in \mathcal{C}} w_c \cdot S_c$$ - -| $w_{\text{FFT}}$ | $w_{\text{LBP}}$ | $w_{\text{color}}$ | $w_{\text{blur}}$ | $w_{\text{deep}}$ | $w_{\text{depth}}$ | -|:-:|:-:|:-:|:-:|:-:|:-:| -| 0.20 | 0.15 | 0.10 | 0.10 | 0.35 | 0.10 | - -**Weight derivation.** Weights were obtained via grid search over a 6-dimensional weight simplex, optimizing ACER on a held-out development set (1200 live + 1200 spoof from OULU-NPU Protocol 1 development split). $w_{\text{depth}}$ reduced from 0.15 to 0.10 to reflect the monocular depth estimate's limitations (Section 4.1.6). - -### 5.4 Liveness Verdict - -$$\text{is\_live} = (L \geq \tau_{\text{live}}) \wedge (\neg \text{veto}_{\text{static}}) \wedge (\text{binding\_pass}) \wedge (\text{blink\_detected})$$ - -where $\tau_{\text{live}} = 70.0$. Hard vetoes: -- $\text{veto}_{\text{static}}$: wrist variance < $3 \times 10^{-4}$ for > 90 frames -- $\text{binding\_pass}$: cross-modal spatial constraints satisfied (Section 4.2) -- $\text{blink\_detected}$: at least one EAR < 0.21 event during session - -**Session nonce binding (partial frame injection defense).** Each session is assigned a server-generated cryptographic nonce. The client must render this nonce as a translucent overlay on the camera frame within 500ms of issuance. The server verifies nonce presence via template matching. This provides weak defense against casual frame injection (virtual camera) but is bypassable by a sophisticated attacker who patches the nonce into injected frames. Full mitigation requires hardware attestation (Play Integrity API, App Attest) — documented as a deployment recommendation, not a claimed defense. - -### 5.5 Threshold Sensitivity - -| $\tau_{\text{live}}$ | APCER (%) | BPCER (%) | ACER (%) | -|---------------------|-----------|-----------|----------| -| 60.0 | 8.4 | 1.2 | 4.8 | -| 65.0 | 5.1 | 2.3 | 3.7 | -| **70.0** | **3.2** | **2.4** | **2.8** | -| 75.0 | 1.9 | 4.8 | 3.4 | -| 80.0 | 0.8 | 9.1 | 5.0 | - -Operating point 70.0 selected to minimize ACER at the crossing of APCER/BPCER curves (approximate EER). - ---- - -## 6. Experimental Evaluation - -### 6.1 Datasets and Protocols - -| Dataset | Subjects | Videos | PAI Species | Protocol | -|---------|----------|--------|-------------|----------| -| OULU-NPU [32] | 55 | 5,940 | Print, Replay (2 printers, 2 displays) | P1–P4 | -| Replay-Attack [33] | 50 | 1,200 | Print, Mobile replay, Highdef replay | Standard | -| Internal-SR | 30 | 720 | Screen replay (5 display types: LCD, IPS, OLED, PenTile, Foldable) | Hold-out | -| Internal-CSS | 30 | 360 | Confederate split-screen (tablet+live hands) | Hold-out | -| Internal-GH | 40 | 800 | Gesture calibration (live subjects, 20 sessions each) | 5-fold CV | - -OULU-NPU protocols: P1 (intra-dataset, leave-one-out on sessions), P2 (unseen attack medium), P3 (unseen input sensor), P4 (cross-attack, cross-sensor — hardest). - -### 6.2 Metrics - -- **APCER:** Attack Presentation Classification Error Rate (spoof falsely accepted) -- **BPCER:** Bona Fide Presentation Classification Error Rate (live falsely rejected) -- **ACER:** Average Classification Error Rate = (APCER + BPCER) / 2 -- **BPCER₁₀:** BPCER at the operating point where APCER = 10% -- **SAR:** Spoof Acceptance Rate per PAI species - -### 6.3 Results - -**OULU-NPU Protocol 1 (main result):** - -| Method | APCER (%) | BPCER (%) | ACER (%) | -|--------|-----------|-----------|----------| -| MiniFASNet alone [5] | 2.1 | 1.6 | 1.8 | -| LBP-only (our impl.) | 7.3 | 3.8 | 5.6 | -| CDCN [4] (reported) | 0.4 | 1.7 | 1.0 | -| FIVUCSAS passive-only | 2.9 | 2.0 | 2.5 | -| **FIVUCSAS full pipeline** | **3.2** | **2.4** | **2.8** | - -*Note: FIVUCSAS full pipeline ACER is slightly higher than passive-only on OULU-NPU because the active challenge subsystem is not exercised in the standard OULU-NPU evaluation protocol (pre-recorded videos cannot respond to challenges). The full pipeline's advantage manifests in live attack scenarios (Internal-SR, Internal-CSS).* - -**Cross-dataset generalization (Protocol 4):** - -| Method | APCER (%) | BPCER (%) | ACER (%) | -|--------|-----------|-----------|----------| -| FaceBagNet [19] | 4.1 | 3.3 | 3.7 | -| CDCN [4] | 2.8 | 3.5 | 3.2 | -| FIVUCSAS passive-only | 5.2 | 3.1 | 4.2 | - -**Internal Screen Replay corpus (live attack scenario):** - -| PAI Species | SAR (passive only) | SAR (full pipeline) | Δ | -|-------------|--------------------|--------------------|---| -| LCD replay | 4.2% | 0.8% | −3.4 | -| IPS replay | 5.8% | 1.2% | −4.6 | -| OLED replay | 12.3% | 2.1% | −10.2 | -| PenTile OLED | 15.7% | 3.4% | −12.3 | -| Foldable OLED | 18.2% | 4.8% | −13.4 | - -The full pipeline provides greatest marginal benefit against OLED attacks where passive Moiré detection is weakest. - -**Confederate Split-Screen (CSS) attacks:** - -| Configuration | SAR (no binding) | SAR (with binding) | -|---------------|------------------|--------------------| -| Tablet face + live hands (same person) | 73.2% | 4.6% | -| Tablet face + confederate hands | 68.9% | 3.8% | -| Phone face (small display) + live hands | 41.3% | 2.1% | - -Cross-modal binding is the critical defense against CSS attacks. - -### 6.4 Ablation Study - -Removal of individual channels from the full pipeline (evaluated on Internal-SR): - -| Removed Channel | ACER (%) | Δ from full | -|-----------------|----------|-------------| -| Full pipeline | 2.8 | — | -| − MiniFASNet | 6.4 | +3.6 | -| − FFT Moiré | 4.1 | +1.3 | -| − LBP texture | 3.5 | +0.7 | -| − Pseudo-depth | 3.0 | +0.2 | -| − Color naturalness | 2.9 | +0.1 | -| − Cross-modal binding | 5.1* | +2.3* | -| − MATH challenge | 3.9† | +1.1† | -| − All hand challenges | 4.6† | +1.8† | -| − Static-hand detector | 3.1 | +0.3 | - -*Measured on CSS corpus only. -†Measured on live attack scenarios where challenges can be evaluated. - -**Key findings:** MiniFASNet contributes the largest single-channel marginal improvement (+3.6 ACER points), confirming it should receive the highest fusion weight. Cross-modal binding is essential for CSS defense. The MATH challenge provides 1.1 points independent of passive channels — validating the cognitive liveness contribution. - -### 6.5 Threshold Sensitivity Analysis - -**Finger state hysteresis thresholds (τ_open):** - -| τ_open | Finger count F1 | False transition rate | -|--------|-----------------|---------------------| -| 0.15 | 91.2% | 8.3% | -| 0.18 | 94.1% | 4.7% | -| **0.20** | **96.3%** | **2.1%** | -| 0.22 | 95.8% | 1.4% | -| 0.25 | 93.4% | 0.8% | - -**DTW acceptance threshold (τ_DTW):** - -| τ_DTW | Genuine Accept Rate | Impostor Accept Rate | -|-------|--------------------|--------------------| -| 0.20 | 78.4% | 1.2% | -| **0.25** | **91.7%** | **3.8%** | -| 0.30 | 96.2% | 8.9% | -| 0.35 | 98.1% | 14.3% | - -### 6.6 Latency Profile - -| Component | Latency (P50) | Frequency | Notes | -|-----------|---------------|-----------|-------| -| Face detection (OpenCV) | 15ms | Every frame | | -| Face Mesh landmarks | 12ms | Every frame | | -| Hand landmarker (dual) | 18ms | Every frame | | -| LBP (128×128) | 25ms | Every 3rd frame | Half-resolution | -| MiniFASNet (ONNX) | 15ms | Every 3rd frame | | -| FFT + Gabor | 12ms | Every 3rd frame | | -| DTW verification | 2ms | Once per trace | | -| Cross-modal binding | <1ms | Every frame | Landmark arithmetic | - -**Concurrency model.** Face/hand detection run sequentially per frame (~45ms). Passive channels (LBP, MiniFASNet, FFT) run every 3rd frame in a thread pool (`asyncio.run_in_executor` with `ThreadPoolExecutor(max_workers=4)`). The GIL releases during NumPy/OpenCV C-extension calls, enabling true parallelism for these CPU-bound operations. - -**Effective per-frame latency:** 45ms (detection) + 15ms (amortized passive: 45ms / 3 frames) = ~60ms per frame (P50). P99: 95ms. Measured on Hetzner CX43 (8 vCPU AMD EPYC, 16 GB RAM). - ---- - -## 7. Privacy, Compliance & Accessibility - -### 7.1 KVKK/GDPR Compliance - -Biometric data constitutes "özel nitelikli kişisel veri" under KVKK Art. 6 and "special category data" under GDPR Art. 9, requiring enhanced protections: - -**Data minimization.** Only the 512-dimensional embedding vector is stored — never raw images. Embeddings are L2-normalized, making direct image reconstruction more difficult (though not impossible — Mai et al. [34] demonstrated partial face reconstruction from embeddings via optimization-based attacks). - -**Encryption at rest.** PostgreSQL TDE (Transparent Data Encryption) with AES-256. Embedding columns additionally encrypted at the application layer via envelope encryption (data key encrypted by a KMS master key, rotated quarterly). - -**Right to erasure.** Per-user deletion removes the embedding row. pgvector HNSW indexes are *not* rebuilt on individual deletion (graph neighbors route around deleted nodes). Full index rebuild is scheduled quarterly or triggered when deletion count exceeds 5% of indexed population. - -**Retention policy.** Embeddings retained for active enrollment duration + 30 days post-deletion (soft-delete with scheduled purge). Session frames are never persisted — processed in memory and discarded. - -**Template protection (future work).** Fuzzy extractors or cancelable biometrics [35] would provide stronger protection than raw embedding storage. This is a known gap; current defense relies on encryption. - -### 7.2 Accessibility - -The multi-challenge system creates accessibility barriers for users with: - -- **Motor impairments:** Hand gesture, finger touch, and shape tracing challenges require fine motor control. -- **Cognitive impairments:** MATH challenges assume arithmetic ability. -- **Amputees / prosthetic users:** Hand detection may fail or produce abnormal landmark patterns. -- **Tremor disorders:** Paradoxically, physiological tremor disorders produce *higher* σ_wrist (passing the static-hand check) but may fail gesture accuracy thresholds. - -**Fallback architecture.** The system supports per-tenant configuration of challenge difficulty and modality: - -| Level | Available Challenges | Use Case | -|-------|---------------------|----------| -| Standard | All 4 categories | Default | -| Reduced motor | GESTURE + MATH only | Motor impairment | -| Passive-only | No active challenges | Severe disability | -| Human escalation | Flag for manual review | Unable to complete any challenge | - -Tenants operating under EU European Accessibility Act (EAA) or Turkish disability regulations can configure fallback levels. Passive-only mode accepts higher SAR in exchange for accessibility. - ---- - -## 8. Conclusion - -### 8.1 Summary - -We presented FIVUCSAS, a multi-modal PAD framework combining passive texture/frequency analysis with dual-modality active challenges (face + hands) and cross-modal spatial binding. The primary contributions are: - -1. Cross-modal binding reduces SAR against confederate split-screen attacks from 73.2% to 4.6%. -2. The cognitive MATH challenge provides 1.1 ACER points of improvement independent of passive channels. -3. The full pipeline achieves ACER 2.8% on OULU-NPU Protocol 1 and demonstrates significant advantage on OLED replay (SAR reduction of 10–13 points vs. passive-only). - -The system hardens against passive screen replay and confederate attacks within the defined threat model. It does not claim to defeat 3D masks, real-time deepfakes with feedback loops, or client-side frame injection on compromised devices. - -### 8.2 Limitations - -- Cross-modal binding can be defeated by a confederate physically positioned behind the target (faces close together, shared hand space). -- Monocular pseudo-depth provides weak signal against curved displays. -- The evaluation corpus for CSS attacks is small (N=360); larger-scale validation is needed. -- Passive channels underperform CDCN [4] on OULU-NPU Protocol 4 (cross-dataset) — suggesting overfitting to training distribution. -- Accessibility fallbacks weaken security posture; the trade-off is configurable but not eliminable. - -### 8.3 Future Work - -1. **Hardware attestation integration:** Play Integrity API (Android) and App Attest (iOS) for client-side frame injection defense. -2. **Temporal consistency analysis:** Frame-to-frame optical flow anomaly detection for real-time deepfake detection. -3. **Adversarial training:** Augmenting MiniFASNet training with gradient-based adversarial examples. -4. **Cancelable biometrics:** Replacing raw embeddings with revocable template protection. -5. **Spectral tremor analysis:** Upgrading to 60fps capture with FFT-based 8–12 Hz band energy extraction for true physiological tremor verification. -6. **Expanded evaluation:** CelebA-Spoof, SiW-Mv2, and WMCA datasets for cross-domain generalization. - ---- - -## References - -[1] ISO/IEC 30107-3:2017. Information technology — Biometric presentation attack detection — Part 3: Testing and reporting. - -[2] ISO/IEC 19795-1:2021. Information technology — Biometric performance testing and reporting — Part 1: Principles and framework. - -[3] Z. Boulkenafet, J. Komulainen, and A. Hadid, "Face anti-spoofing based on color texture analysis," in *Proc. ICIP*, IEEE, 2015. - -[4] Z. Yu, C. Zhao, Z. Wang, et al., "Searching Central Difference Convolutional Networks for Face Anti-Spoofing," in *Proc. CVPR*, 2020. - -[5] Z. Yu, X. Li, X. Niu, J. Shi, and G. Zhao, "Face Anti-Spoofing with Human Material Perception," in *Proc. ECCV*, 2020. - -[6] T. Soukupova and J. Cech, "Real-time eye blink detection using facial landmarks," in *Proc. CVWW*, 2016. - -[7] F. Schroff, D. Kalenichenko, and J. Philbin, "FaceNet: A unified embedding for face recognition and clustering," in *Proc. CVPR*, 2015. - -[8] K. Zhang, Z. Zhang, Z. Li, and Y. Qiao, "Joint face detection and alignment using multitask cascaded convolutional networks," *IEEE Signal Processing Letters*, vol. 23, no. 10, 2016. - -[9] V. Bazarevsky, Y. Kartynnik, A. Vakunov, et al., "BlazeFace: Sub-millisecond Neural Face Detection on Mobile GPUs," in *Proc. CVPR Workshop*, 2019. - -[10] J. Johnson, M. Douze, and H. Jégou, "Billion-scale similarity search with GPUs," *IEEE Trans. Big Data*, 2019. - -[11] C. Zhang, S. Bazarevsky, A. Vakunov, et al., "MediaPipe Hands: On-device Real-time Hand Tracking," in *Proc. CVPR Workshop*, 2020. - -[12] H. Sakoe and S. Chiba, "Dynamic programming algorithm optimization for spoken word recognition," *IEEE TASSP*, vol. 26, no. 1, 1978. - -[13] I. Chingovska, A. Anjos, and S. Marcel, "On the effectiveness of local binary patterns in face anti-spoofing," in *Proc. BIOSIG*, 2012. - -[14] G. B. de Souza, D. F. da Silva Santos, R. G. Pires, et al., "Deep texture features for robust face spoofing detection," *IEEE Trans. CSVT*, vol. 27, no. 5, 2017. - -[15] J. Li, Y. Wang, T. Tan, and A. K. Jain, "Live face detection based on the analysis of Fourier spectra," in *Proc. SPIE Biometric Technology*, 2004. - -[16] K. Patel, H. Han, and A. K. Jain, "Secure face unlock: Spoof detection on smartphones," *IEEE TIFS*, vol. 11, no. 10, 2016. - -[17] Z. Wang, Z. Yu, C. Zhao, et al., "Deep spatial gradient and temporal depth learning for face anti-spoofing," in *Proc. CVPR*, 2020. - -[18] Y. Liu, A. Jourabloo, and X. Liu, "Learning deep models for face anti-spoofing: Binary or auxiliary supervision," in *Proc. CVPR*, 2018. - -[19] A. George and S. Marcel, "Deep pixel-wise binary supervision for face presentation attack detection," in *Proc. ICB*, 2019. - -[20] Z. Yu, Y. Qin, X. Li, et al., "Deep Learning for Face Anti-Spoofing: A Survey," *IEEE TPAMI*, vol. 45, no. 5, 2023. - -[21] S. Tang, X. Sun, and N. K. Ratha, "Challenge-response face anti-spoofing with randomized multi-modal prompts," in *Proc. IJCB*, 2021. - -[22] M. Kowalski, M. Naruniec, and T. Trzcinski, "Alive! — Real-time liveness detection using hand gestures," in *Proc. WACV*, 2019. - -[23] L. Chen, S. Patel, and H. Haas, "Sign language gestures for inclusive liveness verification," in *Proc. ACM ASSETS*, 2022. - -[24] S. Tirunagari, N. Poh, D. Windridge, et al., "Detection of face spoofing using visual dynamics," *IEEE TIFS*, vol. 10, no. 4, 2015. - -[25] Y. Li, X. Yang, P. Sun, et al., "Celeb-DF: A large-scale challenging dataset for deepfake forensics," in *Proc. CVPR*, 2020. - -[26] J. Zhang, F. Huang, and Z. Lei, "Full-body presentation attack detection through body-face geometric consistency," in *Proc. FG*, 2023. - -[27] A. Cockburn, "Hexagonal architecture," alistair.cockburn.us, 2005. - -[28] S. I. Serengil and A. Ozpinar, "LightFace: A hybrid deep face recognition framework," in *Proc. ASYU*, 2020. - -[29] Y. Kartynnik, A. Ablavatski, I. Grishchenko, and M. Grundmann, "Real-time facial surface geometry from monocular video on mobile GPUs," in *Proc. CVPR Workshop*, 2019. - -[30] A. Katz and J. Walldén, "pgvector: Open-source vector similarity search for Postgres," GitHub, 2023. - -[31] S. van der Walt, J. L. Schönberger, J. Nunez-Iglesias, et al., "scikit-image: Image processing in Python," *PeerJ*, vol. 2, 2014. - -[32] Z. Boulkenafet, J. Komulainen, L. Li, et al., "OULU-NPU: A mobile face presentation attack database with real-world variations," in *Proc. FG*, 2017. - -[33] I. Chingovska, A. Anjos, and S. Marcel, "On the effectiveness of local binary patterns in face anti-spoofing," in *Proc. BIOSIG*, 2012. - -[34] G. Mai, K. Cao, P. C. Yuen, and A. K. Jain, "On the reconstruction of face images from deep face templates," *IEEE TPAMI*, vol. 41, no. 5, 2019. - -[35] A. Nandakumar, A. K. Jain, and S. Pankanti, "Fingerprint-based fuzzy vault: Implementation and performance," *IEEE TIFS*, vol. 2, no. 4, 2007. - -[36] E. Evans, "Domain-Driven Design: Tackling Complexity in the Heart of Software," Addison-Wesley, 2003. - -[37] KVKK (Kişisel Verilerin Korunması Kanunu), Law No. 6698, Official Gazette, 2016. - -[38] Regulation (EU) 2016/679 (GDPR), Article 9: Processing of special categories of personal data. diff --git a/docs/PERFORMANCE_OPTIMIZATION_GUIDE.md b/docs/PERFORMANCE_OPTIMIZATION_GUIDE.md deleted file mode 100644 index 415ad2c..0000000 --- a/docs/PERFORMANCE_OPTIMIZATION_GUIDE.md +++ /dev/null @@ -1,372 +0,0 @@ -# Performance Optimization Guide - -**Date:** December 14, 2025 -**Version:** 1.0.0 -**Status:** Investigation Complete - ---- - -## Executive Summary - -After deep analysis of the codebase, I've identified **critical performance bottlenecks** and opportunities for significant performance improvements. The most impactful issues are: - -| Priority | Issue | Impact | Effort | -|----------|-------|--------|--------| -| **CRITICAL** | CPU-bound ML ops blocking async loop | 10x latency | Medium | -| **CRITICAL** | Single uvicorn worker in Docker | 4x throughput loss | Low | -| **HIGH** | No Redis caching for embeddings | 50ms+ per lookup | Medium | -| **HIGH** | Merge conflicts in core files | System broken | Low | -| **MEDIUM** | No response compression | 30% bandwidth waste | Low | -| **MEDIUM** | Model loading not optimized | Cold start issues | Medium | - ---- - -## Critical Issues - -### 1. CPU-Bound ML Operations Blocking Async Event Loop - -**Location:** `app/infrastructure/ml/extractors/deepface_extractor.py:70` - -**Problem:** -```python -# CURRENT - BLOCKING -async def extract(self, face_image: np.ndarray) -> np.ndarray: - # This call BLOCKS the async event loop! - embedding_objs = DeepFace.represent( - img_path=face_image, - model_name=self._model_name, - ... - ) -``` - -The `DeepFace.represent()` call is synchronous and CPU-intensive (50-200ms). When called inside an `async` function without offloading, it **blocks the entire event loop**, preventing other requests from being processed. - -**Impact:** -- Under load, P99 latency increases dramatically -- Concurrent request handling is effectively serialized -- WebSocket connections may timeout - -**Solution:** -```python -import asyncio -from concurrent.futures import ThreadPoolExecutor - -# Create a thread pool for ML operations -_ml_executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="ml_") - -async def extract(self, face_image: np.ndarray) -> np.ndarray: - loop = asyncio.get_event_loop() - - # Offload CPU-bound work to thread pool - embedding_objs = await loop.run_in_executor( - _ml_executor, - lambda: DeepFace.represent( - img_path=face_image, - model_name=self._model_name, - detector_backend=self._detector_backend, - enforce_detection=self._enforce_detection, - align=True, - normalization="base", - ) - ) - # ... rest of processing -``` - -**Files to Update:** -- `app/infrastructure/ml/extractors/deepface_extractor.py` -- `app/infrastructure/ml/detectors/deepface_detector.py` -- `app/infrastructure/ml/quality/quality_assessor.py` -- `app/infrastructure/ml/liveness/*.py` -- `app/infrastructure/ml/demographics/deepface_demographics.py` -- `app/infrastructure/ml/landmarks/*.py` -- `app/infrastructure/ml/proctoring/*.py` - -**Expected Improvement:** 5-10x better concurrent request handling - ---- - -### 2. Single Uvicorn Worker in Production - -**Location:** `Dockerfile:60` - -**Problem:** -```dockerfile -# CURRENT - Single worker -CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"] -``` - -Running with a single worker limits throughput to what one CPU core can handle. - -**Solution:** -```dockerfile -# OPTION 1: Multiple uvicorn workers -CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001", "--workers", "4"] - -# OPTION 2: Gunicorn with uvicorn workers (recommended for production) -CMD ["gunicorn", "app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8001"] -``` - -**For Kubernetes:** Use single worker per pod and scale horizontally with HPA. - -**Expected Improvement:** 2-4x throughput on multi-core systems - ---- - -### 3. Unresolved Merge Conflicts - -**Location:** -- `app/core/container.py` (lines 53-69, 196-302, 632-644) -- `requirements.txt` (lines 12-34) - FIXED - -**Problem:** Git merge conflicts prevent the application from starting. - -**Solution:** Resolve conflicts by combining both upstream changes: - -```python -# container.py needs to include BOTH: -# 1. Factory imports (demographics, landmarks, etc.) -# 2. Event bus components (RedisEventBus, EventPublisher, etc.) -``` - ---- - -## High Priority Optimizations - -### 4. Add Redis Caching for Embeddings - -**Problem:** Every verification/search requires database lookup even for recently accessed embeddings. - -**Solution:** Add a caching layer: - -```python -# app/infrastructure/cache/embedding_cache.py -import redis -import json -import numpy as np -from typing import Optional - -class EmbeddingCache: - def __init__(self, redis_client: redis.Redis, ttl: int = 3600): - self._redis = redis_client - self._ttl = ttl - - async def get(self, user_id: str, tenant_id: str = "default") -> Optional[np.ndarray]: - key = f"embedding:{tenant_id}:{user_id}" - data = await self._redis.get(key) - if data: - return np.frombuffer(data, dtype=np.float32) - return None - - async def set(self, user_id: str, embedding: np.ndarray, tenant_id: str = "default"): - key = f"embedding:{tenant_id}:{user_id}" - await self._redis.setex(key, self._ttl, embedding.tobytes()) - - async def invalidate(self, user_id: str, tenant_id: str = "default"): - key = f"embedding:{tenant_id}:{user_id}" - await self._redis.delete(key) -``` - -**Expected Improvement:** 10-50ms saved per cached lookup - ---- - -### 5. Add Response Compression - -**Problem:** API responses are uncompressed, wasting bandwidth. - -**Solution:** Add GZip middleware: - -```python -# app/main.py -from fastapi.middleware.gzip import GZipMiddleware - -app.add_middleware(GZipMiddleware, minimum_size=1000) -``` - -**Expected Improvement:** 30-70% bandwidth reduction for JSON responses - ---- - -## Medium Priority Optimizations - -### 6. Optimize Model Loading - -**Current State:** Models are loaded via `lru_cache` singletons at first use. - -**Improvements:** - -```python -# 1. Add model preloading in lifespan -@asynccontextmanager -async def lifespan(app: FastAPI): - # Preload ALL models in parallel using ThreadPoolExecutor - with ThreadPoolExecutor(max_workers=4) as executor: - futures = [ - executor.submit(get_face_detector), - executor.submit(get_embedding_extractor), - executor.submit(get_quality_assessor), - executor.submit(get_liveness_detector), - ] - for future in futures: - future.result() # Wait for completion - - yield - -# 2. Add health check for model readiness -@app.get("/api/v1/health/ready") -async def readiness(): - models_ready = all([ - get_face_detector.cache_info().hits > 0 or get_face_detector.cache_info().currsize > 0, - get_embedding_extractor.cache_info().currsize > 0, - ]) - if not models_ready: - raise HTTPException(503, "Models not ready") - return {"status": "ready"} -``` - ---- - -### 7. Connection Pool Tuning - -**Current Settings:** -```python -DATABASE_POOL_MIN_SIZE = 10 -DATABASE_POOL_MAX_SIZE = 20 -``` - -**Recommendations:** -```python -# For high-throughput scenarios -DATABASE_POOL_MIN_SIZE = 20 -DATABASE_POOL_MAX_SIZE = 50 - -# Add connection health checks -pool_recycle = 1800 # Recycle connections every 30 minutes -pool_pre_ping = True # Verify connection before use -``` - ---- - -### 8. Batch Processing with Multiprocessing - -**Current:** Batch operations use asyncio but are still single-process. - -**Solution for CPU-intensive batches:** -```python -from concurrent.futures import ProcessPoolExecutor -import multiprocessing - -# Use process pool for batch ML operations -_batch_executor = ProcessPoolExecutor( - max_workers=multiprocessing.cpu_count(), - mp_context=multiprocessing.get_context('spawn') -) -``` - ---- - -## Configuration Recommendations - -### Production Environment Variables - -```bash -# Workers and Concurrency -API_WORKERS=4 -UVICORN_WORKERS=4 - -# Database -DATABASE_POOL_MIN_SIZE=20 -DATABASE_POOL_MAX_SIZE=50 - -# Redis -REDIS_MAX_CONNECTIONS=20 - -# ML Thread Pool -ML_THREAD_POOL_SIZE=8 - -# Rate Limiting -RATE_LIMIT_ENABLED=true -RATE_LIMIT_STORAGE=redis - -# Caching -EMBEDDING_CACHE_ENABLED=true -EMBEDDING_CACHE_TTL=3600 -``` - -### Kubernetes Resource Limits - -```yaml -resources: - requests: - memory: "1Gi" - cpu: "500m" - limits: - memory: "2Gi" - cpu: "2000m" -``` - ---- - -## Performance Testing Commands - -```bash -# Load test with Locust -locust -f tests/load/locustfile.py --host=http://localhost:8001 \ - --users 100 --spawn-rate 10 --run-time 5m - -# Benchmark specific endpoint -ab -n 1000 -c 10 http://localhost:8001/api/v1/health - -# Profile Python code -python -m cProfile -o profile.out -m uvicorn app.main:app -snakeviz profile.out -``` - ---- - -## Implementation Priority - -### Week 1 (Critical) -1. Fix merge conflicts in `container.py` -2. Add `run_in_executor` for ML operations -3. Increase uvicorn workers - -### Week 2 (High) -4. Add Redis embedding cache -5. Add response compression -6. Tune connection pools - -### Week 3 (Medium) -7. Optimize model preloading -8. Add batch multiprocessing -9. Performance testing and tuning - ---- - -## Monitoring Checklist - -After implementing optimizations, monitor: - -- [ ] P50/P95/P99 latency via Prometheus -- [ ] Request throughput (RPS) -- [ ] CPU utilization per worker -- [ ] Memory usage trends -- [ ] Database connection pool utilization -- [ ] Redis cache hit ratio -- [ ] Error rates - ---- - -## Summary - -The three most impactful changes are: - -1. **Offload ML operations to thread pool** - Unblocks async event loop -2. **Run multiple uvicorn workers** - Utilizes multiple CPU cores -3. **Add Redis caching** - Reduces database load - -These changes alone should provide **5-10x improvement** in concurrent request handling and **2-4x improvement** in overall throughput. - ---- - -*Generated by performance analysis on December 14, 2025* diff --git a/docs/UNIFACE_BACKEND_BENCHMARKING.md b/docs/UNIFACE_BACKEND_BENCHMARKING.md deleted file mode 100644 index dd5c9ab..0000000 --- a/docs/UNIFACE_BACKEND_BENCHMARKING.md +++ /dev/null @@ -1,11 +0,0 @@ -# UniFace Backend Benchmarking - -Use `scripts/compare_liveness_backends.py` to compare the current `enhanced` and `uniface` backends on the same fixture set and persist the raw output for auditability. - -Example: - -```powershell -python scripts/compare_liveness_backends.py --limit 20 --output logs/uniface_benchmark.json -``` - -The saved JSON file is the benchmark artifact to keep alongside any reported summary numbers such as average scores or pass counts. diff --git a/docs/archive/2026-04-16/BROWSER_BIOMETRIC_RESEARCH.md b/docs/archive/2026-04-16/BROWSER_BIOMETRIC_RESEARCH.md deleted file mode 100644 index 1f73067..0000000 --- a/docs/archive/2026-04-16/BROWSER_BIOMETRIC_RESEARCH.md +++ /dev/null @@ -1,419 +0,0 @@ -# Browser-Based Biometric Processing: Comprehensive Research Report - -**Date**: March 13, 2026 -**Scope**: Analysis of which biometric-processor operations can be handled client-side in browsers -**Covers**: Browser APIs, CPU/GPU utilization, frontend languages, WASM (KMP/CMP), all relevant tools & AI frameworks - ---- - -## Table of Contents - -1. [Server-Side Operations & Browser Feasibility Matrix](#1-current-server-side-operations--browser-feasibility-matrix) -2. [Browser APIs & Platform Capabilities](#2-browser-apis--platform-capabilities-2025-2026) -3. [Frontend ML Frameworks & Libraries](#3-frontend-ml-frameworks--libraries) -4. [KMP & CMP WASM Power for Biometrics](#4-kmp--cmp-wasm-power-for-biometrics) -5. [CPU & GPU Utilization in Browsers](#5-cpu--gpu-utilization-in-browsers) -6. [Commercial & Open-Source Browser Biometric Solutions](#6-commercial--open-source-browser-biometric-solutions) -7. [Rust-to-WASM Biometric Pipeline](#7-rust-to-wasm-biometric-pipeline) -8. [Privacy & Security Considerations](#8-privacy--security-considerations) -9. [Recommended Hybrid Architecture](#9-recommended-architecture-hybrid-client-server-split) -10. [Implementation Technology Recommendations](#10-implementation-technology-recommendations) -11. [Emerging Standards to Watch](#11-emerging-standards-to-watch) -12. [Summary of Key Findings](#12-summary-of-key-findings) - ---- - -## 1. Current Server-Side Operations & Browser Feasibility Matrix - -Based on analysis of all 25+ endpoint groups in the biometric-processor: - -| Operation | Server Time | ML Models | Browser Feasible? | Notes | -|---|---|---|---|---| -| **Quality Assessment** | 50-100ms | OpenCV (no NN) | **YES** | Pure CV: blur (Laplacian), lighting, face size | -| **Face Detection** | 100-300ms | DeepFace CNN | **YES** | MediaPipe/face-api.js can replace | -| **Facial Landmarks (468pt)** | 100-200ms | MediaPipe Face Mesh | **YES** | MediaPipe has native JS/WASM support | -| **Liveness (Passive)** | 200-400ms | Haar + LBP + Color | **PARTIAL** | Texture analysis possible; certification gap | -| **Liveness (Active: blink/smile)** | 200-400ms | Haar cascades | **YES** | OpenCV.js Haar cascades available | -| **Gaze Tracking** | 150-250ms | MediaPipe + solvePnP | **YES** | MediaPipe Face Mesh runs in browser | -| **Head Pose Estimation** | included above | solvePnP | **YES** | Pure math on landmark points | -| **Demographics (age/gender)** | 500-800ms | DeepFace CNNs | **PARTIAL** | Possible with ONNX Runtime Web + WebGPU; heavy | -| **Card Type Detection** | 100-300ms | YOLOv8/v11/v12 | **PARTIAL** | YOLO Nano via ONNX Runtime Web feasible | -| **Deepfake Detection (texture)** | ~50ms | Texture heuristics | **YES** | Laplacian + LBP computable in browser | -| **Embedding Extraction** | 200-400ms | Facenet/ArcFace CNN | **NO** (keep server) | Security-critical; embeddings must stay server-side | -| **1:1 Verification** | 300-500ms | Detection + Extraction | **NO** (keep server) | Requires stored embeddings comparison | -| **1:N Search** | 500ms-2s | Extraction + DB scan | **NO** (keep server) | Requires database access | -| **Enrollment** | 300-500ms | Full pipeline | **NO** (keep server) | Security-critical storage operation | -| **Multi-Image Enrollment** | 1-2.5s | 5x pipeline + fusion | **NO** (keep server) | Too heavy; security-critical | -| **Batch Operations** | scales | All above | **NO** (keep server) | Server-only by nature | -| **Proctoring Frame** | 500ms-2s | Combined pipeline | **HYBRID** | Detection client-side, verification server-side | -| **Object Detection (proctor)** | 100-300ms | YOLOv8n | **PARTIAL** | YOLOv8 Nano runs in ONNX Runtime Web | -| **Similarity Calculation** | <1ms | Cosine distance | **YES** (but shouldn't) | Trivial math, but embeddings are secret | - ---- - -## 2. Browser APIs & Platform Capabilities (2025-2026) - -### 2.1 WebAuthn / FIDO2 -- **Status**: Fully supported across Chrome, Firefox, Safari, Edge -- **Capability**: Platform biometric authenticators (Windows Hello, Touch ID, Face ID, Android) -- **Use case**: Authentication credential management, not raw biometric processing -- **Relevance**: Can replace fingerprint/voice stubs with passkey-based auth - -### 2.2 WebGPU API (PRODUCTION - Nov 2025) -- **Milestone**: All major browsers support WebGPU as of Nov 25, 2025 -- **Performance**: ~10x faster than WebGL for rendering; GPU-accelerated ML inference -- **ML support**: ONNX Runtime Web, Transformers.js v3, TensorFlow.js all have WebGPU backends -- **Limitation**: Browser GPU memory caps (4-6GB before degradation); ~5x slower than native GPU -- **Backend mapping**: Chrome/Edge -> Direct3D 12 (Windows), Metal (macOS); Firefox -> Vulkan/Metal; Safari -> Metal - -### 2.3 WebNN API (EXPERIMENTAL - Jan 2026) -- **Status**: W3C Updated Candidate Recommendation (Jan 22, 2026); comment period until March 22, 2026 -- **Browser support**: Edge and Chrome only (behind flags in Chromium) -- **Capability**: Hardware-accelerated neural network inference on CPU, GPU, and NPU -- **95 operators** now covered across Core ML, DirectML, ONNX Runtime Web, TFLite/XNNPACK -- **NOT production-ready** yet — but will be the future standard for browser ML - -### 2.4 WebCodecs API (PRODUCTION - 2025) -- **Status**: W3C standard; supported in all browsers (Safari: VideoDecoder only) -- **Capability**: Low-level access to video frames (VideoFrame), bypassing media element overhead -- **Relevance**: Efficient frame capture for liveness detection, quality check, real-time tracking - -### 2.5 Media Capture APIs -- **getUserMedia()**: Widely supported; camera/microphone access -- **ImageCapture API**: High-resolution still photos from camera (limited browser support) -- **MediaStream**: Real-time video stream processing - -### 2.6 Web Speech API -- **SpeechRecognition**: Chrome/Chromium only; server-based by default -- **Limitation**: Cannot work offline; not suitable for speaker verification (only speech-to-text) - -### 2.7 W3C Digital Credentials API (DRAFT - 2025) -- **Status**: First draft May 2025; Chrome 141 & Safari 26 ship stable support (Sept 2025) -- **Capability**: Browser-level digital credential management (mDocs, digital passports) -- **Future**: Will integrate with biometric verification for decentralized identity - ---- - -## 3. Frontend ML Frameworks & Libraries - -### 3.1 MediaPipe Web (RECOMMENDED for face operations) -- **Runtime**: WASM + GPU acceleration via FilesetResolver -- **Face Landmarker**: 468 landmarks, 3D coordinates, blendshape scores, real-time -- **Face Detector**: Bounding boxes, facial features detection -- **Capabilities**: Blink detection, smile detection, head pose, gaze direction -- **Performance**: Real-time (30+ FPS) on modern hardware -- **Directly replaces**: Server-side MediaPipe landmarks, gaze tracking, active liveness checks - -### 3.2 TensorFlow.js -- **Backends**: WebGL, WebGPU, WASM, CPU -- **Models**: MobileNet, PoseNet, BlazeFace, FaceMesh (via MediaPipe) -- **Use case**: General-purpose browser ML inference -- **face-api.js**: Built on TensorFlow.js; face detection, landmarks, recognition, age/gender - -### 3.3 ONNX Runtime Web (RECOMMENDED for model inference) -- **Backends**: WebGPU, WebGL, WebNN, WASM -- **Capability**: Run any ONNX model in browser (including converted PyTorch/TF models) -- **Key advantage**: Can run the same models used server-side (Facenet, ArcFace, YOLO) in browser -- **Performance**: WebGPU backend enables GPU-accelerated inference - -### 3.4 OpenCV.js / OpenCV WASM -- **Capability**: Full OpenCV compiled to WASM -- **Use case**: Image quality assessment (Laplacian blur, brightness), Haar cascades, LBP -- **Directly replaces**: Server-side quality assessor, passive liveness texture checks -- **Limitation**: Large WASM binary (~8MB); initialization overhead - -### 3.5 Transformers.js v3 -- **WebGPU support**: Run Hugging Face models in browser -- **Capability**: Image classification, object detection, feature extraction -- **Use case**: Could run face embedding models if security model permits - -### 3.6 face-api.js / Modern Face API -- **Status**: Maintained fork with WASM optimizations (2025) -- **Capability**: Face detection, landmarks (68-point), recognition, age/gender/expression -- **Privacy**: Fully local processing -- **Limitation**: Less accurate than server-side DeepFace; 68 landmarks vs 468 - -### 3.7 Beyond Reality Face SDK (BRFv5) -- **Status**: Stable; includes WASM export -- **Capability**: Multi-face detection, blink/smile/yawn detection, AR overlays -- **Use case**: Active liveness detection UI, face tracking for enrollment guidance - -### 3.8 jeelizFaceFilter -- **Capability**: Real-time face detection, rotation, blink detection, multi-face -- **Use case**: Lightweight liveness indicators, AR overlays - ---- - -## 4. KMP & CMP WASM Power for Biometrics - -### 4.1 Kotlin/WASM (Beta - 2025) -- **Status**: Promoted to Beta; no longer experimental -- **Performance**: Nearly 3x faster than JavaScript in UI rendering scenarios -- **Browser support**: All major browsers (WasmGC supported since Dec 2024) -- **Multi-module compilation**: Being added for dynamic loading and plugin systems -- **Biometric use**: KMP documentation lists biometric authentication as use case -- **Gap**: No pre-built biometric algorithm libraries for Kotlin/WASM; would need wrapping - -### 4.2 Compose Multiplatform for Web (Beta - Sept 2025) -- **Status**: Production-ready for early adopters (v1.9.0) -- **Capability**: Full Compose UI compiled to WASM; runs in browser -- **Real deployments**: Kotlin Playground, KotlinConf app, Rijksmuseum -- **Strengths for biometrics**: - - Responsive camera overlay UI for enrollment/liveness - - Cross-platform shared code (Android, iOS, Desktop, Web) - - Smooth animations for liveness prompts - - Shared domain logic with Android/iOS apps -- **Integration path**: Compose UI + JavaScript interop for MediaPipe/ONNX Runtime Web calls - -### 4.3 KMP + CMP Architecture for Biometric Client - -``` -+---------------------------------------------+ -| Compose Multiplatform UI (WASM) | -| +--------------+ +----------------------+ | -| | Camera View | | Liveness Prompts | | -| | Face Overlay | | Quality Feedback | | -| | AR Guides | | Enrollment Progress | | -| +--------------+ +----------------------+ | -+---------------------------------------------+ -| KMP Shared Logic (WASM) | -| - State management, validation | -| - API client for server calls | -| - Quality threshold logic | -+---------------------------------------------+ -| JS Interop Layer | -| +----------+ +----------+ +------------+ | -| |MediaPipe | |ONNX RT | |OpenCV.js | | -| |Face Mesh | |Web+WebGPU| |WASM | | -| +----------+ +----------+ +------------+ | -+---------------------------------------------+ -| Browser APIs | -| getUserMedia | WebGPU | WebCodecs | WebAuthn| -+---------------------------------------------+ -``` - -### 4.4 Flutter Web (Alternative) -- **WASM compilation**: Available since Flutter 3.22 -- **Biometric plugins**: local_auth, flutter_biometric_login -- **Status**: Production-ready for web; but WASM performance still catching up to native - ---- - -## 5. CPU & GPU Utilization in Browsers - -### 5.1 CPU Constraints -- JavaScript: Single-threaded by default (one core) -- **Web Workers**: Multi-threading; offload ML inference to worker threads -- **SharedArrayBuffer**: Enables shared memory between workers (requires COOP/COEP headers) -- **WASM threads**: WASM can use multiple threads via Web Workers + SharedArrayBuffer -- **Practical limit**: 4-8 worker threads reasonable; diminishing returns beyond that - -### 5.2 GPU Constraints -- **WebGPU memory**: 4-6GB practical limit before degradation/crashes -- **Safari Metal**: 256MB-993MB buffer limits depending on device -- **Performance gap**: Browser GPU inference ~5x slower than native GPU -- **Model size limit**: Small-to-medium models (up to ~1B parameters quantized) feasible -- **Face models**: Facenet (128-D, ~24MB), ArcFace (512-D, ~120MB) — well within GPU limits - -### 5.3 Performance Benchmarks -- Browser ML inference: ~5x slower than native GPU, ~15-17x slower than native CPU -- MediaPipe Face Mesh in browser: 30+ FPS on modern devices -- OpenCV.js face detection: 50-60 FPS for lightweight operations -- Kotlin/WASM UI rendering: 3x faster than JavaScript -- LLM in browser (Llama-3.2-1B): ~10 tokens/sec via WebGPU (Sept 2025) - ---- - -## 6. Commercial & Open-Source Browser Biometric Solutions - -### 6.1 Commercial SDKs -| Vendor | Client-Side Support | Technology | Notes | -|---|---|---|---| -| **iProov** | Partial | Flashmark + server AI | Liveness verification primarily server-side | -| **Jumio** | Partial | iProov integration | Cloud-first; deepfake detection server-side | -| **Onfido** | Partial | SDK for capture | Processing server-side | -| **FaceTec** | Partial | 3D FaceScan | Liveness detection; IP disputes ongoing | -| **BioID** | Yes | Browser face recognition | Offers web-based face recognition | -| **Aware** | Yes | Face+Document Capture SDK | Web capture SDK available | -| **OCR Studio** | Yes | WASM liveness | Browser-based liveness with WASM | -| **Amazon Rekognition** | React SDK | Face liveness component | Client capture, server verify | -| **Azure Face** | SDK | Passive liveness | Client capture, server verify | -| **Identy.io** | Mobile focus | Touchless biometrics | On-device; limited browser | - -### 6.2 Open-Source Libraries -| Library | Type | Stars | Status | Best For | -|---|---|---|---|---| -| **MediaPipe** (Google) | Face/Hand/Pose | 29k+ | Active | Face detection, landmarks, liveness | -| **face-api.js** | Face recognition | 16k+ | Maintained fork | Detection, recognition, age/gender | -| **OpenCV.js** | Computer vision | - | Stable | Quality assessment, image processing | -| **BRFv5** | Face tracking | - | Stable | AR overlays, blink/smile detection | -| **pico.js** | Face detection | - | Stable | Ultra-lightweight detection (~200 lines) | -| **jeelizFaceFilter** | Face tracking | - | Stable | Real-time face filters, AR | -| **clmtrackr** | Face tracking | - | Legacy | Face swapping/masking | -| **SourceAFIS-Rust** | Fingerprint | - | Available | 1:N fingerprint (compilable to WASM) | -| **rustface** | Face detection | - | Available | Rust face detection (compilable to WASM) | - ---- - -## 7. Rust-to-WASM Biometric Pipeline - -Rust biometric libraries can compile to WASM via `wasm32-unknown-unknown`: - -| Library | Capability | WASM Ready | -|---|---|---| -| **SourceAFIS-Rust** | Fingerprint recognition, 1:N search | Compilable | -| **rustface** | Face detection | Compilable | -| **image (Rust crate)** | Image processing | Yes | -| **ndarray** | Numerical computation | Yes | -| **robius-authentication** | Platform biometric abstraction | Partial | - -**Performance**: Rust-compiled WASM achieves near-native speeds; suitable for image preprocessing, feature extraction, and matching algorithms. - ---- - -## 8. Privacy & Security Considerations - -### 8.1 Client-Side Advantages -- Biometric data never leaves user's device -- GDPR/privacy compliance simplified -- No biometric template storage on server (for detection/quality operations) -- Reduced attack surface for biometric data theft - -### 8.2 Client-Side Risks -- JavaScript/WASM code is inspectable -> model theft, bypass attacks -- No tamper-proof execution environment in browser -- Liveness detection bypass possible without server verification -- Embedding extraction on client exposes biometric templates - -### 8.3 Recommended Security Model -- **Client**: Detection, quality, UI guidance, preliminary liveness — non-secret operations -- **Server**: Embedding extraction, comparison, enrollment, verification — security-critical -- **Hybrid liveness**: Client captures challenge-response video -> server verifies -- **WebAuthn**: For fingerprint/voice authentication via platform authenticators - -### 8.4 Regulatory Trends (2026) -- Gartner 2025: Decentralized (on-device) biometrics are best practice -- EU Digital Identity Wallet: W3C Digital Credentials API integration -- Privacy-preserving computation: Homomorphic encryption for biometric matching emerging - ---- - -## 9. Recommended Architecture: Hybrid Client-Server Split - -### Operations to Move to Browser (Client-Side) - -| Operation | Technology Stack | Benefit | -|---|---|---| -| **Face Detection** | MediaPipe Face Detector (WASM) | Eliminates server round-trip; instant feedback | -| **468-Point Landmarks** | MediaPipe Face Landmarker (WASM) | Real-time face mesh; guides enrollment | -| **Image Quality Check** | OpenCV.js (WASM) | Instant quality feedback before upload | -| **Active Liveness (blink/smile)** | MediaPipe + BRFv5 | Interactive liveness prompts client-side | -| **Head Pose Estimation** | MediaPipe landmarks + math | Real-time pose feedback | -| **Gaze Tracking** | MediaPipe Face Mesh + solvePnP | Real-time gaze for proctoring UI | -| **Texture Deepfake Check** | OpenCV.js Laplacian + LBP | Preliminary spoof detection | -| **Frame Quality Filter** | OpenCV.js blur + brightness | Only send good frames to server | -| **Camera Guidance UI** | Compose Multiplatform / React | Face centering, distance, lighting guides | -| **Fingerprint Auth** | WebAuthn / FIDO2 Passkeys | Replace stub with platform biometrics | -| **Object Detection (proctor)** | ONNX Runtime Web + YOLOv8n | Phone/book detection client-side | - -### Operations to Keep Server-Side - -| Operation | Reason | -|---|---| -| **Embedding Extraction** | Security-critical; model IP protection | -| **1:1 Verification** | Requires stored embeddings | -| **1:N Search** | Requires database access | -| **Enrollment Storage** | Database write; audit trail | -| **Demographics (production)** | Accuracy requirements; model size | -| **Card Type Detection** | Custom YOLO model protection | -| **Liveness Verification** | Server must validate client claims | -| **Batch Operations** | Server-only by nature | -| **Proctoring Decisions** | Tamper-proof incident recording | - -### Bandwidth & Latency Savings - -Moving detection + quality + liveness to client: -- **Eliminates**: 3-5 server round-trips per enrollment attempt -- **Reduces upload**: Only quality-verified images sent to server -- **User experience**: Instant feedback (<100ms vs 300-800ms server round-trip) -- **Server load**: ~60% reduction in compute for face operations - ---- - -## 10. Implementation Technology Recommendations - -### Option A: React + MediaPipe + ONNX Runtime Web (Fastest to implement) -- Aligns with existing web-app (React on port 3000) -- MediaPipe WASM for face operations -- ONNX Runtime Web + WebGPU for model inference -- OpenCV.js for quality assessment -- **Pros**: Large ecosystem, existing integration path, battle-tested -- **Cons**: No cross-platform code sharing with mobile - -### Option B: Compose Multiplatform (KMP/CMP) + JS Interop (Cross-platform) -- Compose UI compiled to WASM for biometric capture screens -- JS interop layer for MediaPipe/ONNX Runtime Web -- Shared Kotlin logic across Android, iOS, Desktop, Web -- **Pros**: Single codebase; 3x faster UI than JS; type-safe -- **Cons**: Beta maturity; JS interop overhead; smaller ecosystem - -### Option C: Hybrid — React shell + KMP WASM modules (Pragmatic) -- Keep React app shell -- Add KMP WASM modules for shared biometric logic (validation, state, API client) -- Use MediaPipe/ONNX directly from React components -- **Pros**: Incremental adoption; best of both worlds -- **Cons**: Dual build system complexity - -### Recommendation: **Option A** for near-term, with **Option C** migration path as CMP matures. - ---- - -## 11. Emerging Standards to Watch - -| Standard | Status | Impact | Timeline | -|---|---|---|---| -| **WebNN API** | Candidate Rec. | Hardware-accelerated ML in browser | Production 2027+ | -| **W3C Digital Credentials API** | Draft + shipping | Decentralized identity credentials | Standards 2026-2027 | -| **Verifiable Credentials 2.0** | W3C Rec. (May 2025) | Cryptographic credential proofs | Available now | -| **WebGPU Compute** | Production | GPU compute shaders for ML | Available now | -| **WASM Component Model** | Proposal | Cross-language WASM modules | 2026-2027 | -| **WASM GC** | Production | Managed language support (Kotlin, Dart) | Available now | -| **WebCodecs** | Production | Efficient video frame processing | Available now | - ---- - -## 12. Summary of Key Findings - -1. **60-70% of biometric UI operations can move to browser** — face detection, landmarks, quality, active liveness, gaze tracking, head pose - -2. **Security-critical operations must stay server-side** — embedding extraction, verification, enrollment, 1:N search - -3. **WebGPU is the game-changer** — all major browsers support GPU-accelerated ML inference as of Nov 2025 - -4. **MediaPipe Web is the recommended face processing stack** — production-ready, Google-maintained, WASM-based, 468 landmarks - -5. **KMP/CMP WASM is promising but early** — excellent for cross-platform UI, but biometric algorithm bindings need manual wrapping - -6. **WebAuthn can replace fingerprint/voice stubs** — platform authenticators provide real biometric auth without custom implementations - -7. **Privacy trend favors client-side processing** — Gartner 2025 recommends decentralized biometrics; regulators pushing on-device processing - -8. **Hybrid architecture is optimal** — client-side detection + quality + liveness UI; server-side matching + storage + verification - ---- - -## Sources - -- [WebGPU now supported by all major browsers (Nov 2025)](https://videocardz.com/newz/webgpu-is-now-supported-by-all-major-browsers) -- [Compose Multiplatform 1.9.0 - Web Goes Beta (Sept 2025)](https://blog.jetbrains.com/kotlin/2025/09/compose-multiplatform-1-9-0-compose-for-web-beta/) -- [KMP Roadmap Aug 2025](https://blog.jetbrains.com/kotlin/2025/08/kmp-roadmap-aug-2025/) -- [W3C Digital Credentials API Draft (May 2025)](https://idtechwire.com/w3c-releases-digital-credentials-api-draft/) -- [W3C Web Neural Network API](https://www.w3.org/TR/webnn/) -- [WebCodecs API - MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API) -- [MediaPipe Face Detector for Web](https://developers.google.com/mediapipe/solutions/vision/face_detector/web_js) -- [ONNX Runtime Web](https://onnxruntime.ai/) -- [face-api.js](https://github.com/justadudewhohacks/face-api.js/) -- [Beyond Reality Face SDK (BRFv5)](https://github.com/Tastenkunst/brfv5-browser) -- [SourceAFIS-Rust](https://github.com/robertvazan/sourceafis-rust) -- [Verifiable Credentials 2.0 W3C Standard (May 2025)](https://www.biometricupdate.com/202505/verifiable-credentials-2-0-now-a-w3c-standard) -- [Gartner: Biometrics in 2026](https://www.biometricupdate.com/202601/biometrics-tapped-to-tame-the-internet-in-2026) diff --git a/docs/archive/2026-04-16/DEMO_APP_DESIGN.md b/docs/archive/2026-04-16/DEMO_APP_DESIGN.md deleted file mode 100644 index f4e2cc5..0000000 --- a/docs/archive/2026-04-16/DEMO_APP_DESIGN.md +++ /dev/null @@ -1,2936 +0,0 @@ -# Biometric Processor Demo Application - Professional Design Document - -**Version:** 1.0.0 -**Date:** December 12, 2025 -**Status:** Design Complete - ---- - -## Table of Contents - -1. [Executive Summary](#executive-summary) -2. [Architecture Overview](#architecture-overview) -3. [Technology Stack](#technology-stack) -4. [Application Structure](#application-structure) -5. [Feature Modules](#feature-modules) -6. [UI/UX Design](#uiux-design) -7. [Software Engineering Compliance](#software-engineering-compliance) - - [SOLID Principles Implementation](#solid-principles-implementation) - - [Design Patterns Applied](#design-patterns-applied) - - [Code Quality Standards](#code-quality-standards) - - [Error Handling Design](#error-handling-design) - - [Testing Strategy](#testing-strategy) - - [Performance Optimization](#performance-optimization) - - [Anti-Pattern Avoidance](#anti-pattern-avoidance) - - [Version Control Best Practices](#version-control-best-practices) - - [Documentation Standards](#documentation-standards) -8. [Implementation Plan](#implementation-plan) -9. [Deployment Options](#deployment-options) - ---- - -## Executive Summary - -This document outlines the design for a comprehensive **Streamlit-based demo application** that showcases ALL features of the Biometric Processor v1.0.0. The demo is designed for: - -- **Sales Demos** - Impressive visual demonstrations for potential clients -- **Technical Evaluations** - Hands-on testing for technical teams -- **Training** - Educational tool for new users and integrators -- **Trade Shows** - Interactive booth demonstrations - -### Key Design Principles - -1. **100% Feature Coverage** - Every single feature is demonstrable -2. **Zero Installation for Viewers** - Web-based, runs in browser -3. **Self-Contained** - Works with local backend, no external dependencies -4. **Professional UI** - Clean, modern interface suitable for enterprise demos -5. **Interactive** - Real-time feedback and visualization - ---- - -## Architecture Overview - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ DEMO APPLICATION │ -├─────────────────────────────────────────────────────────────────────────┤ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Welcome │ │ Core │ │ Advanced │ │ Proctoring │ │ -│ │ Page │ │ Biometrics │ │ Analysis │ │ Suite │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Batch │ │ Admin │ │ Config │ │ API │ │ -│ │ Processing │ │ Dashboard │ │ Viewer │ │ Explorer │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────────────────────┤ -│ STREAMLIT FRAMEWORK │ -├─────────────────────────────────────────────────────────────────────────┤ -│ BIOMETRIC PROCESSOR API (localhost:8001) │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Technology Stack - -| Component | Technology | Justification | -|-----------|------------|---------------| -| **Frontend Framework** | Streamlit 1.29+ | Rapid ML demo development, built-in components | -| **Real-time Video** | streamlit-webrtc | WebRTC for live camera access | -| **Charts/Graphs** | Plotly | Interactive, professional visualizations | -| **Image Processing** | OpenCV, Pillow | Client-side image handling | -| **WebSocket Client** | websockets | Real-time proctoring demo | -| **HTTP Client** | httpx | Async API calls | -| **Styling** | Custom CSS | Professional enterprise look | - ---- - -## Application Structure - -``` -demo/ -├── app.py # Main entry point -├── requirements.txt # Dependencies -├── config.py # Demo configuration -├── assets/ -│ ├── logo.png # Company logo -│ ├── sample_faces/ # Sample face images -│ │ ├── person_1.jpg -│ │ ├── person_2.jpg -│ │ └── ... -│ ├── sample_documents/ # Sample ID cards -│ │ ├── tc_kimlik.jpg -│ │ ├── ehliyet.jpg -│ │ └── pasaport.jpg -│ └── styles.css # Custom styling -├── components/ -│ ├── __init__.py -│ ├── sidebar.py # Navigation sidebar -│ ├── header.py # Page headers -│ ├── metrics_card.py # Metric display cards -│ ├── image_uploader.py # Enhanced image upload -│ ├── webcam_capture.py # Webcam component -│ ├── result_display.py # Result visualization -│ ├── json_viewer.py # JSON response viewer -│ └── video_stream.py # Video streaming component -├── utils/ -│ ├── __init__.py -│ ├── api_client.py # API client wrapper -│ ├── image_utils.py # Image processing helpers -│ ├── websocket_client.py # WebSocket handler -│ └── session_state.py # State management -├── pages/ -│ ├── 01_Welcome.py # Landing page -│ ├── 02_Face_Enrollment.py # Enrollment demo -│ ├── 03_Face_Verification.py # Verification demo -│ ├── 04_Face_Search.py # 1:N search demo -│ ├── 05_Liveness_Detection.py # Liveness demo -│ ├── 06_Quality_Analysis.py # Quality assessment -│ ├── 07_Demographics.py # Demographics analysis -│ ├── 08_Facial_Landmarks.py # Landmark detection -│ ├── 09_Face_Comparison.py # Direct comparison -│ ├── 10_Similarity_Matrix.py # Multi-face similarity -│ ├── 11_Multi_Face_Detection.py # Detect all faces -│ ├── 12_Card_Type_Detection.py # ID card detection -│ ├── 13_Batch_Processing.py # Batch operations -│ ├── 14_Proctoring_Session.py # Full proctoring demo -│ ├── 15_Proctoring_Realtime.py # WebSocket streaming -│ ├── 16_Admin_Dashboard.py # Admin panel demo -│ ├── 17_Webhooks.py # Webhook management -│ ├── 18_Configuration.py # Config viewer -│ ├── 19_API_Explorer.py # Interactive API testing -│ └── 20_Embeddings_Management.py # Export/Import -└── tests/ - └── test_demo_app.py # Demo app tests -``` - ---- - -## Feature Modules - -### Module 1: Welcome Page (`01_Welcome.py`) - -**Purpose:** Landing page with overview and quick navigation - -**Features Demonstrated:** -- System overview and capabilities -- Health check status -- Quick stats (enrollments, sessions, etc.) -- Feature navigation cards -- API connectivity test - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ [LOGO] BIOMETRIC PROCESSOR DEMO v1.0.0 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ ✅ API │ │ ✅ Database │ │ ✅ Redis │ │ -│ │ Online │ │ Connected │ │ Connected │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ │ -│ ════════════════ FEATURE CATEGORIES ════════════════ │ -│ │ -│ ┌─────────────────────────────────────────────────┐ │ -│ │ 🔐 CORE BIOMETRICS │ │ -│ │ Enrollment • Verification • Search • Liveness │ │ -│ │ [START DEMO →] │ │ -│ └─────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────┐ │ -│ │ 🔬 ADVANCED ANALYSIS │ │ -│ │ Quality • Demographics • Landmarks • Compare │ │ -│ │ [START DEMO →] │ │ -│ └─────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────┐ │ -│ │ 👁️ REAL-TIME PROCTORING │ │ -│ │ Sessions • Gaze • Objects • Incidents │ │ -│ │ [START DEMO →] │ │ -│ └─────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -### Module 2: Face Enrollment (`02_Face_Enrollment.py`) - -**Purpose:** Demonstrate face registration workflow - -**Features Demonstrated:** -- Single face enrollment -- Quality validation before enrollment -- Embedding extraction visualization -- Duplicate detection -- Multi-tenant enrollment -- Metadata attachment - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ FACE ENROLLMENT │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────┐ ┌─────────────────────────────────┐│ -│ │ │ │ ENROLLMENT SETTINGS ││ -│ │ [Upload Image] │ │ ││ -│ │ or │ │ User ID: [________________] ││ -│ │ [Use Webcam] │ │ Tenant: [Default ▼] ││ -│ │ │ │ ││ -│ │ [Sample Images ▼] │ │ ☑ Validate Quality First ││ -│ │ │ │ ☑ Check for Duplicates ││ -│ └─────────────────────┘ │ ☐ Skip if Exists ││ -│ │ ││ -│ │ Metadata (JSON): ││ -│ │ ┌───────────────────────────┐ ││ -│ │ │ {"department": "HR"} │ ││ -│ │ └───────────────────────────┘ ││ -│ │ ││ -│ │ [ENROLL FACE] ││ -│ └─────────────────────────────────┘│ -│ │ -│ ════════════════ RESULTS ════════════════ │ -│ │ -│ ┌─────────────────────┐ ┌─────────────────────────────────┐│ -│ │ [Detected Face] │ │ Enrollment ID: abc-123-def ││ -│ │ with bounding box │ │ Quality Score: 94.2% ││ -│ │ │ │ Face Confidence: 99.8% ││ -│ │ │ │ Created At: 2025-12-12 14:30 ││ -│ └─────────────────────┘ │ ││ -│ │ Embedding Vector (128-D): ││ -│ ┌─────────────────────────────────────────────────────────┐ ││ -│ │ [Embedding Visualization - Bar Chart / Heatmap] │ ││ -│ └─────────────────────────────────────────────────────────┘ ││ -│ └─────────────────────────────────┘│ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `POST /enroll` - Main enrollment -- `POST /quality/analyze` - Pre-validation -- `POST /faces/detect-all` - Face detection preview - ---- - -### Module 3: Face Verification (`03_Face_Verification.py`) - -**Purpose:** Demonstrate 1:1 face matching - -**Features Demonstrated:** -- Verify against enrolled user -- Adjustable similarity threshold -- Confidence scoring -- Match/No-match visualization -- Side-by-side comparison - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ FACE VERIFICATION (1:1 Matching) │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ VERIFICATION MODE │ │ -│ │ ○ Against Enrolled User ● Direct Image Comparison │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────┐ ┌─────────────────────┐ │ -│ │ REFERENCE IMAGE │ │ PROBE IMAGE │ │ -│ │ │ │ │ │ -│ │ [Upload/Webcam] │ │ [Upload/Webcam] │ │ -│ │ │ │ │ │ -│ │ [person_1.jpg] │ │ [Live Capture] │ │ -│ └─────────────────────┘ └─────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Similarity Threshold: [====●=====] 0.60 │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ [VERIFY FACES] │ -│ │ -│ ════════════════ VERIFICATION RESULT ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ ┌─────────┐ ┌─────────┐ │ │ -│ │ │ Face A │ ═══════════════════│ Face B │ │ │ -│ │ └─────────┘ SIMILARITY └─────────┘ │ │ -│ │ │ │ -│ │ ████████████████████░░░░ 87.3% │ │ -│ │ │ │ -│ │ ┌─────────────────────────────────────────┐ │ │ -│ │ │ ✅ VERIFIED - SAME PERSON │ │ │ -│ │ │ Confidence: HIGH (87.3% > 60.0%) │ │ │ -│ │ └─────────────────────────────────────────┘ │ │ -│ │ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ 📊 DETAILED METRICS │ │ -│ │ • Similarity Score: 0.873 │ │ -│ │ • Threshold Used: 0.600 │ │ -│ │ • Processing Time: 142ms │ │ -│ │ • Face Detection Confidence: 99.2% │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `POST /verify` - Against enrolled user -- `POST /compare` - Direct comparison - ---- - -### Module 4: Face Search (`04_Face_Search.py`) - -**Purpose:** Demonstrate 1:N identification - -**Features Demonstrated:** -- Search across all enrollments -- Top-N results with similarity ranking -- Threshold filtering -- Result visualization -- Multi-tenant search - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ FACE SEARCH (1:N Identification) │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────┐ ┌─────────────────────────────────┐│ -│ │ │ │ SEARCH SETTINGS ││ -│ │ [Upload Image] │ │ ││ -│ │ or │ │ Max Results: [10 ▼] ││ -│ │ [Use Webcam] │ │ Min Threshold: [====●===] 0.6 ││ -│ │ │ │ Tenant: [All Tenants ▼] ││ -│ │ │ │ ││ -│ └─────────────────────┘ │ [SEARCH DATABASE] ││ -│ └─────────────────────────────────┘│ -│ │ -│ ════════════════ SEARCH RESULTS (5 matches) ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ RANK │ USER ID │ SIMILARITY │ VISUAL │ ENROLLED │ │ -│ ├──────────────────────────────────────────────────────────┤ │ -│ │ 1 │ john_doe │ ███████ 92%│ [thumb] │ 2025-12-01│ │ -│ │ 2 │ jane_smith │ ██████░ 78%│ [thumb] │ 2025-12-05│ │ -│ │ 3 │ bob_wilson │ █████░░ 71%│ [thumb] │ 2025-12-10│ │ -│ │ 4 │ alice_jones │ ████░░░ 65%│ [thumb] │ 2025-12-11│ │ -│ │ 5 │ charlie_b │ ████░░░ 62%│ [thumb] │ 2025-12-12│ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ 📈 SIMILARITY DISTRIBUTION │ │ -│ │ [Bar chart showing all match scores] │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `POST /search` - Main search - ---- - -### Module 5: Liveness Detection (`05_Liveness_Detection.py`) - -**Purpose:** Demonstrate anti-spoofing capabilities - -**Features Demonstrated:** -- Passive liveness (texture analysis) -- Active liveness (blink/smile detection) -- Combined mode -- Real-time webcam challenge -- Spoof detection visualization - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ LIVENESS DETECTION │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ DETECTION MODE │ │ -│ │ ○ Passive (Texture) ○ Active (Blink/Smile) ● Combined │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌────────────────────────────────────┐ ┌──────────────────┐ │ -│ │ │ │ CHALLENGE │ │ -│ │ │ │ │ │ -│ │ [LIVE WEBCAM FEED] │ │ Please: │ │ -│ │ │ │ 1. BLINK twice │ │ -│ │ │ │ 2. SMILE │ │ -│ │ │ │ │ │ -│ │ [Face Mesh Overlay] │ │ Progress: │ │ -│ │ [Eye Tracking Points] │ │ Blink: ✅✅ │ │ -│ │ │ │ Smile: ⏳ │ │ -│ └────────────────────────────────────┘ └──────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ REAL-TIME METRICS │ │ -│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ -│ │ │ EAR: 0.28 │ │ MAR: 0.45 │ │ Score: 85 │ │ │ -│ │ │ [Eye Ratio]│ │[Mouth Ratio]│ │ [Liveness] │ │ │ -│ │ └────────────┘ └────────────┘ └────────────┘ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ════════════════ LIVENESS RESULT ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ ✅ LIVE PERSON DETECTED │ │ -│ │ │ │ -│ │ Texture Score: ████████░░ 82% (No print artifacts) │ │ -│ │ Behavioral Score: █████████░ 91% (Natural movements) │ │ -│ │ Combined Score: █████████░ 87% (Above threshold) │ │ -│ │ │ │ -│ │ Anti-Spoof Checks: │ │ -│ │ ✅ Texture Analysis - No paper/screen patterns │ │ -│ │ ✅ Frequency Analysis - Natural frequency spectrum │ │ -│ │ ✅ Blink Detection - 2 natural blinks detected │ │ -│ │ ✅ Smile Detection - Genuine smile detected │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ 🧪 TRY SPOOFING (Educational) │ │ -│ │ Try holding a photo or showing a screen to see │ │ -│ │ how the system detects spoofing attempts. │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `POST /liveness` - Liveness check - ---- - -### Module 6: Quality Analysis (`06_Quality_Analysis.py`) - -**Purpose:** Demonstrate image quality assessment - -**Features Demonstrated:** -- Blur detection -- Lighting analysis -- Face size validation -- Pose assessment -- Actionable feedback - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ IMAGE QUALITY ANALYSIS │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────┐ ┌─────────────────────────────────┐│ -│ │ │ │ QUALITY METRICS ││ -│ │ [Uploaded Image] │ │ ││ -│ │ │ │ Overall: █████████░ 89% ││ -│ │ │ │ Sharpness: ████████░░ 82% ││ -│ │ Quality: GOOD │ │ Lighting: █████████░ 91% ││ -│ │ │ │ Face Size: ██████████ 95% ││ -│ └─────────────────────┘ │ Pose: ████████░░ 85% ││ -│ │ ││ -│ │ [Analyze Another Image] ││ -│ └─────────────────────────────────┘│ -│ │ -│ ════════════════ DETAILED FEEDBACK ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ ✅ PASSED CHECKS │ │ -│ │ • Face clearly visible and centered │ │ -│ │ • Good lighting conditions │ │ -│ │ • Sufficient image resolution (1920x1080) │ │ -│ │ • Face size adequate (320x380 pixels) │ │ -│ │ │ │ -│ │ ⚠️ SUGGESTIONS FOR IMPROVEMENT │ │ -│ │ • Slight blur detected - hold camera steady │ │ -│ │ • Head tilted 8° - face camera directly │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ 📊 QUALITY SCORE BREAKDOWN │ │ -│ │ [Radar Chart: Blur, Brightness, Contrast, Size, Pose] │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `POST /quality/analyze` - Quality analysis - ---- - -### Module 7: Demographics Analysis (`07_Demographics.py`) - -**Purpose:** Demonstrate demographic attribute extraction - -**Features Demonstrated:** -- Age estimation -- Gender classification -- Emotion recognition -- Privacy controls (race opt-out) -- Confidence scores - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ DEMOGRAPHICS ANALYSIS │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────┐ ┌─────────────────────────────────┐│ -│ │ │ │ ANALYSIS OPTIONS ││ -│ │ [Uploaded Image] │ │ ││ -│ │ │ │ ☑ Age Estimation ││ -│ │ │ │ ☑ Gender Classification ││ -│ │ │ │ ☑ Emotion Recognition ││ -│ │ │ │ ☐ Race Estimation (Privacy) ││ -│ │ │ │ ││ -│ └─────────────────────┘ │ [ANALYZE DEMOGRAPHICS] ││ -│ └─────────────────────────────────┘│ -│ │ -│ ════════════════ ANALYSIS RESULTS ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ -│ │ │ AGE │ │ GENDER │ │ EMOTION │ │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ │ 28-32 │ │ Female │ │ Happy │ │ │ -│ │ │ ±3 years │ │ 97.2% │ │ 89.5% │ │ │ -│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ 😊 EMOTION DISTRIBUTION │ │ -│ │ │ │ -│ │ Happy: ████████████████████░░░░░░ 89.5% │ │ -│ │ Neutral: ██░░░░░░░░░░░░░░░░░░░░░░░░ 6.2% │ │ -│ │ Surprise: █░░░░░░░░░░░░░░░░░░░░░░░░░ 2.8% │ │ -│ │ Other: ░░░░░░░░░░░░░░░░░░░░░░░░░░ 1.5% │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `POST /demographics/analyze` - Demographics analysis - ---- - -### Module 8: Facial Landmarks (`08_Facial_Landmarks.py`) - -**Purpose:** Demonstrate landmark detection - -**Features Demonstrated:** -- MediaPipe 468-point detection -- dlib 68-point detection -- 3D coordinate extraction -- Facial region mapping -- Interactive visualization - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ FACIAL LANDMARKS DETECTION │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ MODEL: ○ MediaPipe (468 points) ● dlib (68 points) │ │ -│ │ ☑ Include 3D Coordinates │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌───────────────────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ [IMAGE WITH LANDMARK OVERLAY] │ │ -│ │ │ │ -│ │ • Green dots: Eye landmarks (12 points) │ │ -│ │ • Blue dots: Nose landmarks (9 points) │ │ -│ │ • Red dots: Mouth landmarks (20 points) │ │ -│ │ • Yellow dots: Face contour (17 points) │ │ -│ │ • Purple dots: Eyebrows (10 points) │ │ -│ │ │ │ -│ └───────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ FACIAL REGIONS │ │ -│ │ [Tabs: All | Eyes | Nose | Mouth | Jaw | Eyebrows] │ │ -│ │ │ │ -│ │ Left Eye: [(x:145, y:203, z:0.02), (x:148, y:205)...] │ │ -│ │ Right Eye: [(x:298, y:201, z:0.02), (x:301, y:203)...] │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ 📐 3D VISUALIZATION (if enabled) │ │ -│ │ [Interactive 3D scatter plot of landmark points] │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `POST /landmarks/detect` - Landmark detection - ---- - -### Module 9: Face Comparison (`09_Face_Comparison.py`) - -**Purpose:** Demonstrate direct 1:1 comparison without enrollment - -**Features Demonstrated:** -- Side-by-side comparison -- Distance metrics -- Similarity visualization -- No enrollment required - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ DIRECT FACE COMPARISON │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────┐ ┌─────────────────────┐ │ -│ │ IMAGE 1 │ │ IMAGE 2 │ │ -│ │ │ │ │ │ -│ │ [Upload/Webcam] │ │ [Upload/Webcam] │ │ -│ │ │ │ │ │ -│ │ [Sample ▼] │ │ [Sample ▼] │ │ -│ └─────────────────────┘ └─────────────────────┘ │ -│ │ -│ [COMPARE FACES] │ -│ │ -│ ════════════════ COMPARISON RESULT ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ ┌────────┐ SIMILARITY ┌────────┐ │ │ -│ │ │ Face 1 │ ◄══════════════════════════════► │ Face 2 │ │ │ -│ │ └────────┘ └────────┘ │ │ -│ │ │ │ -│ │ ┌─────────────────────────┐ │ │ -│ │ │ SAME PERSON │ │ │ -│ │ │ Similarity: 91.4% │ │ │ -│ │ └─────────────────────────┘ │ │ -│ │ │ │ -│ │ Distance Metrics: │ │ -│ │ • Cosine Distance: 0.086 │ │ -│ │ • Euclidean Distance: 0.412 │ │ -│ │ • Model Used: Facenet (128-D) │ │ -│ │ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `POST /compare` - Direct comparison - ---- - -### Module 10: Similarity Matrix (`10_Similarity_Matrix.py`) - -**Purpose:** Demonstrate multi-face similarity analysis - -**Features Demonstrated:** -- N×N similarity matrix -- Clustering visualization -- Group analysis -- Heatmap display - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ SIMILARITY MATRIX (Multi-Face Analysis) │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Upload Multiple Images (2-10 faces) │ │ -│ │ [+ Add Image] [+ Add Image] [+ Add Image] ... │ │ -│ │ │ │ -│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ -│ │ │ A │ │ B │ │ C │ │ D │ │ E │ │ │ -│ │ │[img]│ │[img]│ │[img]│ │[img]│ │[img]│ │ │ -│ │ │ ✕ │ │ ✕ │ │ ✕ │ │ ✕ │ │ ✕ │ │ │ -│ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ [COMPUTE SIMILARITY MATRIX] │ -│ │ -│ ════════════════ N×N SIMILARITY MATRIX ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ │ A │ B │ C │ D │ E │ │ │ -│ │ A │ 1.00 │ 0.89 │ 0.34 │ 0.28 │ 0.31 │ │ │ -│ │ B │ 0.89 │ 1.00 │ 0.32 │ 0.29 │ 0.30 │ │ │ -│ │ C │ 0.34 │ 0.32 │ 1.00 │ 0.91 │ 0.88 │ │ │ -│ │ D │ 0.28 │ 0.29 │ 0.91 │ 1.00 │ 0.93 │ │ │ -│ │ E │ 0.31 │ 0.30 │ 0.88 │ 0.93 │ 1.00 │ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ 🔥 HEATMAP VISUALIZATION │ │ -│ │ [Interactive Plotly Heatmap with hover values] │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ 🎯 DETECTED CLUSTERS │ │ -│ │ Cluster 1: [A, B] - Same person (avg similarity: 89%) │ │ -│ │ Cluster 2: [C, D, E] - Same person (avg similarity: 91%)│ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `POST /similarity/matrix` - Matrix computation - ---- - -### Module 11: Multi-Face Detection (`11_Multi_Face_Detection.py`) - -**Purpose:** Demonstrate detecting all faces in an image - -**Features Demonstrated:** -- Multiple face detection -- Bounding boxes -- Per-face quality scores -- Face counting -- Group photo analysis - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ MULTI-FACE DETECTION │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────┐ ┌─────────────────────────────────┐│ -│ │ │ │ DETECTION SETTINGS ││ -│ │ [Group Photo] │ │ ││ -│ │ │ │ Min Face Size: [80] pixels ││ -│ │ │ │ Max Faces: [20 ▼] ││ -│ │ │ │ Backend: [MTCNN ▼] ││ -│ │ │ │ ││ -│ │ │ │ ☑ Show Quality Scores ││ -│ │ │ │ ☑ Extract Thumbnails ││ -│ │ │ │ ││ -│ └─────────────────────┘ │ [DETECT ALL FACES] ││ -│ └─────────────────────────────────┘│ -│ │ -│ ════════════════ DETECTION RESULTS (7 faces) ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ [Original Image with Bounding Boxes] │ │ -│ │ │ │ -│ │ ┌──┐ Each face labeled: #1 (94%), #2 (87%), etc. │ │ -│ │ └──┘ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ DETECTED FACES │ │ -│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐│ │ -│ │ │ #1 │ │ #2 │ │ #3 │ │ #4 │ │ #5 │ │ #6 │ │ #7 ││ │ -│ │ │94% │ │87% │ │92% │ │78% │ │95% │ │89% │ │82% ││ │ -│ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘│ │ -│ │ │ │ -│ │ Click any face for detailed metrics │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `POST /faces/detect-all` - Multi-face detection - ---- - -### Module 12: Card Type Detection (`12_Card_Type_Detection.py`) - -**Purpose:** Demonstrate ID document classification - -**Features Demonstrated:** -- TC Kimlik detection -- Ehliyet (driver's license) detection -- Pasaport detection -- Student card detection -- Real-time camera preview mode - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ CARD TYPE DETECTION │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ MODE: ○ Single Image Upload ● Live Camera Preview │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌────────────────────────────────────┐ ┌──────────────────┐ │ -│ │ │ │ DETECTED CARD │ │ -│ │ │ │ │ │ -│ │ [LIVE CAMERA FEED] │ │ Type: TC Kimlik │ │ -│ │ │ │ Confidence: 97% │ │ -│ │ │ │ │ │ -│ │ [Bounding Box Around Card] │ │ ┌────────────┐ │ │ -│ │ │ │ │[Card Icon] │ │ │ -│ │ │ │ └────────────┘ │ │ -│ └────────────────────────────────────┘ │ │ │ -│ │ Position: Center │ │ -│ │ Hold Steady... │ │ -│ └──────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ SUPPORTED CARD TYPES │ │ -│ │ │ │ -│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ -│ │ │TC Kimlik│ │ Ehliyet │ │Pasaport │ │ Student │ │ │ -│ │ │ ID │ │ License │ │Passport │ │ Card │ │ │ -│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `POST /card-type/detect-live` - Card detection - ---- - -### Module 13: Batch Processing (`13_Batch_Processing.py`) - -**Purpose:** Demonstrate async bulk operations - -**Features Demonstrated:** -- Batch enrollment -- Batch verification -- Progress tracking -- Async task status -- Results download - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ BATCH PROCESSING │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ OPERATION: ○ Batch Enrollment ● Batch Verification │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ UPLOAD BATCH │ │ -│ │ │ │ -│ │ [Drag & Drop Zone] │ │ -│ │ Drop multiple images here or click to browse │ │ -│ │ Supported: ZIP, folder, multiple files │ │ -│ │ │ │ -│ │ Files Selected: 50 images │ │ -│ │ [Preview: img1.jpg, img2.jpg, img3.jpg ...] │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ BATCH SETTINGS │ │ -│ │ ☑ Skip Duplicates ☐ Continue on Error │ │ -│ │ Callback URL: [_________________________________] │ │ -│ │ │ │ -│ │ [START BATCH PROCESSING] │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ════════════════ BATCH PROGRESS ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Batch ID: batch-abc-123-def │ │ -│ │ Status: PROCESSING │ │ -│ │ │ │ -│ │ Progress: ████████████████░░░░░░░░░░ 65% (32/50) │ │ -│ │ │ │ -│ │ ✅ Success: 30 ❌ Failed: 2 ⏳ Pending: 18 │ │ -│ │ │ │ -│ │ Recent Results: │ │ -│ │ • img32.jpg - ✅ Enrolled (Quality: 94%) │ │ -│ │ • img31.jpg - ❌ Failed (No face detected) │ │ -│ │ • img30.jpg - ✅ Enrolled (Quality: 87%) │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ [Download Results CSV] [View Detailed Report] │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `POST /batch/enroll` - Batch enrollment -- `POST /batch/verify` - Batch verification -- `GET /batch/{batch_id}/status` - Progress tracking - ---- - -### Module 14: Proctoring Session (`14_Proctoring_Session.py`) - -**Purpose:** Demonstrate full proctoring workflow (REST API) - -**Features Demonstrated:** -- Session creation -- Session lifecycle (start/pause/resume/end) -- Frame submission -- Incident detection -- Risk scoring -- Session reports - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ PROCTORING SESSION MANAGEMENT │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ SESSION CONTROLS │ │ -│ │ │ │ -│ │ Exam ID: [exam-demo-001_________] │ │ -│ │ User ID: [demo-user______________] │ │ -│ │ Tenant ID: [demo-tenant____________] │ │ -│ │ │ │ -│ │ [CREATE SESSION] [START] [PAUSE] [RESUME] [END] │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ SESSION STATUS │ │ -│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ -│ │ │ Session ID │ │ Status │ │ Risk Score │ │ │ -│ │ │ abc-123 │ │ ACTIVE │ │ 0.25 │ │ │ -│ │ │ │ │ ● Live │ │ ████░░ │ │ │ -│ │ └────────────┘ └────────────┘ └────────────┘ │ │ -│ │ │ │ -│ │ Duration: 00:15:32 Frames: 924 Incidents: 3 │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ════════════════ FRAME ANALYSIS ════════════════ │ -│ │ -│ ┌────────────────────────────────┐ ┌──────────────────────┐ │ -│ │ │ │ LAST FRAME RESULTS │ │ -│ │ [WEBCAM / UPLOAD] │ │ │ │ -│ │ │ │ Face: ✅ Detected │ │ -│ │ [Submit Frame] │ │ Gaze: Center │ │ -│ │ │ │ Objects: None │ │ -│ │ │ │ Liveness: 94% │ │ -│ │ │ │ Risk: Low (0.12) │ │ -│ └────────────────────────────────┘ └──────────────────────┘ │ -│ │ -│ ════════════════ INCIDENTS ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ TIME │ TYPE │ SEVERITY │ DESCRIPTION │ │ -│ ├──────────────────────────────────────────────────────────┤ │ -│ │ 00:05:12 │ GAZE_AWAY_PROLONGED│ MEDIUM │ 5.2s off-screen│ │ -│ │ 00:10:45 │ PHONE_DETECTED │ HIGH │ Mobile phone │ │ -│ │ 00:14:22 │ HEAD_TURNED_AWAY │ MEDIUM │ Looking left │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ [View Full Report] [Export Incidents] │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `POST /proctoring/sessions` - Create session -- `POST /proctoring/sessions/{id}/start` - Start -- `POST /proctoring/sessions/{id}/frames` - Submit frame -- `GET /proctoring/sessions/{id}/incidents` - Get incidents -- `GET /proctoring/sessions/{id}/report` - Get report -- `POST /proctoring/sessions/{id}/end` - End session - ---- - -### Module 15: Real-Time Proctoring (`15_Proctoring_Realtime.py`) - -**Purpose:** Demonstrate WebSocket streaming proctoring - -**Features Demonstrated:** -- Live video streaming -- Real-time analysis overlay -- Instant incident alerts -- Gaze visualization -- Object detection boxes -- Risk score gauge - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ REAL-TIME PROCTORING (WebSocket) 🔴 LIVE│ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌───────────────────────────────────────┐ ┌─────────────────┐ │ -│ │ │ │ LIVE METRICS │ │ -│ │ │ │ │ │ -│ │ [LIVE WEBCAM FEED] │ │ Risk Score │ │ -│ │ │ │ ┌───────────┐ │ │ -│ │ [Gaze Direction Arrow Overlay] │ │ │ 0.18 │ │ │ -│ │ [Face Bounding Box] │ │ │ ████░░░ │ │ │ -│ │ [Object Detection Boxes] │ │ └───────────┘ │ │ -│ │ │ │ │ │ -│ │ Frame: #1247 FPS: 5 │ │ Gaze: CENTER │ │ -│ │ │ │ Head: OK │ │ -│ └───────────────────────────────────────┘ │ Face: ✅ │ │ -│ │ Live: ✅ 96% │ │ -│ ┌───────────────────────────────────────┐ │ │ │ -│ │ GAZE TRACKING VISUALIZATION │ │ Objects: │ │ -│ │ │ │ None detected │ │ -│ │ ┌─────────────────────────┐ │ │ │ │ -│ │ │ ↑ │ │ │ Persons: 1 │ │ -│ │ │ ← ● → │ │ │ │ │ -│ │ │ ↓ │ │ └─────────────────┘ │ -│ │ │ │ │ │ -│ │ │ [Current gaze point] │ │ ┌─────────────────┐ │ -│ │ └─────────────────────────┘ │ │ SESSION INFO │ │ -│ │ Head: Yaw: 5° Pitch: -2° Roll: 1° │ │ │ │ -│ └───────────────────────────────────────┘ │ ID: abc-123 │ │ -│ │ Time: 00:12:45 │ │ -│ ┌───────────────────────────────────────┐ │ Frames: 3825 │ │ -│ │ LIVE INCIDENT FEED │ │ Incidents: 2 │ │ -│ │ │ │ │ │ -│ │ 🟡 00:12:30 - Gaze away (3.2s) │ │ [STOP SESSION] │ │ -│ │ 🔴 00:08:15 - Phone detected │ └─────────────────┘ │ -│ │ 🟡 00:05:42 - Head turned away │ │ -│ │ │ │ -│ └───────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `WS /proctoring/sessions/{id}/stream` - WebSocket streaming - ---- - -### Module 16: Admin Dashboard (`16_Admin_Dashboard.py`) - -**Purpose:** Demonstrate administrative capabilities - -**Features Demonstrated:** -- System health monitoring -- Real-time metrics -- Session management -- Incident review workflow -- Tenant statistics - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ ADMIN DASHBOARD │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ -│ │ API │ │ Database │ │ Redis │ │ Celery │ │ -│ │ ✅ OK │ │ ✅ OK │ │ ✅ OK │ │ ✅ OK │ │ -│ │ 12ms │ │ Pool: 8 │ │ Conn: 5 │ │ Workers:3│ │ -│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ DASHBOARD METRICS (Last 24 hours) │ │ -│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ -│ │ │ Enrollments │ │Verifications│ │ Sessions │ │ │ -│ │ │ 1,247 │ │ 8,542 │ │ 156 │ │ │ -│ │ │ +12.3% │ │ +8.7% │ │ -2.1% │ │ │ -│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ ACTIVE SESSIONS [Refresh] │ │ -│ │ ┌────────────────────────────────────────────────────┐ │ │ -│ │ │ ID │ User │ Risk │ Incidents │ Action │ │ │ -│ │ │ sess-001 │ user_123 │ 0.25 │ 3 │[Terminate]│ │ │ -│ │ │ sess-002 │ user_456 │ 0.72 │ 8 │[Terminate]│ │ │ -│ │ │ sess-003 │ user_789 │ 0.15 │ 1 │[Terminate]│ │ │ -│ │ └────────────────────────────────────────────────────┘ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ PENDING INCIDENT REVIEWS │ │ -│ │ ┌────────────────────────────────────────────────────┐ │ │ -│ │ │ ID │ Type │ Severity │ Actions │ │ │ -│ │ │ i-01 │ PHONE_DETECTED│ HIGH │[Confirm][Dismiss]│ │ │ -│ │ │ i-02 │ MULTIPLE_FACES│ CRITICAL │[Confirm][Dismiss]│ │ │ -│ │ └────────────────────────────────────────────────────┘ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ 📊 PERFORMANCE METRICS │ │ -│ │ [Line chart: Response times over 24h] │ │ -│ │ Avg: 85ms P95: 220ms P99: 380ms │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `GET /api/admin/health` - System health -- `GET /api/admin/metrics/dashboard` - Dashboard metrics -- `GET /api/admin/metrics/realtime` - Real-time metrics -- `GET /api/admin/sessions` - List sessions -- `POST /api/admin/sessions/{id}/terminate` - Terminate -- `GET /api/admin/incidents` - List incidents -- `POST /api/admin/incidents/{id}/review` - Review incident - ---- - -### Module 17: Webhooks (`17_Webhooks.py`) - -**Purpose:** Demonstrate webhook integration - -**Features Demonstrated:** -- Webhook registration -- Event subscription -- Test delivery -- Webhook management - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ WEBHOOK MANAGEMENT │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ REGISTER NEW WEBHOOK │ │ -│ │ │ │ -│ │ URL: [https://your-server.com/webhook___________] │ │ -│ │ Secret: [your-webhook-secret____________________] │ │ -│ │ │ │ -│ │ Events: │ │ -│ │ ☑ enrollment.completed ☑ verification.completed │ │ -│ │ ☑ liveness.completed ☑ session.started │ │ -│ │ ☑ session.ended ☑ incident.created │ │ -│ │ │ │ -│ │ [REGISTER WEBHOOK] │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ════════════════ REGISTERED WEBHOOKS ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ ID │ URL │ Events │ Actions │ │ -│ ├──────────────────────────────────────────────────────────┤ │ -│ │ wh-001 │ https://api.ex.com/wh │ 4 │[Test][Delete]│ │ -│ │ wh-002 │ https://other.com/hook │ 2 │[Test][Delete]│ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ TEST WEBHOOK DELIVERY │ │ -│ │ │ │ -│ │ Select Webhook: [wh-001 ▼] │ │ -│ │ │ │ -│ │ [SEND TEST] │ │ -│ │ │ │ -│ │ Result: ✅ 200 OK (145ms) │ │ -│ │ Response: {"status": "received"} │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `POST /webhooks/register` - Register webhook -- `GET /webhooks` - List webhooks -- `DELETE /webhooks/{id}` - Delete webhook -- `POST /webhooks/{id}/test` - Test webhook - ---- - -### Module 18: Configuration (`18_Configuration.py`) - -**Purpose:** Demonstrate all configuration options - -**Features Demonstrated:** -- All 80+ configuration parameters -- Live config viewer -- Config reload capability -- Parameter documentation - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ SYSTEM CONFIGURATION │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ [Tabs: ML Models | Thresholds | Proctoring | Security | All] │ -│ │ -│ ════════════════ ML MODEL CONFIGURATION ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Face Detection │ │ -│ │ Backend: [mtcnn ▼] Options: opencv, ssd, mtcnn, │ │ -│ │ retinaface, mediapipe │ │ -│ │ │ │ -│ │ Face Recognition │ │ -│ │ Model: [Facenet ▼] Options: VGG-Face, Facenet, │ │ -│ │ Facenet512, ArcFace, etc. │ │ -│ │ │ │ -│ │ Landmark Model: [mediapipe_468 ▼] Options: dlib_68 │ │ -│ │ │ │ -│ │ Device: [cpu ▼] Options: cpu, cuda │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ════════════════ THRESHOLD CONFIGURATION ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Verification Threshold [====●=====] 0.60 │ │ -│ │ Liveness Threshold [=====●====] 70.0 │ │ -│ │ Quality Threshold [=====●====] 70.0 │ │ -│ │ Blur Threshold [====●=====] 100.0 │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ════════════════ PROCTORING CONFIGURATION ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ ☑ Gaze Tracking Enabled │ │ -│ │ • Gaze Away Threshold: [5.0] seconds │ │ -│ │ • Head Pitch Threshold: [20.0] degrees │ │ -│ │ • Head Yaw Threshold: [30.0] degrees │ │ -│ │ │ │ -│ │ ☑ Object Detection Enabled │ │ -│ │ • Model Size: [nano ▼] │ │ -│ │ • Confidence: [0.5] │ │ -│ │ • Max Persons: [1] │ │ -│ │ │ │ -│ │ ☑ Deepfake Detection Enabled │ │ -│ │ • Threshold: [0.6] │ │ -│ │ • Temporal Window: [10] frames │ │ -│ │ │ │ -│ │ ☐ Audio Analysis Enabled │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ [View Raw JSON] [Reload Configuration] │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `GET /api/admin/config` - Get configuration -- `POST /api/admin/config/reload` - Reload config - ---- - -### Module 19: API Explorer (`19_API_Explorer.py`) - -**Purpose:** Interactive API testing interface - -**Features Demonstrated:** -- All 46+ endpoints -- Request builder -- Response viewer -- Code generation -- OpenAPI documentation - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ API EXPLORER │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ ENDPOINT SELECTION │ │ -│ │ │ │ -│ │ Category: [Enrollment ▼] │ │ -│ │ Endpoint: [POST /enroll ▼] │ │ -│ │ │ │ -│ │ Description: Enroll a user's face with image validation │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ REQUEST BUILDER │ │ -│ │ │ │ -│ │ Headers: │ │ -│ │ Authorization: [Bearer ___________] │ │ -│ │ X-Tenant-ID: [demo-tenant________________] │ │ -│ │ │ │ -│ │ Body (form-data): │ │ -│ │ image: [Choose File] person_1.jpg │ │ -│ │ user_id: [demo_user_001________________] │ │ -│ │ │ │ -│ │ [SEND REQUEST] │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ RESPONSE (200 OK - 185ms) │ │ -│ │ ┌────────────────────────────────────────────────────┐ │ │ -│ │ │ { │ │ │ -│ │ │ "enrollment_id": "abc-123-def", │ │ │ -│ │ │ "user_id": "demo_user_001", │ │ │ -│ │ │ "quality_score": 0.94, │ │ │ -│ │ │ "created_at": "2025-12-12T14:30:00Z" │ │ │ -│ │ │ } │ │ │ -│ │ └────────────────────────────────────────────────────┘ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ CODE GENERATION │ │ -│ │ [Tabs: cURL | Python | JavaScript | Go] │ │ -│ │ ┌────────────────────────────────────────────────────┐ │ │ -│ │ │ curl -X POST http://localhost:8001/api/v1/enroll │ │ │ -│ │ │ -H "Authorization: Bearer " │ │ │ -│ │ │ -F "image=@person_1.jpg" │ │ │ -│ │ │ -F "user_id=demo_user_001" │ │ │ -│ │ └────────────────────────────────────────────────────┘ │ │ -│ │ [Copy to Clipboard] │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- All 46+ endpoints (dynamic) - ---- - -### Module 20: Embeddings Management (`20_Embeddings_Management.py`) - -**Purpose:** Demonstrate embedding export/import - -**Features Demonstrated:** -- Export all embeddings -- Import embeddings -- Merge/Replace/Skip modes -- Multi-tenant support - -**UI Elements:** -``` -┌─────────────────────────────────────────────────────────────────┐ -│ EMBEDDINGS MANAGEMENT │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ [Tabs: Export | Import] │ -│ │ -│ ════════════════ EXPORT EMBEDDINGS ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Export Settings │ │ -│ │ │ │ -│ │ Tenant: [All Tenants ▼] │ │ -│ │ Format: [JSON ▼] │ │ -│ │ │ │ -│ │ ☑ Include Metadata │ │ -│ │ ☑ Include Timestamps │ │ -│ │ │ │ -│ │ [EXPORT EMBEDDINGS] │ │ -│ │ │ │ -│ │ Status: Ready to export 1,247 embeddings │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ════════════════ IMPORT EMBEDDINGS ════════════════ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Import Settings │ │ -│ │ │ │ -│ │ File: [Choose File] embeddings_backup.json │ │ -│ │ │ │ -│ │ Import Mode: │ │ -│ │ ○ Merge (add new, keep existing) │ │ -│ │ ● Replace (overwrite existing) │ │ -│ │ ○ Skip Existing (only add new) │ │ -│ │ │ │ -│ │ Target Tenant: [demo-tenant ▼] │ │ -│ │ │ │ -│ │ [IMPORT EMBEDDINGS] │ │ -│ │ │ │ -│ │ Preview: 500 embeddings in file │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ IMPORT RESULTS │ │ -│ │ │ │ -│ │ ✅ Imported: 485 ⏭️ Skipped: 15 ❌ Failed: 0 │ │ -│ │ │ │ -│ │ [View Details] │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**API Calls:** -- `GET /embeddings/export` - Export embeddings -- `POST /embeddings/import` - Import embeddings - ---- - -## UI/UX Design - -### Color Scheme - -```css -/* Enterprise-grade color palette */ -:root { - --primary: #1E40AF; /* Deep blue - trust, security */ - --secondary: #0D9488; /* Teal - technology */ - --success: #059669; /* Green - success states */ - --warning: #D97706; /* Amber - warnings */ - --danger: #DC2626; /* Red - errors, critical */ - --background: #F8FAFC; /* Light gray - clean background */ - --surface: #FFFFFF; /* White - cards, surfaces */ - --text-primary: #1E293B; /* Dark slate - primary text */ - --text-secondary: #64748B; /* Gray - secondary text */ -} -``` - -### Typography - -| Element | Font | Size | Weight | -|---------|------|------|--------| -| Page Title | Inter | 28px | 700 | -| Section Header | Inter | 20px | 600 | -| Body Text | Inter | 14px | 400 | -| Labels | Inter | 12px | 500 | -| Code/Mono | JetBrains Mono | 13px | 400 | - -### Component Styling - -```css -/* Card component */ -.demo-card { - background: var(--surface); - border-radius: 12px; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); - padding: 24px; - border: 1px solid #E2E8F0; -} - -/* Metric card */ -.metric-card { - text-align: center; - padding: 20px; -} -.metric-value { font-size: 32px; font-weight: 700; } -.metric-label { font-size: 12px; color: var(--text-secondary); } - -/* Status badge */ -.status-badge { - padding: 4px 12px; - border-radius: 9999px; - font-size: 12px; - font-weight: 500; -} -.status-success { background: #D1FAE5; color: #065F46; } -.status-warning { background: #FEF3C7; color: #92400E; } -.status-danger { background: #FEE2E2; color: #991B1B; } -``` - ---- - -## Software Engineering Compliance - -This section ensures the Demo Application adheres to all software engineering best practices as defined in the SE Checklist. - ---- - -### SOLID Principles Implementation - -#### S - Single Responsibility Principle - -Each module has ONE reason to change: - -| Module | Single Responsibility | -|--------|----------------------| -| `api_client.py` | HTTP communication with backend API | -| `image_utils.py` | Image processing and validation | -| `websocket_client.py` | WebSocket connection management | -| `session_state.py` | Streamlit session state management | -| Each page file | Demo UI for ONE specific feature | - -**Enforcement:** -- Maximum 200 lines per file (excluding imports/docstrings) -- Each class/module handles exactly one concern -- If a file grows beyond scope, extract to new module - -#### O - Open/Closed Principle - -**Extension Points:** - -```python -# components/base_component.py -from abc import ABC, abstractmethod - -class BaseComponent(ABC): - """Base class for all UI components - open for extension, closed for modification.""" - - @abstractmethod - def render(self) -> None: - """Render the component. Override in subclasses.""" - pass - - @abstractmethod - def get_state(self) -> dict: - """Get component state. Override in subclasses.""" - pass - - -# New components extend without modifying base -class MetricsCard(BaseComponent): - def render(self) -> None: - # Custom rendering logic - pass -``` - -**Factory Pattern for Extension:** - -```python -# utils/component_factory.py -from typing import Protocol, Type - -class ComponentFactory: - """Factory for creating components - add new types without modifying existing code.""" - - _registry: dict[str, Type[BaseComponent]] = {} - - @classmethod - def register(cls, name: str, component_class: Type[BaseComponent]) -> None: - cls._registry[name] = component_class - - @classmethod - def create(cls, name: str, **kwargs) -> BaseComponent: - if name not in cls._registry: - raise ValueError(f"Unknown component: {name}") - return cls._registry[name](**kwargs) -``` - -#### L - Liskov Substitution Principle - -All implementations are substitutable for their base types: - -```python -# utils/protocols.py -from typing import Protocol, runtime_checkable - -@runtime_checkable -class IAPIClient(Protocol): - """Protocol for API clients - any implementation must satisfy this contract.""" - - async def get(self, endpoint: str) -> dict: - """Perform GET request.""" - ... - - async def post(self, endpoint: str, data: dict | None = None, files: dict | None = None) -> dict: - """Perform POST request.""" - ... - - async def delete(self, endpoint: str) -> dict: - """Perform DELETE request.""" - ... - - def health_check(self) -> bool: - """Check API health.""" - ... - - -# Both implementations satisfy the protocol -class HTTPAPIClient(IAPIClient): - """Production API client using httpx.""" - pass - -class MockAPIClient(IAPIClient): - """Mock API client for testing.""" - pass -``` - -#### I - Interface Segregation Principle - -Focused interfaces instead of one large interface: - -```python -# utils/protocols.py - -class IImageProcessor(Protocol): - """Interface for image processing only.""" - def resize(self, image: bytes, max_size: tuple[int, int]) -> bytes: ... - def compress(self, image: bytes, quality: int) -> bytes: ... - def validate(self, image: bytes) -> bool: ... - -class IWebSocketHandler(Protocol): - """Interface for WebSocket handling only.""" - async def connect(self, url: str) -> None: ... - async def send(self, data: bytes) -> None: ... - async def receive(self) -> bytes: ... - async def close(self) -> None: ... - -class ISessionManager(Protocol): - """Interface for session state management only.""" - def get(self, key: str) -> Any: ... - def set(self, key: str, value: Any) -> None: ... - def clear(self) -> None: ... - -class ICacheManager(Protocol): - """Interface for caching only.""" - def get(self, key: str) -> Any | None: ... - def set(self, key: str, value: Any, ttl: int) -> None: ... - def invalidate(self, key: str) -> None: ... -``` - -#### D - Dependency Inversion Principle - -High-level modules depend on abstractions, not concretions: - -```python -# utils/container.py -from dataclasses import dataclass -from utils.protocols import IAPIClient, IImageProcessor, ICacheManager - -@dataclass -class DependencyContainer: - """Dependency injection container - invert dependencies via abstractions.""" - - api_client: IAPIClient - image_processor: IImageProcessor - cache_manager: ICacheManager - - @classmethod - def create_production(cls) -> "DependencyContainer": - """Create container with production dependencies.""" - return cls( - api_client=HTTPAPIClient(base_url="http://localhost:8001"), - image_processor=PILImageProcessor(), - cache_manager=StreamlitCacheManager(), - ) - - @classmethod - def create_testing(cls) -> "DependencyContainer": - """Create container with mock dependencies for testing.""" - return cls( - api_client=MockAPIClient(), - image_processor=MockImageProcessor(), - cache_manager=InMemoryCacheManager(), - ) - - -# Pages receive dependencies via injection -def render_enrollment_page(container: DependencyContainer) -> None: - """Render enrollment page with injected dependencies.""" - api = container.api_client # Depends on abstraction, not HTTPAPIClient - processor = container.image_processor - # ... -``` - ---- - -### Design Patterns Applied - -| Pattern | Usage | Location | -|---------|-------|----------| -| **Factory** | Create components/clients dynamically | `utils/component_factory.py` | -| **Strategy** | Interchangeable API client strategies | `utils/api_client.py` | -| **Observer** | Real-time metric updates | `components/metrics_card.py` | -| **Facade** | Simplified API interface | `utils/api_client.py` | -| **Singleton** | Single dependency container instance | `utils/container.py` | -| **Template Method** | Base page rendering workflow | `components/base_page.py` | -| **Adapter** | Adapt WebSocket to async interface | `utils/websocket_client.py` | -| **Decorator** | Add caching to API calls | `utils/decorators.py` | - -**Pattern Implementations:** - -```python -# Strategy Pattern - Interchangeable liveness modes -class LivenessStrategy(Protocol): - def check(self, image: bytes) -> dict: ... - -class PassiveLivenessStrategy: - def check(self, image: bytes) -> dict: - return api.post("/liveness", {"mode": "passive"}, files={"file": image}) - -class ActiveLivenessStrategy: - def check(self, image: bytes) -> dict: - return api.post("/liveness", {"mode": "active"}, files={"file": image}) - -class CombinedLivenessStrategy: - def check(self, image: bytes) -> dict: - return api.post("/liveness", {"mode": "combined"}, files={"file": image}) - - -# Decorator Pattern - Add caching to API calls -from functools import wraps - -def cached(ttl_seconds: int = 60): - """Decorator to cache API responses.""" - def decorator(func): - @wraps(func) - async def wrapper(*args, **kwargs): - cache_key = f"{func.__name__}:{hash(str(args) + str(kwargs))}" - cached_value = cache_manager.get(cache_key) - if cached_value is not None: - return cached_value - result = await func(*args, **kwargs) - cache_manager.set(cache_key, result, ttl_seconds) - return result - return wrapper - return decorator - - -# Template Method - Base page workflow -class BaseDemoPage(ABC): - """Template method pattern for consistent page structure.""" - - def render(self) -> None: - """Template method defining page rendering workflow.""" - self._render_header() - self._render_sidebar_options() - self._render_main_content() # Abstract - subclasses implement - self._render_results() - self._render_footer() - - def _render_header(self) -> None: - st.title(self.title) - st.markdown(self.description) - - @abstractmethod - def _render_main_content(self) -> None: - """Subclasses must implement main content rendering.""" - pass -``` - ---- - -### Code Quality Standards - -#### Formatting & Linting - -```toml -# pyproject.toml -[tool.black] -line-length = 100 -target-version = ['py311'] -include = '\.pyi?$' - -[tool.ruff] -line-length = 100 -select = ["E", "F", "W", "I", "N", "D", "UP", "B", "C4", "SIM"] -ignore = ["D100", "D104"] # Allow missing module/package docstrings - -[tool.isort] -profile = "black" -line_length = 100 - -[tool.mypy] -python_version = "3.11" -strict = true -warn_return_any = true -warn_unused_ignores = true -disallow_untyped_defs = true -``` - -#### Code Metrics Limits - -| Metric | Maximum | Rationale | -|--------|---------|-----------| -| **Lines per file** | 300 | Maintainability | -| **Lines per function** | 25 | Single responsibility | -| **Function arguments** | 4 | Reduce complexity | -| **Cyclomatic complexity** | 10 | Testability | -| **Nesting depth** | 3 | Readability | -| **Class methods** | 10 | Cohesion | - -#### Naming Conventions - -```python -# Variables: snake_case, descriptive -user_face_image: bytes -enrollment_result: dict -similarity_threshold: float - -# Functions: snake_case, verb-noun -def process_face_image(image: bytes) -> dict: ... -def validate_enrollment_request(request: EnrollmentRequest) -> bool: ... -def calculate_similarity_score(embedding1: list, embedding2: list) -> float: ... - -# Classes: PascalCase, noun -class FaceEnrollmentPage(BaseDemoPage): ... -class APIClientFactory: ... -class WebSocketStreamHandler: ... - -# Constants: UPPER_SNAKE_CASE -MAX_IMAGE_SIZE_MB: int = 10 -DEFAULT_SIMILARITY_THRESHOLD: float = 0.6 -API_TIMEOUT_SECONDS: int = 30 - -# Private members: leading underscore -def _internal_helper(self) -> None: ... -_cache: dict[str, Any] = {} -``` - -#### Type Hints (Required) - -```python -from typing import Any, TypeVar, Generic, Callable -from collections.abc import Sequence, Mapping - -T = TypeVar("T") - -# All functions must have complete type hints -def process_enrollment( - image: bytes, - user_id: str, - tenant_id: str = "default", - metadata: dict[str, Any] | None = None, -) -> EnrollmentResult: - """Process face enrollment with full type safety.""" - ... - -# Generic types for reusable components -class ResultContainer(Generic[T]): - def __init__(self, data: T, success: bool, message: str) -> None: - self.data = data - self.success = success - self.message = message -``` - ---- - -### Error Handling Design - -#### Exception Hierarchy - -```python -# utils/exceptions.py - -class DemoAppError(Exception): - """Base exception for all demo app errors.""" - - def __init__(self, message: str, code: str, details: dict | None = None) -> None: - super().__init__(message) - self.message = message - self.code = code - self.details = details or {} - - def to_user_message(self) -> str: - """Convert to user-friendly message.""" - return self.message - - -class APIConnectionError(DemoAppError): - """Raised when API is unreachable.""" - - def __init__(self, details: dict | None = None) -> None: - super().__init__( - message="Cannot connect to Biometric Processor API. Please ensure the server is running.", - code="API_CONNECTION_ERROR", - details=details, - ) - - -class APIResponseError(DemoAppError): - """Raised when API returns an error response.""" - - def __init__(self, status_code: int, response_body: dict) -> None: - super().__init__( - message=f"API returned error: {response_body.get('detail', 'Unknown error')}", - code="API_RESPONSE_ERROR", - details={"status_code": status_code, "response": response_body}, - ) - - -class ImageValidationError(DemoAppError): - """Raised when image validation fails.""" - - def __init__(self, reason: str) -> None: - super().__init__( - message=f"Image validation failed: {reason}", - code="IMAGE_VALIDATION_ERROR", - details={"reason": reason}, - ) - - -class WebSocketError(DemoAppError): - """Raised for WebSocket connection issues.""" - - def __init__(self, reason: str) -> None: - super().__init__( - message=f"WebSocket error: {reason}", - code="WEBSOCKET_ERROR", - details={"reason": reason}, - ) - - -class SessionExpiredError(DemoAppError): - """Raised when proctoring session has expired.""" - pass - - -class RateLimitExceededError(DemoAppError): - """Raised when API rate limit is exceeded.""" - - def __init__(self, retry_after: int) -> None: - super().__init__( - message=f"Rate limit exceeded. Please wait {retry_after} seconds.", - code="RATE_LIMIT_EXCEEDED", - details={"retry_after": retry_after}, - ) -``` - -#### Error Handler Decorator - -```python -# utils/error_handler.py -import streamlit as st -from functools import wraps -from typing import Callable, TypeVar, ParamSpec - -P = ParamSpec("P") -R = TypeVar("R") - -def handle_errors(show_traceback: bool = False) -> Callable[[Callable[P, R]], Callable[P, R | None]]: - """Decorator for consistent error handling across all pages.""" - - def decorator(func: Callable[P, R]) -> Callable[P, R | None]: - @wraps(func) - def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None: - try: - return func(*args, **kwargs) - except APIConnectionError as e: - st.error(f"🔌 {e.to_user_message()}") - st.info("💡 Tip: Run `uvicorn app.main:app --port 8001` to start the API") - return None - except APIResponseError as e: - st.error(f"❌ {e.to_user_message()}") - if e.details.get("status_code") == 422: - st.warning("Check your input parameters") - return None - except ImageValidationError as e: - st.warning(f"🖼️ {e.to_user_message()}") - return None - except RateLimitExceededError as e: - st.warning(f"⏳ {e.to_user_message()}") - return None - except WebSocketError as e: - st.error(f"🔗 {e.to_user_message()}") - return None - except Exception as e: - st.error(f"💥 An unexpected error occurred: {str(e)}") - if show_traceback: - st.exception(e) - return None - return wrapper - return decorator - - -# Usage in pages -@handle_errors(show_traceback=False) -def perform_enrollment(image: bytes, user_id: str) -> dict | None: - result = api_client.post("/enroll", files={"file": image}, data={"user_id": user_id}) - return result -``` - -#### Graceful Degradation - -```python -# utils/graceful_degradation.py - -class GracefulDegradation: - """Handle API unavailability gracefully.""" - - @staticmethod - def with_fallback(primary_func: Callable, fallback_value: Any, error_message: str) -> Any: - """Execute primary function, return fallback on failure.""" - try: - return primary_func() - except DemoAppError: - st.warning(error_message) - return fallback_value - - @staticmethod - def render_offline_mode() -> None: - """Render offline mode UI when API is unavailable.""" - st.warning("⚠️ API is currently unavailable. Running in offline demo mode.") - st.info(""" - **Available in Offline Mode:** - - View UI layouts and interactions - - See sample responses - - Explore configuration options - - **Requires API Connection:** - - Actual face processing - - Real-time proctoring - - Live data operations - """) -``` - ---- - -### Testing Strategy - -#### Test Structure - -``` -tests/ -├── __init__.py -├── conftest.py # Shared fixtures -├── unit/ -│ ├── __init__.py -│ ├── test_api_client.py # API client unit tests -│ ├── test_image_utils.py # Image processing tests -│ ├── test_session_state.py # Session management tests -│ ├── test_websocket_client.py # WebSocket tests -│ └── components/ -│ ├── test_metrics_card.py -│ ├── test_result_display.py -│ └── test_image_uploader.py -├── integration/ -│ ├── __init__.py -│ ├── test_api_integration.py # API integration tests -│ ├── test_page_rendering.py # Page rendering tests -│ └── test_websocket_flow.py # WebSocket flow tests -├── e2e/ -│ ├── __init__.py -│ ├── test_enrollment_workflow.py -│ ├── test_verification_workflow.py -│ └── test_proctoring_workflow.py -└── fixtures/ - ├── sample_images/ - │ ├── valid_face.jpg - │ ├── no_face.jpg - │ ├── multiple_faces.jpg - │ └── blurry_face.jpg - └── mock_responses/ - ├── enrollment_success.json - ├── verification_match.json - └── liveness_pass.json -``` - -#### Test Configuration - -```python -# conftest.py -import pytest -from unittest.mock import AsyncMock, MagicMock -from utils.container import DependencyContainer -from utils.protocols import IAPIClient - -@pytest.fixture -def mock_api_client() -> IAPIClient: - """Create mock API client for testing.""" - client = AsyncMock(spec=IAPIClient) - client.health_check.return_value = True - return client - -@pytest.fixture -def test_container(mock_api_client: IAPIClient) -> DependencyContainer: - """Create test dependency container.""" - return DependencyContainer.create_testing() - -@pytest.fixture -def sample_face_image() -> bytes: - """Load sample face image for testing.""" - with open("tests/fixtures/sample_images/valid_face.jpg", "rb") as f: - return f.read() - -@pytest.fixture -def mock_enrollment_response() -> dict: - """Load mock enrollment response.""" - return { - "enrollment_id": "test-123", - "user_id": "test_user", - "quality_score": 0.95, - "created_at": "2025-12-12T14:30:00Z" - } -``` - -#### Unit Test Example - -```python -# tests/unit/test_api_client.py -import pytest -from unittest.mock import AsyncMock, patch -from utils.api_client import HTTPAPIClient -from utils.exceptions import APIConnectionError, APIResponseError - -class TestHTTPAPIClient: - """Unit tests for HTTP API client.""" - - @pytest.mark.asyncio - async def test_get_success(self, mock_api_client: AsyncMock) -> None: - """Test successful GET request.""" - mock_api_client.get.return_value = {"status": "healthy"} - - result = await mock_api_client.get("/health") - - assert result["status"] == "healthy" - mock_api_client.get.assert_called_once_with("/health") - - @pytest.mark.asyncio - async def test_post_with_file(self, mock_api_client: AsyncMock, sample_face_image: bytes) -> None: - """Test POST request with file upload.""" - mock_api_client.post.return_value = {"success": True} - - result = await mock_api_client.post( - "/enroll", - data={"user_id": "test"}, - files={"file": sample_face_image} - ) - - assert result["success"] is True - - @pytest.mark.asyncio - async def test_connection_error_handling(self) -> None: - """Test that connection errors are properly wrapped.""" - client = HTTPAPIClient(base_url="http://invalid-host:9999") - - with pytest.raises(APIConnectionError): - await client.get("/health") - - @pytest.mark.asyncio - async def test_rate_limit_response(self, mock_api_client: AsyncMock) -> None: - """Test rate limit response handling.""" - mock_api_client.post.side_effect = APIResponseError( - status_code=429, - response_body={"detail": "Rate limit exceeded", "retry_after": 60} - ) - - with pytest.raises(APIResponseError) as exc_info: - await mock_api_client.post("/enroll", data={}) - - assert exc_info.value.details["status_code"] == 429 -``` - -#### Integration Test Example - -```python -# tests/integration/test_enrollment_workflow.py -import pytest -from pages.enrollment import EnrollmentPage -from utils.container import DependencyContainer - -class TestEnrollmentWorkflow: - """Integration tests for enrollment workflow.""" - - @pytest.fixture - def enrollment_page(self, test_container: DependencyContainer) -> EnrollmentPage: - return EnrollmentPage(container=test_container) - - def test_enrollment_with_valid_image( - self, - enrollment_page: EnrollmentPage, - sample_face_image: bytes, - mock_enrollment_response: dict - ) -> None: - """Test complete enrollment workflow with valid image.""" - # Arrange - enrollment_page.container.api_client.post.return_value = mock_enrollment_response - - # Act - result = enrollment_page.process_enrollment( - image=sample_face_image, - user_id="test_user" - ) - - # Assert - assert result is not None - assert result["enrollment_id"] == "test-123" - assert result["quality_score"] >= 0.9 - - def test_enrollment_with_no_face( - self, - enrollment_page: EnrollmentPage - ) -> None: - """Test enrollment fails gracefully when no face detected.""" - no_face_image = load_fixture("no_face.jpg") - enrollment_page.container.api_client.post.return_value = { - "success": False, - "error": "No face detected" - } - - result = enrollment_page.process_enrollment( - image=no_face_image, - user_id="test_user" - ) - - assert result["success"] is False -``` - -#### Coverage Requirements - -```toml -# pyproject.toml -[tool.pytest.ini_options] -minversion = "7.0" -testpaths = ["tests"] -python_files = ["test_*.py"] -python_functions = ["test_*"] -addopts = [ - "--cov=.", - "--cov-report=term-missing", - "--cov-report=html:coverage_html", - "--cov-fail-under=80", - "-v", - "--strict-markers", -] - -[tool.coverage.run] -source = ["components", "utils", "pages"] -omit = ["tests/*", "*/__pycache__/*"] - -[tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "def __repr__", - "raise NotImplementedError", - "if TYPE_CHECKING:", -] -``` - ---- - -### Performance Optimization - -#### Image Optimization - -```python -# utils/image_utils.py -from PIL import Image -import io - -class ImageOptimizer: - """Optimize images before upload to reduce bandwidth and processing time.""" - - MAX_DIMENSION: int = 1920 - QUALITY: int = 85 - TARGET_SIZE_KB: int = 500 - - @classmethod - def optimize_for_upload(cls, image_bytes: bytes) -> bytes: - """Optimize image for API upload.""" - image = Image.open(io.BytesIO(image_bytes)) - - # Convert to RGB if necessary - if image.mode in ("RGBA", "P"): - image = image.convert("RGB") - - # Resize if too large - if max(image.size) > cls.MAX_DIMENSION: - image.thumbnail((cls.MAX_DIMENSION, cls.MAX_DIMENSION), Image.Resampling.LANCZOS) - - # Compress with quality adjustment - output = io.BytesIO() - quality = cls.QUALITY - - while quality > 20: - output.seek(0) - output.truncate() - image.save(output, format="JPEG", quality=quality, optimize=True) - - if output.tell() <= cls.TARGET_SIZE_KB * 1024: - break - quality -= 10 - - return output.getvalue() -``` - -#### API Response Caching - -```python -# utils/cache.py -import streamlit as st -from datetime import datetime, timedelta -from typing import Any, Callable -from functools import wraps - -class CacheManager: - """Manage API response caching with TTL.""" - - def __init__(self) -> None: - if "cache" not in st.session_state: - st.session_state.cache = {} - if "cache_timestamps" not in st.session_state: - st.session_state.cache_timestamps = {} - - def get(self, key: str) -> Any | None: - """Get cached value if not expired.""" - if key not in st.session_state.cache: - return None - - timestamp = st.session_state.cache_timestamps.get(key) - if timestamp and datetime.now() > timestamp: - self.invalidate(key) - return None - - return st.session_state.cache[key] - - def set(self, key: str, value: Any, ttl_seconds: int = 60) -> None: - """Set cached value with TTL.""" - st.session_state.cache[key] = value - st.session_state.cache_timestamps[key] = datetime.now() + timedelta(seconds=ttl_seconds) - - def invalidate(self, key: str) -> None: - """Invalidate cached value.""" - st.session_state.cache.pop(key, None) - st.session_state.cache_timestamps.pop(key, None) - - -# Cache decorator -def cached_api_call(ttl_seconds: int = 60): - """Decorator to cache API call results.""" - cache = CacheManager() - - def decorator(func: Callable) -> Callable: - @wraps(func) - def wrapper(*args, **kwargs) -> Any: - cache_key = f"{func.__name__}:{hash(str(args) + str(kwargs))}" - - cached = cache.get(cache_key) - if cached is not None: - return cached - - result = func(*args, **kwargs) - cache.set(cache_key, result, ttl_seconds) - return result - return wrapper - return decorator - - -# Usage -@cached_api_call(ttl_seconds=300) # Cache config for 5 minutes -def get_system_config() -> dict: - return api_client.get("/api/admin/config") -``` - -#### Lazy Loading - -```python -# utils/lazy_loader.py -import streamlit as st -from typing import Callable, Any - -class LazyLoader: - """Lazy load heavy resources only when needed.""" - - @staticmethod - @st.cache_resource(ttl=3600) - def load_sample_images() -> dict[str, bytes]: - """Load sample images only once, cache for 1 hour.""" - samples = {} - sample_dir = Path("assets/sample_faces") - for img_path in sample_dir.glob("*.jpg"): - with open(img_path, "rb") as f: - samples[img_path.stem] = f.read() - return samples - - @staticmethod - @st.cache_data(ttl=60) - def fetch_dashboard_metrics() -> dict: - """Fetch dashboard metrics with 1-minute cache.""" - return api_client.get("/api/admin/metrics/dashboard") - - @staticmethod - def load_page_on_demand(page_name: str) -> Callable: - """Dynamically import page module only when needed.""" - import importlib - module = importlib.import_module(f"pages.{page_name}") - return module.render -``` - -#### WebSocket Connection Pooling - -```python -# utils/websocket_pool.py -import asyncio -from collections import deque -from websockets import connect, WebSocketClientProtocol - -class WebSocketPool: - """Pool WebSocket connections for reuse.""" - - def __init__(self, max_size: int = 5) -> None: - self._pool: deque[WebSocketClientProtocol] = deque(maxlen=max_size) - self._lock = asyncio.Lock() - - async def acquire(self, url: str) -> WebSocketClientProtocol: - """Acquire a WebSocket connection from pool or create new.""" - async with self._lock: - if self._pool: - ws = self._pool.popleft() - if ws.open: - return ws - - return await connect(url) - - async def release(self, ws: WebSocketClientProtocol) -> None: - """Return WebSocket connection to pool.""" - async with self._lock: - if ws.open and len(self._pool) < self._pool.maxlen: - self._pool.append(ws) - else: - await ws.close() -``` - ---- - -### Anti-Pattern Avoidance - -#### God Object Prevention - -```python -# BAD - God Object (DON'T DO THIS) -class DemoApp: - def __init__(self): - self.api_client = HTTPClient() - self.image_processor = ImageProcessor() - self.websocket = WebSocketClient() - self.cache = CacheManager() - self.session = SessionManager() - # ... 20 more responsibilities - -# GOOD - Single Responsibility -class EnrollmentService: - """Handles ONLY enrollment-related operations.""" - - def __init__(self, api_client: IAPIClient, image_processor: IImageProcessor) -> None: - self._api = api_client - self._processor = image_processor - - def enroll(self, image: bytes, user_id: str) -> EnrollmentResult: - optimized = self._processor.optimize(image) - return self._api.post("/enroll", files={"file": optimized}, data={"user_id": user_id}) -``` - -#### Avoiding Spaghetti Code - -```python -# BAD - Spaghetti code with deep nesting -def process_verification(image, user_id): - if image: - if validate_image(image): - if check_face(image): - result = api.verify(image, user_id) - if result: - if result['verified']: - if result['confidence'] > 0.8: - return "High confidence match" - else: - return "Low confidence match" - else: - return "No match" - -# GOOD - Early returns, flat structure -def process_verification(image: bytes, user_id: str) -> VerificationResult: - """Process verification with early returns for clarity.""" - if not image: - raise ImageValidationError("No image provided") - - if not validate_image(image): - raise ImageValidationError("Invalid image format") - - if not check_face(image): - raise ImageValidationError("No face detected in image") - - result = api.verify(image, user_id) - - if not result.verified: - return VerificationResult(matched=False, confidence=0.0, message="No match found") - - confidence_level = "high" if result.confidence > 0.8 else "low" - return VerificationResult( - matched=True, - confidence=result.confidence, - message=f"{confidence_level.title()} confidence match" - ) -``` - -#### No Magic Numbers - -```python -# BAD - Magic numbers -if score > 0.6: - return "verified" -if size < 80: - return "face too small" - -# GOOD - Named constants -class Thresholds: - """Configuration thresholds as named constants.""" - VERIFICATION_SIMILARITY: float = 0.6 - MIN_FACE_SIZE_PX: int = 80 - LIVENESS_SCORE: float = 70.0 - QUALITY_SCORE: float = 70.0 - BLUR_VARIANCE: float = 100.0 - MAX_IMAGE_SIZE_MB: int = 10 - API_TIMEOUT_SECONDS: int = 30 - CACHE_TTL_SECONDS: int = 60 - WEBSOCKET_RECONNECT_ATTEMPTS: int = 3 - -# Usage -if score > Thresholds.VERIFICATION_SIMILARITY: - return "verified" -``` - -#### No Dead Code - -```python -# Enforced via pre-commit hook -# .pre-commit-config.yaml -repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 - hooks: - - id: ruff - args: [--fix, --select, F401, F841] # Remove unused imports and variables -``` - ---- - -### Version Control Best Practices - -#### Commit Message Convention - -``` -# Format: (): - -# Types: -# feat: New feature -# fix: Bug fix -# docs: Documentation only -# style: Formatting, no code change -# refactor: Code restructure, no feature change -# test: Adding tests -# chore: Maintenance tasks - -# Examples: -feat(enrollment): add webcam capture option -fix(api-client): handle timeout errors gracefully -docs(readme): update installation instructions -refactor(utils): extract image processing to separate module -test(verification): add integration tests for 1:1 matching -chore(deps): update streamlit to 1.29.0 -``` - -#### Branch Strategy - -``` -main # Production-ready code -├── develop # Integration branch -│ ├── feature/enrollment-webcam -│ ├── feature/proctoring-realtime -│ └── feature/admin-dashboard -├── release/1.0.0 # Release candidates -└── hotfix/api-timeout # Emergency fixes -``` - -#### Pre-commit Hooks - -```yaml -# .pre-commit-config.yaml -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-added-large-files - args: ['--maxkb=1000'] - - id: check-merge-conflict - - id: detect-private-key - - - repo: https://github.com/psf/black - rev: 23.12.1 - hooks: - - id: black - - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 - hooks: - - id: ruff - args: [--fix] - - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 - hooks: - - id: mypy - additional_dependencies: [types-all] -``` - ---- - -### Documentation Standards - -#### Docstring Format (Google Style) - -```python -def process_face_enrollment( - image: bytes, - user_id: str, - tenant_id: str = "default", - metadata: dict[str, Any] | None = None, -) -> EnrollmentResult: - """Process face enrollment with quality validation. - - Validates the image quality, extracts face embedding, and stores - the enrollment in the database. - - Args: - image: Raw image bytes (JPEG or PNG format). - user_id: Unique identifier for the user being enrolled. - tenant_id: Tenant identifier for multi-tenant isolation. - metadata: Optional metadata to attach to enrollment. - - Returns: - EnrollmentResult containing enrollment_id, quality_score, - and created_at timestamp. - - Raises: - ImageValidationError: If image format is invalid or no face detected. - APIConnectionError: If API server is unreachable. - DuplicateEnrollmentError: If user_id already enrolled in tenant. - - Example: - >>> result = process_face_enrollment( - ... image=face_bytes, - ... user_id="john_doe", - ... metadata={"department": "engineering"} - ... ) - >>> print(result.enrollment_id) - 'abc-123-def' - """ -``` - -#### Module Documentation - -```python -"""Face Enrollment Demo Page. - -This module provides the UI for demonstrating face enrollment functionality. -Users can upload images or use webcam to enroll faces with quality validation. - -Features: - - Single face enrollment with quality assessment - - Webcam capture support - - Duplicate detection - - Multi-tenant enrollment - - Metadata attachment - -Usage: - This page is automatically loaded by Streamlit multipage app. - Navigate to "Face Enrollment" in the sidebar to access. - -Dependencies: - - streamlit >= 1.29.0 - - httpx >= 0.25.0 - - Pillow >= 10.0.0 -""" -``` - -#### README Template - -```markdown -# Demo Application - -## Quick Start -\`\`\`bash -pip install -r requirements.txt -streamlit run app.py -\`\`\` - -## Features -- [Feature list with status] - -## Configuration -- [Environment variables] - -## API Reference -- [Link to API docs] - -## Testing -\`\`\`bash -pytest --cov -\`\`\` - -## Contributing -- [Contribution guidelines] -``` - ---- - -## Implementation Plan - -### Phase 1: Foundation (Core Setup + SE Compliance) -- [ ] Project structure setup (following SE folder conventions) -- [ ] Configuration management with typed settings -- [ ] API client wrapper with Protocol interfaces (DIP) -- [ ] Custom CSS styling following design system -- [ ] Reusable components (sidebar, header, cards) with BaseComponent ABC (OCP) -- [ ] Session state management with ISessionManager interface -- [ ] Exception hierarchy implementation (error handling) -- [ ] Dependency injection container setup -- [ ] Pre-commit hooks configuration -- [ ] pyproject.toml with all quality tools (Black, Ruff, mypy) - -### Phase 2: Core Biometrics -- [ ] Welcome page -- [ ] Face Enrollment demo -- [ ] Face Verification demo -- [ ] Face Search demo -- [ ] Liveness Detection demo - -### Phase 3: Advanced Analysis -- [ ] Quality Analysis demo -- [ ] Demographics demo -- [ ] Facial Landmarks demo -- [ ] Face Comparison demo -- [ ] Similarity Matrix demo -- [ ] Multi-Face Detection demo -- [ ] Card Type Detection demo - -### Phase 4: Proctoring Suite -- [ ] Proctoring Session Management -- [ ] Real-time Proctoring (WebSocket) -- [ ] Incident visualization - -### Phase 5: Administration -- [ ] Admin Dashboard -- [ ] Webhooks Management -- [ ] Configuration Viewer -- [ ] API Explorer -- [ ] Embeddings Management - -### Phase 6: Quality Assurance & SE Compliance Verification -- [ ] Sample data/images for all features -- [ ] Error handling with graceful degradation -- [ ] Performance optimization (caching, lazy loading, image compression) -- [ ] Documentation (Google-style docstrings, README) -- [ ] Unit tests (80%+ coverage target) -- [ ] Integration tests for critical workflows -- [ ] E2E tests for complete user journeys -- [ ] SE Checklist compliance audit -- [ ] Code quality metrics verification (complexity, line limits) -- [ ] Security review (no secrets, input validation) - ---- - -## Deployment Options - -### Option 1: Local Development -```bash -# Run locally -cd demo/ -pip install -r requirements.txt -streamlit run app.py -# Opens at http://localhost:8501 -``` - -### Option 2: Docker Deployment -```dockerfile -FROM python:3.11-slim -WORKDIR /app -COPY demo/ . -RUN pip install -r requirements.txt -EXPOSE 8501 -CMD ["streamlit", "run", "app.py", "--server.port=8501"] -``` - -### Option 3: Streamlit Cloud -- Push to GitHub -- Connect to Streamlit Cloud -- Auto-deploy on push - -### Option 4: Kubernetes -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: biometric-demo -spec: - replicas: 2 - template: - spec: - containers: - - name: demo - image: biometric-demo:1.0.0 - ports: - - containerPort: 8501 -``` - ---- - -## Feature Coverage Checklist - -### Core Biometrics (5/5) -- [x] Face Enrollment -- [x] Face Verification -- [x] Face Search -- [x] Liveness Detection -- [x] Batch Processing - -### Advanced Analysis (7/7) -- [x] Quality Analysis -- [x] Demographics -- [x] Facial Landmarks -- [x] Face Comparison -- [x] Similarity Matrix -- [x] Multi-Face Detection -- [x] Card Type Detection - -### Proctoring (20+ features) -- [x] Session Management (CRUD) -- [x] Session Lifecycle (Start/Pause/Resume/End) -- [x] Frame Analysis -- [x] Gaze Tracking -- [x] Object Detection -- [x] Deepfake Detection -- [x] Audio Analysis -- [x] Incident Management -- [x] Risk Scoring -- [x] Session Reports -- [x] WebSocket Streaming -- [x] Rate Limiting -- [x] Circuit Breaker - -### Administration (6/6) -- [x] Health Monitoring -- [x] Dashboard Metrics -- [x] Session Management -- [x] Incident Review -- [x] Configuration -- [x] Webhooks - -### Infrastructure (5/5) -- [x] Export Embeddings -- [x] Import Embeddings -- [x] API Explorer -- [x] Multi-tenant Support -- [x] All 80+ Configuration Options - ---- - -## SE Checklist Compliance Summary - -### SOLID Principles ✅ - -| Principle | Implementation | Status | -|-----------|----------------|--------| -| **S - Single Responsibility** | Each module has one reason to change | ✅ Complete | -| **O - Open/Closed** | BaseComponent ABC, ComponentFactory | ✅ Complete | -| **L - Liskov Substitution** | Protocol-based interfaces (IAPIClient, etc.) | ✅ Complete | -| **I - Interface Segregation** | Separate protocols (IImageProcessor, ICacheManager) | ✅ Complete | -| **D - Dependency Inversion** | DependencyContainer with abstraction injection | ✅ Complete | - -### Design Principles ✅ - -| Principle | Implementation | Status | -|-----------|----------------|--------| -| **DRY** | Reusable components/, utils/, decorators | ✅ Complete | -| **KISS** | Simple folder structure, clear naming | ✅ Complete | -| **YAGNI** | Only implemented features, no speculation | ✅ Complete | -| **Separation of Concerns** | Pages, Components, Utils, Assets layers | ✅ Complete | -| **Composition Over Inheritance** | Component composition via DI container | ✅ Complete | - -### Design Patterns Applied ✅ - -| Pattern | Location | Status | -|---------|----------|--------| -| Factory | ComponentFactory, DependencyContainer | ✅ | -| Strategy | LivenessStrategy implementations | ✅ | -| Observer | Real-time metrics updates | ✅ | -| Facade | Simplified API client interface | ✅ | -| Singleton | Dependency container instance | ✅ | -| Template Method | BaseDemoPage rendering workflow | ✅ | -| Adapter | WebSocket to async interface | ✅ | -| Decorator | Caching, error handling decorators | ✅ | - -### Code Quality ✅ - -| Aspect | Standard | Status | -|--------|----------|--------| -| Formatter | Black (line-length=100) | ✅ | -| Linter | Ruff (E, F, W, I, N, D, UP, B, C4, SIM) | ✅ | -| Type Checker | mypy (strict mode) | ✅ | -| Import Sorter | isort (black profile) | ✅ | -| Max Lines/File | 300 | ✅ | -| Max Lines/Function | 25 | ✅ | -| Max Arguments | 4 | ✅ | -| Max Complexity | 10 | ✅ | -| Max Nesting | 3 | ✅ | - -### Anti-Patterns Avoided ✅ - -| Anti-Pattern | Prevention | Status | -|--------------|------------|--------| -| God Object | Single responsibility per class | ✅ | -| Spaghetti Code | Early returns, flat structure | ✅ | -| Magic Numbers | Thresholds class with constants | ✅ | -| Dead Code | Ruff F401/F841 enforcement | ✅ | -| Copy-Paste | DRY via reusable components | ✅ | -| Hard Coding | Configuration via env/settings | ✅ | - -### Testing ✅ - -| Type | Coverage | Status | -|------|----------|--------| -| Unit Tests | 80%+ target | ✅ Defined | -| Integration Tests | Critical workflows | ✅ Defined | -| E2E Tests | Complete user journeys | ✅ Defined | -| Fixtures | Sample images, mock responses | ✅ Defined | - -### Documentation ✅ - -| Aspect | Standard | Status | -|--------|----------|--------| -| Docstrings | Google Style | ✅ | -| Module Docs | Purpose, Features, Usage, Dependencies | ✅ | -| README | Quick Start, Features, Testing | ✅ | -| Comments | "Why" not "what" | ✅ | - -### Version Control ✅ - -| Aspect | Standard | Status | -|--------|----------|--------| -| Commit Messages | Conventional Commits | ✅ | -| Branch Strategy | main/develop/feature/release/hotfix | ✅ | -| Pre-commit Hooks | Black, Ruff, mypy, trailing whitespace | ✅ | - -### Performance ✅ - -| Optimization | Implementation | Status | -|--------------|----------------|--------| -| Image Compression | ImageOptimizer class | ✅ | -| API Caching | CacheManager with TTL | ✅ | -| Lazy Loading | LazyLoader class | ✅ | -| WebSocket Pooling | WebSocketPool class | ✅ | - ---- - -## Summary - -**Total Pages:** 20 -**Total Features Covered:** 100% (36+ features) -**API Endpoints Demonstrated:** 46+ -**SE Checklist Compliance:** 100% ✅ - -### Compliance Metrics - -| Category | Score | -|----------|-------| -| SOLID Principles | 5/5 ✅ | -| Design Patterns | 8/8 ✅ | -| Code Quality Standards | 10/10 ✅ | -| Anti-Pattern Prevention | 6/6 ✅ | -| Testing Strategy | 4/4 ✅ | -| Documentation | 4/4 ✅ | -| Version Control | 3/3 ✅ | -| Performance | 4/4 ✅ | -| **TOTAL** | **44/44 (100%)** ✅ | - -This design ensures **every single feature** of the Biometric Processor v1.0.0 is demonstrable through an intuitive, professional interface suitable for enterprise sales demos, technical evaluations, and training purposes. - -**The design now fully complies with all Software Engineering best practices as defined in the SE Checklist.** diff --git a/docs/archive/2026-04-16/DEMO_NEXTJS_DESIGN.md b/docs/archive/2026-04-16/DEMO_NEXTJS_DESIGN.md deleted file mode 100644 index 223e355..0000000 --- a/docs/archive/2026-04-16/DEMO_NEXTJS_DESIGN.md +++ /dev/null @@ -1,4705 +0,0 @@ -# Biometric Processor Demo - Next.js Professional UI Design Document - -**Version:** 1.0.0 -**Date:** December 14, 2025 -**Status:** Design Complete - ---- - -## Table of Contents - -1. [Executive Summary](#executive-summary) -2. [Why Next.js Over Streamlit](#why-nextjs-over-streamlit) -3. [Technology Stack](#technology-stack) -4. [Architecture Overview](#architecture-overview) -5. [Application Structure](#application-structure) -6. [Feature Modules](#feature-modules) -7. [Component Library](#component-library) -8. [API Integration](#api-integration) -9. [Real-Time Features](#real-time-features) -10. [UI/UX Design System](#uiux-design-system) -11. [Software Engineering Compliance](#software-engineering-compliance) -12. [Implementation Plan](#implementation-plan) -13. [Deployment Strategy](#deployment-strategy) - ---- - -## Executive Summary - -This document outlines the design for a **professional Next.js demo application** that showcases ALL features of the Biometric Processor v1.0.0. This replaces the Streamlit prototype with a production-grade, enterprise-ready demonstration platform. - -### Target Use Cases - -| Use Case | Requirements | Next.js Advantage | -|----------|--------------|-------------------| -| **Sales Demos** | Professional polish, impressive animations | Framer Motion, shadcn/ui | -| **Trade Shows** | Mobile/tablet support, offline capable | PWA support, responsive | -| **Technical Evaluations** | Real-time features, WebSocket streaming | Native browser APIs | -| **Training** | Interactive, self-guided | Rich component interactions | -| **Client POCs** | Customizable branding | Tailwind theming | - -### Key Design Principles - -1. **Enterprise-Grade UI** - shadcn/ui components with professional polish -2. **Full WebRTC Support** - Native camera/video streaming -3. **Real-Time Capable** - WebSocket for proctoring demos -4. **Mobile-First Responsive** - Works on any device -5. **Type-Safe** - Full TypeScript implementation -6. **Testable** - Comprehensive testing with Vitest/Playwright - ---- - -## Why Next.js Over Streamlit - -| Aspect | Streamlit | Next.js | Winner | -|--------|-----------|---------|--------| -| **Professional Appearance** | Basic, data-science look | Enterprise polish with shadcn/ui | Next.js | -| **Real-time Webcam** | streamlit-webrtc (clunky) | Native MediaStream API | Next.js | -| **WebSocket Streaming** | Difficult, hacky | Native browser support | Next.js | -| **Mobile Responsiveness** | Poor | Tailwind makes trivial | Next.js | -| **Animations/Transitions** | Limited | Framer Motion | Next.js | -| **Load Time** | Slow (Python server) | Fast (edge/static) | Next.js | -| **SEO/Sharing** | None | Full SSR/SSG support | Next.js | -| **Code Reusability** | Demo only | Can become production app | Next.js | -| **Developer Experience** | Limited tooling | Hot reload, DevTools | Next.js | -| **Rapid Prototyping** | Excellent | Good | Streamlit | - -**Decision:** Next.js provides the professional polish required for sales demos and trade shows, with full real-time capabilities for proctoring features. - ---- - -## Technology Stack - -### Core Framework - -| Component | Technology | Version | Justification | -|-----------|------------|---------|---------------| -| **Framework** | Next.js | 14.x | App Router, Server Components, Edge Runtime | -| **Language** | TypeScript | 5.x | Type safety, better DX | -| **Styling** | TailwindCSS | 3.x | Utility-first, rapid styling | -| **Components** | shadcn/ui | Latest | Beautiful, accessible, customizable | -| **State** | Zustand | 4.x | Lightweight, TypeScript-first | -| **API Client** | TanStack Query | 5.x | Caching, refetching, mutations | - -### UI/UX Libraries - -| Component | Technology | Purpose | -|-----------|------------|---------| -| **Animations** | Framer Motion | Page transitions, micro-interactions | -| **Charts** | Recharts | Data visualization | -| **Icons** | Lucide React | Consistent iconography | -| **Forms** | React Hook Form + Zod | Validation, type-safe forms | -| **Tables** | TanStack Table | Sortable, filterable data tables | -| **Toasts** | Sonner | Beautiful notifications | - -### Real-Time & Media - -| Component | Technology | Purpose | -|-----------|------------|---------| -| **WebRTC** | Native API | Camera capture, video streaming | -| **WebSocket** | Native + reconnecting-websocket | Real-time proctoring | -| **Image Processing** | Canvas API | Client-side image manipulation | -| **File Upload** | react-dropzone | Drag-and-drop file handling | - -### Development & Testing - -| Component | Technology | Purpose | -|-----------|------------|---------| -| **Testing** | Vitest | Unit/Integration tests | -| **E2E Testing** | Playwright | End-to-end testing | -| **Linting** | ESLint + Prettier | Code quality | -| **Type Checking** | TypeScript strict mode | Compile-time safety | - ---- - -## Architecture Overview - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ NEXT.JS DEMO APPLICATION │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ PRESENTATION LAYER │ -│ ┌──────────────────────────────────────────────────────────────────────┐ │ -│ │ Pages (App Router) │ │ -│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ -│ │ │Dashboard│ │Enroll │ │Verify │ │Search │ │Liveness │ │ │ -│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ -│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ -│ │ │Quality │ │Demo- │ │Proctor │ │Admin │ │Settings │ │ │ -│ │ │Analysis │ │graphics │ │Session │ │Panel │ │ │ │ │ -│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ -│ └──────────────────────────────────────────────────────────────────────┘ │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ UI COMPONENT LAYER │ -│ ┌──────────────────────────────────────────────────────────────────────┐ │ -│ │ shadcn/ui Base │ Custom Components │ Feature Components │ │ -│ │ ───────────────── ────────────────────── ─────────────────────── │ │ -│ │ Button, Card │ WebcamCapture │ SimilarityGauge │ │ -│ │ Dialog, Sheet │ ImageUploader │ FaceOverlay │ │ -│ │ Table, Form │ ResultDisplay │ LivenessChallenge │ │ -│ │ Tabs, Accordion │ MetricsCard │ ProctoringFeed │ │ -│ └──────────────────────────────────────────────────────────────────────┘ │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ STATE & DATA LAYER │ -│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ -│ │ Zustand Store │ │ TanStack Query │ │ WebSocket │ │ -│ │ ──────────── │ │ ────────────── │ │ Manager │ │ -│ │ • UI State │ │ • API Cache │ │ • Connection │ │ -│ │ • Settings │ │ • Mutations │ │ • Events │ │ -│ │ • Theme │ │ • Prefetch │ │ • Reconnect │ │ -│ └────────────────┘ └────────────────┘ └────────────────┘ │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ API INTEGRATION LAYER │ -│ ┌──────────────────────────────────────────────────────────────────────┐ │ -│ │ TypeScript API Client (Type-safe, Auto-generated from OpenAPI) │ │ -│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ -│ │ │ /faces/* │ │ /liveness/* │ │ /proctoring │ │ /admin/* │ │ │ -│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ -│ └──────────────────────────────────────────────────────────────────────┘ │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ BIOMETRIC PROCESSOR API (localhost:8001) │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### Layer Responsibilities - -| Layer | Responsibility | Key Patterns | -|-------|---------------|--------------| -| **Presentation** | Page routing, layout, composition | App Router, Layouts | -| **UI Component** | Reusable visual components | Compound Components, Composition | -| **State & Data** | Application state, server cache | Observer, Cache-Aside | -| **API Integration** | HTTP/WebSocket communication | Repository, Adapter | - ---- - -## Application Structure - -``` -biometric-processor/demo-ui/ -├── src/ -│ ├── app/ # Next.js App Router -│ │ ├── layout.tsx # Root layout with providers -│ │ ├── page.tsx # Dashboard/Welcome -│ │ ├── globals.css # Global styles -│ │ ├── (features)/ # Feature route group -│ │ │ ├── enrollment/ -│ │ │ │ └── page.tsx # Face Enrollment -│ │ │ ├── verification/ -│ │ │ │ └── page.tsx # 1:1 Verification -│ │ │ ├── search/ -│ │ │ │ └── page.tsx # 1:N Search -│ │ │ ├── liveness/ -│ │ │ │ └── page.tsx # Liveness Detection -│ │ │ ├── quality/ -│ │ │ │ └── page.tsx # Quality Analysis -│ │ │ ├── demographics/ -│ │ │ │ └── page.tsx # Demographics Analysis -│ │ │ ├── landmarks/ -│ │ │ │ └── page.tsx # Facial Landmarks -│ │ │ ├── comparison/ -│ │ │ │ └── page.tsx # Face Comparison -│ │ │ └── batch/ -│ │ │ └── page.tsx # Batch Processing -│ │ ├── (proctoring)/ # Proctoring route group -│ │ │ ├── session/ -│ │ │ │ └── page.tsx # Proctoring Session -│ │ │ └── realtime/ -│ │ │ └── page.tsx # Real-time Feed -│ │ ├── (admin)/ # Admin route group -│ │ │ ├── dashboard/ -│ │ │ │ └── page.tsx # Admin Dashboard -│ │ │ ├── webhooks/ -│ │ │ │ └── page.tsx # Webhook Management -│ │ │ ├── config/ -│ │ │ │ └── page.tsx # Configuration -│ │ │ └── api-explorer/ -│ │ │ └── page.tsx # Interactive API Testing -│ │ └── settings/ -│ │ └── page.tsx # App Settings -│ │ -│ ├── components/ # React Components -│ │ ├── ui/ # shadcn/ui components -│ │ │ ├── button.tsx -│ │ │ ├── card.tsx -│ │ │ ├── dialog.tsx -│ │ │ ├── form.tsx -│ │ │ ├── input.tsx -│ │ │ ├── select.tsx -│ │ │ ├── table.tsx -│ │ │ ├── tabs.tsx -│ │ │ └── ... -│ │ ├── layout/ # Layout components -│ │ │ ├── header.tsx # App header -│ │ │ ├── sidebar.tsx # Navigation sidebar -│ │ │ ├── footer.tsx # App footer -│ │ │ └── page-container.tsx # Page wrapper -│ │ ├── media/ # Media components -│ │ │ ├── webcam-capture.tsx # WebRTC camera -│ │ │ ├── image-uploader.tsx # Drag-drop upload -│ │ │ ├── image-preview.tsx # Image display -│ │ │ └── video-stream.tsx # WebSocket video -│ │ ├── biometric/ # Biometric-specific -│ │ │ ├── face-overlay.tsx # Face bounding box -│ │ │ ├── similarity-gauge.tsx # Radial gauge -│ │ │ ├── quality-meter.tsx # Quality visualization -│ │ │ ├── liveness-challenge.tsx # Challenge UI -│ │ │ ├── landmark-viewer.tsx # 468-point display -│ │ │ └── embedding-visual.tsx # Embedding heatmap -│ │ ├── charts/ # Data visualization -│ │ │ ├── similarity-chart.tsx # Bar/radar charts -│ │ │ ├── metrics-chart.tsx # Performance metrics -│ │ │ └── timeline-chart.tsx # Event timeline -│ │ └── common/ # Shared components -│ │ ├── loading-spinner.tsx -│ │ ├── error-boundary.tsx -│ │ ├── result-card.tsx -│ │ ├── json-viewer.tsx -│ │ └── metrics-card.tsx -│ │ -│ ├── lib/ # Utilities & Core Logic -│ │ ├── api/ # API Client -│ │ │ ├── client.ts # Base HTTP client -│ │ │ ├── endpoints.ts # API endpoint definitions -│ │ │ ├── types.ts # API response types -│ │ │ └── hooks.ts # TanStack Query hooks -│ │ ├── websocket/ # WebSocket Client -│ │ │ ├── manager.ts # Connection manager -│ │ │ ├── events.ts # Event type definitions -│ │ │ └── hooks.ts # React hooks -│ │ ├── media/ # Media Utilities -│ │ │ ├── camera.ts # Camera access -│ │ │ ├── image-processing.ts # Canvas operations -│ │ │ └── file-utils.ts # File handling -│ │ ├── store/ # Zustand Stores -│ │ │ ├── app-store.ts # Global app state -│ │ │ ├── settings-store.ts # User preferences -│ │ │ └── proctoring-store.ts # Proctoring session -│ │ └── utils/ # General Utilities -│ │ ├── cn.ts # Class name merge -│ │ ├── format.ts # Data formatting -│ │ └── validation.ts # Input validation -│ │ -│ ├── hooks/ # Custom React Hooks -│ │ ├── use-webcam.ts # Camera hook -│ │ ├── use-websocket.ts # WebSocket hook -│ │ ├── use-api-health.ts # Health check hook -│ │ └── use-media-query.ts # Responsive hook -│ │ -│ └── types/ # TypeScript Types -│ ├── api.ts # API types -│ ├── biometric.ts # Domain types -│ └── proctoring.ts # Proctoring types -│ -├── public/ # Static Assets -│ ├── images/ -│ │ └── sample-faces/ # Demo face images -│ └── icons/ -│ -├── tests/ # Test Files -│ ├── unit/ # Vitest unit tests -│ ├── integration/ # Integration tests -│ └── e2e/ # Playwright E2E tests -│ -├── .env.local # Environment variables -├── .env.example # Environment template -├── next.config.js # Next.js configuration -├── tailwind.config.ts # Tailwind configuration -├── tsconfig.json # TypeScript configuration -├── vitest.config.ts # Vitest configuration -├── playwright.config.ts # Playwright configuration -├── components.json # shadcn/ui configuration -└── package.json # Dependencies -``` - ---- - -## Feature Modules - -### Phase 1: Core Biometrics - -| Page | Route | Features | -|------|-------|----------| -| **Dashboard** | `/` | Feature overview, API health, quick stats, navigation cards | -| **Face Enrollment** | `/enrollment` | Webcam capture, file upload, quality validation, duplicate check | -| **Face Verification** | `/verification` | 1:1 matching, similarity gauge, side-by-side comparison | -| **Face Search** | `/search` | 1:N identification, ranked results, threshold filtering | -| **Liveness Detection** | `/liveness` | Passive/active modes, challenge-response, spoof detection | - -### Phase 2: Advanced Analysis - -| Page | Route | Features | -|------|-------|----------| -| **Quality Analysis** | `/quality` | Multi-factor quality scoring, improvement suggestions | -| **Demographics** | `/demographics` | Age, gender, emotion detection with confidence | -| **Facial Landmarks** | `/landmarks` | 468-point visualization, interactive canvas | -| **Face Comparison** | `/comparison` | Direct image-to-image comparison | -| **Batch Processing** | `/batch` | Multi-file processing, progress tracking, export | - -### Phase 3: Proctoring Suite - -| Page | Route | Features | -|------|-------|----------| -| **Proctoring Session** | `/session` | Full session management, real-time analysis | -| **Real-time Feed** | `/realtime` | WebSocket video streaming, live detection | - -### Phase 4: Administration - -| Page | Route | Features | -|------|-------|----------| -| **Admin Dashboard** | `/admin/dashboard` | System metrics, enrollment stats | -| **Webhooks** | `/admin/webhooks` | Event subscription management | -| **Configuration** | `/admin/config` | System configuration viewer | -| **API Explorer** | `/admin/api-explorer` | Interactive API testing (like Swagger) | - ---- - -## Component Library - -### Base Components (shadcn/ui) - -```typescript -// Components to install from shadcn/ui -const shadcnComponents = [ - 'button', - 'card', - 'dialog', - 'dropdown-menu', - 'form', - 'input', - 'label', - 'select', - 'slider', - 'switch', - 'table', - 'tabs', - 'toast', - 'tooltip', - 'progress', - 'skeleton', - 'separator', - 'badge', - 'alert', - 'avatar', - 'sheet', - 'accordion', -]; -``` - -### Custom Components - -#### WebcamCapture Component - -```typescript -interface WebcamCaptureProps { - onCapture: (imageData: string) => void; - onError?: (error: Error) => void; - aspectRatio?: '1:1' | '4:3' | '16:9'; - showOverlay?: boolean; - overlayType?: 'oval' | 'rectangle' | 'none'; - resolution?: 'hd' | 'fhd' | '4k'; - facingMode?: 'user' | 'environment'; - mirrored?: boolean; -} -``` - -#### SimilarityGauge Component - -```typescript -interface SimilarityGaugeProps { - value: number; // 0-1 similarity score - threshold?: number; // Match threshold (default 0.6) - size?: 'sm' | 'md' | 'lg'; - showLabel?: boolean; - animated?: boolean; - colorScheme?: 'default' | 'gradient'; -} -``` - -#### ImageUploader Component - -```typescript -interface ImageUploaderProps { - onUpload: (file: File) => void; - accept?: string[]; // ['image/jpeg', 'image/png'] - maxSize?: number; // Max file size in bytes - preview?: boolean; - multiple?: boolean; - dropzoneText?: string; -} -``` - ---- - -## API Integration - -### Type-Safe API Client - -```typescript -// lib/api/client.ts -import { QueryClient } from '@tanstack/react-query'; - -const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8001'; - -class BiometricAPIClient { - private baseUrl: string; - - constructor(baseUrl: string = API_BASE) { - this.baseUrl = baseUrl; - } - - // Face Operations - async enrollFace(params: EnrollFaceParams): Promise { ... } - async verifyFace(params: VerifyFaceParams): Promise { ... } - async searchFace(params: SearchFaceParams): Promise { ... } - async deleteFace(userId: string): Promise { ... } - - // Liveness - async checkLiveness(params: LivenessParams): Promise { ... } - - // Analysis - async analyzeQuality(image: File): Promise { ... } - async detectDemographics(image: File): Promise { ... } - async detectLandmarks(image: File): Promise { ... } - - // Admin - async getHealth(): Promise { ... } - async getMetrics(): Promise { ... } -} - -export const apiClient = new BiometricAPIClient(); -``` - -### TanStack Query Hooks - -```typescript -// lib/api/hooks.ts -export function useEnrollFace() { - return useMutation({ - mutationFn: (params: EnrollFaceParams) => apiClient.enrollFace(params), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['faces'] }); - }, - }); -} - -export function useVerifyFace() { - return useMutation({ - mutationFn: (params: VerifyFaceParams) => apiClient.verifyFace(params), - }); -} - -export function useApiHealth() { - return useQuery({ - queryKey: ['health'], - queryFn: () => apiClient.getHealth(), - refetchInterval: 30000, // Check every 30 seconds - }); -} -``` - ---- - -## Real-Time Features - -### WebSocket Manager - -```typescript -// lib/websocket/manager.ts -class WebSocketManager { - private ws: WebSocket | null = null; - private reconnectAttempts = 0; - private maxReconnectAttempts = 5; - private listeners: Map void>> = new Map(); - - connect(sessionId: string): void { - const url = `${WS_BASE}/proctoring/${sessionId}/stream`; - this.ws = new WebSocket(url); - - this.ws.onmessage = (event) => { - const data = JSON.parse(event.data); - this.emit(data.type, data.payload); - }; - - this.ws.onclose = () => this.handleReconnect(sessionId); - } - - subscribe(event: string, callback: (data: any) => void): () => void { - if (!this.listeners.has(event)) { - this.listeners.set(event, new Set()); - } - this.listeners.get(event)!.add(callback); - - return () => this.listeners.get(event)?.delete(callback); - } - - send(type: string, payload: any): void { - this.ws?.send(JSON.stringify({ type, payload })); - } -} -``` - -### Proctoring Session Hook - -```typescript -// hooks/use-proctoring-session.ts -export function useProctoringSession(sessionId: string) { - const [status, setStatus] = useState<'connecting' | 'connected' | 'error'>('connecting'); - const [events, setEvents] = useState([]); - const [alerts, setAlerts] = useState([]); - - useEffect(() => { - const ws = new WebSocketManager(); - ws.connect(sessionId); - - ws.subscribe('face_detected', (data) => { - setEvents((prev) => [...prev, { type: 'face', ...data }]); - }); - - ws.subscribe('alert', (data) => { - setAlerts((prev) => [...prev, data]); - }); - - return () => ws.disconnect(); - }, [sessionId]); - - return { status, events, alerts }; -} -``` - ---- - -## UI/UX Design System - -This section defines a comprehensive, professional design system ensuring consistency, accessibility, and optimal user experience across all demo pages. - ---- - -### Design Principles - -| Principle | Description | Implementation | -|-----------|-------------|----------------| -| **Clarity** | Users understand what's happening | Clear labels, visible feedback, progressive disclosure | -| **Efficiency** | Minimize steps to complete tasks | Smart defaults, keyboard shortcuts, batch operations | -| **Forgiveness** | Easy to recover from errors | Undo actions, confirmation dialogs, clear error messages | -| **Feedback** | System responds to every action | Loading states, success/error toasts, progress indicators | -| **Consistency** | Same patterns everywhere | Unified component library, standard interactions | -| **Accessibility** | Usable by everyone | WCAG 2.1 AA compliance, keyboard navigation, screen readers | - ---- - -### Color System - -#### Complete Color Palette - -```typescript -// tailwind.config.ts -const colors = { - // Primary - Blue for trust/security (biometric context) - primary: { - 50: '#eff6ff', - 100: '#dbeafe', - 200: '#bfdbfe', - 300: '#93c5fd', - 400: '#60a5fa', - 500: '#3b82f6', // Default - 600: '#2563eb', // Hover - 700: '#1d4ed8', // Active - 800: '#1e40af', - 900: '#1e3a8a', - 950: '#172554', - }, - - // Neutral - Gray scale for text and backgrounds - neutral: { - 50: '#fafafa', // Page background (light) - 100: '#f4f4f5', // Card background - 200: '#e4e4e7', // Borders - 300: '#d4d4d8', // Disabled - 400: '#a1a1aa', // Placeholder text - 500: '#71717a', // Secondary text - 600: '#52525b', // Primary text - 700: '#3f3f46', // Headings - 800: '#27272a', // Card background (dark) - 900: '#18181b', // Page background (dark) - 950: '#09090b', - }, - - // Success - Green for matches and positive outcomes - success: { - 50: '#f0fdf4', - 100: '#dcfce7', - 200: '#bbf7d0', - 500: '#22c55e', // Default - 600: '#16a34a', // Hover - 700: '#15803d', // Active - }, - - // Warning - Amber for thresholds and caution - warning: { - 50: '#fffbeb', - 100: '#fef3c7', - 200: '#fde68a', - 500: '#f59e0b', // Default - 600: '#d97706', // Hover - 700: '#b45309', // Active - }, - - // Danger - Red for failures and destructive actions - danger: { - 50: '#fef2f2', - 100: '#fee2e2', - 200: '#fecaca', - 500: '#ef4444', // Default - 600: '#dc2626', // Hover - 700: '#b91c1c', // Active - }, - - // Info - Cyan for informational messages - info: { - 50: '#ecfeff', - 100: '#cffafe', - 500: '#06b6d4', - 600: '#0891b2', - }, -}; -``` - -#### Color Contrast Ratios (WCAG 2.1 AA) - -| Combination | Ratio | Usage | -|-------------|-------|-------| -| neutral-700 on neutral-50 | 10.5:1 | Body text on light bg | -| neutral-50 on primary-600 | 8.6:1 | Button text | -| neutral-50 on danger-600 | 7.2:1 | Error button text | -| primary-600 on neutral-50 | 4.8:1 | Links on light bg | -| neutral-400 on neutral-50 | 3.5:1 | Placeholder (passes AA large) | - -#### Semantic Color Usage - -| Context | Light Mode | Dark Mode | -|---------|------------|-----------| -| **Page Background** | neutral-50 | neutral-900 | -| **Card Background** | white | neutral-800 | -| **Primary Text** | neutral-700 | neutral-100 | -| **Secondary Text** | neutral-500 | neutral-400 | -| **Border** | neutral-200 | neutral-700 | -| **Focus Ring** | primary-500 | primary-400 | -| **Match Found** | success-500 | success-400 | -| **No Match** | danger-500 | danger-400 | -| **Low Quality** | warning-500 | warning-400 | - ---- - -### Typography Scale - -```typescript -// Typography system based on 16px base -const typography = { - // Font families - fontFamily: { - sans: ['Inter', 'system-ui', 'sans-serif'], - mono: ['JetBrains Mono', 'Consolas', 'monospace'], - }, - - // Type scale (rem units) - fontSize: { - 'xs': ['0.75rem', { lineHeight: '1rem' }], // 12px - Captions - 'sm': ['0.875rem', { lineHeight: '1.25rem' }], // 14px - Small text - 'base': ['1rem', { lineHeight: '1.5rem' }], // 16px - Body - 'lg': ['1.125rem', { lineHeight: '1.75rem' }], // 18px - Lead text - 'xl': ['1.25rem', { lineHeight: '1.75rem' }], // 20px - H4 - '2xl': ['1.5rem', { lineHeight: '2rem' }], // 24px - H3 - '3xl': ['1.875rem', { lineHeight: '2.25rem' }], // 30px - H2 - '4xl': ['2.25rem', { lineHeight: '2.5rem' }], // 36px - H1 - '5xl': ['3rem', { lineHeight: '1' }], // 48px - Display - }, - - // Font weights - fontWeight: { - normal: '400', // Body text - medium: '500', // Emphasis, buttons - semibold: '600', // Subheadings - bold: '700', // Headings - }, -}; -``` - -#### Typography Usage - -| Element | Size | Weight | Color | -|---------|------|--------|-------| -| **Page Title** | 4xl (36px) | bold | neutral-900 | -| **Section Title** | 2xl (24px) | semibold | neutral-800 | -| **Card Title** | xl (20px) | semibold | neutral-700 | -| **Body Text** | base (16px) | normal | neutral-600 | -| **Small/Caption** | sm (14px) | normal | neutral-500 | -| **Label** | sm (14px) | medium | neutral-700 | -| **Button** | sm (14px) | medium | varies | -| **Code/Data** | sm (14px) | normal | mono font | - ---- - -### Spacing System - -```typescript -// 4px base unit spacing scale -const spacing = { - px: '1px', - 0: '0', - 0.5: '0.125rem', // 2px - 1: '0.25rem', // 4px - 1.5: '0.375rem', // 6px - 2: '0.5rem', // 8px - 2.5: '0.625rem', // 10px - 3: '0.75rem', // 12px - 4: '1rem', // 16px - Base unit - 5: '1.25rem', // 20px - 6: '1.5rem', // 24px - 8: '2rem', // 32px - 10: '2.5rem', // 40px - 12: '3rem', // 48px - 16: '4rem', // 64px - 20: '5rem', // 80px - 24: '6rem', // 96px -}; -``` - -#### Spacing Usage Guidelines - -| Context | Spacing | Example | -|---------|---------|---------| -| **Inline elements** | 1-2 (4-8px) | Icon + text gap | -| **Form field gap** | 3-4 (12-16px) | Label to input | -| **Card padding** | 4-6 (16-24px) | Content padding | -| **Section gap** | 8-12 (32-48px) | Between sections | -| **Page padding** | 4-8 (16-32px) | Responsive margins | - ---- - -### Component States - -Every interactive component must define all states: - -```typescript -// Button states example -const buttonStates = { - // Default state - default: { - bg: 'primary-500', - text: 'white', - border: 'transparent', - }, - - // Hover state (mouse over) - hover: { - bg: 'primary-600', - text: 'white', - cursor: 'pointer', - transform: 'translateY(-1px)', - shadow: 'md', - }, - - // Focus state (keyboard navigation) - focus: { - bg: 'primary-500', - ring: '2px primary-500', - ringOffset: '2px white', - outline: 'none', - }, - - // Active state (pressed) - active: { - bg: 'primary-700', - transform: 'translateY(0)', - shadow: 'none', - }, - - // Disabled state - disabled: { - bg: 'neutral-200', - text: 'neutral-400', - cursor: 'not-allowed', - opacity: 0.6, - }, - - // Loading state - loading: { - bg: 'primary-400', - cursor: 'wait', - content: '', - }, -}; -``` - -#### State Indicators - -| State | Visual Indicator | -|-------|------------------| -| **Hover** | Darker bg, slight lift shadow, cursor pointer | -| **Focus** | 2px focus ring with 2px offset | -| **Active** | Darkest bg, pressed effect | -| **Disabled** | Grayed out, 60% opacity, not-allowed cursor | -| **Loading** | Spinner icon, reduced opacity | -| **Error** | Red border, error icon, error message below | -| **Success** | Green checkmark, success message | - ---- - -### Accessibility (WCAG 2.1 AA Compliance) - -#### Focus Management - -```css -/* Focus ring style - visible on all interactive elements */ -.focus-visible { - outline: none; - ring: 2px; - ring-color: primary-500; - ring-offset: 2px; -} - -/* Skip to main content link */ -.skip-link { - position: absolute; - top: -40px; - left: 0; - padding: 8px 16px; - background: primary-600; - color: white; - z-index: 100; -} -.skip-link:focus { - top: 0; -} -``` - -#### Keyboard Navigation - -| Key | Action | -|-----|--------| -| `Tab` | Move to next focusable element | -| `Shift+Tab` | Move to previous focusable element | -| `Enter/Space` | Activate button/link | -| `Escape` | Close modal/dropdown | -| `Arrow keys` | Navigate within component (tabs, menus) | -| `Home/End` | Jump to first/last item | - -#### Touch Targets - -```typescript -// Minimum touch target sizes -const touchTargets = { - minimum: '44px', // WCAG minimum - comfortable: '48px', // Recommended - large: '56px', // Primary actions -}; - -// Button sizes -const buttonSizes = { - sm: { height: '32px', padding: '0 12px' }, // Desktop secondary - md: { height: '40px', padding: '0 16px' }, // Desktop primary - lg: { height: '48px', padding: '0 24px' }, // Mobile primary (meets touch target) -}; -``` - -#### Screen Reader Support - -```tsx -// ARIA labels for biometric components - -

- Position your face within the oval guide. Ensure good lighting. -

- -// Live regions for dynamic content -
- {matchResult && `Match score: ${matchResult.similarity}%`} -
- -// Progress announcements -
- Processing image... -
-``` - -#### Color Blind Considerations - -| Condition | Accommodation | -|-----------|---------------| -| **Red-Green** | Use icons + text with colors, not color alone | -| **Blue-Yellow** | Avoid blue/yellow only distinctions | -| **Monochromacy** | Ensure sufficient contrast (7:1 for text) | - -```tsx -// ✅ GOOD: Color + Icon + Text -}>Match Found -}>No Match - -// ❌ BAD: Color only -
// Success -
// Error -``` - ---- - -### Responsive Design - -#### Breakpoint Behaviors - -```typescript -const responsivePatterns = { - // Mobile (< 640px) - mobile: { - sidebar: 'hidden, toggle via hamburger', - layout: 'single column', - cards: 'full width, stacked', - navigation: 'bottom sheet or drawer', - webcam: 'full width, 4:3 ratio', - buttons: 'full width, stacked', - tables: 'card view instead', - }, - - // Tablet (640px - 1024px) - tablet: { - sidebar: 'collapsible icons only', - layout: 'two columns where appropriate', - cards: '2 per row', - navigation: 'top bar + icon sidebar', - webcam: 'centered, max 480px width', - buttons: 'inline, auto width', - tables: 'horizontal scroll', - }, - - // Desktop (> 1024px) - desktop: { - sidebar: 'expanded with labels', - layout: 'sidebar + main content', - cards: '3-4 per row', - navigation: 'full sidebar', - webcam: 'sidebar or modal, 400px width', - buttons: 'inline, fixed width', - tables: 'full table view', - }, -}; -``` - -#### Mobile-First Component Adaptations - -```tsx -// Responsive webcam container -
- -
- -// Responsive button layout -
- - -
-``` - ---- - -### User Flows - -#### Primary User Journey: Face Enrollment - -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ 1. ENTRY │────▶│ 2. INPUT │────▶│ 3. CAPTURE │────▶│ 4. REVIEW │ -│ Dashboard │ │ User ID │ │ Webcam │ │ Quality │ -└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ - │ - ┌─────────────┐ ┌─────────────┐ │ - │ 6. DONE │◀────│ 5. SUBMIT │◀───────────┘ - │ Success │ │ Confirm │ - └─────────────┘ └─────────────┘ -``` - -| Step | User Action | System Response | Error Path | -|------|-------------|-----------------|------------| -| 1 | Click "Enroll Face" | Navigate to enrollment page | - | -| 2 | Enter User ID | Validate format, check duplicates | Show validation error | -| 3 | Position face, click Capture | Analyze quality in real-time | Show quality feedback | -| 4 | Review captured image | Display quality score, face box | Allow retake if low quality | -| 5 | Click "Confirm Enrollment" | Submit to API, show progress | Show error, allow retry | -| 6 | View success confirmation | Display face ID, quality metrics | - | - -#### Error Recovery Flows - -``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ ERROR STATE │────▶│ EXPLAIN ERROR │────▶│ OFFER ACTION │ -│ API failed │ │ "Server busy" │ │ "Retry" button │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ - │ ┌─────────────────┐ │ - └─────────────▶│ ALTERNATIVE │◀─────────────┘ - │ "Try offline" │ - └─────────────────┘ -``` - ---- - -### Loading States - -#### Skeleton Loaders - -```tsx -// Page loading skeleton -function PageSkeleton() { - return ( -
-
-
- {[1, 2, 3].map(i => ( -
- ))} -
-
- ); -} - -// Component-level skeleton -function CardSkeleton() { - return ( - -
-
- - ); -} -``` - -#### Progress Indicators - -| Type | Use Case | Visual | -|------|----------|--------| -| **Spinner** | Button loading, quick operations | Rotating circle | -| **Progress Bar** | File upload, known duration | Horizontal bar with % | -| **Skeleton** | Page/component loading | Gray placeholder shapes | -| **Shimmer** | List loading | Moving gradient overlay | -| **Pulse** | Awaiting response | Fading opacity animation | - -```tsx -// Biometric processing progress -
- -
- {stages[currentStage]} - {progress}% -
-
- -// Stage indicators -const stages = [ - 'Analyzing image quality...', - 'Detecting face...', - 'Extracting features...', - 'Checking for duplicates...', - 'Enrolling face...', -]; -``` - ---- - -### Empty States - -```tsx -// Generic empty state component -interface EmptyStateProps { - icon: React.ReactNode; - title: string; - description: string; - action?: { - label: string; - onClick: () => void; - }; -} - -function EmptyState({ icon, title, description, action }: EmptyStateProps) { - return ( -
-
{icon}
-

{title}

-

{description}

- {action && ( - - )} -
- ); -} - -// Usage examples -} - title="No faces enrolled" - description="Start by enrolling a face to enable verification and search features." - action={{ label: "Enroll First Face", onClick: goToEnrollment }} -/> - -} - title="No matches found" - description="The uploaded face doesn't match any enrolled faces in the database." -/> -``` - ---- - -### Error States - -```tsx -// Error display component -interface ErrorStateProps { - type: 'validation' | 'api' | 'biometric' | 'permission'; - message: string; - details?: string; - onRetry?: () => void; - onDismiss?: () => void; -} - -function ErrorState({ type, message, details, onRetry, onDismiss }: ErrorStateProps) { - const icons = { - validation: , - api: , - biometric: , - permission: , - }; - - return ( - -
- {icons[type]} -
- {message} - {details && {details}} -
-
- {onRetry && ( - - )} - {onDismiss && ( - - )} -
-
-
- ); -} -``` - -#### Biometric-Specific Errors - -| Error | Message | Recovery Action | -|-------|---------|-----------------| -| **No face detected** | "We couldn't detect a face in the image" | "Retake photo with face visible" | -| **Multiple faces** | "Multiple faces detected. Please ensure only one person is visible" | "Retake with single person" | -| **Low quality** | "Image quality is too low (score: 45%)" | "Improve lighting and try again" | -| **Liveness failed** | "Could not verify you're a live person" | "Follow the on-screen prompts" | -| **Duplicate found** | "This face is already enrolled as User X" | "View existing enrollment" | - ---- - -### Notification System (Toasts) - -```tsx -// Toast configuration -const toastConfig = { - position: 'bottom-right', - duration: { - success: 3000, - error: 5000, // Longer for errors - warning: 4000, - info: 3000, - }, - maxVisible: 3, -}; - -// Toast variants -}> - Face enrolled successfully - - -} action={{ label: 'Retry', onClick: retry }}> - Enrollment failed. Server unavailable. - - -}> - Low image quality detected (65%) - - -}> - Processing may take up to 30 seconds - -``` - ---- - -### Form Design Patterns - -#### Form Layout - -```tsx -// Standard form layout -
- {/* Form section */} -
-

User Information

- - {/* Form field */} -
- - -

- Alphanumeric characters, 3-50 characters -

- {error && ( -

- - {error} -

- )} -
-
- - {/* Form actions */} -
- - -
-
-``` - -#### Validation Patterns - -| Timing | Use Case | -|--------|----------| -| **On blur** | Field validation after user leaves field | -| **On change** | Real-time for format validation (e.g., email) | -| **On submit** | Final validation before API call | -| **Debounced** | Expensive validations (e.g., duplicate check) | - -```tsx -// Real-time validation feedback - : - valid ? : - error ? : - null - } -/> -``` - ---- - -### Biometric-Specific UX - -#### Camera UI Design - -```tsx -// Face capture overlay component -function FaceCaptureOverlay({ faceDetected, quality, position }) { - return ( -
- {/* Video feed */} -
- ); -} - -const getPositionHint = (position) => { - if (!position.faceDetected) return 'Position your face in the oval'; - if (position.tooClose) return 'Move further from camera'; - if (position.tooFar) return 'Move closer to camera'; - if (position.tooLeft) return 'Move slightly right'; - if (position.tooRight) return 'Move slightly left'; - return 'Perfect! Hold still...'; -}; -``` - -#### Similarity Score Visualization - -```tsx -// Radial gauge for similarity score -function SimilarityGauge({ score, threshold = 0.6 }) { - const percentage = Math.round(score * 100); - const isMatch = score >= threshold; - - const color = isMatch ? 'success' : score >= threshold - 0.1 ? 'warning' : 'danger'; - - return ( -
- {/* Background circle */} - - - - - - {/* Center content */} -
- - {percentage}% - - - {isMatch ? 'Match' : 'No Match'} - -
- - {/* Threshold indicator */} -
-
- ); -} -``` - -#### Liveness Challenge UI - -```tsx -// Active liveness challenge component -function LivenessChallenge({ challenge, onComplete }) { - const challenges = { - blink: { - instruction: 'Blink your eyes', - icon: , - animation: 'animate-pulse', - }, - smile: { - instruction: 'Smile', - icon: , - animation: 'animate-bounce', - }, - turnLeft: { - instruction: 'Turn your head left', - icon: , - animation: 'animate-slide-left', - }, - turnRight: { - instruction: 'Turn your head right', - icon: , - animation: 'animate-slide-right', - }, - }; - - const current = challenges[challenge]; - - return ( -
-
- {current.icon} -
-

{current.instruction}

- -

- Hold for {remainingSeconds} seconds -

-
- ); -} -``` - ---- - -### Navigation Design - -#### Sidebar Navigation - -```tsx -// Responsive sidebar -function Sidebar() { - return ( - - ); -} - -// Navigation item with tooltip for collapsed state -function NavItem({ href, icon, label, active }) { - return ( - - - {icon} - {label} - - - ); -} -``` - -#### Mobile Navigation - -```tsx -// Bottom navigation for mobile -function MobileNav() { - return ( - - ); -} -``` - ---- - -### Animation Guidelines (Extended) - -```typescript -// Framer Motion animation presets -const animations = { - // Page transitions - pageEnter: { - initial: { opacity: 0, y: 20 }, - animate: { opacity: 1, y: 0 }, - exit: { opacity: 0, y: -20 }, - transition: { duration: 0.3, ease: 'easeOut' }, - }, - - // Modal animations - modalOverlay: { - initial: { opacity: 0 }, - animate: { opacity: 1 }, - exit: { opacity: 0 }, - transition: { duration: 0.2 }, - }, - modalContent: { - initial: { opacity: 0, scale: 0.95, y: 10 }, - animate: { opacity: 1, scale: 1, y: 0 }, - exit: { opacity: 0, scale: 0.95, y: 10 }, - transition: { duration: 0.2, ease: 'easeOut' }, - }, - - // List item stagger - listContainer: { - animate: { transition: { staggerChildren: 0.05 } }, - }, - listItem: { - initial: { opacity: 0, x: -10 }, - animate: { opacity: 1, x: 0 }, - }, - - // Success celebration - successPop: { - initial: { scale: 0 }, - animate: { scale: [0, 1.2, 1] }, - transition: { duration: 0.4, ease: 'easeOut' }, - }, - - // Gauge fill animation - gaugeFill: { - initial: { strokeDashoffset: 283 }, - animate: { strokeDashoffset: 0 }, - transition: { duration: 1, ease: 'easeInOut', delay: 0.3 }, - }, - - // Reduced motion fallback - reducedMotion: { - initial: { opacity: 0 }, - animate: { opacity: 1 }, - transition: { duration: 0.01 }, - }, -}; - -// Respect user preferences -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; -``` - ---- - -### Dark Mode Design - -```typescript -// Dark mode color mappings -const darkModeColors = { - // Backgrounds - 'bg-white': 'dark:bg-neutral-800', - 'bg-neutral-50': 'dark:bg-neutral-900', - 'bg-neutral-100': 'dark:bg-neutral-800', - - // Text - 'text-neutral-900': 'dark:text-neutral-50', - 'text-neutral-700': 'dark:text-neutral-200', - 'text-neutral-500': 'dark:text-neutral-400', - - // Borders - 'border-neutral-200': 'dark:border-neutral-700', - - // Primary adjustments (slightly lighter in dark mode) - 'bg-primary-500': 'dark:bg-primary-400', - 'text-primary-600': 'dark:text-primary-400', -}; - -// Component dark mode example -function Card({ children }) { - return ( -
- {children} -
- ); -} -``` - ---- - -### Onboarding Flow - -```tsx -// First-time user onboarding -function OnboardingFlow() { - const steps = [ - { - target: '[data-onboarding="api-status"]', - title: 'API Connection', - content: 'This indicator shows if the biometric API is running.', - }, - { - target: '[data-onboarding="enroll-button"]', - title: 'Start Here', - content: 'Begin by enrolling a face to enable all features.', - }, - { - target: '[data-onboarding="webcam"]', - title: 'Camera Access', - content: 'Allow camera access for real-time face capture.', - }, - ]; - - return ( - - ); -} -``` - ---- - -### Contextual Help - -```tsx -// Help tooltip component -function HelpTooltip({ content }) { - return ( - - - - ); -} - -// Usage - - -// Inline help text -
-
- -
-

Pro Tip

-

- For best results, ensure the face is well-lit and centered in the frame. -

-
-
-
-``` - ---- - -## Software Engineering Compliance - -This section ensures full compliance with the SE Checklist. Every principle, pattern, and practice is explicitly addressed. - ---- - -### Core Design Principles - -#### DRY (Don't Repeat Yourself) - -| Violation | Solution | -|-----------|----------| -| Duplicate API calls | Centralized `lib/api/client.ts` with reusable methods | -| Repeated form validation | Shared Zod schemas in `lib/validation/schemas.ts` | -| Similar component styles | Tailwind utility classes + `cn()` helper | -| Duplicate error handling | Centralized `ErrorBoundary` + `useErrorHandler` hook | - -```typescript -// lib/validation/schemas.ts - Single source of truth -export const userIdSchema = z.string().min(1).max(100); -export const thresholdSchema = z.number().min(0).max(1); -export const imageSchema = z.instanceof(File).refine( - (file) => ['image/jpeg', 'image/png'].includes(file.type), - 'Must be JPEG or PNG' -); -``` - -#### KISS (Keep It Simple, Stupid) - -| Principle | Implementation | -|-----------|----------------| -| Simple component APIs | Max 5-7 props per component, sensible defaults | -| Flat state structure | Avoid nested state objects, use normalized data | -| Direct data flow | Props down, events up - avoid prop drilling with context only when necessary | -| No premature abstraction | Create abstractions only after 3+ repetitions | - -#### YAGNI (You Aren't Gonna Need It) - -| Avoid | Instead | -|-------|---------| -| Generic "future-proof" utilities | Build specific solutions for current needs | -| Unused component variants | Add variants only when needed | -| Over-configurable components | Start simple, add options when required | -| Premature optimization | Profile first, optimize bottlenecks only | - -#### Separation of Concerns - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ PRESENTATION (pages/) │ What to render │ -├─────────────────────────────────────────────────────────────────┤ -│ COMPONENTS (components/) │ How to render │ -├─────────────────────────────────────────────────────────────────┤ -│ BUSINESS LOGIC (hooks/) │ What to do │ -├─────────────────────────────────────────────────────────────────┤ -│ DATA ACCESS (lib/api/) │ Where to get/send data │ -├─────────────────────────────────────────────────────────────────┤ -│ STATE (lib/store/) │ What to remember │ -└─────────────────────────────────────────────────────────────────┘ -``` - -#### Composition Over Inheritance - -```typescript -// ❌ BAD: Inheritance -class EnrollmentForm extends BaseForm { ... } - -// ✅ GOOD: Composition -function EnrollmentForm() { - return ( - - - - - - ); -} -``` - ---- - -### SOLID Principles (Detailed) - -#### S - Single Responsibility Principle - -Each module has ONE reason to change: - -| Module | Single Responsibility | Changes Only When | -|--------|----------------------|-------------------| -| `WebcamCapture` | Camera access & capture | Camera API changes | -| `ImageUploader` | File upload & validation | Upload requirements change | -| `SimilarityGauge` | Score visualization | Display requirements change | -| `useEnrollFace` | Enrollment API calls | Enrollment API changes | -| `apiClient` | HTTP communication | API protocol changes | - -```typescript -// ✅ GOOD: Single responsibility -// webcam-capture.tsx - ONLY handles camera -export function WebcamCapture({ onCapture }: Props) { - // Only camera logic here -} - -// image-processor.ts - ONLY handles image processing -export function processImage(image: File): ProcessedImage { - // Only processing logic here -} -``` - -#### O - Open/Closed Principle - -Components open for extension, closed for modification: - -```typescript -// ✅ GOOD: Extend via props, not modification -interface ResultCardProps { - title: string; - value: string | number; - icon?: React.ReactNode; // Extension point - variant?: 'default' | 'success' | 'error'; // Extension point - footer?: React.ReactNode; // Extension point - className?: string; // Extension point -} - -// Usage - extended without modifying ResultCard -} - variant="success" - footer={} -/> -``` - -#### L - Liskov Substitution Principle - -All input components implement common interface: - -```typescript -// lib/types/form.ts - Common interface -interface FormInputProps { - value: T; - onChange: (value: T) => void; - error?: string; - disabled?: boolean; - required?: boolean; -} - -// All inputs are substitutable -const TextInput: FC> = ... -const NumberInput: FC> = ... -const FileInput: FC> = ... - -// Can be used interchangeably in FormField - - - -``` - -#### I - Interface Segregation Principle - -Clients depend only on what they need: - -```typescript -// ❌ BAD: One fat interface -interface BiometricAPI { - enrollFace(): Promise; - verifyFace(): Promise; - searchFace(): Promise; - checkLiveness(): Promise; - getMetrics(): Promise; - configureWebhooks(): Promise; - // ... 20 more methods -} - -// ✅ GOOD: Segregated interfaces -interface FaceEnrollmentAPI { - enrollFace(params: EnrollParams): Promise; - checkDuplicate(image: File): Promise; -} - -interface FaceVerificationAPI { - verifyFace(params: VerifyParams): Promise; -} - -interface LivenessAPI { - checkPassiveLiveness(image: File): Promise; - checkActiveLiveness(frames: File[]): Promise; -} -``` - -#### D - Dependency Inversion Principle - -Depend on abstractions, not concretions: - -```typescript -// lib/api/interfaces.ts - Abstractions -interface IAPIClient { - get(url: string): Promise; - post(url: string, data: unknown): Promise; -} - -interface IFaceService { - enroll(params: EnrollParams): Promise; - verify(params: VerifyParams): Promise; -} - -// lib/api/client.ts - Concrete implementation -class HTTPAPIClient implements IAPIClient { ... } -class FaceService implements IFaceService { - constructor(private client: IAPIClient) {} // Injected dependency -} - -// lib/api/mock-client.ts - Mock for testing -class MockAPIClient implements IAPIClient { ... } - -// Context provider injects the dependency -const APIContext = createContext(new HTTPAPIClient()); -``` - ---- - -### Design Patterns (Complete) - -#### Creational Patterns - -| Pattern | Implementation | Location | -|---------|----------------|----------| -| **Factory** | Component factories for dynamic rendering | `lib/factories/` | -| **Builder** | Complex form/request construction | `lib/builders/` | -| **Singleton** | API client, WebSocket manager instances | `lib/api/client.ts` | - -```typescript -// Factory Pattern - Component Factory -// lib/factories/result-factory.tsx -export function createResultComponent(type: ResultType): React.FC { - switch (type) { - case 'enrollment': return EnrollmentResult; - case 'verification': return VerificationResult; - case 'search': return SearchResult; - default: return GenericResult; - } -} - -// Builder Pattern - Request Builder -// lib/builders/enrollment-builder.ts -export class EnrollmentRequestBuilder { - private request: Partial = {}; - - withUserId(userId: string): this { - this.request.userId = userId; - return this; - } - - withImage(image: File): this { - this.request.image = image; - return this; - } - - withMetadata(metadata: Record): this { - this.request.metadata = metadata; - return this; - } - - build(): EnrollmentRequest { - if (!this.request.userId || !this.request.image) { - throw new Error('userId and image are required'); - } - return this.request as EnrollmentRequest; - } -} - -// Usage -const request = new EnrollmentRequestBuilder() - .withUserId('user-123') - .withImage(imageFile) - .withMetadata({ source: 'webcam' }) - .build(); -``` - -#### Structural Patterns - -| Pattern | Implementation | Location | -|---------|----------------|----------| -| **Adapter** | API response normalization | `lib/adapters/` | -| **Facade** | Simplified biometric operations | `lib/facades/` | -| **Decorator** | HOCs for auth, loading states | `lib/decorators/` | -| **Composite** | Nested form/layout structures | Components | - -```typescript -// Adapter Pattern - Normalize API responses -// lib/adapters/face-adapter.ts -export function adaptEnrollmentResponse(raw: RawAPIResponse): EnrollmentResult { - return { - userId: raw.user_id, - faceId: raw.face_id, - quality: raw.quality_score, - enrolledAt: new Date(raw.created_at), - }; -} - -// Facade Pattern - Simplified interface -// lib/facades/biometric-facade.ts -export class BiometricFacade { - constructor( - private faceService: IFaceService, - private livenessService: ILivenessService, - private qualityService: IQualityService - ) {} - - async enrollWithValidation(image: File, userId: string): Promise { - // Step 1: Check quality - const quality = await this.qualityService.analyze(image); - if (quality.score < 0.7) throw new QualityError(quality); - - // Step 2: Check liveness - const liveness = await this.livenessService.check(image); - if (!liveness.isLive) throw new LivenessError(liveness); - - // Step 3: Enroll - return this.faceService.enroll({ image, userId }); - } -} - -// Decorator Pattern - HOC for loading state -// lib/decorators/with-loading.tsx -export function withLoading

( - Component: React.FC

-): React.FC

{ - return function WithLoading({ isLoading, ...props }) { - if (isLoading) return ; - return ; - }; -} -``` - -#### Behavioral Patterns - -| Pattern | Implementation | Location | -|---------|----------------|----------| -| **Observer** | Event subscriptions, WebSocket | `lib/websocket/` | -| **Strategy** | Interchangeable algorithms | `lib/strategies/` | -| **Command** | Undo/redo, action queue | `lib/commands/` | -| **State** | UI state machines | `lib/state-machines/` | - -```typescript -// Strategy Pattern - Interchangeable liveness algorithms -// lib/strategies/liveness-strategy.ts -interface LivenessStrategy { - check(image: File): Promise; -} - -class PassiveLivenessStrategy implements LivenessStrategy { - async check(image: File): Promise { - return apiClient.post('/liveness/passive', { image }); - } -} - -class ActiveLivenessStrategy implements LivenessStrategy { - async check(frames: File[]): Promise { - return apiClient.post('/liveness/active', { frames }); - } -} - -// Context uses strategy -class LivenessChecker { - constructor(private strategy: LivenessStrategy) {} - - setStrategy(strategy: LivenessStrategy): void { - this.strategy = strategy; - } - - async check(input: File | File[]): Promise { - return this.strategy.check(input); - } -} - -// State Pattern - UI State Machine -// lib/state-machines/enrollment-machine.ts -type EnrollmentState = 'idle' | 'capturing' | 'processing' | 'success' | 'error'; - -interface EnrollmentMachine { - state: EnrollmentState; - transition(action: EnrollmentAction): void; -} - -const enrollmentMachine: EnrollmentMachine = { - state: 'idle', - transition(action) { - switch (this.state) { - case 'idle': - if (action === 'START_CAPTURE') this.state = 'capturing'; - break; - case 'capturing': - if (action === 'CAPTURE_COMPLETE') this.state = 'processing'; - if (action === 'CANCEL') this.state = 'idle'; - break; - case 'processing': - if (action === 'SUCCESS') this.state = 'success'; - if (action === 'ERROR') this.state = 'error'; - break; - // ... - } - } -}; -``` - ---- - -### Anti-Patterns Avoidance - -#### Code Smells to Prevent - -| Anti-Pattern | Prevention Strategy | Enforcement | -|--------------|---------------------|-------------| -| **God Object** | Max 200 lines per component, single responsibility | ESLint rule | -| **Spaghetti Code** | Clear layer boundaries, no cross-layer imports | Import restrictions | -| **Magic Numbers** | Named constants in `lib/constants/` | ESLint no-magic-numbers | -| **Dead Code** | CI removes unused exports | ts-prune in CI | -| **Feature Envy** | Colocate logic with data it operates on | Code review | -| **Long Methods** | Max 30 lines per function | ESLint max-lines-per-function | -| **Large Classes** | Max 300 lines per file | ESLint max-lines | - -```typescript -// lib/constants/thresholds.ts - No magic numbers -export const SIMILARITY_THRESHOLDS = { - HIGH_CONFIDENCE: 0.85, - MEDIUM_CONFIDENCE: 0.70, - LOW_CONFIDENCE: 0.55, - MINIMUM: 0.40, -} as const; - -export const FILE_SIZE_LIMITS = { - MAX_IMAGE_SIZE_MB: 10, - MAX_IMAGE_SIZE_BYTES: 10 * 1024 * 1024, -} as const; - -export const TIMING = { - DEBOUNCE_MS: 300, - API_TIMEOUT_MS: 30000, - TOAST_DURATION_MS: 5000, -} as const; -``` - -#### Architecture Anti-Patterns - -| Anti-Pattern | Prevention | -|--------------|------------| -| **Big Ball of Mud** | Strict folder structure, layer boundaries | -| **Golden Hammer** | Choose right tool for each problem | -| **Lava Flow** | Regular dead code removal, no commented code | -| **Vendor Lock-in** | Abstract external dependencies behind interfaces | -| **Premature Optimization** | Profile before optimizing | - ---- - -### Clean Code Guidelines - -#### Naming Conventions - -| Element | Convention | Example | -|---------|------------|---------| -| **Components** | PascalCase | `WebcamCapture`, `SimilarityGauge` | -| **Hooks** | camelCase with `use` prefix | `useWebcam`, `useEnrollFace` | -| **Utilities** | camelCase | `formatSimilarity`, `validateImage` | -| **Constants** | SCREAMING_SNAKE_CASE | `MAX_FILE_SIZE`, `API_TIMEOUT` | -| **Types/Interfaces** | PascalCase | `EnrollmentResult`, `VerifyParams` | -| **Files** | kebab-case | `webcam-capture.tsx`, `use-webcam.ts` | -| **Folders** | kebab-case | `components/`, `lib/api/` | - -#### Function Guidelines - -```typescript -// ✅ GOOD: Small, focused functions -function validateImageSize(file: File): ValidationResult { - if (file.size > MAX_FILE_SIZE_BYTES) { - return { valid: false, error: 'File too large' }; - } - return { valid: true }; -} - -function validateImageType(file: File): ValidationResult { - if (!ALLOWED_IMAGE_TYPES.includes(file.type)) { - return { valid: false, error: 'Invalid file type' }; - } - return { valid: true }; -} - -function validateImage(file: File): ValidationResult { - const sizeResult = validateImageSize(file); - if (!sizeResult.valid) return sizeResult; - - const typeResult = validateImageType(file); - if (!typeResult.valid) return typeResult; - - return { valid: true }; -} -``` - -#### Comment Guidelines - -```typescript -// ✅ GOOD: Comments explain WHY, not WHAT -// Using 0.6 threshold because facial recognition accuracy drops -// significantly below this value based on our benchmarks -const MINIMUM_SIMILARITY_THRESHOLD = 0.6; - -// ❌ BAD: Comment explains WHAT (obvious from code) -// Set the threshold to 0.6 -const threshold = 0.6; -``` - ---- - -### Error Handling Strategy - -#### Error Hierarchy - -```typescript -// lib/errors/index.ts -export class AppError extends Error { - constructor( - message: string, - public readonly code: string, - public readonly isOperational: boolean = true - ) { - super(message); - this.name = this.constructor.name; - } -} - -export class APIError extends AppError { - constructor( - message: string, - public readonly statusCode: number, - public readonly endpoint: string - ) { - super(message, `API_${statusCode}`, true); - } -} - -export class ValidationError extends AppError { - constructor( - message: string, - public readonly field: string - ) { - super(message, 'VALIDATION_ERROR', true); - } -} - -export class BiometricError extends AppError { - constructor( - message: string, - public readonly type: 'quality' | 'liveness' | 'duplicate' - ) { - super(message, `BIOMETRIC_${type.toUpperCase()}`, true); - } -} -``` - -#### Error Handling Patterns - -```typescript -// hooks/use-error-handler.ts -export function useErrorHandler() { - const { toast } = useToast(); - - const handleError = useCallback((error: unknown) => { - if (error instanceof ValidationError) { - toast.error(`Validation failed: ${error.message}`); - } else if (error instanceof APIError) { - toast.error(`API error: ${error.message}`); - // Log to monitoring service - logError(error); - } else if (error instanceof BiometricError) { - toast.warning(`Biometric check failed: ${error.message}`); - } else { - toast.error('An unexpected error occurred'); - logError(error); - } - }, [toast]); - - return { handleError }; -} - -// Usage in components -function EnrollmentForm() { - const { handleError } = useErrorHandler(); - const enrollMutation = useEnrollFace(); - - const handleSubmit = async (data: FormData) => { - try { - await enrollMutation.mutateAsync(data); - toast.success('Enrollment successful'); - } catch (error) { - handleError(error); - } - }; -} -``` - -#### Null Safety - -```typescript -// ✅ GOOD: Use optional chaining and nullish coalescing -const userName = user?.name ?? 'Unknown'; -const threshold = settings?.threshold ?? DEFAULT_THRESHOLD; - -// ✅ GOOD: Type-safe null checks -function processResult(result: Result | null): void { - if (!result) { - throw new AppError('Result is required', 'NULL_RESULT'); - } - // TypeScript now knows result is not null - console.log(result.value); -} -``` - ---- - -### Security Guidelines - -#### Input Validation - -```typescript -// lib/validation/sanitize.ts -import DOMPurify from 'dompurify'; - -export function sanitizeInput(input: string): string { - return DOMPurify.sanitize(input.trim()); -} - -export function validateUserId(userId: string): void { - const sanitized = sanitizeInput(userId); - if (sanitized.length < 1 || sanitized.length > 100) { - throw new ValidationError('User ID must be 1-100 characters', 'userId'); - } - if (!/^[a-zA-Z0-9_-]+$/.test(sanitized)) { - throw new ValidationError('User ID contains invalid characters', 'userId'); - } -} -``` - -#### XSS Prevention - -```typescript -// ✅ React automatically escapes JSX - safe by default -return

{userInput}
; - -// ⚠️ DANGEROUS: Only use when absolutely necessary -return
; - -// ✅ GOOD: Use DOMPurify for any HTML content -const safeHTML = DOMPurify.sanitize(untrustedHTML); -``` - -#### CSRF Protection - -```typescript -// lib/api/client.ts -class SecureAPIClient { - private csrfToken: string | null = null; - - async request(config: RequestConfig): Promise { - const headers: Record = { - 'Content-Type': 'application/json', - }; - - // Include CSRF token for mutating requests - if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(config.method)) { - if (this.csrfToken) { - headers['X-CSRF-Token'] = this.csrfToken; - } - } - - return fetch(config.url, { ...config, headers }); - } -} -``` - -#### Secure Data Handling - -| Data Type | Handling | -|-----------|----------| -| **API Keys** | Never in client code, use server-side only | -| **User Images** | Temporary URLs, auto-expire, no localStorage | -| **Session Data** | httpOnly cookies, not localStorage | -| **Sensitive Logs** | Never log PII, mask sensitive data | - ---- - -### Performance Best Practices - -#### Lazy Loading - -```typescript -// ✅ GOOD: Lazy load heavy components -const FacialLandmarksViewer = lazy(() => import('@/components/biometric/landmark-viewer')); -const BatchProcessor = lazy(() => import('@/components/batch-processor')); - -// Usage with Suspense -}> - - -``` - -#### Memoization - -```typescript -// ✅ GOOD: Memoize expensive computations -const processedResults = useMemo(() => { - return results.map(processResult).filter(filterResult); -}, [results]); - -// ✅ GOOD: Memoize callbacks passed to children -const handleCapture = useCallback((image: string) => { - setImage(image); -}, []); -``` - -#### Image Optimization - -```typescript -// ✅ GOOD: Resize images before upload -async function optimizeImage(file: File): Promise { - const maxDimension = 1920; - const quality = 0.85; - - const img = await createImageBitmap(file); - const scale = Math.min(1, maxDimension / Math.max(img.width, img.height)); - - const canvas = new OffscreenCanvas( - img.width * scale, - img.height * scale - ); - const ctx = canvas.getContext('2d')!; - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); - - const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality }); - return new File([blob], file.name, { type: 'image/jpeg' }); -} -``` - ---- - -### Testing Strategy (Detailed) - -#### AAA Pattern - -```typescript -// tests/unit/use-enroll-face.test.ts -describe('useEnrollFace', () => { - it('should enroll face successfully', async () => { - // Arrange - const mockImage = new File([''], 'test.jpg', { type: 'image/jpeg' }); - const mockUserId = 'user-123'; - const mockResponse = { faceId: 'face-456', quality: 0.95 }; - server.use( - rest.post('/api/faces/enroll', (req, res, ctx) => - res(ctx.json(mockResponse)) - ) - ); - - // Act - const { result } = renderHook(() => useEnrollFace()); - await act(async () => { - await result.current.mutateAsync({ image: mockImage, userId: mockUserId }); - }); - - // Assert - expect(result.current.data).toEqual(mockResponse); - expect(result.current.isSuccess).toBe(true); - }); -}); -``` - -#### Coverage Targets - -| Area | Minimum Coverage | -|------|-----------------| -| **Hooks** | 90% | -| **Utilities** | 95% | -| **Components** | 80% | -| **Integration** | 70% | -| **E2E Critical Paths** | 100% | - -#### Edge Case Testing - -```typescript -describe('ImageUploader', () => { - // Happy path - it('should accept valid JPEG image'); - it('should accept valid PNG image'); - - // Edge cases - it('should reject file over size limit'); - it('should reject invalid file type'); - it('should handle empty file'); - it('should handle corrupted image'); - it('should handle network failure during upload'); - it('should handle concurrent uploads'); - it('should handle upload cancellation'); -}); -``` - ---- - -### Version Control Guidelines - -#### Commit Message Format - -``` -(): - -[optional body] - -[optional footer] -``` - -| Type | Description | -|------|-------------| -| `feat` | New feature | -| `fix` | Bug fix | -| `refactor` | Code refactoring | -| `docs` | Documentation | -| `test` | Test additions/changes | -| `style` | Formatting, no code change | -| `chore` | Build, config changes | - -Example: -``` -feat(enrollment): add duplicate face detection - -- Implement check for existing face embeddings -- Show warning dialog when duplicate detected -- Add option to proceed or cancel enrollment - -Closes #123 -``` - -#### Branching Strategy - -``` -main (protected) - └── develop - ├── feature/enrollment-page - ├── feature/verification-page - ├── fix/camera-permission-error - └── refactor/api-client -``` - ---- - -### Documentation Standards - -#### Component Documentation - -```typescript -/** - * Captures images from user's webcam with face detection overlay. - * - * @component - * @example - * ```tsx - * console.log('Captured:', image)} - * aspectRatio="1:1" - * showOverlay - * /> - * ``` - * - * @remarks - * Requires camera permissions. Will show error state if permission denied. - */ -export function WebcamCapture({ - onCapture, - onError, - aspectRatio = '4:3', - showOverlay = false, -}: WebcamCaptureProps): JSX.Element { - // ... -} -``` - -#### API Documentation - -```typescript -/** - * Enrolls a face for future verification/search operations. - * - * @param params - Enrollment parameters - * @param params.image - Face image file (JPEG/PNG, max 10MB) - * @param params.userId - Unique identifier for the user - * @param params.metadata - Optional metadata to store with enrollment - * - * @returns Promise resolving to enrollment result with face ID and quality score - * - * @throws {ValidationError} If image or userId is invalid - * @throws {BiometricError} If face quality is too low or duplicate detected - * @throws {APIError} If server returns error response - * - * @example - * ```ts - * const result = await enrollFace({ - * image: capturedImage, - * userId: 'user-123', - * metadata: { source: 'webcam' } - * }); - * console.log(`Enrolled with face ID: ${result.faceId}`); - * ``` - */ -export async function enrollFace(params: EnrollParams): Promise { - // ... -} -``` - ---- - -## API Design Standards - -### RESTful Conventions - -#### URL Structure - -``` -GET /api/v1/faces # List all faces -POST /api/v1/faces # Create/enroll face -GET /api/v1/faces/{faceId} # Get specific face -DELETE /api/v1/faces/{faceId} # Delete face -POST /api/v1/faces/verify # Verify face (action) -POST /api/v1/faces/search # Search faces (action) - -GET /api/v1/liveness/challenges # Get available challenges -POST /api/v1/liveness/check # Perform liveness check - -GET /api/v1/proctoring/sessions # List sessions -POST /api/v1/proctoring/sessions # Create session -GET /api/v1/proctoring/sessions/{id} # Get session details -WS /api/v1/proctoring/sessions/{id}/stream # WebSocket stream -``` - -#### HTTP Methods - -| Method | Purpose | Idempotent | Safe | -|--------|---------|------------|------| -| `GET` | Retrieve resource(s) | Yes | Yes | -| `POST` | Create resource or action | No | No | -| `PUT` | Replace resource entirely | Yes | No | -| `PATCH` | Partial update | No | No | -| `DELETE` | Remove resource | Yes | No | - -#### API Versioning - -```typescript -// URL versioning (recommended) -const API_V1 = '/api/v1'; -const API_V2 = '/api/v2'; - -// Version in Accept header (alternative) -// Accept: application/vnd.biometric.v1+json -``` - -### Request/Response Standards - -#### Request Format - -```typescript -// POST /api/v1/faces -interface EnrollFaceRequest { - userId: string; // Required - image: string; // Base64 or multipart - metadata?: Record; // Optional - options?: { - qualityThreshold?: number; // Default: 0.7 - checkDuplicate?: boolean; // Default: true - }; -} - -// Query parameters for filtering/pagination -// GET /api/v1/faces?page=1&limit=20&sort=-createdAt&filter[quality_gte]=0.8 -``` - -#### Response Format - -```typescript -// Success response -interface APIResponse { - success: true; - data: T; - meta?: { - page?: number; - limit?: number; - total?: number; - totalPages?: number; - }; -} - -// Error response -interface APIErrorResponse { - success: false; - error: { - code: string; // Machine-readable: "FACE_NOT_DETECTED" - message: string; // Human-readable: "No face detected in image" - details?: unknown; // Additional context - field?: string; // For validation errors - requestId: string; // For debugging/support - }; -} -``` - -#### HTTP Status Codes - -| Code | Meaning | Use Case | -|------|---------|----------| -| `200` | OK | Successful GET, PUT, PATCH | -| `201` | Created | Successful POST creating resource | -| `204` | No Content | Successful DELETE | -| `400` | Bad Request | Validation error, malformed request | -| `401` | Unauthorized | Missing/invalid authentication | -| `403` | Forbidden | Authenticated but not authorized | -| `404` | Not Found | Resource doesn't exist | -| `409` | Conflict | Duplicate resource (e.g., face already enrolled) | -| `422` | Unprocessable | Business logic error (e.g., low quality) | -| `429` | Too Many Requests | Rate limit exceeded | -| `500` | Internal Error | Server error | -| `503` | Service Unavailable | Maintenance/overload | - -### Pagination - -```typescript -// Cursor-based pagination (preferred for large datasets) -interface PaginatedResponse { - data: T[]; - pagination: { - cursor: string | null; // Current position - nextCursor: string | null; // Next page cursor - hasMore: boolean; - limit: number; - }; -} - -// Offset-based pagination (for smaller datasets) -interface OffsetPaginatedResponse { - data: T[]; - pagination: { - page: number; - limit: number; - total: number; - totalPages: number; - }; -} -``` - -### Error Handling - -```typescript -// lib/api/errors.ts -const API_ERROR_CODES = { - // Validation errors (400) - INVALID_REQUEST: 'INVALID_REQUEST', - INVALID_IMAGE_FORMAT: 'INVALID_IMAGE_FORMAT', - IMAGE_TOO_LARGE: 'IMAGE_TOO_LARGE', - MISSING_REQUIRED_FIELD: 'MISSING_REQUIRED_FIELD', - - // Authentication errors (401) - INVALID_TOKEN: 'INVALID_TOKEN', - TOKEN_EXPIRED: 'TOKEN_EXPIRED', - - // Business logic errors (422) - FACE_NOT_DETECTED: 'FACE_NOT_DETECTED', - MULTIPLE_FACES_DETECTED: 'MULTIPLE_FACES_DETECTED', - LOW_QUALITY_IMAGE: 'LOW_QUALITY_IMAGE', - LIVENESS_CHECK_FAILED: 'LIVENESS_CHECK_FAILED', - DUPLICATE_FACE: 'DUPLICATE_FACE', - USER_NOT_FOUND: 'USER_NOT_FOUND', - - // Rate limiting (429) - RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED', - - // Server errors (500) - INTERNAL_ERROR: 'INTERNAL_ERROR', - MODEL_INFERENCE_FAILED: 'MODEL_INFERENCE_FAILED', -} as const; -``` - ---- - -## Internationalization (i18n) - -### Setup - -```typescript -// lib/i18n/config.ts -import { createInstance } from 'i18next'; -import { initReactI18next } from 'react-i18next'; - -export const SUPPORTED_LOCALES = ['en', 'tr'] as const; -export type Locale = typeof SUPPORTED_LOCALES[number]; - -export const DEFAULT_LOCALE: Locale = 'en'; - -const i18n = createInstance(); - -i18n - .use(initReactI18next) - .init({ - lng: DEFAULT_LOCALE, - fallbackLng: DEFAULT_LOCALE, - supportedLngs: SUPPORTED_LOCALES, - defaultNS: 'common', - interpolation: { - escapeValue: false, // React already escapes - }, - }); - -export default i18n; -``` - -### Translation Structure - -``` -src/ -├── locales/ -│ ├── en/ -│ │ ├── common.json # Shared translations -│ │ ├── enrollment.json # Enrollment page -│ │ ├── verification.json # Verification page -│ │ ├── errors.json # Error messages -│ │ └── validation.json # Form validation -│ └── tr/ -│ ├── common.json -│ ├── enrollment.json -│ ├── verification.json -│ ├── errors.json -│ └── validation.json -``` - -### Translation Files - -```json -// locales/en/common.json -{ - "app": { - "name": "Biometric Demo", - "tagline": "Professional Face Recognition System" - }, - "nav": { - "dashboard": "Dashboard", - "enrollment": "Face Enrollment", - "verification": "Verification", - "search": "Face Search", - "liveness": "Liveness Detection" - }, - "actions": { - "submit": "Submit", - "cancel": "Cancel", - "retry": "Try Again", - "capture": "Capture", - "upload": "Upload Image" - }, - "status": { - "loading": "Loading...", - "processing": "Processing...", - "success": "Success", - "error": "Error" - } -} - -// locales/tr/common.json -{ - "app": { - "name": "Biyometrik Demo", - "tagline": "Profesyonel Yüz Tanıma Sistemi" - }, - "nav": { - "dashboard": "Ana Sayfa", - "enrollment": "Yüz Kaydı", - "verification": "Doğrulama", - "search": "Yüz Arama", - "liveness": "Canlılık Tespiti" - }, - "actions": { - "submit": "Gönder", - "cancel": "İptal", - "retry": "Tekrar Dene", - "capture": "Yakala", - "upload": "Resim Yükle" - }, - "status": { - "loading": "Yükleniyor...", - "processing": "İşleniyor...", - "success": "Başarılı", - "error": "Hata" - } -} -``` - -### Usage in Components - -```tsx -// Using translations -import { useTranslation } from 'react-i18next'; - -function EnrollmentPage() { - const { t } = useTranslation(['enrollment', 'common']); - - return ( -
-

{t('enrollment:title')}

- -
- ); -} - -// With interpolation -t('enrollment:quality_score', { score: 95 }) -// "Quality Score: 95%" - -// With pluralization -t('search:results', { count: 5 }) -// "5 matches found" -``` - -### Date/Time/Number Formatting - -```typescript -// lib/i18n/formatters.ts -import { format, formatDistance } from 'date-fns'; -import { enUS, tr } from 'date-fns/locale'; - -const locales = { en: enUS, tr }; - -export function formatDate(date: Date, locale: Locale): string { - return format(date, 'PPP', { locale: locales[locale] }); -} - -export function formatRelativeTime(date: Date, locale: Locale): string { - return formatDistance(date, new Date(), { - addSuffix: true, - locale: locales[locale], - }); -} - -export function formatNumber(num: number, locale: Locale): string { - return new Intl.NumberFormat(locale).format(num); -} - -export function formatPercent(num: number, locale: Locale): string { - return new Intl.NumberFormat(locale, { - style: 'percent', - minimumFractionDigits: 1, - }).format(num); -} -``` - -### RTL Support (Future) - -```typescript -// For future Arabic/Hebrew support -const RTL_LOCALES = ['ar', 'he'] as const; - -export function isRTL(locale: Locale): boolean { - return RTL_LOCALES.includes(locale as any); -} - -// In layout - -``` - ---- - -## Monitoring & Observability - -### Logging Strategy - -```typescript -// lib/logging/logger.ts -import pino from 'pino'; - -const logger = pino({ - level: process.env.LOG_LEVEL || 'info', - browser: { - asObject: true, - }, - base: { - env: process.env.NODE_ENV, - version: process.env.NEXT_PUBLIC_APP_VERSION, - }, -}); - -// Structured logging -export function logInfo(message: string, context?: object): void { - logger.info({ ...context }, message); -} - -export function logError(error: Error, context?: object): void { - logger.error({ - error: { - name: error.name, - message: error.message, - stack: error.stack, - }, - ...context, - }, error.message); -} - -export function logAPICall(endpoint: string, duration: number, status: number): void { - logger.info({ - type: 'api_call', - endpoint, - duration, - status, - }, `API ${endpoint} responded in ${duration}ms`); -} -``` - -### Error Tracking (Sentry) - -```typescript -// lib/monitoring/sentry.ts -import * as Sentry from '@sentry/nextjs'; - -Sentry.init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, - environment: process.env.NODE_ENV, - tracesSampleRate: 0.1, // 10% of transactions - beforeSend(event) { - // Scrub PII - if (event.user) { - delete event.user.ip_address; - delete event.user.email; - } - return event; - }, -}); - -// Capture errors with context -export function captureError(error: Error, context?: Record): void { - Sentry.withScope((scope) => { - if (context) { - Object.entries(context).forEach(([key, value]) => { - scope.setExtra(key, value); - }); - } - Sentry.captureException(error); - }); -} - -// Track user actions -export function trackAction(action: string, data?: object): void { - Sentry.addBreadcrumb({ - category: 'user-action', - message: action, - data, - level: 'info', - }); -} -``` - -### Performance Monitoring - -```typescript -// lib/monitoring/performance.ts -import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'; - -export function initWebVitals(): void { - getCLS(sendToAnalytics); - getFID(sendToAnalytics); - getFCP(sendToAnalytics); - getLCP(sendToAnalytics); - getTTFB(sendToAnalytics); -} - -function sendToAnalytics(metric: { name: string; value: number; id: string }): void { - // Send to analytics service - const body = JSON.stringify({ - name: metric.name, - value: metric.value, - id: metric.id, - page: window.location.pathname, - }); - - // Use sendBeacon for reliability - if (navigator.sendBeacon) { - navigator.sendBeacon('/api/vitals', body); - } -} -``` - -### Health Checks - -```typescript -// app/api/health/route.ts -export async function GET() { - const checks = { - api: await checkAPIHealth(), - timestamp: new Date().toISOString(), - version: process.env.NEXT_PUBLIC_APP_VERSION, - uptime: process.uptime(), - }; - - const isHealthy = checks.api.healthy; - - return Response.json(checks, { - status: isHealthy ? 200 : 503, - }); -} - -async function checkAPIHealth() { - try { - const start = Date.now(); - const response = await fetch(`${API_URL}/health`, { timeout: 5000 }); - const latency = Date.now() - start; - - return { - healthy: response.ok, - latency, - status: response.status, - }; - } catch (error) { - return { - healthy: false, - error: error.message, - }; - } -} -``` - -### Metrics Dashboard - -| Metric | Description | Alert Threshold | -|--------|-------------|-----------------| -| **API Latency (p95)** | 95th percentile response time | > 2000ms | -| **Error Rate** | Percentage of failed requests | > 1% | -| **LCP** | Largest Contentful Paint | > 2500ms | -| **FID** | First Input Delay | > 100ms | -| **CLS** | Cumulative Layout Shift | > 0.1 | -| **Active Users** | Concurrent users | N/A | -| **Enrollment Success Rate** | % of successful enrollments | < 95% | - ---- - -## CI/CD Pipeline - -### GitHub Actions Workflow - -```yaml -# .github/workflows/ci.yml -name: CI/CD Pipeline - -on: - push: - branches: [main, develop] - pull_request: - branches: [main, develop] - -env: - NODE_VERSION: '20' - PNPM_VERSION: '8' - -jobs: - # ============================================ - # Quality Gates - # ============================================ - lint: - name: Lint & Type Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 - with: - version: ${{ env.PNPM_VERSION }} - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'pnpm' - - run: pnpm install --frozen-lockfile - - run: pnpm lint - - run: pnpm type-check - - test: - name: Unit & Integration Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 - with: - version: ${{ env.PNPM_VERSION }} - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'pnpm' - - run: pnpm install --frozen-lockfile - - run: pnpm test:coverage - - name: Upload Coverage - uses: codecov/codecov-action@v3 - with: - files: ./coverage/lcov.info - fail_ci_if_error: true - - e2e: - name: E2E Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 - with: - version: ${{ env.PNPM_VERSION }} - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'pnpm' - - run: pnpm install --frozen-lockfile - - run: pnpm exec playwright install --with-deps - - run: pnpm build - - run: pnpm test:e2e - - uses: actions/upload-artifact@v3 - if: failure() - with: - name: playwright-report - path: playwright-report/ - - security: - name: Security Scan - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Run Snyk - uses: snyk/actions/node@master - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - with: - args: --severity-threshold=high - - # ============================================ - # Build & Deploy - # ============================================ - build: - name: Build - needs: [lint, test] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 - with: - version: ${{ env.PNPM_VERSION }} - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'pnpm' - - run: pnpm install --frozen-lockfile - - run: pnpm build - - name: Upload Build - uses: actions/upload-artifact@v3 - with: - name: build - path: .next/ - - deploy-preview: - name: Deploy Preview - needs: [build] - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 - with: - name: build - path: .next/ - - name: Deploy to Vercel Preview - uses: amondnet/vercel-action@v25 - with: - vercel-token: ${{ secrets.VERCEL_TOKEN }} - vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} - vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} - - deploy-production: - name: Deploy Production - needs: [build, e2e, security] - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - environment: production - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 - with: - name: build - path: .next/ - - name: Deploy to Vercel Production - uses: amondnet/vercel-action@v25 - with: - vercel-token: ${{ secrets.VERCEL_TOKEN }} - vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} - vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} - vercel-args: '--prod' -``` - -### Quality Gates - -| Gate | Tool | Threshold | -|------|------|-----------| -| **Linting** | ESLint | 0 errors | -| **Type Check** | TypeScript | 0 errors | -| **Unit Tests** | Vitest | 100% pass | -| **Coverage** | Vitest | 80% minimum | -| **E2E Tests** | Playwright | 100% pass | -| **Security** | Snyk | No high/critical | -| **Bundle Size** | size-limit | < 200KB JS | -| **Lighthouse** | Lighthouse CI | Score > 90 | - -### Pre-commit Hooks - -```json -// package.json -{ - "lint-staged": { - "*.{ts,tsx}": [ - "eslint --fix", - "prettier --write" - ], - "*.{json,md}": [ - "prettier --write" - ] - } -} - -// .husky/pre-commit -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" -npx lint-staged - -// .husky/commit-msg -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" -npx --no -- commitlint --edit "$1" -``` - ---- - -## Advanced Security - -### OWASP Top 10 Compliance - -| Vulnerability | Prevention | Implementation | -|---------------|------------|----------------| -| **A01: Broken Access Control** | Authorization checks | Middleware + route guards | -| **A02: Cryptographic Failures** | TLS, secure storage | HTTPS only, no localStorage for secrets | -| **A03: Injection** | Input validation | Zod schemas, parameterized queries | -| **A04: Insecure Design** | Threat modeling | Security review in design phase | -| **A05: Security Misconfiguration** | Secure defaults | Security headers, CSP | -| **A06: Vulnerable Components** | Dependency scanning | Snyk, npm audit | -| **A07: Auth Failures** | Strong auth | JWT validation, session management | -| **A08: Data Integrity** | Input validation | Schema validation, checksums | -| **A09: Logging Failures** | Audit logging | Structured logs, no PII | -| **A10: SSRF** | URL validation | Allowlist, sanitization | - -### Security Headers - -```typescript -// next.config.js -const securityHeaders = [ - { - key: 'X-DNS-Prefetch-Control', - value: 'on', - }, - { - key: 'Strict-Transport-Security', - value: 'max-age=63072000; includeSubDomains; preload', - }, - { - key: 'X-Frame-Options', - value: 'SAMEORIGIN', - }, - { - key: 'X-Content-Type-Options', - value: 'nosniff', - }, - { - key: 'X-XSS-Protection', - value: '1; mode=block', - }, - { - key: 'Referrer-Policy', - value: 'strict-origin-when-cross-origin', - }, - { - key: 'Permissions-Policy', - value: 'camera=(self), microphone=(), geolocation=()', - }, -]; - -module.exports = { - async headers() { - return [ - { - source: '/:path*', - headers: securityHeaders, - }, - ]; - }, -}; -``` - -### Content Security Policy - -```typescript -// middleware.ts -const CSP = ` - default-src 'self'; - script-src 'self' 'unsafe-eval' 'unsafe-inline'; - style-src 'self' 'unsafe-inline'; - img-src 'self' blob: data:; - font-src 'self'; - connect-src 'self' ${process.env.NEXT_PUBLIC_API_URL} ws:; - media-src 'self' blob:; - frame-ancestors 'none'; - base-uri 'self'; - form-action 'self'; -`.replace(/\n/g, ' ').trim(); - -export function middleware(request: NextRequest) { - const response = NextResponse.next(); - response.headers.set('Content-Security-Policy', CSP); - return response; -} -``` - -### Rate Limiting - -```typescript -// lib/security/rate-limit.ts -import { Ratelimit } from '@upstash/ratelimit'; -import { Redis } from '@upstash/redis'; - -const redis = new Redis({ - url: process.env.UPSTASH_REDIS_URL!, - token: process.env.UPSTASH_REDIS_TOKEN!, -}); - -// Different limits for different actions -export const rateLimiters = { - // General API calls: 100 per minute - api: new Ratelimit({ - redis, - limiter: Ratelimit.slidingWindow(100, '1 m'), - prefix: 'ratelimit:api', - }), - - // Enrollment: 10 per minute (expensive operation) - enrollment: new Ratelimit({ - redis, - limiter: Ratelimit.slidingWindow(10, '1 m'), - prefix: 'ratelimit:enrollment', - }), - - // Verification: 30 per minute - verification: new Ratelimit({ - redis, - limiter: Ratelimit.slidingWindow(30, '1 m'), - prefix: 'ratelimit:verification', - }), - - // Search: 20 per minute - search: new Ratelimit({ - redis, - limiter: Ratelimit.slidingWindow(20, '1 m'), - prefix: 'ratelimit:search', - }), -}; - -// Usage in API route -export async function checkRateLimit( - identifier: string, - limiter: keyof typeof rateLimiters -): Promise<{ success: boolean; remaining: number }> { - const { success, remaining } = await rateLimiters[limiter].limit(identifier); - return { success, remaining }; -} -``` - -### Biometric Data Security - -```typescript -// lib/security/biometric-handling.ts - -// Never store raw biometric data client-side -const BIOMETRIC_RULES = { - // Images are processed and immediately discarded - imageRetention: 'none', - - // Only store embeddings (not reversible to image) - storageType: 'embeddings-only', - - // Encrypt embeddings at rest - encryption: 'AES-256-GCM', - - // Auto-delete after session ends - sessionCleanup: true, - - // No caching of biometric data - caching: 'disabled', -}; - -// Secure image handling -export async function processSecureImage(file: File): Promise { - // Convert to processing format - const imageData = await fileToBase64(file); - - try { - // Send to API for processing - await enrollFace(imageData); - } finally { - // Clear from memory immediately - // Note: JavaScript doesn't guarantee immediate GC - // but we null references to help - imageData = null; - } -} - -// Clear sensitive data on unmount -export function useBiometricCleanup() { - useEffect(() => { - return () => { - // Clear any cached images - sessionStorage.removeItem('capturedImage'); - // Revoke object URLs - URL.revokeObjectURL(imageUrl); - }; - }, []); -} -``` - ---- - -## Data Privacy (GDPR/KVKK) - -### Biometric Data Classification - -| Data Type | Classification | Retention | Legal Basis | -|-----------|---------------|-----------|-------------| -| **Face Image** | Special Category (Biometric) | Session only | Explicit Consent | -| **Face Embedding** | Special Category (Biometric) | Until deletion request | Explicit Consent | -| **User ID** | Personal Data | With embedding | Legitimate Interest | -| **Metadata** | Personal/Non-personal | With embedding | Legitimate Interest | -| **Logs** | Personal Data | 30 days | Legitimate Interest | - -### Consent Management - -```typescript -// lib/privacy/consent.ts -interface ConsentRecord { - userId: string; - biometricProcessing: boolean; - dataRetention: boolean; - timestamp: Date; - ipAddress?: string; // Anonymized - version: string; // Consent policy version -} - -export async function recordConsent(consent: ConsentRecord): Promise { - // Store consent with timestamp - await api.post('/consent', { - ...consent, - ipAddress: anonymizeIP(consent.ipAddress), - }); -} - -// Consent UI component -function BiometricConsentDialog({ onAccept, onDecline }) { - return ( - - Biometric Data Processing - -

This demo processes your facial data for:

-
    -
  • Face enrollment and recognition
  • -
  • Liveness detection
  • -
  • Quality analysis
  • -
-

Your data will be:

-
    -
  • Processed locally when possible
  • -
  • Encrypted in transit and at rest
  • -
  • Deleted upon your request
  • -
- - I consent to biometric data processing - -
- - - - -
- ); -} -``` - -### Data Subject Rights (GDPR Article 15-22) - -```typescript -// lib/privacy/data-rights.ts - -// Right to Access (Article 15) -export async function exportUserData(userId: string): Promise { - const data = await api.get(`/users/${userId}/export`); - return { - enrollments: data.enrollments, - metadata: data.metadata, - consentRecords: data.consents, - activityLog: data.activities, - exportedAt: new Date().toISOString(), - }; -} - -// Right to Erasure (Article 17) -export async function deleteUserData(userId: string): Promise { - await api.delete(`/users/${userId}`, { - // Confirm deletion - headers: { 'X-Confirm-Delete': 'true' }, - }); -} - -// Right to Rectification (Article 16) -export async function updateUserData( - userId: string, - updates: Partial -): Promise { - await api.patch(`/users/${userId}`, updates); -} - -// Right to Data Portability (Article 20) -export async function exportPortableData(userId: string): Promise { - const response = await api.get(`/users/${userId}/export`, { - headers: { 'Accept': 'application/json' }, - }); - return new Blob([JSON.stringify(response, null, 2)], { - type: 'application/json', - }); -} -``` - -### KVKK Compliance (Turkish Data Protection) - -```typescript -// Additional requirements for KVKK -const KVKK_REQUIREMENTS = { - // Explicit consent required for biometric data - explicitConsent: true, - - // Data controller registration with VERBIS - verbisRegistration: 'REQUIRED', - - // Cross-border transfer restrictions - crossBorderTransfer: { - allowed: false, // Unless adequate protection - adequateCountries: ['EU', 'EEA'], - }, - - // Data retention limits - retentionPeriod: { - biometric: '2 years max', - logs: '30 days', - }, - - // Turkish language requirement - consentLanguage: 'tr', -}; -``` - ---- - -## Browser Compatibility - -### Supported Browsers - -| Browser | Minimum Version | Notes | -|---------|-----------------|-------| -| **Chrome** | 90+ | Full support | -| **Firefox** | 88+ | Full support | -| **Safari** | 14+ | WebRTC requires HTTPS | -| **Edge** | 90+ | Chromium-based | -| **Opera** | 76+ | Chromium-based | -| **Samsung Internet** | 14+ | Mobile | -| **iOS Safari** | 14.5+ | WebRTC limitations | - -### Feature Detection - -```typescript -// lib/compatibility/feature-detection.ts -export interface BrowserCapabilities { - webrtc: boolean; - mediaDevices: boolean; - webgl: boolean; - webSocket: boolean; - serviceWorker: boolean; - indexedDB: boolean; -} - -export function detectCapabilities(): BrowserCapabilities { - return { - webrtc: !!window.RTCPeerConnection, - mediaDevices: !!(navigator.mediaDevices?.getUserMedia), - webgl: (() => { - try { - const canvas = document.createElement('canvas'); - return !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl')); - } catch { - return false; - } - })(), - webSocket: 'WebSocket' in window, - serviceWorker: 'serviceWorker' in navigator, - indexedDB: 'indexedDB' in window, - }; -} - -// Show warning for unsupported browsers -export function BrowserCompatibilityCheck({ children }: { children: React.ReactNode }) { - const [isSupported, setIsSupported] = useState(true); - const [missingFeatures, setMissingFeatures] = useState([]); - - useEffect(() => { - const caps = detectCapabilities(); - const missing = []; - - if (!caps.mediaDevices) missing.push('Camera access'); - if (!caps.webrtc) missing.push('WebRTC'); - if (!caps.webSocket) missing.push('WebSocket'); - - if (missing.length > 0) { - setIsSupported(false); - setMissingFeatures(missing); - } - }, []); - - if (!isSupported) { - return ( - - Browser Not Supported - - Your browser is missing: {missingFeatures.join(', ')}. - Please use a modern browser like Chrome, Firefox, or Edge. - - - ); - } - - return children; -} -``` - -### Polyfills - -```typescript -// lib/polyfills.ts -// Only load polyfills for older browsers - -export async function loadPolyfills(): Promise { - const polyfills: Promise[] = []; - - // ResizeObserver - if (!('ResizeObserver' in window)) { - polyfills.push( - import('resize-observer-polyfill').then(({ default: RO }) => { - window.ResizeObserver = RO; - }) - ); - } - - // IntersectionObserver - if (!('IntersectionObserver' in window)) { - polyfills.push(import('intersection-observer')); - } - - // Fetch (very old browsers) - if (!('fetch' in window)) { - polyfills.push(import('whatwg-fetch')); - } - - await Promise.all(polyfills); -} -``` - ---- - -## Offline/PWA Support - -### Service Worker Strategy - -```typescript -// next.config.js with next-pwa -const withPWA = require('next-pwa')({ - dest: 'public', - disable: process.env.NODE_ENV === 'development', - register: true, - skipWaiting: true, - runtimeCaching: [ - { - // Cache static assets - urlPattern: /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\/.*/i, - handler: 'CacheFirst', - options: { - cacheName: 'google-fonts', - expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 }, - }, - }, - { - // Cache API responses (except biometric) - urlPattern: /^https:\/\/api\..*\/(?!faces|liveness).*/i, - handler: 'NetworkFirst', - options: { - cacheName: 'api-cache', - expiration: { maxEntries: 50, maxAgeSeconds: 60 * 5 }, - networkTimeoutSeconds: 10, - }, - }, - { - // Never cache biometric data - urlPattern: /^https:\/\/api\..*\/(faces|liveness).*/i, - handler: 'NetworkOnly', - }, - ], -}); -``` - -### Offline UI - -```tsx -// hooks/use-online-status.ts -export function useOnlineStatus() { - const [isOnline, setIsOnline] = useState(navigator.onLine); - - useEffect(() => { - const handleOnline = () => setIsOnline(true); - const handleOffline = () => setIsOnline(false); - - window.addEventListener('online', handleOnline); - window.addEventListener('offline', handleOffline); - - return () => { - window.removeEventListener('online', handleOnline); - window.removeEventListener('offline', handleOffline); - }; - }, []); - - return isOnline; -} - -// Offline banner component -function OfflineBanner() { - const isOnline = useOnlineStatus(); - - if (isOnline) return null; - - return ( -
- - You're offline. Some features may be unavailable. -
- ); -} -``` - -### PWA Manifest - -```json -// public/manifest.json -{ - "name": "Biometric Demo", - "short_name": "BiometricDemo", - "description": "Professional Face Recognition Demo", - "start_url": "/", - "display": "standalone", - "background_color": "#ffffff", - "theme_color": "#2563eb", - "icons": [ - { - "src": "/icons/icon-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "any maskable" - }, - { - "src": "/icons/icon-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "any maskable" - } - ] -} -``` - ---- - -## Bundle Optimization - -### Performance Budgets - -```javascript -// size-limit.config.js -module.exports = [ - { - name: 'Total Bundle', - path: '.next/static/chunks/*.js', - limit: '200 KB', - gzip: true, - }, - { - name: 'First Load JS', - path: '.next/static/chunks/pages/_app*.js', - limit: '100 KB', - gzip: true, - }, - { - name: 'Main Bundle', - path: '.next/static/chunks/main*.js', - limit: '50 KB', - gzip: true, - }, -]; -``` - -### Code Splitting Strategy - -```typescript -// Dynamic imports for heavy components -const FacialLandmarks = dynamic( - () => import('@/components/biometric/facial-landmarks'), - { - loading: () => , - ssr: false, // Client-only component - } -); - -const BatchProcessor = dynamic( - () => import('@/components/batch-processor'), - { loading: () => } -); - -const AdminDashboard = dynamic( - () => import('@/components/admin/dashboard'), - { loading: () => } -); - -// Route-based code splitting (automatic with Next.js App Router) -// Each page in app/ is automatically code-split -``` - -### Tree Shaking - -```typescript -// ✅ GOOD: Named imports enable tree shaking -import { Button, Card, Dialog } from '@/components/ui'; -import { formatDate, formatNumber } from '@/lib/utils'; - -// ❌ BAD: Namespace imports prevent tree shaking -import * as UI from '@/components/ui'; -import * as Utils from '@/lib/utils'; - -// ✅ GOOD: Import only needed lodash functions -import debounce from 'lodash/debounce'; -import throttle from 'lodash/throttle'; - -// ❌ BAD: Import entire lodash -import _ from 'lodash'; -``` - -### Image Optimization - -```tsx -// Use Next.js Image for automatic optimization -import Image from 'next/image'; - -// Optimized image with responsive sizes -Sample face - -// SVG icons - use inline for small icons -import { CheckIcon } from 'lucide-react'; - -``` - ---- - -## Feature Flags - -### Feature Flag System - -```typescript -// lib/feature-flags/flags.ts -export const FEATURE_FLAGS = { - // Enrollment features - ENROLLMENT_DUPLICATE_CHECK: 'enrollment.duplicate_check', - ENROLLMENT_QUALITY_THRESHOLD: 'enrollment.quality_threshold', - - // New features (gradual rollout) - ACTIVE_LIVENESS: 'liveness.active_mode', - BATCH_PROCESSING: 'batch.enabled', - REAL_TIME_PROCTORING: 'proctoring.realtime', - - // Experiments - NEW_SIMILARITY_GAUGE: 'experiment.new_gauge', - DARK_MODE: 'ui.dark_mode', - - // Operational - MAINTENANCE_MODE: 'ops.maintenance', - DEBUG_MODE: 'ops.debug', -} as const; - -type FeatureFlag = typeof FEATURE_FLAGS[keyof typeof FEATURE_FLAGS]; - -// lib/feature-flags/provider.tsx -interface FeatureFlagContext { - isEnabled: (flag: FeatureFlag) => boolean; - flags: Record; -} - -export function useFeatureFlag(flag: FeatureFlag): boolean { - const { flags } = useContext(FeatureFlagContext); - return flags[flag] ?? false; -} - -// Usage -function EnrollmentPage() { - const showDuplicateCheck = useFeatureFlag(FEATURE_FLAGS.ENROLLMENT_DUPLICATE_CHECK); - const showActiveLiveness = useFeatureFlag(FEATURE_FLAGS.ACTIVE_LIVENESS); - - return ( - <> - {showDuplicateCheck && } - {showActiveLiveness && } - - ); -} -``` - -### A/B Testing - -```typescript -// lib/experiments/ab-testing.ts -interface Experiment { - id: string; - name: string; - variants: string[]; - weights: number[]; // Must sum to 1 -} - -const EXPERIMENTS: Experiment[] = [ - { - id: 'gauge-design', - name: 'Similarity Gauge Design', - variants: ['control', 'radial', 'linear'], - weights: [0.34, 0.33, 0.33], - }, -]; - -export function getExperimentVariant( - experimentId: string, - userId: string -): string { - const experiment = EXPERIMENTS.find(e => e.id === experimentId); - if (!experiment) return 'control'; - - // Deterministic assignment based on userId hash - const hash = hashString(`${experimentId}:${userId}`); - const bucket = (hash % 100) / 100; - - let cumulative = 0; - for (let i = 0; i < experiment.variants.length; i++) { - cumulative += experiment.weights[i]; - if (bucket < cumulative) { - return experiment.variants[i]; - } - } - - return experiment.variants[0]; -} -``` - ---- - -## Analytics & Telemetry - -### Analytics Events - -```typescript -// lib/analytics/events.ts -export const ANALYTICS_EVENTS = { - // Page views - PAGE_VIEW: 'page_view', - - // User actions - FACE_CAPTURED: 'face_captured', - FACE_ENROLLED: 'face_enrolled', - FACE_VERIFIED: 'face_verified', - FACE_SEARCHED: 'face_searched', - LIVENESS_CHECKED: 'liveness_checked', - - // Errors - ENROLLMENT_FAILED: 'enrollment_failed', - VERIFICATION_FAILED: 'verification_failed', - CAMERA_PERMISSION_DENIED: 'camera_permission_denied', - - // Performance - API_LATENCY: 'api_latency', - PAGE_LOAD: 'page_load', -} as const; - -// lib/analytics/tracker.ts -interface AnalyticsEvent { - name: string; - properties?: Record; - timestamp: number; -} - -class AnalyticsTracker { - private queue: AnalyticsEvent[] = []; - private userId: string | null = null; - - identify(userId: string): void { - this.userId = userId; - } - - track(name: string, properties?: Record): void { - const event: AnalyticsEvent = { - name, - properties: { - ...properties, - userId: this.userId, - sessionId: this.getSessionId(), - page: window.location.pathname, - }, - timestamp: Date.now(), - }; - - this.queue.push(event); - this.flush(); - } - - private async flush(): Promise { - if (this.queue.length === 0) return; - - const events = [...this.queue]; - this.queue = []; - - try { - await fetch('/api/analytics', { - method: 'POST', - body: JSON.stringify({ events }), - keepalive: true, - }); - } catch { - // Re-queue on failure - this.queue.unshift(...events); - } - } -} - -export const analytics = new AnalyticsTracker(); -``` - -### Usage Tracking (Privacy-Respecting) - -```typescript -// Track without PII -analytics.track(ANALYTICS_EVENTS.FACE_ENROLLED, { - qualityScore: result.quality, // OK: not PII - duration: enrollmentTime, // OK: performance metric - source: 'webcam', // OK: not PII - // userId: user.id, // NO: PII - // faceId: result.faceId, // NO: could be PII -}); - -// Anonymized error tracking -analytics.track(ANALYTICS_EVENTS.ENROLLMENT_FAILED, { - errorCode: error.code, // OK: machine code - errorType: error.type, // OK: category - // errorMessage: error.message, // MAYBE: could contain PII -}); -``` - ---- - -## Developer Experience - -### Local Development Setup - -```bash -# Quick start script -# scripts/setup.sh - -#!/bin/bash -echo "Setting up Biometric Demo UI..." - -# Check Node version -NODE_VERSION=$(node -v | cut -d'.' -f1 | sed 's/v//') -if [ "$NODE_VERSION" -lt 18 ]; then - echo "Error: Node 18+ required" - exit 1 -fi - -# Install dependencies -pnpm install - -# Copy environment file -if [ ! -f .env.local ]; then - cp .env.example .env.local - echo "Created .env.local - please update with your values" -fi - -# Generate types from API schema (if available) -pnpm generate:types - -# Run initial build to catch errors -pnpm build - -echo "Setup complete! Run 'pnpm dev' to start development server" -``` - -### Development Scripts - -```json -// package.json -{ - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "eslint . --ext .ts,.tsx --fix", - "type-check": "tsc --noEmit", - "test": "vitest", - "test:watch": "vitest --watch", - "test:coverage": "vitest --coverage", - "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui", - "storybook": "storybook dev -p 6006", - "storybook:build": "storybook build", - "generate:types": "openapi-typescript $API_URL/openapi.json -o src/types/api.generated.ts", - "analyze": "ANALYZE=true next build", - "prepare": "husky install" - } -} -``` - -### Storybook for Components - -```typescript -// .storybook/main.ts -import type { StorybookConfig } from '@storybook/nextjs'; - -const config: StorybookConfig = { - stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'], - addons: [ - '@storybook/addon-links', - '@storybook/addon-essentials', - '@storybook/addon-interactions', - '@storybook/addon-a11y', - ], - framework: { - name: '@storybook/nextjs', - options: {}, - }, -}; - -export default config; - -// Example story -// src/components/ui/button.stories.tsx -import type { Meta, StoryObj } from '@storybook/react'; -import { Button } from './button'; - -const meta: Meta = { - title: 'UI/Button', - component: Button, - tags: ['autodocs'], - argTypes: { - variant: { - control: 'select', - options: ['default', 'primary', 'destructive', 'outline', 'ghost'], - }, - size: { - control: 'select', - options: ['sm', 'md', 'lg'], - }, - }, -}; - -export default meta; -type Story = StoryObj; - -export const Primary: Story = { - args: { - variant: 'primary', - children: 'Enroll Face', - }, -}; - -export const Loading: Story = { - args: { - variant: 'primary', - children: 'Processing...', - isLoading: true, - }, -}; -``` - -### Mock API for Development - -```typescript -// lib/api/mock-server.ts -import { http, HttpResponse } from 'msw'; -import { setupWorker } from 'msw/browser'; - -const handlers = [ - // Health check - http.get('/api/v1/health', () => { - return HttpResponse.json({ status: 'healthy', version: '1.0.0' }); - }), - - // Enroll face - http.post('/api/v1/faces', async ({ request }) => { - await delay(1000); // Simulate processing - return HttpResponse.json({ - success: true, - data: { - faceId: `face_${Date.now()}`, - quality: 0.92, - enrolledAt: new Date().toISOString(), - }, - }); - }), - - // Verify face - http.post('/api/v1/faces/verify', async () => { - await delay(800); - return HttpResponse.json({ - success: true, - data: { - match: true, - similarity: 0.94, - confidence: 'high', - }, - }); - }), -]; - -export const mockServer = setupWorker(...handlers); - -// Start in development -if (process.env.NODE_ENV === 'development' && process.env.NEXT_PUBLIC_USE_MOCKS === 'true') { - mockServer.start({ onUnhandledRequest: 'bypass' }); -} -``` - ---- - -## Dependency Management - -### Dependency Policy - -| Category | Policy | -|----------|--------| -| **Security Updates** | Apply within 24 hours for critical, 1 week for high | -| **Major Updates** | Evaluate quarterly, test thoroughly | -| **Minor Updates** | Apply monthly with testing | -| **Patch Updates** | Apply weekly, auto-merge if tests pass | - -### Automated Dependency Updates - -```yaml -# .github/dependabot.yml -version: 2 -updates: - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 10 - groups: - # Group minor/patch updates - dependencies: - patterns: - - "*" - exclude-patterns: - - "eslint*" - - "typescript" - update-types: - - "minor" - - "patch" - ignore: - # Don't auto-update major versions - - dependency-name: "*" - update-types: ["version-update:semver-major"] -``` - -### License Compliance - -```javascript -// license-checker.config.js -module.exports = { - // Allowed licenses - allowed: [ - 'MIT', - 'ISC', - 'BSD-2-Clause', - 'BSD-3-Clause', - 'Apache-2.0', - '0BSD', - 'CC0-1.0', - ], - - // Explicitly approved packages with other licenses - exceptions: { - // Example: package@version: 'reason for approval' - }, - - // Packages to skip (dev-only, not bundled) - skip: [ - '@types/*', - 'typescript', - 'eslint*', - ], -}; -``` - ---- - -## Disaster Recovery - -### Backup Strategy - -| Data | Backup Frequency | Retention | Location | -|------|------------------|-----------|----------| -| **User Enrollments** | Real-time (API handles) | 30 days | API database | -| **Application State** | N/A (stateless) | N/A | N/A | -| **Configuration** | Git | Forever | GitHub | -| **Logs** | Daily | 30 days | Log service | - -### Rollback Procedures - -```bash -# Vercel automatic rollback -# 1. Go to Vercel dashboard -# 2. Select previous deployment -# 3. Click "Promote to Production" - -# Manual rollback via CLI -vercel rollback [deployment-url] - -# Git-based rollback -git revert HEAD -git push origin main -# Triggers new deployment with reverted code -``` - -### Incident Response - -```markdown -## Incident Severity Levels - -| Level | Description | Response Time | Example | -|-------|-------------|---------------|---------| -| **P1** | Service down | 15 minutes | App unreachable | -| **P2** | Major feature broken | 1 hour | Enrollment failing | -| **P3** | Minor feature broken | 4 hours | Dark mode not working | -| **P4** | Cosmetic issue | Next sprint | Button misaligned | - -## Incident Checklist - -1. [ ] Acknowledge incident in Slack -2. [ ] Assess severity level -3. [ ] Create incident channel if P1/P2 -4. [ ] Investigate root cause -5. [ ] Implement fix or rollback -6. [ ] Verify fix in production -7. [ ] Update status page -8. [ ] Write post-mortem (P1/P2 only) -``` - ---- - -## Technical Debt Management - -### Debt Classification - -| Type | Example | Priority | -|------|---------|----------| -| **Architectural** | Monolith needs splitting | High | -| **Code Quality** | Duplicated logic | Medium | -| **Testing** | Missing E2E tests | Medium | -| **Documentation** | Outdated API docs | Low | -| **Tooling** | Old build system | Low | - -### Debt Tracking - -```typescript -// Use TODO comments with tickets -// TODO(DEMO-123): Refactor this to use shared hook -// FIXME(DEMO-456): This breaks on mobile Safari -// HACK(DEMO-789): Temporary workaround for API bug - -// eslint-disable-next-line - requires comment -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- DEMO-101: API returns untyped response -``` - -### Debt Reduction Policy - -```markdown -## Technical Debt Policy - -1. **20% Rule**: Allocate 20% of each sprint to debt reduction -2. **No New Debt**: PRs adding debt require ticket creation -3. **Debt Reviews**: Monthly review of debt backlog -4. **Debt Metrics**: Track debt count and age in dashboard - -## Debt Prioritization Matrix - -| Impact / Effort | Low Effort | High Effort | -|-----------------|------------|-------------| -| **High Impact** | Do Now | Plan Sprint | -| **Low Impact** | Do When Free | Backlog | -``` - ---- - -## Implementation Plan - -### Phase 1: Foundation (Days 1-2) - -- [ ] Initialize Next.js 14 project with TypeScript -- [ ] Configure TailwindCSS and shadcn/ui -- [ ] Set up project structure -- [ ] Implement API client with mock mode -- [ ] Create layout components (Header, Sidebar, Footer) -- [ ] Implement settings store (Zustand) -- [ ] Add API health check - -### Phase 2: Core Features (Days 3-5) - -- [ ] Dashboard page with feature cards -- [ ] WebcamCapture component -- [ ] ImageUploader component -- [ ] Face Enrollment page -- [ ] Face Verification page with SimilarityGauge -- [ ] Face Search page with results table -- [ ] Liveness Detection page - -### Phase 3: Advanced Analysis (Days 6-7) - -- [ ] Quality Analysis page -- [ ] Demographics page -- [ ] Facial Landmarks page with canvas overlay -- [ ] Face Comparison page -- [ ] Batch Processing page - -### Phase 4: Proctoring (Days 8-9) - -- [ ] WebSocket manager -- [ ] Real-time video component -- [ ] Proctoring Session page -- [ ] Real-time Feed page -- [ ] Alert visualization - -### Phase 5: Administration (Day 10) - -- [ ] Admin Dashboard -- [ ] Webhooks management -- [ ] Configuration viewer -- [ ] API Explorer - -### Phase 6: Polish & Testing (Days 11-12) - -- [ ] Add Framer Motion animations -- [ ] Dark mode support -- [ ] Mobile responsiveness testing -- [ ] Write unit tests -- [ ] Write E2E tests -- [ ] Performance optimization - ---- - -## Deployment Strategy - -### Development - -```bash -npm run dev -# Runs on http://localhost:3000 -# API expected at http://localhost:8001 -``` - -### Production Build - -```bash -npm run build -npm run start -``` - -### Docker Deployment - -```dockerfile -FROM node:20-alpine AS builder -WORKDIR /app -COPY package*.json ./ -RUN npm ci -COPY . . -RUN npm run build - -FROM node:20-alpine AS runner -WORKDIR /app -COPY --from=builder /app/.next/standalone ./ -COPY --from=builder /app/.next/static ./.next/static -EXPOSE 3000 -CMD ["node", "server.js"] -``` - -### Environment Variables - -```env -# .env.local -NEXT_PUBLIC_API_URL=http://localhost:8001 -NEXT_PUBLIC_WS_URL=ws://localhost:8001 -NEXT_PUBLIC_APP_NAME="Biometric Demo" -``` - ---- - -## Conclusion - -This Next.js-based demo application provides: - -1. **Professional Enterprise UI** - shadcn/ui components with polished styling -2. **Full Real-Time Support** - Native WebRTC and WebSocket integration -3. **Mobile-First Design** - Responsive across all devices -4. **Type-Safe Implementation** - Full TypeScript coverage -5. **Testable Architecture** - Comprehensive testing strategy -6. **Production-Ready** - Can evolve into actual client-facing application - -The architecture follows software engineering best practices with SOLID principles, design patterns, and clean code standards suitable for a university engineering project. diff --git a/docs/archive/2026-04-16/DETAILED_IMPROVEMENT_DESIGNS.md b/docs/archive/2026-04-16/DETAILED_IMPROVEMENT_DESIGNS.md deleted file mode 100644 index 92984d8..0000000 --- a/docs/archive/2026-04-16/DETAILED_IMPROVEMENT_DESIGNS.md +++ /dev/null @@ -1,2274 +0,0 @@ -# Biometric Processor - Complete Quality Improvement Design Document - -**Version**: 1.0.0 -**Date**: 2025-12-25 -**Status**: Design Complete, Ready for Implementation -**Scope**: 40+ improvements across Security, Performance, Architecture, Quality, Testing, Features - ---- - -## 📋 TABLE OF CONTENTS - -1. [Executive Summary](#executive-summary) -2. [Critical Security Fixes](#critical-security-fixes) -3. [High Priority Improvements](#high-priority-improvements) -4. [Medium Priority Enhancements](#medium-priority-enhancements) -5. [Testing Strategy](#testing-strategy) -6. [Deployment Plan](#deployment-plan) -7. [Migration Guide](#migration-guide) -8. [Appendices](#appendices) - ---- - -## 📊 EXECUTIVE SUMMARY - -### Overview -This document provides complete design specifications for 40+ identified improvements to the biometric processor system. Each improvement includes: -- Detailed problem analysis -- Complete implementation design -- Code examples and architecture diagrams -- Testing strategy -- Deployment considerations - -### Scope Summary - -| Category | Count | Total Effort | Priority Distribution | -|----------|-------|--------------|----------------------| -| Security | 6 | 2 days | 1 Critical, 3 High, 2 Medium | -| Performance | 6 | 3 days | 2 High, 4 Medium | -| Architecture | 5 | 4 days | 2 High, 3 Medium | -| Code Quality | 6 | 1.5 days | 1 High, 5 Medium | -| Testing | 5 | 2 days | 2 High, 3 Medium | -| Features | 8 | 5 days | 3 High, 5 Medium | -| Other | 4 | 1 day | 4 Medium | -| **Total** | **40** | **18.5 days** | **1C, 13H, 26M** | - -### Success Metrics - -| Metric | Current | Target | Improvement | -|--------|---------|--------|-------------| -| Security Score | B | A+ | +2 grades | -| Verification Latency (p95) | 200ms | 50ms | -75% | -| Webhook Delivery Rate | 85% | 99.5% | +14.5% | -| Batch Success Rate | 60% | 95% | +35% | -| Test Coverage | 70% | 85% | +15% | -| Code Quality Score | 7.5/10 | 9.5/10 | +2 points | - ---- - -## 🔴 CRITICAL SECURITY FIXES - -### 1. CODE INJECTION VULNERABILITY VIA eval() - -#### Problem Analysis - -**Location**: `app/infrastructure/persistence/repositories/postgres_embedding_repository.py:178` - -**Vulnerable Code**: -```python -def _parse_embedding_from_str(self, embedding_str: str) -> np.ndarray: - """Parse embedding from string representation.""" - # CRITICAL VULNERABILITY: eval() can execute arbitrary code! - embedding_list = eval(embedding_str) - return np.array(embedding_list, dtype=np.float32) -``` - -**Attack Vector**: -```python -# Attacker compromises PostgreSQL or injects malicious data -malicious_input = "__import__('os').system('rm -rf /')" -embedding_list = eval(malicious_input) # EXECUTES ARBITRARY CODE! -``` - -**Risk Assessment**: -- **Severity**: CRITICAL -- **Exploitability**: HIGH (if PostgreSQL is compromised or input validation is bypassed) -- **Impact**: Complete system compromise, data loss, remote code execution -- **CVSS Score**: 9.8 (Critical) - -**Current Mitigations**: None - -#### Complete Solution Design - -**Phase 1: Immediate Patch (15 minutes)** - -```python -# app/infrastructure/persistence/repositories/postgres_embedding_repository.py -import ast -import logging - -logger = logging.getLogger(__name__) - -def _parse_embedding_from_str(self, embedding_str: str) -> np.ndarray: - """Parse embedding from string representation safely. - - Uses ast.literal_eval() which only evaluates literal Python - expressions (strings, numbers, tuples, lists, dicts, booleans, None). - Cannot execute arbitrary code. - - Args: - embedding_str: String representation of embedding list - - Returns: - Numpy array of embedding - - Raises: - ValueError: If embedding string is invalid or malformed - SyntaxError: If embedding string contains invalid syntax - """ - if not embedding_str: - raise ValueError("Embedding string cannot be empty") - - # Validate string doesn't contain dangerous patterns - dangerous_patterns = [ - '__import__', 'exec', 'eval', 'compile', 'open', - 'os.', 'sys.', 'subprocess', '__builtins__' - ] - - for pattern in dangerous_patterns: - if pattern in embedding_str: - logger.error( - f"Dangerous pattern '{pattern}' detected in embedding string", - extra={"embedding_preview": embedding_str[:100]} - ) - raise ValueError(f"Invalid embedding format: contains '{pattern}'") - - try: - # SAFE: ast.literal_eval only evaluates literals - embedding_list = ast.literal_eval(embedding_str) - - # Validate it's actually a list - if not isinstance(embedding_list, list): - raise ValueError(f"Expected list, got {type(embedding_list)}") - - # Validate all elements are numbers - if not all(isinstance(x, (int, float)) for x in embedding_list): - raise ValueError("All embedding elements must be numbers") - - # Validate reasonable dimension - if len(embedding_list) < 128 or len(embedding_list) > 4096: - raise ValueError( - f"Invalid embedding dimension: {len(embedding_list)}, " - f"expected 128-4096" - ) - - return np.array(embedding_list, dtype=np.float32) - - except (ValueError, SyntaxError) as e: - logger.error( - f"Failed to parse embedding: {e}", - extra={"embedding_preview": embedding_str[:100]}, - exc_info=True - ) - raise ValueError(f"Invalid embedding format: {str(e)}") -``` - -**Phase 2: Input Validation Layer (1 hour)** - -```python -# app/infrastructure/persistence/validators/embedding_validator.py -import re -from typing import List -import numpy as np - -class EmbeddingValidator: - """Validates embedding data for security and correctness.""" - - # Only allow digits, decimals, brackets, commas, spaces, minus signs - SAFE_PATTERN = re.compile(r'^[\d\.,\[\]\s\-eE]+$') - - MIN_DIMENSION = 128 - MAX_DIMENSION = 4096 - MIN_VALUE = -100.0 - MAX_VALUE = 100.0 - - @classmethod - def validate_embedding_string(cls, embedding_str: str) -> None: - """Validate embedding string is safe to parse. - - Raises: - ValueError: If validation fails - """ - if not embedding_str: - raise ValueError("Embedding string is empty") - - if len(embedding_str) > 100000: # ~25k floats max - raise ValueError("Embedding string too long") - - # Check for safe characters only - if not cls.SAFE_PATTERN.match(embedding_str): - raise ValueError("Embedding contains invalid characters") - - @classmethod - def validate_embedding_array(cls, embedding: np.ndarray) -> None: - """Validate numpy embedding array. - - Raises: - ValueError: If validation fails - """ - # Check dimension - if len(embedding.shape) != 1: - raise ValueError(f"Embedding must be 1D, got shape {embedding.shape}") - - dim = embedding.shape[0] - if not cls.MIN_DIMENSION <= dim <= cls.MAX_DIMENSION: - raise ValueError( - f"Invalid dimension {dim}, expected {cls.MIN_DIMENSION}-{cls.MAX_DIMENSION}" - ) - - # Check for NaN or Inf - if np.any(np.isnan(embedding)) or np.any(np.isinf(embedding)): - raise ValueError("Embedding contains NaN or Inf values") - - # Check value range - if np.any(embedding < cls.MIN_VALUE) or np.any(embedding > cls.MAX_VALUE): - raise ValueError( - f"Embedding values outside range [{cls.MIN_VALUE}, {cls.MAX_VALUE}]" - ) - - @classmethod - def validate_and_parse(cls, embedding_str: str) -> np.ndarray: - """Validate and safely parse embedding string. - - Args: - embedding_str: String representation of embedding - - Returns: - Validated numpy array - - Raises: - ValueError: If validation or parsing fails - """ - cls.validate_embedding_string(embedding_str) - - # Safe parsing with ast.literal_eval - import ast - embedding_list = ast.literal_eval(embedding_str) - - # Convert to numpy array - embedding = np.array(embedding_list, dtype=np.float32) - - # Validate array - cls.validate_embedding_array(embedding) - - return embedding -``` - -**Updated Repository with Validation**: -```python -# app/infrastructure/persistence/repositories/postgres_embedding_repository.py -from app.infrastructure.persistence.validators.embedding_validator import EmbeddingValidator - -class PgVectorEmbeddingRepository: - def _parse_embedding_from_str(self, embedding_str: str) -> np.ndarray: - """Parse embedding from string representation with validation.""" - try: - return EmbeddingValidator.validate_and_parse(embedding_str) - except ValueError as e: - logger.error(f"Embedding validation failed: {e}") - raise -``` - -**Phase 3: Security Testing (30 minutes)** - -```python -# tests/security/test_embedding_injection.py -import pytest -import numpy as np -from app.infrastructure.persistence.validators.embedding_validator import EmbeddingValidator - -class TestEmbeddingInjectionPrevention: - """Security tests for embedding parsing.""" - - def test_prevent_code_execution_via_import(self): - """Test that __import__ injection is blocked.""" - malicious = "__import__('os').system('echo pwned')" - - with pytest.raises(ValueError, match="Invalid embedding format"): - EmbeddingValidator.validate_and_parse(malicious) - - def test_prevent_code_execution_via_eval(self): - """Test that eval injection is blocked.""" - malicious = "eval('1+1')" - - with pytest.raises(ValueError, match="Invalid embedding format"): - EmbeddingValidator.validate_and_parse(malicious) - - def test_prevent_exec_injection(self): - """Test that exec injection is blocked.""" - malicious = "exec('print(1)')" - - with pytest.raises(ValueError, match="Invalid embedding format"): - EmbeddingValidator.validate_and_parse(malicious) - - def test_prevent_file_access(self): - """Test that file access is blocked.""" - malicious = "open('/etc/passwd').read()" - - with pytest.raises(ValueError, match="Invalid embedding format"): - EmbeddingValidator.validate_and_parse(malicious) - - def test_allow_valid_embedding(self): - """Test that valid embeddings are accepted.""" - valid = "[0.1, 0.2, 0.3]" + ", 0.0" * 125 # 128-D - - embedding = EmbeddingValidator.validate_and_parse(valid) - - assert isinstance(embedding, np.ndarray) - assert embedding.shape == (128,) - - def test_reject_non_numeric_values(self): - """Test that non-numeric values are rejected.""" - invalid = "['a', 'b', 'c']" - - with pytest.raises(ValueError): - EmbeddingValidator.validate_and_parse(invalid) - - def test_reject_invalid_dimension(self): - """Test that invalid dimensions are rejected.""" - # Too small - too_small = "[0.1, 0.2, 0.3]" - with pytest.raises(ValueError, match="Invalid dimension"): - EmbeddingValidator.validate_and_parse(too_small) - - # Too large - too_large = "[0.0] * 10000" - with pytest.raises(ValueError): - EmbeddingValidator.validate_and_parse(too_large) - - def test_reject_nan_values(self): - """Test that NaN values are rejected.""" - with_nan = "[float('nan')] * 128" - - with pytest.raises(ValueError, match="NaN or Inf"): - EmbeddingValidator.validate_and_parse(with_nan) - - def test_reject_inf_values(self): - """Test that Inf values are rejected.""" - with_inf = "[float('inf')] * 128" - - with pytest.raises(ValueError, match="NaN or Inf"): - EmbeddingValidator.validate_and_parse(with_inf) -``` - -**Deployment Checklist**: -- [ ] Replace `eval()` with `ast.literal_eval()` in postgres_embedding_repository.py -- [ ] Create `EmbeddingValidator` class -- [ ] Add security tests -- [ ] Run full test suite -- [ ] Perform security audit -- [ ] Deploy to staging -- [ ] Monitor for errors -- [ ] Deploy to production - -**Rollback Plan**: -If issues arise, can temporarily revert to old code with additional logging: -```python -# Temporary rollback with logging -def _parse_embedding_from_str_unsafe(self, embedding_str: str) -> np.ndarray: - logger.warning("USING UNSAFE eval() - SECURITY RISK!") - embedding_list = eval(embedding_str) - return np.array(embedding_list, dtype=np.float32) -``` - ---- - -## 🟠 HIGH PRIORITY IMPROVEMENTS - -### 2. WEBSOCKET AUTHENTICATION ENHANCEMENT - -#### Problem Analysis - -**Location**: `app/api/routes/proctor_ws.py:48-73` - -**Current Implementation**: -```python -@router.websocket("/ws/proctor/{session_id}") -async def proctoring_websocket( - websocket: WebSocket, - session_id: str, - token: str = Query(None) -): - # WEAK: Only checks token length! - if not token or len(token) < 10: - await websocket.close(code=1008) - return - - # No validation of: - # - Token signature - # - Token expiration - # - User/tenant association - # - Session ownership -``` - -**Security Issues**: -1. No cryptographic validation of tokens -2. No expiration checking (tokens valid forever) -3. No user identity verification -4. No tenant isolation -5. No rate limiting per token -6. No audit logging - -**Risk Assessment**: -- **Severity**: HIGH -- **Exploitability**: MEDIUM (requires network access) -- **Impact**: Unauthorized access to proctoring streams, privacy violation -- **CVSS Score**: 7.5 (High) - -#### Complete Solution Design - -**Architecture Overview**: -``` -┌─────────────┐ ┌──────────────────┐ -│ Client │ │ Auth Service │ -│ (Browser) │ │ (JWT Validator) │ -└──────┬──────┘ └────────┬─────────┘ - │ │ - │ 1. Request JWT token │ - │──────────────────────────────────>│ - │ │ - │ 2. JWT token (signed) │ - │<──────────────────────────────────│ - │ │ - │ 3. WebSocket connect + JWT │ - │──────────────────────────────────>│ - │ ┌────────▼─────────┐ - │ │ WebSocket │ - │ │ Endpoint │ - │ └────────┬─────────┘ - │ │ - │ ┌────────▼─────────┐ - │ │ JWT Validator │ - │ │ - Verify sig │ - │ │ - Check exp │ - │ │ - Extract claims │ - │ └────────┬─────────┘ - │ │ - │ ┌────────▼─────────┐ - │ │ Session │ - │ │ Ownership Check │ - │ └────────┬─────────┘ - │ │ - │ 4. Accept connection │ - │<──────────────────────────────────│ -``` - -**Implementation Phase 1: JWT Service (2 hours)** - -```python -# app/infrastructure/auth/jwt_service.py -from datetime import datetime, timedelta -from typing import Optional, Dict -import jwt -from app.core.config import settings -from app.domain.exceptions.auth_errors import ( - TokenExpiredError, - TokenInvalidError, - TokenMissingClaimError, -) - -class JWTService: - """Service for creating and validating JWT tokens.""" - - def __init__( - self, - secret_key: str, - algorithm: str = "HS256", - token_expiry_minutes: int = 60, - issuer: str = "biometric-processor", - ): - self._secret_key = secret_key - self._algorithm = algorithm - self._token_expiry_minutes = token_expiry_minutes - self._issuer = issuer - - def create_token( - self, - user_id: str, - tenant_id: Optional[str] = None, - session_id: Optional[str] = None, - additional_claims: Optional[Dict] = None, - ) -> str: - """Create a JWT token with claims. - - Args: - user_id: User identifier - tenant_id: Optional tenant identifier - session_id: Optional session identifier - additional_claims: Optional additional claims - - Returns: - Signed JWT token string - """ - now = datetime.utcnow() - expiry = now + timedelta(minutes=self._token_expiry_minutes) - - claims = { - "iss": self._issuer, - "sub": user_id, - "iat": now, - "exp": expiry, - "nbf": now, - } - - if tenant_id: - claims["tenant_id"] = tenant_id - - if session_id: - claims["session_id"] = session_id - - if additional_claims: - claims.update(additional_claims) - - token = jwt.encode(claims, self._secret_key, algorithm=self._algorithm) - - logger.debug( - f"Created JWT token for user_id={user_id}, " - f"expires_at={expiry.isoformat()}" - ) - - return token - - def validate_token(self, token: str) -> Dict: - """Validate JWT token and return claims. - - Args: - token: JWT token string - - Returns: - Dictionary of claims - - Raises: - TokenExpiredError: If token has expired - TokenInvalidError: If token is invalid or signature doesn't match - TokenMissingClaimError: If required claims are missing - """ - try: - # Decode and verify signature - claims = jwt.decode( - token, - self._secret_key, - algorithms=[self._algorithm], - issuer=self._issuer, - ) - - # Verify required claims - required_claims = ["iss", "sub", "iat", "exp"] - for claim in required_claims: - if claim not in claims: - raise TokenMissingClaimError(f"Missing claim: {claim}") - - # Check expiration (jwt.decode already checks this, but explicit) - exp_timestamp = claims.get("exp") - if exp_timestamp: - exp_time = datetime.fromtimestamp(exp_timestamp) - if exp_time < datetime.utcnow(): - raise TokenExpiredError("Token has expired") - - logger.debug(f"Token validated successfully for sub={claims.get('sub')}") - - return claims - - except jwt.ExpiredSignatureError: - logger.warning("Token validation failed: expired") - raise TokenExpiredError("Token has expired") - - except jwt.InvalidIssuerError: - logger.warning("Token validation failed: invalid issuer") - raise TokenInvalidError("Invalid token issuer") - - except jwt.InvalidTokenError as e: - logger.warning(f"Token validation failed: {e}") - raise TokenInvalidError(f"Invalid token: {str(e)}") - - def refresh_token(self, old_token: str) -> str: - """Refresh a token if it's within refresh window. - - Args: - old_token: Existing valid token - - Returns: - New token with extended expiry - - Raises: - TokenExpiredError: If token is too old to refresh - TokenInvalidError: If token is invalid - """ - claims = self.validate_token(old_token) - - # Create new token with same claims - return self.create_token( - user_id=claims["sub"], - tenant_id=claims.get("tenant_id"), - session_id=claims.get("session_id"), - ) -``` - -**Phase 2: Domain Exceptions (15 minutes)** - -```python -# app/domain/exceptions/auth_errors.py -from app.domain.exceptions.base import BiometricProcessorError - -class AuthenticationError(BiometricProcessorError): - """Base class for authentication errors.""" - pass - -class TokenExpiredError(AuthenticationError): - """Raised when JWT token has expired.""" - - def __init__(self, message: str = "Token has expired"): - super().__init__( - message=message, - error_code="TOKEN_EXPIRED", - ) - -class TokenInvalidError(AuthenticationError): - """Raised when JWT token is invalid.""" - - def __init__(self, message: str = "Invalid token"): - super().__init__( - message=message, - error_code="TOKEN_INVALID", - ) - -class TokenMissingClaimError(AuthenticationError): - """Raised when JWT token is missing required claims.""" - - def __init__(self, claim: str): - super().__init__( - message=f"Token missing required claim: {claim}", - error_code="TOKEN_MISSING_CLAIM", - ) - self.claim = claim - -class SessionOwnershipError(AuthenticationError): - """Raised when user doesn't own the session.""" - - def __init__(self, session_id: str, user_id: str): - super().__init__( - message=f"User {user_id} does not own session {session_id}", - error_code="SESSION_OWNERSHIP_ERROR", - ) - self.session_id = session_id - self.user_id = user_id -``` - -**Phase 3: Session Ownership Validator (1 hour)** - -```python -# app/application/services/session_ownership_validator.py -from typing import Optional -from app.domain.interfaces.proctor_session_repository import IProctorSessionRepository -from app.domain.exceptions.auth_errors import SessionOwnershipError - -class SessionOwnershipValidator: - """Validates that users own the sessions they're accessing.""" - - def __init__(self, session_repository: IProctorSessionRepository): - self._session_repository = session_repository - - async def validate_ownership( - self, - session_id: str, - user_id: str, - tenant_id: Optional[str] = None, - ) -> bool: - """Validate user owns the session. - - Args: - session_id: Session identifier - user_id: User identifier - tenant_id: Optional tenant identifier - - Returns: - True if user owns session - - Raises: - SessionOwnershipError: If user doesn't own session - """ - session = await self._session_repository.get(session_id) - - if not session: - raise SessionOwnershipError(session_id, user_id) - - # Check user ID matches - if session.user_id != user_id: - logger.warning( - f"Session ownership violation: session={session_id}, " - f"expected_user={session.user_id}, actual_user={user_id}" - ) - raise SessionOwnershipError(session_id, user_id) - - # Check tenant ID matches (if multi-tenant) - if tenant_id and session.tenant_id != tenant_id: - logger.warning( - f"Tenant mismatch: session={session_id}, " - f"expected_tenant={session.tenant_id}, actual_tenant={tenant_id}" - ) - raise SessionOwnershipError(session_id, user_id) - - logger.debug(f"Session ownership validated: session={session_id}, user={user_id}") - return True -``` - -**Phase 4: Updated WebSocket Endpoint (1 hour)** - -```python -# app/api/routes/proctor_ws.py -from fastapi import WebSocket, WebSocketDisconnect, Query, Depends, status -from app.infrastructure.auth.jwt_service import JWTService -from app.application.services.session_ownership_validator import SessionOwnershipValidator -from app.domain.exceptions.auth_errors import ( - TokenExpiredError, - TokenInvalidError, - SessionOwnershipError, -) -from app.core.container import get_jwt_service, get_session_ownership_validator - -@router.websocket("/ws/proctor/{session_id}") -async def proctoring_websocket( - websocket: WebSocket, - session_id: str, - token: str = Query(..., description="JWT authentication token"), - jwt_service: JWTService = Depends(get_jwt_service), - ownership_validator: SessionOwnershipValidator = Depends(get_session_ownership_validator), -): - """WebSocket endpoint for proctoring frame submission. - - Security: - - Requires valid JWT token with signature verification - - Validates token expiration - - Verifies user owns the session - - Enforces tenant isolation - - Args: - websocket: WebSocket connection - session_id: Proctoring session identifier - token: JWT authentication token - jwt_service: Injected JWT service - ownership_validator: Injected session ownership validator - - Raises: - WebSocket close codes: - - 1008: Token invalid/expired or unauthorized access - - 1011: Internal server error - """ - connection_id = str(uuid.uuid4()) - - try: - # Step 1: Validate JWT token - try: - claims = jwt_service.validate_token(token) - except TokenExpiredError: - logger.warning( - f"WebSocket connection rejected: token expired, " - f"session={session_id}" - ) - await websocket.close(code=1008, reason="Token expired") - return - except TokenInvalidError as e: - logger.warning( - f"WebSocket connection rejected: invalid token, " - f"session={session_id}, error={e}" - ) - await websocket.close(code=1008, reason="Invalid token") - return - - # Step 2: Extract identity from claims - user_id = claims.get("sub") - tenant_id = claims.get("tenant_id") - - if not user_id: - logger.error("JWT token missing user_id (sub claim)") - await websocket.close(code=1008, reason="Invalid token claims") - return - - # Step 3: Validate session ownership - try: - await ownership_validator.validate_ownership( - session_id=session_id, - user_id=user_id, - tenant_id=tenant_id, - ) - except SessionOwnershipError as e: - logger.warning( - f"WebSocket connection rejected: ownership violation, " - f"session={session_id}, user={user_id}" - ) - await websocket.close(code=1008, reason="Unauthorized") - return - - # Step 4: Accept connection - await websocket.accept() - - logger.info( - f"WebSocket connection established: " - f"connection_id={connection_id}, session={session_id}, " - f"user={user_id}, tenant={tenant_id}" - ) - - # Step 5: Register connection - await _connection_manager.connect(session_id, websocket) - - # Step 6: Frame processing loop - try: - while True: - # Receive frame data - data = await websocket.receive_json() - - # Process frame (existing logic) - await _process_frame( - session_id=session_id, - user_id=user_id, - tenant_id=tenant_id, - frame_data=data, - ) - - except WebSocketDisconnect: - logger.info( - f"WebSocket disconnected normally: " - f"connection_id={connection_id}, session={session_id}" - ) - except Exception as e: - logger.error( - f"WebSocket error: connection_id={connection_id}, " - f"session={session_id}, error={e}", - exc_info=True, - ) - await websocket.close(code=1011, reason="Internal error") - - finally: - # Step 7: Cleanup connection - await _connection_manager.disconnect(session_id, websocket) - - logger.info( - f"WebSocket connection closed: " - f"connection_id={connection_id}, session={session_id}" - ) -``` - -**Phase 5: DI Container Integration (30 minutes)** - -```python -# app/core/container.py -from app.infrastructure.auth.jwt_service import JWTService -from app.application.services.session_ownership_validator import SessionOwnershipValidator - -@lru_cache() -def get_jwt_service() -> JWTService: - """Get JWT service singleton. - - Returns: - Configured JWT service instance - """ - return JWTService( - secret_key=settings.JWT_SECRET_KEY, - algorithm=settings.JWT_ALGORITHM, - token_expiry_minutes=settings.JWT_EXPIRY_MINUTES, - issuer=settings.APP_NAME, - ) - -def get_session_ownership_validator() -> SessionOwnershipValidator: - """Get session ownership validator. - - Returns: - SessionOwnershipValidator instance - """ - return SessionOwnershipValidator( - session_repository=get_proctor_session_repository(), - ) -``` - -**Phase 6: Configuration (15 minutes)** - -```python -# app/core/config.py -class Settings(BaseSettings): - # ... existing settings ... - - # JWT Authentication - JWT_SECRET_KEY: str = Field( - default="", # Must be set in production - description="Secret key for JWT signing (REQUIRED in production)" - ) - JWT_ALGORITHM: str = Field( - default="HS256", - description="JWT signing algorithm" - ) - JWT_EXPIRY_MINUTES: int = Field( - default=60, - ge=5, - le=1440, - description="JWT token expiry in minutes" - ) - - @field_validator("JWT_SECRET_KEY") - @classmethod - def validate_jwt_secret(cls, v, info): - """Ensure JWT secret is set in production.""" - env = info.data.get("ENVIRONMENT", "development") - if env == "production" and not v: - raise ValueError("JWT_SECRET_KEY must be set in production") - if env == "production" and len(v) < 32: - raise ValueError("JWT_SECRET_KEY must be at least 32 characters in production") - return v -``` - -**.env Configuration**: -```env -# JWT Authentication (REQUIRED for production) -JWT_SECRET_KEY=your-super-secret-key-min-32-chars-recommended-64 -JWT_ALGORITHM=HS256 -JWT_EXPIRY_MINUTES=60 -``` - -**Phase 7: Testing (2 hours)** - -```python -# tests/unit/infrastructure/auth/test_jwt_service.py -import pytest -from datetime import datetime, timedelta -from app.infrastructure.auth.jwt_service import JWTService -from app.domain.exceptions.auth_errors import ( - TokenExpiredError, - TokenInvalidError, - TokenMissingClaimError, -) - -class TestJWTService: - """Test JWT service functionality.""" - - @pytest.fixture - def jwt_service(self): - return JWTService( - secret_key="test-secret-key-at-least-32-characters-long", - algorithm="HS256", - token_expiry_minutes=60, - ) - - def test_create_token_with_all_claims(self, jwt_service): - """Test token creation with all claims.""" - token = jwt_service.create_token( - user_id="user123", - tenant_id="tenant456", - session_id="session789", - ) - - assert isinstance(token, str) - assert len(token) > 0 - - # Validate created token - claims = jwt_service.validate_token(token) - - assert claims["sub"] == "user123" - assert claims["tenant_id"] == "tenant456" - assert claims["session_id"] == "session789" - - def test_validate_expired_token_raises_error(self, jwt_service): - """Test that expired token raises error.""" - # Create service with 0 minute expiry - short_lived_service = JWTService( - secret_key="test-secret-key-at-least-32-characters-long", - token_expiry_minutes=0, - ) - - token = short_lived_service.create_token(user_id="user123") - - # Token should be expired immediately - with pytest.raises(TokenExpiredError): - jwt_service.validate_token(token) - - def test_validate_tampered_token_raises_error(self, jwt_service): - """Test that tampered token raises error.""" - token = jwt_service.create_token(user_id="user123") - - # Tamper with token - tampered_token = token[:-10] + "tampered00" - - with pytest.raises(TokenInvalidError): - jwt_service.validate_token(tampered_token) - - def test_validate_token_with_wrong_secret_raises_error(self): - """Test that token signed with different secret fails.""" - service1 = JWTService(secret_key="secret1" + "0" * 24) - service2 = JWTService(secret_key="secret2" + "0" * 24) - - token = service1.create_token(user_id="user123") - - with pytest.raises(TokenInvalidError): - service2.validate_token(token) - - def test_refresh_token_extends_expiry(self, jwt_service): - """Test that refresh extends token expiry.""" - original_token = jwt_service.create_token(user_id="user123") - original_claims = jwt_service.validate_token(original_token) - - # Wait a bit - import time - time.sleep(1) - - # Refresh token - new_token = jwt_service.refresh_token(original_token) - new_claims = jwt_service.validate_token(new_token) - - # New token should have later expiry - assert new_claims["exp"] > original_claims["exp"] - assert new_claims["sub"] == original_claims["sub"] - -# tests/integration/test_websocket_auth.py -from fastapi.testclient import TestClient -import pytest - -@pytest.mark.asyncio -async def test_websocket_requires_token(): - """Test WebSocket connection requires token.""" - async with AsyncClient(app=app, base_url="http://test") as client: - # Try to connect without token - with pytest.raises(Exception): # Should be rejected - async with client.websocket_connect("/ws/proctor/session123"): - pass - -@pytest.mark.asyncio -async def test_websocket_rejects_invalid_token(): - """Test WebSocket rejects invalid token.""" - async with AsyncClient(app=app, base_url="http://test") as client: - # Try with invalid token - with pytest.raises(Exception): # Should be rejected - async with client.websocket_connect( - "/ws/proctor/session123?token=invalid" - ): - pass - -@pytest.mark.asyncio -async def test_websocket_accepts_valid_token(): - """Test WebSocket accepts valid token.""" - # Create valid token - jwt_service = JWTService(secret_key=settings.JWT_SECRET_KEY) - token = jwt_service.create_token( - user_id="user123", - session_id="session123" - ) - - async with AsyncClient(app=app, base_url="http://test") as client: - async with client.websocket_connect( - f"/ws/proctor/session123?token={token}" - ) as websocket: - # Should connect successfully - data = await websocket.receive_json() - assert data is not None -``` - -**Deployment Plan**: - -**Stage 1: Preparation (1 day)** -- [ ] Create JWT service and validators -- [ ] Add domain exceptions -- [ ] Write unit tests -- [ ] Update configuration - -**Stage 2: Integration (1 day)** -- [ ] Update WebSocket endpoint -- [ ] Wire DI container -- [ ] Write integration tests -- [ ] Test in development environment - -**Stage 3: Staging Deployment (2 days)** -- [ ] Deploy to staging -- [ ] Generate JWT tokens for test users -- [ ] Test WebSocket connections -- [ ] Load test with concurrent connections -- [ ] Monitor for errors - -**Stage 4: Production Deployment (1 day)** -- [ ] Generate production JWT secret (64+ chars) -- [ ] Deploy to production -- [ ] Monitor authentication metrics -- [ ] Set up alerts for failed auth attempts - -**Rollback Plan**: -```python -# Feature flag for gradual rollout -JWT_AUTH_ENABLED = os.getenv("JWT_AUTH_ENABLED", "false").lower() == "true" - -if JWT_AUTH_ENABLED: - # New JWT validation - claims = jwt_service.validate_token(token) -else: - # Old validation (temporary) - if not token or len(token) < 10: - await websocket.close(code=1008) - return -``` - -**Success Metrics**: -- [ ] 0% unauthorized WebSocket connections -- [ ] < 1% false rejections (valid tokens rejected) -- [ ] < 100ms token validation latency -- [ ] 100% audit logging coverage - ---- - -### 3. EMBEDDING LOOKUP CACHING - -#### Problem Analysis - -**Location**: `app/application/use_cases/verify_face.py:99` - -**Current Implementation**: -```python -async def execute(self, user_id: str, image_path: str) -> VerificationResult: - # ... face detection and extraction ... - - # SLOW: Database lookup on every verification - stored_embedding = await self._repository.find_by_user_id(user_id) - - # ... similarity calculation ... -``` - -**Performance Impact**: -``` -Request → API → Use Case → Repository → Database - ↓ (100-150ms) - PostgreSQL - Query -``` - -**Benchmarks**: -- Database query: 100-150ms (p95) -- Cache lookup: 1-5ms (p95) -- **Potential improvement: 95-98% latency reduction** - -**Issues**: -1. Every verification queries database -2. No caching for hot users (repeatedly verified) -3. Increased database load -4. Higher latency -5. Higher cloud database costs - -**Use Case Analysis**: -- Airport security: Same person verified 2-5x per visit -- Office access: Same person verified daily -- Exam proctoring: Same student verified every 60s for 3 hours -- **Cache hit rate potential: 80-95%** - -#### Complete Solution Design - -**Architecture**: -``` -┌──────────────┐ -│ API │ -│ Endpoint │ -└──────┬───────┘ - │ - ▼ -┌──────────────────────────────────────────────┐ -│ CachedEmbeddingRepository (Decorator) │ -│ ┌─────────────────────────────────────┐ │ -│ │ 1. Check Cache │ │ -│ │ ├─ Hit → Return cached │ │ -│ │ └─ Miss → Query DB │ │ -│ └─────────────────────────────────────┘ │ -└──────┬───────────────────────────────────────┘ - │ - ├─────────────────┬──────────────────┐ - ▼ ▼ ▼ -┌─────────────┐ ┌─────────────┐ ┌──────────────┐ -│ In-Memory │ │ Postgres │ │ Redis │ -│ LRU Cache │ │ Repository │ │ (Optional) │ -│ (Fast L1) │ │ (Fallback) │ │ (Shared L2) │ -└─────────────┘ └─────────────┘ └──────────────┘ -``` - -**Implementation - Phase 1: In-Memory Cache (4 hours)** - -```python -# app/infrastructure/cache/embedding_cache.py -from functools import wraps -from typing import Optional, Dict, Tuple -import numpy as np -from cachetools import TTLCache -import threading -import logging - -logger = logging.getLogger(__name__) - -class EmbeddingCache: - """Thread-safe LRU cache for face embeddings with TTL. - - Features: - - LRU eviction policy - - TTL-based expiration - - Thread-safe operations - - Hit/miss statistics - - Size monitoring - """ - - def __init__( - self, - max_size: int = 10000, - ttl_seconds: int = 3600, - ): - """Initialize embedding cache. - - Args: - max_size: Maximum number of embeddings to cache - ttl_seconds: Time-to-live in seconds (default: 1 hour) - """ - self._cache = TTLCache(maxsize=max_size, ttl=ttl_seconds) - self._lock = threading.RLock() # Reentrant lock for thread safety - - # Statistics - self._hit_count = 0 - self._miss_count = 0 - self._eviction_count = 0 - - logger.info( - f"EmbeddingCache initialized: max_size={max_size}, " - f"ttl={ttl_seconds}s" - ) - - def get( - self, - user_id: str, - tenant_id: Optional[str] = None, - ) -> Optional[np.ndarray]: - """Get cached embedding. - - Args: - user_id: User identifier - tenant_id: Optional tenant identifier - - Returns: - Cached embedding array, or None if not found - """ - cache_key = self._make_key(user_id, tenant_id) - - with self._lock: - if cache_key in self._cache: - self._hit_count += 1 - embedding = self._cache[cache_key] - - logger.debug( - f"Cache HIT: user_id={user_id}, tenant_id={tenant_id}, " - f"hit_rate={self.get_hit_rate():.1f}%" - ) - - # Return a copy to prevent cache corruption - return embedding.copy() - - self._miss_count += 1 - - logger.debug( - f"Cache MISS: user_id={user_id}, tenant_id={tenant_id}, " - f"hit_rate={self.get_hit_rate():.1f}%" - ) - - return None - - def set( - self, - user_id: str, - embedding: np.ndarray, - tenant_id: Optional[str] = None, - ) -> None: - """Cache an embedding. - - Args: - user_id: User identifier - embedding: Embedding array to cache - tenant_id: Optional tenant identifier - """ - cache_key = self._make_key(user_id, tenant_id) - - with self._lock: - # Store a copy to prevent external modifications - self._cache[cache_key] = embedding.copy() - - logger.debug( - f"Cache SET: user_id={user_id}, tenant_id={tenant_id}, " - f"size={len(self._cache)}/{self._cache.maxsize}" - ) - - def invalidate( - self, - user_id: str, - tenant_id: Optional[str] = None, - ) -> bool: - """Invalidate cached embedding. - - Called when user re-enrolls to ensure fresh embedding is fetched. - - Args: - user_id: User identifier - tenant_id: Optional tenant identifier - - Returns: - True if entry was invalidated, False if not in cache - """ - cache_key = self._make_key(user_id, tenant_id) - - with self._lock: - if cache_key in self._cache: - del self._cache[cache_key] - self._eviction_count += 1 - - logger.info( - f"Cache INVALIDATE: user_id={user_id}, tenant_id={tenant_id}" - ) - - return True - - return False - - def clear(self) -> None: - """Clear all cached embeddings.""" - with self._lock: - size_before = len(self._cache) - self._cache.clear() - - logger.warning(f"Cache CLEARED: removed {size_before} entries") - - def get_stats(self) -> Dict: - """Get cache statistics. - - Returns: - Dictionary with cache metrics - """ - with self._lock: - total_requests = self._hit_count + self._miss_count - hit_rate = ( - (self._hit_count / total_requests * 100) - if total_requests > 0 - else 0.0 - ) - - return { - "hits": self._hit_count, - "misses": self._miss_count, - "evictions": self._eviction_count, - "hit_rate_percent": round(hit_rate, 2), - "current_size": len(self._cache), - "max_size": self._cache.maxsize, - "ttl_seconds": self._cache.ttl, - } - - def get_hit_rate(self) -> float: - """Get cache hit rate percentage. - - Returns: - Hit rate as percentage (0-100) - """ - total = self._hit_count + self._miss_count - return (self._hit_count / total * 100) if total > 0 else 0.0 - - def _make_key(self, user_id: str, tenant_id: Optional[str]) -> str: - """Create cache key from user_id and tenant_id. - - Args: - user_id: User identifier - tenant_id: Optional tenant identifier - - Returns: - Cache key string - """ - # Include tenant_id in key for multi-tenant isolation - return f"{tenant_id or 'default'}:{user_id}" -``` - -**Phase 2: Cached Repository Decorator (2 hours)** - -```python -# app/infrastructure/persistence/repositories/cached_embedding_repository.py -from typing import Optional -import numpy as np -import logging - -from app.domain.interfaces.embedding_repository import IEmbeddingRepository -from app.infrastructure.cache.embedding_cache import EmbeddingCache - -logger = logging.getLogger(__name__) - -class CachedEmbeddingRepository: - """Embedding repository with transparent caching layer. - - Implements decorator pattern to add caching to any repository. - Cache-aside strategy: check cache first, then database. - """ - - def __init__( - self, - repository: IEmbeddingRepository, - cache: EmbeddingCache, - ): - """Initialize cached repository. - - Args: - repository: Underlying repository implementation - cache: Cache instance - """ - self._repository = repository - self._cache = cache - - logger.info( - f"CachedEmbeddingRepository initialized with " - f"{repository.__class__.__name__}" - ) - - async def save( - self, - user_id: str, - embedding: np.ndarray, - quality_score: float, - tenant_id: Optional[str] = None, - ) -> None: - """Save embedding to database and invalidate cache. - - Args: - user_id: User identifier - embedding: Embedding vector - quality_score: Quality score - tenant_id: Optional tenant identifier - """ - # Save to database first - await self._repository.save( - user_id=user_id, - embedding=embedding, - quality_score=quality_score, - tenant_id=tenant_id, - ) - - # Invalidate cache to ensure next read gets fresh data - self._cache.invalidate(user_id, tenant_id) - - logger.debug( - f"Saved embedding and invalidated cache: " - f"user_id={user_id}, tenant_id={tenant_id}" - ) - - async def find_by_user_id( - self, - user_id: str, - tenant_id: Optional[str] = None, - ) -> Optional[np.ndarray]: - """Find embedding with caching. - - Cache-aside pattern: - 1. Check cache first - 2. If hit, return cached value - 3. If miss, query database - 4. Cache result and return - - Args: - user_id: User identifier - tenant_id: Optional tenant identifier - - Returns: - Embedding array if found, None otherwise - """ - # Step 1: Check cache - cached = self._cache.get(user_id, tenant_id) - if cached is not None: - logger.debug(f"Returning cached embedding for user_id={user_id}") - return cached - - # Step 2: Cache miss - query database - logger.debug(f"Cache miss, querying database for user_id={user_id}") - - embedding = await self._repository.find_by_user_id(user_id, tenant_id) - - # Step 3: Cache result if found - if embedding is not None: - self._cache.set(user_id, embedding, tenant_id) - logger.debug(f"Cached embedding from database for user_id={user_id}") - else: - logger.debug(f"No embedding found in database for user_id={user_id}") - - return embedding - - async def find_similar( - self, - embedding: np.ndarray, - threshold: float, - limit: int = 5, - tenant_id: Optional[str] = None, - ) -> list: - """Find similar embeddings. - - Note: Similarity search bypasses cache and queries database directly. - Caching similarity results is complex due to query variability. - - Args: - embedding: Query embedding - threshold: Similarity threshold - limit: Maximum results - tenant_id: Optional tenant identifier - - Returns: - List of (user_id, distance) tuples - """ - # Bypass cache for similarity search - return await self._repository.find_similar( - embedding=embedding, - threshold=threshold, - limit=limit, - tenant_id=tenant_id, - ) - - async def delete( - self, - user_id: str, - tenant_id: Optional[str] = None, - ) -> bool: - """Delete embedding from database and cache. - - Args: - user_id: User identifier - tenant_id: Optional tenant identifier - - Returns: - True if deleted, False if not found - """ - # Delete from database - deleted = await self._repository.delete(user_id, tenant_id) - - # Invalidate cache regardless - self._cache.invalidate(user_id, tenant_id) - - logger.debug( - f"Deleted embedding and invalidated cache: " - f"user_id={user_id}, tenant_id={tenant_id}, deleted={deleted}" - ) - - return deleted - - def get_cache_stats(self) -> dict: - """Get cache statistics. - - Returns: - Cache statistics dictionary - """ - return self._cache.get_stats() -``` - -**Phase 3: DI Container Integration (30 minutes)** - -```python -# app/core/container.py -from app.infrastructure.cache.embedding_cache import EmbeddingCache -from app.infrastructure.persistence.repositories.cached_embedding_repository import ( - CachedEmbeddingRepository, -) - -@lru_cache() -def get_embedding_cache() -> EmbeddingCache: - """Get embedding cache singleton. - - Returns: - Configured embedding cache instance - """ - cache = EmbeddingCache( - max_size=settings.EMBEDDING_CACHE_MAX_SIZE, - ttl_seconds=settings.EMBEDDING_CACHE_TTL_SECONDS, - ) - - logger.info( - f"Embedding cache created: max_size={settings.EMBEDDING_CACHE_MAX_SIZE}, " - f"ttl={settings.EMBEDDING_CACHE_TTL_SECONDS}s" - ) - - return cache - -@lru_cache() -def get_embedding_repository() -> IEmbeddingRepository: - """Get embedding repository with caching. - - Returns: - Cached embedding repository instance - """ - # Get base repository (in-memory or PostgreSQL) - if settings.USE_PGVECTOR: - from app.infrastructure.persistence.repositories.pgvector_embedding_repository import ( - PgVectorEmbeddingRepository, - ) - base_repo = PgVectorEmbeddingRepository() - else: - from app.infrastructure.persistence.repositories.memory_embedding_repository import ( - InMemoryEmbeddingRepository, - ) - base_repo = InMemoryEmbeddingRepository() - - # Wrap with caching layer - cache = get_embedding_cache() - cached_repo = CachedEmbeddingRepository(base_repo, cache) - - logger.info( - f"Embedding repository created with caching: " - f"backend={base_repo.__class__.__name__}" - ) - - return cached_repo -``` - -**Phase 4: Configuration (15 minutes)** - -```python -# app/core/config.py -class Settings(BaseSettings): - # ... existing settings ... - - # Embedding Cache Configuration - EMBEDDING_CACHE_ENABLED: bool = Field( - default=True, - description="Enable embedding caching for performance" - ) - EMBEDDING_CACHE_MAX_SIZE: int = Field( - default=10000, - ge=100, - le=1000000, - description="Maximum number of embeddings to cache" - ) - EMBEDDING_CACHE_TTL_SECONDS: int = Field( - default=3600, - ge=60, - le=86400, - description="Cache TTL in seconds (1 hour default)" - ) - - # Redis Cache (Optional, for distributed deployments) - REDIS_CACHE_ENABLED: bool = Field( - default=False, - description="Use Redis for distributed caching" - ) -``` - -**.env Configuration**: -```env -# Embedding Cache -EMBEDDING_CACHE_ENABLED=true -EMBEDDING_CACHE_MAX_SIZE=10000 # Cache 10k users -EMBEDDING_CACHE_TTL_SECONDS=3600 # 1 hour TTL - -# Optional: Redis for distributed cache -REDIS_CACHE_ENABLED=false -``` - -**Phase 5: Monitoring Endpoint (30 minutes)** - -```python -# app/api/routes/metrics.py -from fastapi import APIRouter, Depends -from app.core.container import get_embedding_repository - -@router.get("/metrics/cache") -async def get_cache_metrics( - repository: CachedEmbeddingRepository = Depends(get_embedding_repository), -): - """Get embedding cache statistics. - - Returns: - Cache performance metrics - """ - stats = repository.get_cache_stats() - - return { - "cache": stats, - "recommendations": _generate_recommendations(stats), - } - -def _generate_recommendations(stats: dict) -> list: - """Generate cache tuning recommendations. - - Args: - stats: Cache statistics - - Returns: - List of recommendation strings - """ - recommendations = [] - - hit_rate = stats.get("hit_rate_percent", 0) - - if hit_rate < 50: - recommendations.append( - "Low hit rate (<50%). Consider increasing cache size or TTL." - ) - elif hit_rate > 95: - recommendations.append( - "Excellent hit rate (>95%). Cache is well-tuned." - ) - - utilization = stats.get("current_size", 0) / stats.get("max_size", 1) - - if utilization > 0.9: - recommendations.append( - "Cache >90% full. Consider increasing max_size to reduce evictions." - ) - - if stats.get("evictions", 0) > stats.get("hits", 1): - recommendations.append( - "High eviction rate. Cache may be too small or TTL too short." - ) - - if not recommendations: - recommendations.append("Cache performance is optimal.") - - return recommendations -``` - -**Phase 6: Testing (2 hours)** - -```python -# tests/unit/infrastructure/cache/test_embedding_cache.py -import pytest -import numpy as np -from app.infrastructure.cache.embedding_cache import EmbeddingCache -import time - -class TestEmbeddingCache: - """Test embedding cache functionality.""" - - @pytest.fixture - def cache(self): - """Create cache with short TTL for testing.""" - return EmbeddingCache(max_size=5, ttl_seconds=2) - - def test_cache_miss_returns_none(self, cache): - """Test cache miss returns None.""" - result = cache.get("user123") - assert result is None - - def test_cache_set_and_get(self, cache): - """Test caching and retrieval.""" - embedding = np.random.rand(128).astype(np.float32) - - cache.set("user123", embedding) - cached = cache.get("user123") - - assert cached is not None - assert np.array_equal(cached, embedding) - - def test_cache_returns_copy(self, cache): - """Test cache returns copy to prevent corruption.""" - embedding = np.random.rand(128).astype(np.float32) - - cache.set("user123", embedding) - cached1 = cache.get("user123") - cached2 = cache.get("user123") - - # Modify one copy - cached1[0] = 999.0 - - # Other copy should be unchanged - assert cached2[0] != 999.0 - - def test_cache_eviction_on_size_limit(self, cache): - """Test LRU eviction when cache is full.""" - # Cache size is 5 - for i in range(6): - embedding = np.random.rand(128).astype(np.float32) - cache.set(f"user{i}", embedding) - - # First user should be evicted - assert cache.get("user0") is None - # Last user should still be cached - assert cache.get("user5") is not None - - def test_cache_expiration_after_ttl(self, cache): - """Test cache expiration after TTL.""" - embedding = np.random.rand(128).astype(np.float32) - cache.set("user123", embedding) - - # Should be cached - assert cache.get("user123") is not None - - # Wait for TTL to expire (2 seconds) - time.sleep(2.1) - - # Should be expired - assert cache.get("user123") is None - - def test_cache_invalidate(self, cache): - """Test cache invalidation.""" - embedding = np.random.rand(128).astype(np.float32) - cache.set("user123", embedding) - - # Should be cached - assert cache.get("user123") is not None - - # Invalidate - invalidated = cache.invalidate("user123") - assert invalidated is True - - # Should be gone - assert cache.get("user123") is None - - # Invalidating again should return False - invalidated = cache.invalidate("user123") - assert invalidated is False - - def test_cache_with_tenant_isolation(self, cache): - """Test tenant isolation in cache.""" - embedding1 = np.random.rand(128).astype(np.float32) - embedding2 = np.random.rand(128).astype(np.float32) - - # Same user_id, different tenants - cache.set("user123", embedding1, tenant_id="tenant1") - cache.set("user123", embedding2, tenant_id="tenant2") - - cached1 = cache.get("user123", tenant_id="tenant1") - cached2 = cache.get("user123", tenant_id="tenant2") - - assert np.array_equal(cached1, embedding1) - assert np.array_equal(cached2, embedding2) - assert not np.array_equal(cached1, cached2) - - def test_cache_stats(self, cache): - """Test cache statistics.""" - # Generate some hits and misses - embedding = np.random.rand(128).astype(np.float32) - - cache.get("user1") # Miss - cache.set("user1", embedding) - cache.get("user1") # Hit - cache.get("user1") # Hit - cache.get("user2") # Miss - - stats = cache.get_stats() - - assert stats["hits"] == 2 - assert stats["misses"] == 2 - assert stats["hit_rate_percent"] == 50.0 - assert stats["current_size"] == 1 - -# tests/integration/test_cached_repository.py -@pytest.mark.asyncio -async def test_cached_repository_caches_lookup(): - """Test repository caches database lookups.""" - # Create mock base repository - mock_repo = Mock(spec=IEmbeddingRepository) - mock_repo.find_by_user_id = AsyncMock( - return_value=np.random.rand(128).astype(np.float32) - ) - - # Create cached repository - cache = EmbeddingCache(max_size=100, ttl_seconds=60) - cached_repo = CachedEmbeddingRepository(mock_repo, cache) - - # First lookup - should hit database - result1 = await cached_repo.find_by_user_id("user123") - assert mock_repo.find_by_user_id.call_count == 1 - - # Second lookup - should hit cache - result2 = await cached_repo.find_by_user_id("user123") - assert mock_repo.find_by_user_id.call_count == 1 # Not called again - - # Results should be equal - assert np.array_equal(result1, result2) - -@pytest.mark.asyncio -async def test_cached_repository_invalidates_on_save(): - """Test repository invalidates cache on save.""" - mock_repo = Mock(spec=IEmbeddingRepository) - mock_repo.find_by_user_id = AsyncMock( - return_value=np.random.rand(128).astype(np.float32) - ) - mock_repo.save = AsyncMock() - - cache = EmbeddingCache(max_size=100, ttl_seconds=60) - cached_repo = CachedEmbeddingRepository(mock_repo, cache) - - # Lookup and cache - await cached_repo.find_by_user_id("user123") - - # Save new embedding - new_embedding = np.random.rand(128).astype(np.float32) - await cached_repo.save("user123", new_embedding, quality_score=85.0) - - # Next lookup should query database again (cache invalidated) - await cached_repo.find_by_user_id("user123") - assert mock_repo.find_by_user_id.call_count == 2 -``` - -**Phase 7: Load Testing (1 hour)** - -```python -# tests/load/test_cache_performance.py -import asyncio -import time -import statistics - -async def benchmark_without_cache(): - """Benchmark verification without caching.""" - repo = PgVectorEmbeddingRepository() # No caching - - times = [] - for i in range(100): - start = time.time() - await repo.find_by_user_id(f"user{i % 10}") # 10 users, repeated - elapsed = time.time() - start - times.append(elapsed) - - return { - "mean": statistics.mean(times) * 1000, # ms - "p95": statistics.quantiles(times, n=20)[18] * 1000, # ms - "p99": statistics.quantiles(times, n=100)[98] * 1000, # ms - } - -async def benchmark_with_cache(): - """Benchmark verification with caching.""" - base_repo = PgVectorEmbeddingRepository() - cache = EmbeddingCache(max_size=100, ttl_seconds=60) - repo = CachedEmbeddingRepository(base_repo, cache) - - times = [] - for i in range(100): - start = time.time() - await repo.find_by_user_id(f"user{i % 10}") # 10 users, repeated - elapsed = time.time() - start - times.append(elapsed) - - stats = cache.get_stats() - - return { - "mean": statistics.mean(times) * 1000, # ms - "p95": statistics.quantiles(times, n=20)[18] * 1000, # ms - "p99": statistics.quantiles(times, n=100)[98] * 1000, # ms - "hit_rate": stats["hit_rate_percent"], - } - -# Expected Results: -# Without cache: mean=120ms, p95=150ms, p99=180ms -# With cache: mean=15ms, p95=20ms, p99=25ms (87% improvement) -``` - -**Deployment Plan**: - -**Stage 1: Development Testing (1 day)** -- [ ] Implement cache and repository -- [ ] Run unit tests -- [ ] Run load tests -- [ ] Measure performance improvement - -**Stage 2: Staging Deployment (2 days)** -- [ ] Deploy with caching enabled -- [ ] Monitor cache hit rate -- [ ] Adjust cache size/TTL based on metrics -- [ ] Load test with production-like traffic - -**Stage 3: Production Rollout (Gradual, 1 week)** -- [ ] Deploy with feature flag (10% traffic) -- [ ] Monitor performance metrics -- [ ] Increase to 50% traffic -- [ ] Monitor for errors -- [ ] Roll out to 100% - -**Monitoring Dashboard**: -``` -Cache Performance Metrics: -- Hit Rate: [###########------] 85% -- Avg Latency: 18ms (vs 145ms uncached) -- Cache Size: 8,234 / 10,000 (82% full) -- Evictions/hour: 342 -- TTL: 3600s (1 hour) - -Recommendations: -✓ Excellent hit rate (>80%) -⚠ Cache 82% full - consider increasing size to 15,000 -✓ Latency within target (<20ms) -``` - -**Expected Results**: -``` -Before Caching: -- Verification latency (p95): 200ms -- Database queries/sec: 500 -- Database CPU: 65% -- Cost: $500/month - -After Caching: -- Verification latency (p95): 50ms (-75%) -- Database queries/sec: 75 (-85%) -- Database CPU: 15% (-50% points) -- Cost: $150/month (-70%) - -ROI: -- Performance: 4x faster -- Cost savings: $350/month -- User experience: Significantly improved -``` - ---- - -## 🟡 MEDIUM PRIORITY ENHANCEMENTS - -### 4. FIX PLACEHOLDER QUALITY SCORES - -**File**: `app/api/routes/enrollment.py:166` - -**Current Code**: -```python -# Line 166 - WRONG! -individual_scores = [70.0] * len(files) # Placeholder - -return MultiImageEnrollmentResponse( - ... - individual_quality_scores=individual_scores, # Fake data! - ... -) -``` - -**Problem**: Returns fake quality scores to clients, misleading users about actual image quality. - -**Solution Design**: - -**Step 1: Update Use Case to Return Session Data (1 hour)** - -```python -# app/application/use_cases/enroll_multi_image.py -from dataclasses import dataclass -from typing import List - -@dataclass -class MultiImageEnrollmentResult: - """Complete result of multi-image enrollment.""" - - face_embedding: FaceEmbedding - session: EnrollmentSession - images_processed: int - - def get_individual_quality_scores(self) -> List[float]: - """Get quality scores for each processed image.""" - return self.session.get_quality_scores() - - def get_average_quality(self) -> float: - """Get average quality across all images.""" - return self.session.get_average_quality() - - def get_fused_quality(self) -> float: - """Get quality score of fused template.""" - return self.face_embedding.quality_score - -class EnrollMultiImageUseCase: - async def execute(...) -> MultiImageEnrollmentResult: - # ... existing processing ... - - # Return comprehensive result - return MultiImageEnrollmentResult( - face_embedding=face_embedding, - session=session, - images_processed=len(image_paths), - ) -``` - -**Step 2: Update API Endpoint (15 minutes)** - -```python -# app/api/routes/enrollment.py -@router.post("/enroll/multi", ...) -async def enroll_face_multi_image(...): - # ... existing validation ... - - # Execute use case - result = await use_case.execute( - user_id=user_id, - image_paths=image_paths, - tenant_id=tenant_id, - ) - - # FIXED: Return ACTUAL quality scores from session - return MultiImageEnrollmentResponse( - success=True, - user_id=result.face_embedding.user_id, - images_processed=result.images_processed, - fused_quality_score=result.get_fused_quality(), - average_quality_score=result.get_average_quality(), - individual_quality_scores=result.get_individual_quality_scores(), # REAL DATA! - message="Multi-image enrollment completed successfully", - embedding_dimension=result.face_embedding.get_embedding_dimension(), - fusion_strategy=settings.MULTI_IMAGE_FUSION_STRATEGY, - ) -``` - -**Step 3: Update Tests (30 minutes)** - -```python -# tests/integration/test_multi_image_enrollment.py -@pytest.mark.asyncio -async def test_multi_image_returns_actual_quality_scores(): - """Test that actual quality scores are returned.""" - async with AsyncClient(app=app, base_url="http://test") as client: - files = [ - ("files", open("tests/fixtures/high_quality.jpg", "rb")), - ("files", open("tests/fixtures/medium_quality.jpg", "rb")), - ("files", open("tests/fixtures/low_quality.jpg", "rb")), - ] - - response = await client.post( - "/api/v1/enroll/multi", - files=files, - data={"user_id": "test_user"}, - ) - - assert response.status_code == 200 - data = response.json() - - # Should have 3 quality scores - assert len(data["individual_quality_scores"]) == 3 - - # Scores should be different (not all 70.0!) - scores = data["individual_quality_scores"] - assert len(set(scores)) > 1 # At least 2 different values - - # Scores should be in valid range - for score in scores: - assert 0 <= score <= 100 -``` - -**Effort**: 1 hour total -**Impact**: Accurate quality feedback to clients -**Testing**: Integration tests verify real scores returned - ---- - -### 5-40. Additional Improvements... - -[Due to length constraints, I'll provide a summary table for remaining improvements] - -| # | Improvement | Category | Effort | Impact | Priority | -|---|-------------|----------|--------|--------|----------| -| 5 | Webhook Delivery Retry | Features | 1 day | High | High | -| 6 | Circuit Breaker for ML Models | Performance | 3 hours | Medium | Medium | -| 7 | Batch Error Resilience | Architecture | 2 hours | Medium | Medium | -| 8 | Async File I/O | Performance | 1 hour | Low | Medium | -| 9-40 | See IMPROVEMENT_ROADMAP.md | Various | 15 days | Various | Medium-Low | - -[Complete implementations for items 5-40 available in detailed appendices] - ---- - -## 📋 TESTING STRATEGY - -### Unit Testing Approach - -**Coverage Goals**: -- Security fixes: 100% coverage -- Performance improvements: 90% coverage -- Feature additions: 85% coverage - -**Test Categories**: -1. **Security Tests**: Injection attempts, auth bypass, validation -2. **Performance Tests**: Load tests, latency measurements -3. **Functional Tests**: Happy path and error cases -4. **Integration Tests**: End-to-end workflows - -### Integration Testing - -**Test Environments**: -- Development: Full test suite -- Staging: Integration + load tests -- Production: Smoke tests + monitoring - -**Test Data**: -- Synthetic: Generated test embeddings -- Anonymized: Real data with PII removed -- Edge cases: Boundary values, malformed inputs - ---- - -## 🚀 DEPLOYMENT PLAN - -### Phased Rollout Strategy - -**Phase 1: Quick Wins (Week 1)** -- Fix eval() vulnerability (CRITICAL) -- Fix quality scores placeholder -- Add batch error resilience -- Deploy to staging - -**Phase 2: Performance (Week 2-3)** -- Implement embedding caching -- Add async file I/O -- Load test and tune -- Deploy to production (10% → 50% → 100%) - -**Phase 3: Security (Week 4)** -- Implement WebSocket JWT auth -- Add audit logging -- Security testing -- Deploy to production - -**Phase 4: Features (Week 5-6)** -- Webhook retry system -- Circuit breakers -- Enhanced monitoring -- Deploy to production - -**Phase 5: Polish (Week 7-8)** -- Additional improvements -- Documentation updates -- Training materials -- Final deployment - ---- - -## 📊 SUCCESS METRICS - -### Performance Metrics - -| Metric | Baseline | Target | Measurement | -|--------|----------|--------|-------------| -| Verification Latency (p95) | 200ms | 50ms | Prometheus | -| Webhook Delivery Rate | 85% | 99.5% | Event tracking | -| Batch Success Rate | 60% | 95% | Job monitoring | -| Cache Hit Rate | N/A | 85%+ | Cache stats | - -### Security Metrics - -| Metric | Baseline | Target | Measurement | -|--------|----------|--------|-------------| -| Code Injection Vulns | 1 | 0 | Security scan | -| Auth Bypass Attempts | Unknown | Logged | Audit log | -| Invalid Token Rate | Unknown | <1% | Auth metrics | - -### Quality Metrics - -| Metric | Baseline | Target | Measurement | -|--------|----------|--------|-------------| -| Test Coverage | 70% | 85% | pytest-cov | -| Code Quality Score | 7.5/10 | 9.5/10 | SonarQube | -| Documentation Coverage | 60% | 95% | Manual review | - ---- - -## 📚 APPENDICES - -### Appendix A: Complete Code Examples -[See individual improvement sections above] - -### Appendix B: Database Schemas -[Webhook events, audit log, cache metadata] - -### Appendix C: API Documentation Updates -[OpenAPI spec changes for new endpoints] - -### Appendix D: Configuration Reference -[Complete .env.example with all new settings] - -### Appendix E: Migration Scripts -[SQL scripts for database changes] - -### Appendix F: Monitoring Dashboards -[Grafana/Prometheus configurations] - ---- - -**Document Status**: ✅ COMPLETE -**Version**: 1.0.0 -**Last Updated**: 2025-12-25 -**Next Review**: After Phase 1 implementation - -**Total Pages**: 85+ -**Total Code Examples**: 40+ -**Total Test Cases**: 30+ -**Ready for Implementation**: YES ✅ diff --git a/docs/archive/2026-04-16/ENHANCED_VISUALIZATION.md b/docs/archive/2026-04-16/ENHANCED_VISUALIZATION.md deleted file mode 100644 index 3deed68..0000000 --- a/docs/archive/2026-04-16/ENHANCED_VISUALIZATION.md +++ /dev/null @@ -1,565 +0,0 @@ -# Enhanced Live Stream Visualization - -The Enhanced Live Stream component provides ultimate real-time visualization capabilities for all biometric analysis modes. It features a canvas-based overlay system with toggleable visualization layers, real-time statistics, and comprehensive visual feedback. - -## Overview - -**Component**: `EnhancedLiveStream` -**Location**: `demo-ui/src/components/demo/enhanced-live-stream.tsx` -**Integration**: Unified Demo Center (`/unified-demo`) - -The Enhanced Live Stream replaces the basic `LiveCameraStream` component with an advanced visualization system that provides: -- Real-time canvas overlays on live video -- 6 toggleable visualization features -- Live statistics dashboard -- Mode-specific visualization adaptation - -## Key Features - -### 1. Canvas Overlay System - -The component uses HTML5 Canvas API to draw real-time annotations directly on the video stream: - -- **Dual-layer rendering**: Video element + Canvas overlay -- **60 FPS rendering**: Uses `requestAnimationFrame` for smooth updates -- **Dynamic resolution**: Automatically adapts to video dimensions -- **Non-destructive**: Overlays don't affect the sent frames - -### 2. Six Toggleable Visualization Features - -#### ✅ Bounding Box -**Default**: ON - -Draws a colored rectangle around detected faces: -- **Green box** (`#22c55e`): Face detected successfully -- **Red box** (`#ef4444`): Face detection failed -- **Line width**: 3px for visibility - -**Example**: -``` -┌────────────────┐ -│ 👤 Face │ ← Green/Red box with 3px border -│ │ -│ │ -└────────────────┘ -``` - -#### 📍 Facial Landmarks -**Default**: ON for `landmarks` mode, OFF otherwise - -Displays 468 facial landmark points as small green dots: -- **Point color**: Green (`#22c55e`) -- **Point size**: 2px radius circles -- **Uses**: Face mesh from MediaPipe - -**Visualization**: -``` - • • • ← Forehead landmarks - • • • • ← Eyebrow landmarks - • ◉ ◉ • ← Eye landmarks (◉ = pupils) - • • • ← Nose landmarks - • • • • • ← Mouth landmarks - • • • • ← Chin landmarks -``` - -#### 🏷️ Info Labels -**Default**: ON - -Shows contextual information based on analysis mode: - -**Demographics Mode**: -- Age: `Age: 32` -- Gender: `Gender: Male` -- Emotion: `Emotion: Happy` - -**Verification Mode**: -- Match: `✓ Verified: user_123` (green) -- No match: `✗ No Match` (red) - -**Search Mode**: -- Found: `Found: user_456` (green) - -**Liveness Mode**: -- Live: `✓ Live Person` (green) -- Spoof: `✗ Spoof` (red) - -**Styling**: -- Black background with 70% opacity -- Color-coded text (blue, green, or red based on context) -- Bold 14px sans-serif font -- Stacked vertically in top-left corner - -#### 📊 Quality Metrics -**Default**: ON for `quality` and `enrollment_ready` modes - -Displays quality metrics as progress bars in the top-right corner: - -**Metrics shown**: -- Overall Quality score -- Brightness -- Sharpness -- Blur -- Centering - -**Color coding**: -- **Green** (`#22c55e`): Score ≥ 75% -- **Yellow** (`#eab308`): Score 50-74% -- **Red** (`#ef4444`): Score < 50% - -**Example**: -``` -┌─────────────────────┐ -│ Quality 87% ████████░│ -│ Brightness 82% ████████░│ -│ Sharpness 91% █████████│ -│ Centering 85% ████████░│ -└─────────────────────┘ -``` - -#### 💯 Confidence Score -**Default**: ON - -Shows the face detection confidence percentage in a green badge above the bounding box: -- **Background**: Green (`#22c55e`) -- **Text**: White, bold 14px -- **Format**: `95%` -- **Position**: Above top-left corner of bounding box - -#### 📈 Frame Statistics -**Default**: ON - -Displays real-time processing statistics at the bottom of the video: - -**Overlay info** (bottom-left): -- Frame number: `Frame: 1247` -- Processing time: `Processing: 45ms` -- Current FPS: `FPS: 15.3` -- Success ratio: `Success: 1195/1247` - -**Dashboard card** (below video): -- Total Frames: Total captured -- Analyzed: Frames that got analysis results -- Success Rate: Percentage of successful analyses -- Avg Time: Average processing time per frame - -### 3. Settings Panel - -Collapsible settings panel accessible via gear icon button: - -**Controls**: -- ⚙️ Settings button (top-right of controls) -- 6 toggle switches for each visualization feature -- Smooth animation on expand/collapse -- Persists state during streaming - -**UI Components**: -- Uses shadcn/ui `Switch` component -- `Label` for accessibility -- `Separator` between options -- `Card` container for clean presentation - -### 4. Mode-Specific Adaptation - -The component intelligently enables/disables features based on analysis mode: - -| Mode | Default Enabled Features | -|------|-------------------------| -| `face_detection` | Bounding Box, Confidence, Stats | -| `quality` | Bounding Box, Quality Metrics, Stats | -| `demographics` | Bounding Box, Labels (Age/Gender/Emotion), Stats | -| `liveness` | Bounding Box, Labels (Live/Spoof), Stats | -| `enrollment_ready` | Bounding Box, Quality Metrics, Stats | -| `verification` | Bounding Box, Labels (Verified/Not), Stats | -| `search` | Bounding Box, Labels (Found user), Stats | -| `landmarks` | Bounding Box, **Landmarks**, Stats | -| `full` | All features enabled | - -## Technical Architecture - -### Component Structure - -``` -EnhancedLiveStream -├── Video Element (hidden) -├── Canvas Overlay (visible, synced size) -├── Live Badge (when streaming) -├── Controls -│ ├── Start/Stop Button -│ └── Settings Toggle Button -├── Settings Panel (collapsible) -│ └── 6 Toggle Switches -└── Statistics Dashboard (when streaming) - └── 4 Metric Cards -``` - -### Data Flow - -``` -Camera Stream - ↓ -Video Element (srcObject) - ↓ -Canvas (drawImage) - ↓ -Add Overlays (drawOverlays) - ↓ -Convert to JPEG Blob - ↓ -Base64 Encode - ↓ -WebSocket Send - ↓ -Receive Analysis Result - ↓ -Update Stats - ↓ -Draw Overlays on Next Frame -``` - -### State Management - -**Component State**: -```typescript -interface State { - // Streaming - isStreaming: boolean; - currentResult: LiveAnalysisResult | null; - showSettings: boolean; - - // Visualization settings - vizSettings: { - showBoundingBox: boolean; - showLandmarks: boolean; - showLabels: boolean; - showQualityMetrics: boolean; - showConfidence: boolean; - showStats: boolean; - }; - - // Statistics - stats: { - totalFrames: number; - analyzedFrames: number; - successfulFrames: number; - errorFrames: number; - avgProcessingTime: number; - currentFPS: number; - startTime: number; - }; -} -``` - -**Refs**: -- `videoRef`: HTMLVideoElement for camera stream -- `canvasRef`: HTMLCanvasElement for overlay rendering -- `streamRef`: MediaStream from getUserMedia -- `animationFrameRef`: requestAnimationFrame ID -- `fpsIntervalRef`: setInterval ID for FPS calculation - -### Rendering Loop - -```typescript -const captureAndSendFrame = useCallback(() => { - // 1. Clear canvas - ctx.clearRect(0, 0, canvas.width, canvas.height); - - // 2. Draw video frame - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - - // 3. Draw overlays based on current result - if (currentResult) { - drawOverlays(ctx, canvas.width, canvas.height, currentResult); - } - - // 4. Convert to JPEG and send - canvas.toBlob((blob) => { - // Base64 encode and send via WebSocket - sendFrame(base64Image); - }); - - // 5. Update frame count - setStats((prev) => ({ ...prev, totalFrames: prev.totalFrames + 1 })); - - // 6. Schedule next frame - animationFrameRef.current = requestAnimationFrame(captureAndSendFrame); -}, [isConnected, currentResult, sendFrame]); -``` - -### Drawing Functions - -#### `drawOverlays(ctx, width, height, result)` -Main overlay rendering function that calls specialized drawing functions based on enabled settings. - -#### `drawLabel(ctx, text, x, y, color)` -Draws a text label with background: -- Black semi-transparent background (70% opacity) -- Colored text (blue, green, or red) -- Auto-sized based on text width - -#### `drawMetricBar(ctx, label, value, x, y, maxX)` -Draws a horizontal progress bar with label: -- Black semi-transparent background -- White text label -- Color-coded fill bar (green/yellow/red) -- Percentage value displayed - -## Usage Examples - -### Basic Usage - -```typescript -import { EnhancedLiveStream } from '@/components/demo/enhanced-live-stream'; - -function DemoPage() { - const handleResult = (result: LiveAnalysisResult) => { - console.log('Frame analyzed:', result); - }; - - return ( - - ); -} -``` - -### With Verification - -```typescript - -``` - -### All Props - -```typescript -interface EnhancedLiveStreamProps { - mode: AnalysisMode; // Required: Analysis mode - onResult?: (result: LiveAnalysisResult) => void; // Optional: Result callback - userId?: string; // Optional: For verification mode - tenantId?: string; // Optional: For multi-tenant -} -``` - -## Performance Considerations - -### Canvas Rendering Performance - -**Rendering cost per frame** (~2-5ms): -- Clear canvas: ~0.5ms -- Draw video: ~1ms -- Draw overlays: ~1-3ms (depends on enabled features) -- Total: ~2-5ms per frame - -**Impact on FPS**: -- Minimal impact on fast modes (face_detection, quality) -- Negligible compared to analysis processing time -- 60 FPS rendering loop independent of analysis FPS - -### Memory Usage - -**Baseline**: ~50-100 MB (video stream + canvas) -**Additional per feature**: -- Bounding box: +0.1 MB -- Landmarks (468 points): +0.5 MB -- Labels: +0.2 MB -- Quality metrics: +0.3 MB -- Stats: +0.1 MB - -**Total typical usage**: ~60-120 MB - -### Optimization Tips - -1. **Disable unused features**: Turn off visualization features you don't need -2. **Lower resolution**: Use 720p instead of 1080p for slower modes -3. **Hide stats when not needed**: Stats dashboard recalculates every second -4. **Limit landmark mode**: Only enable landmarks when specifically needed - -## Browser Compatibility - -**Required APIs**: -- ✅ Canvas 2D Context -- ✅ getUserMedia -- ✅ requestAnimationFrame -- ✅ WebSocket -- ✅ FileReader + Blob - -**Supported Browsers**: -- Chrome 90+ -- Firefox 88+ -- Safari 14+ -- Edge 90+ - -**Mobile Support**: -- iOS Safari 14+ -- Chrome Android 90+ -- Samsung Internet 14+ - -## Troubleshooting - -### Canvas not updating - -**Symptom**: Video plays but no overlays appear - -**Solutions**: -1. Check if `currentResult` has data -2. Verify canvas dimensions match video dimensions -3. Ensure `drawOverlays` is being called -4. Check browser console for errors - -### Performance issues - -**Symptom**: Laggy video or low FPS - -**Solutions**: -1. Disable unnecessary visualization features -2. Lower camera resolution to 720p -3. Check if analysis mode is too slow for real-time -4. Close other browser tabs -5. Check GPU acceleration is enabled - -### Overlays misaligned - -**Symptom**: Bounding boxes or landmarks don't align with faces - -**Solutions**: -1. Ensure canvas size exactly matches video size -2. Check video aspect ratio is maintained -3. Verify coordinate scaling is correct -4. Test with different camera resolutions - -### Labels overlapping - -**Symptom**: Too many labels make text unreadable - -**Solutions**: -1. Disable some label categories in settings -2. Increase video size for more space -3. Consider showing only critical labels -4. Use separate results panel instead - -## Future Enhancements - -Potential additions to the visualization system: - -### 1. Pose Estimation Overlay -- Draw skeleton on body -- Show joint positions -- Track pose confidence - -### 2. Heatmap Visualization -- Quality heatmap overlay -- Attention regions -- Focus areas - -### 3. Recording Capabilities -- Record video with overlays -- Export as MP4 -- Save individual frames - -### 4. Custom Themes -- User-selectable color schemes -- Dark/Light overlay modes -- Accessibility options (high contrast) - -### 5. Advanced Statistics -- FPS history graph -- Processing time chart -- Success rate trend - -### 6. Multi-face Support -- Show multiple bounding boxes -- Track face IDs -- Different colors per person - -## Integration with Unified Demo Center - -The Enhanced Live Stream is integrated into the Unified Demo Center at `/unified-demo`: - -**Location**: Live tab of Input section - -**Features**: -- Works with all 9 analysis modes -- Mode-specific visualization adaptation -- Results shown in both overlay AND results panel -- Settings persist across mode changes - -**User Flow**: -1. User selects analysis mode (e.g., "Quality Analysis") -2. User switches to "Live" input tab -3. Enhanced Live Stream component loads -4. User clicks "Start Stream" -5. Camera opens with live overlays -6. User opens settings panel -7. User toggles visualization features -8. Real-time overlays update based on settings -9. Results also appear in Results panel -10. User clicks "Stop" to end stream - -## Best Practices - -### 1. Choose Appropriate Visualizations - -**For Fast Modes** (face_detection, landmarks): -- Enable all features for rich visualization -- Use for demonstrations and debugging - -**For Moderate Modes** (quality, liveness): -- Enable bounding box, labels, and stats -- Disable landmarks if not needed - -**For Slow Modes** (demographics, search): -- Keep only essential features -- Focus on labels and stats -- Disable landmarks and quality bars - -### 2. Settings Management - -**For Demonstrations**: -- Start with all features enabled -- Show toggles to audience -- Explain each visualization type - -**For Production**: -- Enable only necessary features -- Hide settings panel -- Optimize for performance - -### 3. User Experience - -**Feedback**: -- Always show frame stats for transparency -- Use color coding for quick interpretation -- Provide clear labels for all metrics - -**Performance**: -- Test on target devices before deployment -- Provide fallback for low-end devices -- Allow users to adjust settings - -## Conclusion - -The Enhanced Live Stream visualization system provides a powerful, flexible, and user-friendly way to visualize real-time biometric analysis. With toggleable features, mode-specific adaptation, and comprehensive statistics, it serves both demonstration and production use cases. - -**Key Benefits**: -- ✅ Rich visual feedback -- ✅ User-controllable features -- ✅ Real-time performance -- ✅ Mode-aware adaptation -- ✅ Production-ready quality -- ✅ Comprehensive statistics - -For the best experience, match visualization settings to your use case and hardware capabilities. The component is designed to scale from simple face detection to complex multi-feature analysis. - ---- - -**Quick Reference**: -- Component: `demo-ui/src/components/demo/enhanced-live-stream.tsx` -- Integration: `demo-ui/src/app/(features)/unified-demo/page.tsx` -- Hook: `demo-ui/src/hooks/use-live-camera-analysis.ts` -- Documentation: `docs/ENHANCED_VISUALIZATION.md` diff --git a/docs/archive/2026-04-16/LAPTOP_GPU_RESEARCH.md b/docs/archive/2026-04-16/LAPTOP_GPU_RESEARCH.md deleted file mode 100644 index 1d0da0e..0000000 --- a/docs/archive/2026-04-16/LAPTOP_GPU_RESEARCH.md +++ /dev/null @@ -1,665 +0,0 @@ -# Research: Using RTX Laptops as GPU Inference Servers - -## Executive Summary - -**Verdict: Yes, it is feasible and cost-effective for this project's workload.** - -The biometric-processor runs lightweight CNN inference (FaceNet ~95MB, YOLO ~12MB, MediaPipe ~35MB) — not large language models. These models comfortably fit within 8GB VRAM of an RTX 4060 Laptop, let alone a 4070/4080. The current production deployment already runs on **CPU-only** (TensorFlow-CPU on Cloud Run with 2 cores / 4GB RAM), so any RTX laptop GPU will deliver a significant speedup over the current setup. - ---- - -## Current Deployment Cost Context - -| Platform | Config | Estimated Monthly Cost | -|----------|--------|----------------------| -| Google Cloud Run | 2 CPU / 4GB RAM (CPU-only) | ~$50-150/mo (depending on traffic) | -| Cloud GPU VM (T4) | 1x T4 GPU / 4 vCPU / 16GB | ~$250-400/mo | -| Cloud GPU VM (A100) | 1x A100 / 12 vCPU / 85GB | ~$1,500-2,500/mo | -| **RTX Laptop (one-time)** | **RTX 4060 Laptop** | **$0/mo after purchase (~$1,000-1,500 laptop)** | - ---- - -## Model Memory Requirements vs. Laptop GPU VRAM - -| Component | Model Size | GPU Memory Needed | -|-----------|-----------|-------------------| -| FaceNet (default) | 95MB | ~200MB on GPU | -| FaceNet512 | 120MB | ~250MB on GPU | -| ArcFace | 110MB | ~230MB on GPU | -| OpenCV face detection | 25MB | ~50MB on GPU | -| MediaPipe landmarks | 35MB | ~80MB on GPU | -| YOLO card detector | 12MB | ~50MB on GPU | -| TensorFlow runtime overhead | — | ~500MB | -| **Total (all models loaded)** | **~400MB** | **~1.4GB on GPU** | - -**Available VRAM on RTX laptops:** - -| Laptop GPU | VRAM | Headroom After All Models | -|------------|------|--------------------------| -| RTX 3060 Laptop | 6GB | ~4.6GB free | -| RTX 4060 Laptop | 8GB | ~6.6GB free | -| RTX 4070 Laptop | 8GB | ~6.6GB free | -| RTX 4080 Laptop | 12GB | ~10.6GB free | -| RTX 4090 Laptop | 16GB | ~14.6GB free | - -**All models fit comfortably even on the smallest RTX laptop GPU.** There is ample headroom for batch processing multiple images concurrently. - ---- - -## Expected Performance Improvement - -### Current CPU-Only Benchmarks (from codebase) - -| Operation | CPU Latency (P50) | CPU Throughput | -|-----------|-------------------|----------------| -| Face Enrollment | 150ms | 80 RPS | -| Face Verification | 85ms | 150 RPS | -| Face Search (1K) | 45ms | 100 RPS | -| Liveness Check | 60ms | 200 RPS | -| Demographics | ~150ms | — | - -### Expected GPU Speedup (RTX 4060 Laptop) - -For small CNN inference (FaceNet, YOLO), GPU acceleration typically provides **2-5x speedup** over CPU for single-image inference, and **5-15x** for batched inference due to GPU parallelism. - -| Operation | Estimated GPU Latency | Estimated Speedup | -|-----------|----------------------|-------------------| -| Face Enrollment | 40-70ms | ~2-4x faster | -| Face Verification | 20-40ms | ~2-4x faster | -| Liveness Check | 30-50ms | ~1.5-2x faster | -| Demographics | 40-70ms | ~2-4x faster | -| Batch (10 images) | 100-200ms total | ~5-10x faster | - -> Note: Single-image latency improvements are modest because these models are small. The real win is in **throughput** — a GPU can process many more concurrent requests. - ---- - -## Implementation Difficulty - -### What Needs to Change in the Codebase - -**Difficulty: Low.** The codebase is already GPU-ready. Key changes: - -#### 1. Switch TensorFlow from CPU to GPU (~5 minutes) - -```diff -# requirements.txt -- tensorflow-cpu==2.15.0 -+ tensorflow==2.15.0 -``` - -TensorFlow auto-detects CUDA GPUs. No code changes needed. - -#### 2. Install NVIDIA Drivers + CUDA on the Laptop (~30 minutes) - -```bash -# Ubuntu/Debian -sudo apt install nvidia-driver-545 -sudo apt install nvidia-cuda-toolkit - -# Or use NVIDIA's official CUDA 12.2 installer -# cuDNN 8.x is also needed for TensorFlow -``` - -#### 3. Expose the Laptop to the Internet - -**Option A: Cloudflare Tunnel (Recommended for production)** -```bash -# Install cloudflared -sudo apt install cloudflared - -# Authenticate -cloudflared tunnel login - -# Create tunnel -cloudflared tunnel create biometric-api - -# Run it -cloudflared tunnel route dns biometric-api api.yourdomain.com -cloudflared tunnel run biometric-api -``` -- Free, no bandwidth limits -- Built-in DDoS protection -- TLS termination included -- Requires a domain name - -**Option B: ngrok (Quick setup)** -```bash -ngrok http 8000 -``` -- Fast for testing -- Paid plan needed for custom domains and production use - -**Option C: Tailscale / WireGuard (Internal use only)** -- Best if the API consumers are within your own infrastructure -- No public exposure needed -- Zero-config encrypted networking - -#### 4. Systemd Service for Auto-Start (~10 minutes) - -```ini -# /etc/systemd/system/biometric-api.service -[Unit] -Description=Biometric Processor API -After=network.target - -[Service] -Type=simple -User=biometric -WorkingDirectory=/opt/biometric-processor -ExecStart=/opt/biometric-processor/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 -Restart=always -RestartSec=5 -Environment=NVIDIA_VISIBLE_DEVICES=all - -[Install] -WantedBy=multi-user.target -``` - -#### 5. GPU Memory Configuration (Optional) - -Add to application config to prevent TensorFlow from grabbing all VRAM: - -```python -import tensorflow as tf -gpus = tf.config.experimental.list_physical_devices('GPU') -for gpu in gpus: - tf.config.experimental.set_memory_growth(gpu, True) -``` - ---- - -## Risks and Mitigations - -| Risk | Severity | Mitigation | -|------|----------|------------| -| **Laptop overheating under 24/7 load** | Medium | Use a laptop cooling pad; set GPU power limit (`nvidia-smi -pl 80`); monitor temps with `nvidia-smi` | -| **Network reliability (home internet)** | Medium | Use a UPS for power; set up health checks and auto-restart; consider a backup laptop | -| **Laptop battery swelling from constant charging** | Low-Medium | Remove battery if possible; use a laptop designed for always-on (e.g., Lenovo Legion, ASUS TUF) | -| **No ECC memory** | Low | Consumer GPUs lack ECC, but for inference (not training), bit errors are extremely rare and inconsequential | -| **Single point of failure** | Medium | Run 2 laptops behind a load balancer (Cloudflare or nginx) for redundancy | -| **ISP blocks/throttles** | Low | Cloudflare Tunnel bypasses most ISP restrictions since it uses outbound connections | -| **Driver/OS updates causing downtime** | Low | Use Ubuntu LTS; pin driver versions; schedule maintenance windows | - ---- - -## Architecture: Laptop GPU Setup - -``` - Internet - │ - ┌────────▼────────┐ - │ Cloudflare CDN │ (DDoS protection, TLS, caching) - └────────┬────────┘ - │ (Cloudflare Tunnel - outbound connection) - │ - ┌────────────▼────────────┐ - │ Laptop 1 (Primary) │ - │ RTX 4060/4070 Laptop │ - │ Ubuntu + CUDA 12.2 │ - │ FastAPI + TensorFlow │ - │ PostgreSQL + Redis │ - └─────────────────────────┘ - │ - ┌────────────▼────────────┐ (Optional) - │ Laptop 2 (Backup) │ - │ Same config │ - │ Cloudflare load balance│ - └─────────────────────────┘ -``` - ---- - -## Cost Comparison (12-Month Projection) - -| Option | Setup Cost | Monthly Cost | 12-Month Total | -|--------|-----------|-------------|----------------| -| Cloud Run (CPU-only, current) | $0 | ~$100 | ~$1,200 | -| Cloud GPU VM (T4) | $0 | ~$350 | ~$4,200 | -| Cloud GPU VM (A100) | $0 | ~$2,000 | ~$24,000 | -| **RTX 4060 Laptop** | **~$1,200** | **~$15 electricity** | **~$1,380** | -| **RTX 4070 Laptop** | **~$1,500** | **~$15 electricity** | **~$1,680** | - -> The laptop option pays for itself vs. a T4 GPU VM in roughly **3-4 months**. - ---- - -## Recommendation - -### For This Project: Use RTX Laptops - -**Why it works well here:** - -1. **Models are small** — Total GPU memory ~1.4GB, fits easily in 8GB VRAM -2. **Inference only** — No training needed, inference is gentle on hardware -3. **Already GPU-ready** — Codebase needs only `tensorflow-cpu` → `tensorflow` swap -4. **Significant cost savings** — 60-90% cheaper than cloud GPU VMs over 12 months -5. **Better performance** — Even an RTX 4060 Laptop will be 2-4x faster than current CPU-only Cloud Run - -**Suggested hardware:** - -- **Minimum:** Any RTX 3060+ laptop (~$800-1,000) -- **Recommended:** RTX 4060/4070 laptop (~$1,200-1,500) -- **Ideal (for headroom):** RTX 4070+ laptop with good cooling (~$1,500-2,000) - -**Suggested setup:** - -1. Ubuntu 22.04 LTS + NVIDIA driver 545 + CUDA 12.2 -2. Cloudflare Tunnel for public access -3. Systemd service for auto-start -4. `nvidia-smi` monitoring + alerting -5. Optional: Second laptop for redundancy - ---- - -## Step-by-Step: Exposing the Laptop to the Internet - -### Option Comparison - -| Method | Cost | Setup Time | Production Ready | Best For | -|--------|------|-----------|-----------------|----------| -| **Cloudflare Tunnel** | Free | ~30 min | Yes | Production APIs (recommended) | -| **ngrok** | Free/$8+/mo | ~5 min | Limited | Quick testing / demos | -| **Tailscale Funnel** | Free | ~15 min | Beta | Internal / team-only access | - -### Recommended: Cloudflare Tunnel (Free, Production-Grade) - -Cloudflare Tunnel creates an **outbound-only** connection from your laptop to Cloudflare's edge network. This means: -- No port forwarding needed on your router -- Your home IP is never exposed to the public -- Built-in DDoS protection and WAF -- Automatic HTTPS/TLS -- Free, with no bandwidth limits -- Supports load balancing across multiple tunnels (multiple laptops) - -#### Prerequisites - -1. A domain name (can buy cheaply or use existing one) -2. A free Cloudflare account with the domain added -3. Ubuntu 22.04 LTS on the laptop - -#### Step 1: Install NVIDIA Drivers + CUDA (~30 min, one-time) - -```bash -# Add NVIDIA repository -sudo apt update -sudo apt install -y nvidia-driver-545 - -# Reboot after driver install -sudo reboot - -# Verify driver -nvidia-smi - -# Install CUDA toolkit -sudo apt install -y nvidia-cuda-toolkit - -# Verify CUDA -nvcc --version -``` - -#### Step 2: Set Up the Biometric Processor (~15 min) - -```bash -# Clone the repo -git clone https://github.com/Rollingcat-Software/biometric-processor.git /opt/biometric-processor -cd /opt/biometric-processor - -# Create virtual environment -python3.11 -m venv venv -source venv/bin/activate - -# Install dependencies (GPU version — swap tensorflow-cpu for tensorflow) -pip install tensorflow==2.15.0 -pip install -r requirements.txt - -# Configure environment -cp .env.example .env -``` - -Edit `.env` with production settings: - -```env -ENVIRONMENT=production -API_HOST=0.0.0.0 -API_PORT=8001 - -# IMPORTANT: Set your actual domain -CORS_ORIGINS=https://api.yourdomain.com,https://yourdomain.com - -# Security: Must be enabled in production -API_KEY_ENABLED=true -API_KEY_REQUIRE_AUTH=true - -# GPU memory management -TF_FORCE_GPU_ALLOW_GROWTH=true -``` - -#### Step 3: Test Locally First - -```bash -# Start the server -source /opt/biometric-processor/venv/bin/activate -uvicorn app.main:app --host 0.0.0.0 --port 8001 - -# In another terminal, test health endpoint -curl http://localhost:8001/api/v1/health -# Should return: {"status": "healthy", ...} - -# Verify GPU is being used -nvidia-smi -# Should show the python process using GPU memory -``` - -#### Step 4: Install and Configure Cloudflare Tunnel (~15 min) - -```bash -# Install cloudflared -curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb \ - -o cloudflared.deb -sudo dpkg -i cloudflared.deb - -# Authenticate (opens browser) -cloudflared tunnel login - -# Create a tunnel -cloudflared tunnel create biometric-api -# Save the tunnel ID printed (e.g., a1b2c3d4-...) - -# Route DNS — creates a CNAME record automatically -cloudflared tunnel route dns biometric-api api.yourdomain.com -``` - -Create the tunnel config file: - -```bash -mkdir -p ~/.cloudflared -``` - -Write `~/.cloudflared/config.yml`: - -```yaml -tunnel: -credentials-file: /home//.cloudflared/.json - -ingress: - - hostname: api.yourdomain.com - service: http://localhost:8001 - originRequest: - connectTimeout: 30s - noTLSVerify: false - - service: http_status:404 -``` - -Test the tunnel: - -```bash -cloudflared tunnel run biometric-api - -# From any device on the internet: -curl https://api.yourdomain.com/api/v1/health -``` - -#### Step 5: Set Up Systemd Services (Auto-Start on Boot) - -**Biometric API service:** - -```ini -# /etc/systemd/system/biometric-api.service -[Unit] -Description=Biometric Processor API (GPU) -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -User=biometric -Group=biometric -WorkingDirectory=/opt/biometric-processor -Environment=PATH=/opt/biometric-processor/venv/bin:/usr/local/bin:/usr/bin -Environment=NVIDIA_VISIBLE_DEVICES=all -EnvironmentFile=/opt/biometric-processor/.env -ExecStart=/opt/biometric-processor/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8001 -Restart=always -RestartSec=5 - -[Install] -WantedBy=multi-user.target -``` - -**Cloudflare Tunnel service (or use cloudflared's built-in service install):** - -```bash -# Easiest way — cloudflared has a built-in service installer -sudo cloudflared service install -sudo systemctl enable cloudflared -sudo systemctl start cloudflared -``` - -**Enable and start everything:** - -```bash -sudo systemctl daemon-reload -sudo systemctl enable biometric-api -sudo systemctl start biometric-api -sudo systemctl start cloudflared - -# Verify both are running -sudo systemctl status biometric-api -sudo systemctl status cloudflared -``` - -Now the laptop will **auto-start** the API and tunnel on boot. - -#### Step 6: GPU Thermal Management - -```bash -# Set GPU power limit to prevent overheating (80W is safe for most laptops) -sudo nvidia-smi -pl 80 - -# Make it persistent across reboots -# Add to /etc/rc.local or a systemd service: -# ExecStartPre=/usr/bin/nvidia-smi -pl 80 - -# Monitor GPU temperature -watch -n 1 nvidia-smi -``` - -Recommended: Buy a **laptop cooling pad** ($20-30) for 24/7 operation. - ---- - -### Alternative: ngrok (Quick Testing) - -Good for quick demos but not ideal for production: - -```bash -# Install -curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | \ - sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null -echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | \ - sudo tee /etc/apt/sources.list.d/ngrok.list -sudo apt update && sudo apt install ngrok - -# Authenticate -ngrok config add-authtoken - -# Expose the API -ngrok http 8001 -``` - -Downsides for production: URL changes on restart (unless paid), limited free connections, adds latency, no built-in DDoS protection. - ---- - -### Alternative: Tailscale Funnel (Team-Only Access) - -Best if the API is only consumed by your own services/team: - -```bash -# Install -curl -fsSL https://tailscale.com/install.sh | sh - -# Start and authenticate -sudo tailscale up - -# Expose port 8001 to the internet via Funnel -sudo tailscale funnel 8001 - -# Access at: https://..ts.net/ -``` - -Downsides: Funnel is still in beta, limited customization, no custom domains on free tier. - ---- - -### Multi-Laptop Load Balancing (Optional) - -If you need redundancy or higher throughput, Cloudflare Tunnel supports running **the same tunnel ID on multiple machines**: - -``` - Internet - │ - ┌────────▼────────┐ - │ Cloudflare CDN │ DNS: api.yourdomain.com - │ (Load Balance) │ (auto-failover between tunnels) - └───┬─────────┬───┘ - │ │ - ┌────────▼──┐ ┌──▼────────┐ - │ Laptop 1 │ │ Laptop 2 │ - │ RTX 4060 │ │ RTX 4060 │ - │ Primary │ │ Backup │ - └────────────┘ └────────────┘ -``` - -Just install the same tunnel credentials on both laptops: - -```bash -# On Laptop 2: copy tunnel credentials from Laptop 1 -scp user@laptop1:~/.cloudflared/.json ~/.cloudflared/ -scp user@laptop1:~/.cloudflared/config.yml ~/.cloudflared/ - -# Start the same tunnel -cloudflared tunnel run biometric-api -``` - -Cloudflare automatically load-balances and fails over between the two connectors. - ---- - -## Network Type Analysis: Wi-Fi, Mobile Hotspot, Eduroam - -### Why Cloudflare Tunnel Works on Restrictive Networks - -Cloudflare Tunnel only needs **outbound HTTPS (port 443)** — the same port used by regular web browsing. It does **not** require: -- Inbound ports to be open -- Port forwarding on the router -- A public IP address -- NAT hole-punching (unlike Tailscale) - -This means it works behind **any number of NAT layers**, including Carrier-Grade NAT (CGNAT) used by many ISPs and mobile networks. - -### Network Suitability Rating - -| Network Type | Works? | Stability | Latency Added | Production Suitable? | -|-------------|--------|-----------|---------------|---------------------| -| **Wired Ethernet (home/office)** | Yes | Excellent | +15-30ms | Yes | -| **Home Wi-Fi (5GHz)** | Yes | Very Good | +20-40ms | Yes | -| **Home Wi-Fi (2.4GHz)** | Yes | Good | +25-50ms | Acceptable | -| **Eduroam / University Wi-Fi** | Yes | Good* | +20-45ms | Acceptable* | -| **Mobile Hotspot (4G/LTE)** | Yes | Unstable | +40-80ms | Not recommended | -| **Mobile Hotspot (5G)** | Yes | Moderate | +30-60ms | Emergency only | -| **Coffee shop / Public Wi-Fi** | Yes | Poor | +30-60ms | No | - -### Detailed Breakdown - -#### Home Wi-Fi — Recommended - -Works well for production. The biometric API's typical request/response is small (an image upload + JSON response), so Wi-Fi bandwidth is not a bottleneck. A 5GHz connection on a decent router is nearly as stable as wired for this use case. - -**Tip:** Connect via 5GHz band and position the laptop near the router, or better yet, use a USB Ethernet adapter (~$15) for maximum reliability. - -#### Eduroam / University Networks — Works, With Caveats - -Eduroam allows outbound HTTPS traffic (port 443), which is all Cloudflare Tunnel needs. It works because: -- The tunnel is outbound-only — no firewall rules to open -- It uses standard HTTPS, indistinguishable from normal web traffic -- It works behind the university's NAT without port forwarding - -**Potential issues on eduroam:** -- Some universities apply **captive portals** that require re-authentication every few hours — this will temporarily break the tunnel until you re-authenticate -- Some networks block port **7844** (used by cloudflared for QUIC protocol) — fix by forcing HTTP/2: `cloudflared tunnel --protocol http2 run biometric-api` -- Network congestion during peak hours (classes, exams) can increase latency -- The university may throttle long-lived connections - -**Mitigation for eduroam:** -```bash -# Force HTTP/2 instead of QUIC (more reliable on restrictive networks) -cloudflared tunnel --protocol http2 run biometric-api - -# Or set it in config.yml: -# protocol: http2 -``` - -#### Mobile Hotspot (4G/5G) — Not Recommended for Production - -Community reports show Cloudflare Tunnel can be **unstable on mobile networks**, with frequent disconnections: -- `"lost connection with the edge"` errors -- `"timeout: no recent network activity"` causing tunnel drops -- QUIC protocol is especially unreliable on mobile; switching to HTTP/2 helps but doesn't fully solve it -- Reconnection cycles cause 1-5 second outages - -**If mobile is your only option:** -```bash -# Always use HTTP/2 on mobile networks -cloudflared tunnel --protocol http2 run biometric-api -``` - -This is acceptable as a **backup/emergency** connection, not as a primary. - -#### Public Wi-Fi (cafes, airports) — Avoid - -Captive portals, connection limits, bandwidth throttling, and shared congestion make these networks unreliable for hosting any service. - -### Recommended Network Setup (Priority Order) - -1. **Best:** Wired Ethernet via USB adapter → Home/office router → ISP -2. **Good:** 5GHz Wi-Fi → Home/office router → ISP -3. **Acceptable:** Eduroam (with HTTP/2 protocol forced) -4. **Backup only:** Mobile hotspot (4G/5G with HTTP/2 forced) - -### Handling Network Interruptions - -Cloudflare Tunnel **automatically reconnects** when the network drops and comes back. The systemd service also auto-restarts both `cloudflared` and the biometric API if they crash. During a network interruption: - -- **Tunnel reconnects** in 1-5 seconds after network is restored -- **In-flight requests** during the outage will timeout on the client side -- **No data loss** — the API is stateless per-request, database is local -- **Cloudflare returns 502** to clients while the tunnel is down (clients know to retry) - -For maximum uptime, the **two-laptop setup** is the best solution — if one laptop's network drops, Cloudflare automatically routes to the other. - ---- - -## Sources - -- [Best GPUs for AI Inference 2025 - GPU Mart](https://www.gpu-mart.com/blog/best-gpus-for-ai-inference-2025) -- [RTX 4060 vs 4070 for AI - BestGPUsForAI](https://www.bestgpusforai.com/gpu-comparison/4060-vs-4070) -- [GPU Benchmarks - NVIDIA RTX 3060 vs 4070 - BIZON](https://bizon-tech.com/gpu-benchmarks/NVIDIA-RTX-3060-vs-NVIDIA-RTX-4070/590vs680) -- [Best GPUs for AI 2025 - SabrePC](https://www.sabrepc.com/blog/deep-learning-ai/best-gpus-for-ai) -- [Self-hosted AI workflows with ngrok](https://ngrok.com/blog/self-hosted-local-ai-workflows-with-docker-n8n-ollama-and-ngrok-2025) -- [Cloudflare Tunnels vs ngrok](https://dev.to/amjadmh73/make-your-server-accessible-from-anywhere-55e4) -- [Choosing GPU for Training vs Inference - RunPod](https://www.runpod.io/articles/comparison/choosing-a-gpu-for-training-vs-inference) -- [Best Cloud GPU Platforms - DigitalOcean](https://www.digitalocean.com/resources/articles/best-cloud-gpu-platforms) -- [Cloudflare Tunnel – Set Up Your First Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/) -- [Create a Locally-Managed Tunnel – Cloudflare Docs](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/create-local-tunnel/) -- [Quick Tunnels (TryCloudflare) – Cloudflare Docs](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/) -- [Cloudflare Tunnel vs ngrok vs Tailscale – DEV Community](https://dev.to/mechcloud_academy/cloudflare-tunnel-vs-ngrok-vs-tailscale-choosing-the-right-secure-tunneling-solution-4inm) -- [ngrok Alternatives – Tailscale](https://tailscale.com/learn/ngrok-alternatives) -- [Ngrok vs Cloudflare Tunnel vs Tailscale: Complete 2025-26 – InstaTunnel](https://instatunnel.my/blog/comparing-the-big-three-a-comprehensive-analysis-of-ngrok-cloudflare-tunnel-and-tailscale-for-modern-development-teams) -- [Tunnel with Firewall – Cloudflare Docs](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/tunnel-with-firewall/) -- [Cloudflare Tunnels FAQ – Cloudflare Docs](https://developers.cloudflare.com/cloudflare-one/faq/cloudflare-tunnels-faq/) -- [Setting Up Tunnel Behind CGNAT – Cloudflare Community](https://community.cloudflare.com/t/setting-up-new-tunnel-behind-cgnat/872933) -- [Cloudflare Tunnel Unstable: Frequent Disconnects – Cloudflare Community](https://community.cloudflare.com/t/cloudflare-tunnel-unstable-with-frequent-disconnects/386397) -- [Problems with Connectivity of Cloudflare Tunnel – Cloudflare Community](https://community.cloudflare.com/t/problems-with-connectivity-of-cloudflare-tunnel/761936) -- [Tailscale Funnel vs Cloudflare Tunnel vs Nginx – Onidel](https://onidel.com/blog/tailscale-cloudflare-nginx-vps-2025) diff --git a/docs/archive/2026-04-16/MULTI_IMAGE_ENROLLMENT.md b/docs/archive/2026-04-16/MULTI_IMAGE_ENROLLMENT.md deleted file mode 100644 index 838a93e..0000000 --- a/docs/archive/2026-04-16/MULTI_IMAGE_ENROLLMENT.md +++ /dev/null @@ -1,244 +0,0 @@ -# Multi-Image Enrollment System - -## Overview - -The multi-image enrollment system is a professional biometric enrollment feature that improves verification accuracy by 30-40% compared to single-image enrollment, especially with poor quality photos. - -## Key Features - -- **Template Fusion**: Combines 2-5 face images into a single robust embedding template -- **Quality-Weighted Average**: Higher quality images contribute more to the fused template -- **Backward Compatible**: Works alongside existing single-image enrollment -- **Production Ready**: Built with Clean Architecture principles - -## How It Works - -### 1. Multi-Image Capture -Users submit 2-5 face images during enrollment (e.g., from different angles, lighting conditions, or times). - -### 2. Individual Processing -Each image is processed independently: -- Face detection -- Quality assessment -- Embedding extraction - -### 3. Quality-Weighted Fusion -Embeddings are combined using weighted average: -``` -fused_embedding = Σ(quality_i * embedding_i) / Σ(quality_i) -``` - -Higher quality images contribute more weight to the final template. - -### 4. Template Storage -The fused embedding is stored as the user's template, replacing or updating any existing single-image template. - -## API Usage - -### Endpoint -``` -POST /api/v1/enroll/multi -``` - -### Request -```bash -curl -X POST "http://localhost:8001/api/v1/enroll/multi" \ - -F "user_id=user123" \ - -F "files=@image1.jpg" \ - -F "files=@image2.jpg" \ - -F "files=@image3.jpg" \ - -F "tenant_id=tenant_abc" -``` - -### Response -```json -{ - "success": true, - "user_id": "user123", - "images_processed": 3, - "fused_quality_score": 87.5, - "average_quality_score": 82.3, - "individual_quality_scores": [78.5, 85.0, 83.5], - "message": "Multi-image enrollment completed successfully", - "embedding_dimension": 512, - "fusion_strategy": "weighted_average" -} -``` - -## Configuration - -Add these settings to your `.env` file: - -```env -# Multi-Image Enrollment -MULTI_IMAGE_ENROLLMENT_ENABLED=true -MULTI_IMAGE_MIN_IMAGES=2 -MULTI_IMAGE_MAX_IMAGES=5 -MULTI_IMAGE_FUSION_STRATEGY=weighted_average -MULTI_IMAGE_NORMALIZATION=l2 -MULTI_IMAGE_MIN_QUALITY_PER_IMAGE=60.0 -``` - -### Configuration Options - -| Setting | Default | Description | -|---------|---------|-------------| -| `MULTI_IMAGE_ENROLLMENT_ENABLED` | `true` | Enable/disable multi-image enrollment | -| `MULTI_IMAGE_MIN_IMAGES` | `2` | Minimum number of images required (2-5) | -| `MULTI_IMAGE_MAX_IMAGES` | `5` | Maximum number of images allowed (2-5) | -| `MULTI_IMAGE_FUSION_STRATEGY` | `weighted_average` | Fusion algorithm (`weighted_average` or `simple_average`) | -| `MULTI_IMAGE_NORMALIZATION` | `l2` | Normalization strategy (`l2` or `none`) | -| `MULTI_IMAGE_MIN_QUALITY_PER_IMAGE` | `60.0` | Minimum quality score per image (0-100) | - -## Architecture - -### Domain Layer -- **Entities**: `EnrollmentSession`, `ImageSubmission` -- **Services**: `EmbeddingFusionService` -- **Exceptions**: `EnrollmentSessionError`, `FusionError`, etc. - -### Application Layer -- **Use Case**: `EnrollMultiImageUseCase` - -### API Layer -- **Endpoint**: `POST /api/v1/enroll/multi` -- **Schemas**: `MultiImageEnrollmentResponse` - -### Dependency Injection -- Fully integrated with existing DI container -- All dependencies injected via interfaces - -## Benefits - -### 1. Improved Accuracy -- **30-40% improvement** in verification accuracy with poor quality photos -- More robust to variations in lighting, angle, expression -- Reduces false rejection rate - -### 2. Flexibility -- Works with 2-5 images (configurable) -- Quality-based weighting ensures best images contribute most -- Handles varying image quality gracefully - -### 3. Production Ready -- Clean Architecture (testable, maintainable) -- Comprehensive error handling -- Structured logging -- Backward compatible - -## Use Cases - -### 1. High-Security Enrollment -Government ID systems, financial institutions - multiple images ensure robust templates. - -### 2. Variable Conditions -Enrollment in uncontrolled environments (e.g., mobile apps) where lighting/quality varies. - -### 3. Progressive Enrollment -Users can submit images over time, system fuses them into stronger template. - -### 4. Multi-Device Enrollment -Capture images from different devices (phone, webcam, tablet) for device-agnostic verification. - -## Technical Details - -### Fusion Algorithm -```python -def fuse_embeddings(embeddings, quality_scores): - # 1. Normalize quality scores to weights - weights = quality_scores / sum(quality_scores) - - # 2. Compute weighted average - fused = sum(w * emb for w, emb in zip(weights, embeddings)) - - # 3. L2 normalize - fused = fused / ||fused|| - - return fused -``` - -### Quality Weighting -Higher quality images receive proportionally higher weights: -- Quality 90 → Weight 0.45 (in 2-image scenario) -- Quality 60 → Weight 0.30 -- Quality 30 → Weight 0.15 - -### Error Handling -- `InvalidImageCountError`: Wrong number of images -- `FaceNotDetectedError`: No face in image -- `PoorImageQualityError`: Quality below threshold -- `FusionError`: Embedding fusion failed - -## Testing - -### Unit Tests -```bash -pytest tests/unit/domain/services/test_embedding_fusion_service.py -pytest tests/unit/application/use_cases/test_enroll_multi_image.py -``` - -### Integration Tests -```bash -pytest tests/integration/test_multi_image_enrollment.py -``` - -### Manual Testing -```bash -# Enroll with 3 images -curl -X POST "http://localhost:8001/api/v1/enroll/multi" \ - -F "user_id=test_user" \ - -F "files=@photo1.jpg" \ - -F "files=@photo2.jpg" \ - -F "files=@photo3.jpg" - -# Verify (works with standard verify endpoint) -curl -X POST "http://localhost:8001/api/v1/verify" \ - -F "user_id=test_user" \ - -F "file=@verify_photo.jpg" -``` - -## Performance - -- **Processing Time**: ~500ms per image + 50ms fusion (typical) -- **Memory**: Minimal overhead (stores single fused embedding) -- **Accuracy Improvement**: 30-40% reduction in false rejections - -## Backward Compatibility - -The multi-image enrollment system is fully backward compatible: - -1. **Single-image enrollment** continues to work via `/api/v1/enroll` -2. **Verification** works with both single and multi-image enrolled users -3. **Existing embeddings** are not affected -4. **Can migrate** from single to multi-image by re-enrolling - -## Future Enhancements - -### Planned -- [ ] Session-based enrollment (upload images over time) -- [ ] Automatic quality feedback to user -- [ ] Support for video-based enrollment (extract frames) -- [ ] Advanced fusion strategies (attention-weighted, learned fusion) - -### Research -- [ ] Deep learning-based fusion -- [ ] Temporal fusion for video -- [ ] Cross-pose normalization - -## References - -- **Clean Architecture**: Robert C. Martin -- **Template Fusion**: ISO/IEC 24745:2011 Biometric Template Protection -- **Quality Assessment**: ISO/IEC 29794 Biometric Sample Quality - -## Support - -For issues or questions: -- GitHub Issues: https://github.com/Rollingcat-Software/biometric-processor/issues -- Documentation: See `/docs` directory - ---- - -**Version**: 1.0.0 -**Date**: 2025-12-25 -**Status**: Production Ready ✅ diff --git a/docs/archive/2026-04-16/UNIFIED_DEMO_CENTER.md b/docs/archive/2026-04-16/UNIFIED_DEMO_CENTER.md deleted file mode 100644 index 415914a..0000000 --- a/docs/archive/2026-04-16/UNIFIED_DEMO_CENTER.md +++ /dev/null @@ -1,591 +0,0 @@ -# Unified Demo Center - -The Unified Demo Center is a comprehensive, all-in-one demonstration page that showcases all biometric analysis features in a single, powerful interface. It provides maximum flexibility with multiple input methods and visual result rendering for each analysis mode. - -## Overview - -**Location**: `/unified-demo` -**Navigation**: Main menu → "Demo" (⭐ Sparkles icon) - -The Unified Demo Center combines all biometric capabilities into one modular interface, allowing users to: -- Select any analysis type from a dropdown -- Choose their preferred input method (upload, batch, camera, live stream) -- Get beautiful, mode-specific visual results -- Process single images, multiple images, or continuous video streams - -## Key Features - -### 🎯 9 Analysis Modes - -Select from the complete suite of biometric analysis capabilities: - -1. **Face Detection** 👤 - - Detect and locate faces in images - - Returns bounding box and landmarks - - Fast processing (~10-30ms) - -2. **Quality Analysis** ⭐ - - Assess image quality metrics - - Checks blur, brightness, sharpness, centering - - Provides actionable recommendations - -3. **Demographics** 📊 - - Estimate age, gender, and emotion - - Visual breakdown of all metrics - - Emotion confidence scores - -4. **Liveness Detection** 🔒 - - Detect if face is real person or spoof - - Passive liveness checks - - Security-critical validation - -5. **Enrollment Ready** ✅ - - Combined quality + liveness check - - Real-time feedback for enrollment - - Clear pass/fail indicators - -6. **Face Verification (1:1)** 🔑 - - Verify identity against enrolled user - - Similarity scoring - - Match/no-match determination - -7. **Face Search (1:N)** 🔍 - - Search for face in database - - Returns best match with confidence - - Scalable to thousands of users - -8. **Facial Landmarks** 📍 - - Detect 468 facial landmark points - - High precision tracking - - Useful for facial analysis - -9. **Full Analysis** 🎯 - - Run all analyses at once - - Comprehensive biometric report - - Best for thorough evaluation - -### 📥 4 Input Methods - -Choose the input method that best suits your workflow: - -#### 1. **Single Upload** 📄 -- Upload a single image from your device -- Supports all common image formats (JPG, PNG, etc.) -- Best for: Quick one-off analysis - -**How to use:** -1. Select "Upload" tab -2. Click or drag to upload an image -3. Click "Analyze" button -4. View results instantly - -#### 2. **Batch Upload** 📚 -- Upload multiple images at once -- Process them sequentially -- See individual results for each image - -**How to use:** -1. Select "Batch" tab -2. Choose multiple files (Ctrl/Cmd + click) -3. Review selected file list -4. Click "Process Batch" -5. Watch as each image is processed -6. View success/failure status for each - -**Features:** -- Progress tracking -- Individual result cards -- Success/error indicators -- Detailed statistics - -#### 3. **Camera Capture** 📷 -- Take a photo directly from your webcam -- Real-time preview -- One-time capture - -**How to use:** -1. Select "Camera" tab -2. Grant camera permissions if prompted -3. Position yourself in the frame -4. Click "Capture" to take photo -5. Review the captured image -6. Click "Analyze" to process - -#### 4. **Live Stream** 🎥 -- Continuous real-time analysis -- WebSocket-based streaming -- Instant feedback - -**How to use:** -1. Select "Live" tab -2. Grant camera permissions -3. Click "Start" to begin streaming -4. See real-time results updating -5. Click "Stop" to end stream - -**Features:** -- Real-time frame-by-frame analysis -- Processing time displayed -- Frame number tracking -- Automatic result updates - -## Visual Result Rendering - -The Unified Demo Center includes intelligent result rendering that adapts to each analysis mode, providing beautiful, easy-to-understand visualizations. - -### Result Renderer Features - -**Mode-Specific Layouts:** -- Each analysis type has a custom-designed display -- Color-coded status indicators (green = good, red = bad, yellow = warning) -- Progress bars for metrics -- Clear pass/fail indicators - -**Common Elements:** -- **Live Mode Indicator**: Shows frame number and processing time -- **Status Badges**: Visual success/error indicators -- **Metric Cards**: Gradient backgrounds with prominent values -- **Progress Bars**: Visual representation of scores -- **Recommendations**: Actionable guidance when available - -### Result Examples - -#### Face Detection -``` -✓ Face Detected -Confidence: 98.5% - -Position: (124, 256) -Size: 480 × 640 -``` - -#### Quality Analysis -``` -★ 87% -Good Quality - -Metrics: -Brightness: ████████░░ 82% -Sharpness: █████████░ 91% -Centering: ████████░░ 85% - -💡 Image quality is good for enrollment -``` - -#### Demographics -``` -Age: 32 years -Gender: Male (95.2%) -Emotion: Happy - -All Emotions: -Happy: ████████░░ 78% -Neutral: ███░░░░░░░ 15% -Sad: █░░░░░░░░░ 7% -``` - -#### Liveness Detection -``` -✓ Live Person -Confidence: 96.3% -Method: passive - -Liveness Checks: -✓ Texture -✓ Depth -``` - -## Use Cases - -### 1. Feature Demonstration -**Scenario**: Showing all capabilities to stakeholders - -**Workflow:** -1. Select "Live Stream" input mode -2. Cycle through different analysis modes -3. Show real-time processing -4. Demonstrate accuracy and speed - -**Benefits:** -- Interactive demonstration -- Live feedback -- Impressive visual experience -- Shows all features in context - -### 2. Quality Testing -**Scenario**: Testing image quality thresholds - -**Workflow:** -1. Select "Quality Analysis" mode -2. Use "Batch Upload" for multiple test images -3. Review quality scores and recommendations -4. Identify which images pass/fail - -**Benefits:** -- Batch processing saves time -- Easy comparison across images -- Clear pass/fail criteria -- Actionable recommendations - -### 3. Database Enrollment -**Scenario**: Checking if image is ready for enrollment - -**Workflow:** -1. Select "Enrollment Ready" mode -2. Use "Camera Capture" or "Live Stream" -3. Get real-time feedback on quality and liveness -4. Capture when all checks pass - -**Benefits:** -- Immediate feedback -- Prevents bad enrollments -- Guides user to optimal image -- Improves enrollment success rate - -### 4. Identity Verification -**Scenario**: Verifying someone's identity - -**Workflow:** -1. Select "Face Verification (1:1)" mode -2. Enter user ID to verify against -3. Use "Live Stream" for continuous verification -4. Get instant match/no-match feedback - -**Benefits:** -- Real-time verification -- Clear visual indicators -- Similarity scores -- Audit trail with frame numbers - -### 5. Face Search -**Scenario**: Identifying unknown person from database - -**Workflow:** -1. Select "Face Search (1:N)" mode -2. Upload image or use camera -3. Click "Analyze" -4. Get best match result with confidence - -**Benefits:** -- Fast database search -- Confidence scoring -- Clear found/not-found status -- User ID of best match - -## Configuration Panel - -The configuration panel at the top provides quick access to all settings: - -### Analysis Type Selector -``` -┌─────────────────────────────────────┐ -│ Analysis Type │ -│ ┌─────────────────────────────────┐ │ -│ │ ⭐ Quality Analysis ▼ │ │ -│ └─────────────────────────────────┘ │ -└─────────────────────────────────────┘ -``` - -**Features:** -- Dropdown with all 9 analysis modes -- Icon and full description for each mode -- Displays detailed information about selected mode -- Persists across input mode changes - -### Mode Information Card -``` -💡 Quality Analysis -Assess image quality (blur, brightness, sharpness) -``` - -**Displays:** -- Mode icon -- Full mode name -- Description of what it does -- Updates when mode changes - -### Input Method Tabs -``` -┌───────┬───────┬────────┬──────┐ -│ Single│ Batch │ Camera │ Live │ -└───────┴───────┴────────┴──────┘ -``` - -**Features:** -- 4 tabs for different input methods -- Icons for visual clarity -- Responsive (hides text on mobile) -- Preserves selection when switching modes - -## Layout Structure - -### Desktop Layout (>1024px) -``` -┌────────────────────────────────────────────────────┐ -│ 🌟 Unified Demo Center │ -│ All biometric features in one place │ -├────────────────────────────────────────────────────┤ -│ │ -│ ⚙️ Analysis Configuration │ -│ │ -│ [Analysis Type Dropdown] │ -│ 💡 Mode information card │ -│ [Input Method Tabs: Single|Batch|Camera|Live] │ -│ │ -├─────────────────────────┬──────────────────────────┤ -│ │ │ -│ 📥 Input │ 📊 Results │ -│ │ │ -│ [Input controls │ [Beautiful visual │ -│ based on selected │ result rendering │ -│ input method] │ based on mode] │ -│ │ │ -│ [Analyze/Process │ [Live updates or │ -│ buttons] │ static results] │ -│ │ │ -└─────────────────────────┴──────────────────────────┘ -``` - -### Mobile Layout (<1024px) -``` -┌──────────────────────────┐ -│ 🌟 Unified Demo Center │ -├──────────────────────────┤ -│ ⚙️ Configuration │ -│ [Dropdown & Tabs] │ -├──────────────────────────┤ -│ 📥 Input │ -│ [Stacked vertically] │ -├──────────────────────────┤ -│ 📊 Results │ -│ [Full width] │ -└──────────────────────────┘ -``` - -## Performance Considerations - -### Recommended Settings by Mode - -Based on performance testing: - -| Mode | Single | Batch | Camera | Live FPS | -|------|--------|-------|--------|----------| -| Face Detection | ⚡ Fast | ✓ Good | ✓ Great | 15-30 | -| Quality | ⚡ Fast | ✓ Good | ✓ Great | 10-15 | -| Demographics | ⏱️ Slow | ⚠️ Slow | ✓ OK | 2-5 | -| Liveness | ⏱️ Moderate | ✓ OK | ✓ Good | 5-10 | -| Enrollment Ready | ⚡ Fast | ✓ Good | ✓ Great | 8-12 | -| Verification | ⚡ Fast | ✓ Good | ✓ Great | 8-15 | -| Search | ⏱️ Variable* | ⚠️ Slow* | ✓ OK | 2-5 | -| Landmarks | ⚡ Fast | ✓ Good | ✓ Great | 10-20 | -| Full Analysis | 🐌 Very Slow | ❌ Not Recommended | ⚠️ Slow | 1-2 | - -*Search performance depends on database size - -### Batch Processing Tips - -**For Large Batches:** -- Use faster modes (face detection, quality, landmarks) -- Process during off-peak hours -- Monitor progress in the UI -- Check for errors in failed images - -**For Slow Modes:** -- Limit batch size to 10-20 images -- Use single upload for important images -- Consider server resources - -### Live Streaming Tips - -**For Best Experience:** -- Use recommended FPS for each mode -- Ensure good lighting -- Stable internet connection -- Modern browser (Chrome, Firefox, Safari) - -**Troubleshooting:** -- If lag occurs, mode may be too slow for live streaming -- Try reducing resolution -- Check network connectivity -- Verify server performance - -## Technical Details - -### Architecture - -**Component Structure:** -``` -UnifiedDemoPage -├── Configuration Panel -│ ├── Analysis Mode Selector -│ ├── Mode Info Card -│ └── Input Method Tabs -├── Input Section (Card) -│ ├── ImageUploader (single) -│ ├── Batch File Input (batch) -│ ├── WebcamCapture (camera) -│ ├── LiveCameraStream (live) -│ └── Action Buttons -└── Results Section (Card) - └── AnalysisResultRenderer - ├── Mode-specific layouts - ├── Live mode wrapper - └── Batch result iterator -``` - -**Data Flow:** - -1. **Single/Camera Mode:** - ``` - User Input → API Call → JSON Response → AnalysisResultRenderer - ``` - -2. **Batch Mode:** - ``` - Multiple Files → Sequential API Calls → Array of Results → Individual Renderers - ``` - -3. **Live Mode:** - ``` - Camera → Frame Capture → WebSocket → Live Results → Real-time Renderer Updates - ``` - -### API Integration - -**Endpoints Used:** -- `/detect` - Face detection -- `/quality` - Quality analysis -- `/demographics` - Demographics -- `/liveness` - Liveness detection -- `/enrollment/ready` - Enrollment readiness -- `/verification` - Face verification -- `/search` - Face search -- `/landmarks` - Facial landmarks -- `/analyze/full` - Full analysis - -**WebSocket:** -- `/ws/live-analysis` - Live streaming endpoint - -### State Management - -**Component State:** -```typescript -- analysisMode: AnalysisMode -- inputMode: 'upload' | 'batch' | 'camera' | 'live' -- selectedImage: File | null -- batchImages: File[] -- capturedImage: Blob | null -- liveResult: LiveAnalysisResult | null -- singleResult: any -- batchResults: any[] -- isProcessing: boolean -``` - -## Best Practices - -### 1. Mode Selection -- **Start simple**: Begin with face detection or quality analysis -- **Match use case**: Choose mode based on what you need -- **Understand limitations**: Some modes are slower than others - -### 2. Input Method Selection -- **Testing**: Use single upload for quick tests -- **Production simulation**: Use live stream to test real-world scenarios -- **Batch processing**: Use batch upload for dataset evaluation -- **User onboarding**: Use camera capture for enrollment simulation - -### 3. Result Interpretation -- **Read recommendations**: Pay attention to quality/liveness feedback -- **Check confidence scores**: Higher is generally better -- **Understand thresholds**: Different modes have different acceptance criteria -- **Compare results**: Use batch mode to compare multiple images - -### 4. Performance Optimization -- **Choose appropriate FPS**: Don't use max FPS if mode is slow -- **Limit batch size**: Don't overwhelm with hundreds of images -- **Use fast modes first**: Validate with quality before running slow analyses -- **Monitor processing time**: Live results show processing time per frame - -## Troubleshooting - -### Common Issues - -**"No face detected" in all images:** -- Check image quality (too dark, too blurry) -- Ensure face is visible and not obscured -- Try quality analysis first to check image - -**Batch processing is slow:** -- Normal for certain modes (demographics, search, full) -- Reduce batch size -- Use faster modes when possible - -**Live stream is laggy:** -- Mode may be too slow for real-time (try lower FPS) -- Check network connection -- Verify server performance -- Try simpler analysis mode - -**Camera not working:** -- Grant browser camera permissions -- Check if other apps are using camera -- Try different browser -- Restart browser/computer - -**Results look wrong:** -- Verify image quality -- Check if face is clearly visible -- Try different lighting conditions -- Use quality analysis to diagnose - -## Future Enhancements - -Potential additions to the Unified Demo Center: - -1. **Result Export** - - Download results as JSON - - Export batch results as CSV - - Save images with overlays - -2. **Comparison Mode** - - Side-by-side result comparison - - Diff highlighting - - A/B testing support - -3. **History/Session Management** - - Save analysis sessions - - Recall previous results - - Track statistics over time - -4. **Advanced Configuration** - - Adjust quality thresholds - - Configure FPS for live mode - - Custom verification thresholds - -5. **Result Visualization** - - Overlay detections on images - - Draw bounding boxes and landmarks - - Heat maps for quality metrics - -## Conclusion - -The Unified Demo Center provides a powerful, flexible interface for demonstrating and testing all biometric analysis capabilities. Its modular architecture and multiple input methods make it suitable for: - -- **Sales demonstrations** - Show all features interactively -- **Development testing** - Quick validation during development -- **Quality assurance** - Batch testing with various images -- **User training** - Learn how different modes work -- **Performance evaluation** - Compare modes and measure speed - -By combining all features in one place, it eliminates the need to navigate between multiple pages while providing a comprehensive view of the platform's capabilities. - ---- - -**Quick Start:** -1. Navigate to `/unified-demo` -2. Select an analysis mode (try "Quality Analysis") -3. Choose an input method (try "Camera Capture") -4. Grant camera permissions -5. Capture a photo -6. Click "Analyze" -7. View your beautiful results! diff --git a/docs/archive/README.md b/docs/archive/README.md deleted file mode 100644 index 91b2568..0000000 --- a/docs/archive/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Documentation Archive - -Historical documentation preserved for git history. Content here is **not current** — refer to the top-level `README.md`, `CHANGELOG.md`, `CLAUDE.md`, and `docs/README.md` for authoritative docs. - -## 2026-04-16 - -Bulk cleanup: moved ad-hoc session summaries, dated test reports, overlapping performance critiques, dead deployment design docs, and prior `docs/archive/*` content into a single dated subfolder. Removed from the public documentation tree because their contents are superseded by current docs or reference decommissioned infrastructure (GCP Cloud Run, Railway). - -See `git log docs/archive/2026-04-16/` for authorship and timestamps. diff --git a/docs/design/BROWSER_BIOMETRIC_ARCHITECTURE.md b/docs/design/BROWSER_BIOMETRIC_ARCHITECTURE.md deleted file mode 100644 index db19b2d..0000000 --- a/docs/design/BROWSER_BIOMETRIC_ARCHITECTURE.md +++ /dev/null @@ -1,1600 +0,0 @@ -# Browser-Side Biometric Processing Architecture - -## Document Info - -| Field | Value | -|-------|-------| -| **Status** | Approved Design | -| **Author** | Architecture Team | -| **Date** | 2026-03-13 | -| **Version** | 1.0.0 | -| **Relates To** | ARCHITECTURE.md, TODO.md (BC1, BH1) | - ---- - -## Table of Contents - -1. [Executive Summary](#1-executive-summary) -2. [Problem Statement](#2-problem-statement) -3. [Architecture Overview](#3-architecture-overview) -4. [Layer Design](#4-layer-design) -5. [Domain Layer — Interfaces & Entities](#5-domain-layer--interfaces--entities) -6. [Infrastructure Layer — ML Adapters](#6-infrastructure-layer--ml-adapters) -7. [Application Layer — Use Cases](#7-application-layer--use-cases) -8. [Presentation Layer — React Hooks & Components](#8-presentation-layer--react-hooks--components) -9. [Hybrid Client-Server Flow](#9-hybrid-client-server-flow) -10. [WebAuthn Integration](#10-webauthn-integration) -11. [DI Container](#11-di-container) -12. [Security Architecture](#12-security-architecture) -13. [Testing Strategy](#13-testing-strategy) -14. [Directory Structure](#14-directory-structure) -15. [Migration Plan](#15-migration-plan) - ---- - -## 1. Executive Summary - -This document defines the architecture for moving biometric pre-processing -(face detection, quality assessment, passive liveness) from the Python backend -into the browser using MediaPipe WASM, ONNX Runtime Web, and OpenCV.js. - -The design mirrors the existing Hexagonal Architecture (Ports & Adapters) already -established in the Python backend, translated into idiomatic TypeScript with the -same SOLID principles, dependency inversion, and clean separation of concerns. - -### Goals - -- **Offload ~60% of compute** from biometric-processor to the browser -- **Instant feedback** for users (no network round-trip for quality/detection) -- **Fix critical stubs** (fingerprint/voice) via WebAuthn/FIDO2 passkeys -- **Zero breaking changes** to existing API contracts -- **Isomorphic domain model** — client entities mirror server entities - -### Non-Goals - -- Replacing server-side enrollment/verification (security-critical, stays server-side) -- Supporting browsers without WASM (Safari 14-, IE) -- Client-side embedding storage (vectors never persist in browser) - ---- - -## 2. Problem Statement - -### Current State - -``` -┌──────────┐ every frame ┌───────────────────┐ -│ Browser │ ──────────────→ │ biometric-processor │ -│ (demo-ui) │ HTTP/WS │ Python/FastAPI │ -│ │ ←────────────── │ Port 8001 │ -└──────────┘ JSON result └───────────────────┘ -``` - -**Issues:** -1. Every quality check, face detection, and liveness pre-check requires a server round-trip (~200-500ms) -2. WebSocket live analysis (`/api/v1/live-analysis`) sends raw frames to server — bandwidth-intensive -3. Fingerprint/voice stubs always return `success: false` (TODO.md BC1) -4. No client-side validation — poor images are uploaded, rejected, re-uploaded - -### Target State - -``` -┌──────────────────────────────────────────────┐ -│ Browser │ -│ ┌─────────────┐ ┌──────────┐ ┌─────────┐ │ -│ │ MediaPipe │ │ OpenCV │ │ WebAuthn│ │ -│ │ Face Detect │ │ Quality │ │ Passkey │ │ -│ └──────┬───────┘ └────┬─────┘ └────┬────┘ │ -│ │ │ │ │ -│ ┌────▼───────────────▼──────────────▼────┐ │ -│ │ Client Use Cases (TypeScript) │ │ -│ │ Quality Gate → Hybrid Enrollment │ │ -│ └────────────────┬────────────────────────┘ │ -└─────────────────────┼──────────────────────────┘ - │ only high-quality, - │ pre-validated images - ▼ - ┌───────────────────┐ - │ biometric-processor│ - │ (enrollment, │ - │ verification, │ - │ embedding store) │ - └───────────────────┘ -``` - ---- - -## 3. Architecture Overview - -### Hexagonal Architecture — Client Side - -The client-side architecture mirrors the server-side hexagonal pattern: - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Presentation Layer (React Hooks) │ -│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │ -│ │useClientDetection│ │useClientQuality │ │useHybrid │ │ -│ │ │ │ │ │ Enrollment │ │ -│ └────────┬─────────┘ └────────┬─────────┘ └──────┬───────┘ │ -└───────────┼──────────────────────┼───────────────────┼──────────┘ - │ │ │ - ▼ ▼ ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ Application Layer (Use Cases) │ -│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │ -│ │ClientDetectFace │ │ClientAssess │ │HybridEnroll │ │ -│ │ UseCase │ │ Quality UseCase │ │ UseCase │ │ -│ └────────┬─────────┘ └────────┬─────────┘ └──────┬───────┘ │ -└───────────┼──────────────────────┼───────────────────┼──────────┘ - │ │ │ - ▼ ▼ ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ Domain Layer (Interfaces + Entities) │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ -│ │IFaceDetector │ │IQualityAs- │ │ILivenessDetector │ │ -│ │ │ │ sessor │ │ │ │ -│ └──────────────┘ └──────────────┘ └──────────────────────┘ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ -│ │FaceDetection │ │Quality │ │LivenessResult │ │ -│ │ Result │ │ Assessment │ │ │ │ -│ └──────────────┘ └──────────────┘ └──────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ▲ ▲ ▲ - │ │ │ -┌───────────┴──────────────────────┴───────────────────┴──────────┐ -│ Infrastructure Layer (Adapters) │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ -│ │MediaPipe │ │OpenCV.js │ │MediaPipe Passive │ │ -│ │FaceDetector │ │QualityAs- │ │LivenessDetector │ │ -│ │ (WASM) │ │ sessor │ │ (WASM) │ │ -│ └──────────────┘ └──────────────┘ └──────────────────────┘ │ -│ ┌──────────────┐ ┌──────────────┐ │ -│ │ServerFace │ │ServerQuality │ ← Fallback adapters │ -│ │Detector │ │Assessor │ (delegate to backend) │ -│ │ (HTTP) │ │ (HTTP) │ │ -│ └──────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### SOLID Compliance - -| Principle | How It's Applied | -|-----------|-----------------| -| **SRP** | Each class has one responsibility: `MediaPipeFaceDetector` only detects faces, `OpenCVQualityAssessor` only assesses quality | -| **OCP** | New ML backends (e.g., TFLite, WebNN) are added by creating new adapters — no existing code changes | -| **LSP** | All `IFaceDetector` implementations are interchangeable — `MediaPipeFaceDetector` and `ServerFaceDetector` both satisfy the same contract | -| **ISP** | Small, focused interfaces: `IFaceDetector`, `IQualityAssessor`, `ILivenessDetector` — no "god interface" | -| **DIP** | Use cases depend on `IFaceDetector` (abstraction), never on `MediaPipeFaceDetector` (concrete) | - ---- - -## 4. Layer Design - -### Dependency Rule - -``` -Presentation → Application → Domain ← Infrastructure -``` - -- **Domain Layer**: Zero dependencies — pure TypeScript types and interfaces -- **Application Layer**: Depends only on Domain interfaces -- **Infrastructure Layer**: Implements Domain interfaces using specific libraries -- **Presentation Layer**: Depends on Application layer (use cases) - -### Cross-Cutting Concerns - -| Concern | Implementation | -|---------|---------------| -| **Logging** | `ILogger` interface with `ConsoleLogger` / `SentryLogger` adapters | -| **Error Handling** | Domain-specific error hierarchy (mirrors Python `face_errors.py`) | -| **Configuration** | `BiometricConfig` value object with runtime validation | -| **Model Loading** | `IModelLoader` interface with lazy initialization and caching | - ---- - -## 5. Domain Layer — Interfaces & Entities - -### 5.1 Interfaces (Ports) - -These TypeScript interfaces mirror the Python `Protocol` classes 1:1. - -#### IFaceDetector - -```typescript -// demo-ui/src/lib/biometric/domain/interfaces/face-detector.ts - -/** - * Port for face detection implementations. - * - * Mirrors: app/domain/interfaces/face_detector.py → IFaceDetector - * - * Implementations can use different algorithms (MediaPipe, TFLite, Server API) - * without changing client code (Open/Closed Principle). - */ -export interface IFaceDetector { - /** - * Detect faces in an image. - * - * @param image - Image data (ImageData, HTMLCanvasElement, or HTMLVideoElement) - * @returns Face detection result - * @throws FaceNotDetectedError when no face is found - */ - detect(image: DetectorInput): Promise; - - /** Release resources (WASM memory, GPU textures) */ - dispose(): Promise; -} -``` - -#### IQualityAssessor - -```typescript -// demo-ui/src/lib/biometric/domain/interfaces/quality-assessor.ts - -/** - * Port for image quality assessment. - * - * Mirrors: app/domain/interfaces/quality_assessor.py → IQualityAssessor - */ -export interface IQualityAssessor { - /** - * Assess the quality of a face image. - * - * @param faceImage - Cropped face region as ImageData - * @returns Quality assessment with metrics and overall score - */ - assess(faceImage: ImageData): Promise; - - /** Get minimum acceptable quality score (0-100) */ - getMinimumAcceptableScore(): number; -} -``` - -#### ILivenessDetector - -```typescript -// demo-ui/src/lib/biometric/domain/interfaces/liveness-detector.ts - -/** - * Port for passive liveness detection. - * - * Mirrors: app/domain/interfaces/liveness_detector.py → ILivenessDetector - */ -export interface ILivenessDetector { - /** - * Check if image shows a live person (passive check). - * - * @param image - Input image - * @returns Liveness result with score and challenge info - */ - checkLiveness(image: DetectorInput): Promise; - - /** Get the type of liveness challenge used */ - getChallengeType(): string; - - /** Get the threshold for considering result as live (0-100) */ - getLivenessThreshold(): number; -} -``` - -#### IModelLoader - -```typescript -// demo-ui/src/lib/biometric/domain/interfaces/model-loader.ts - -/** - * Port for ML model lifecycle management. - * - * Handles lazy loading, caching, and disposal of WASM/ONNX models. - * Single Responsibility: only manages model loading, not inference. - */ -export interface IModelLoader { - /** Load model (lazy — only loads on first call, cached after) */ - load(): Promise; - - /** Check if model is currently loaded */ - isLoaded(): boolean; - - /** Release model from memory */ - unload(): Promise; -} -``` - -#### IBiometricApiClient - -```typescript -// demo-ui/src/lib/biometric/domain/interfaces/biometric-api-client.ts - -/** - * Port for server-side biometric API communication. - * - * Abstracts the HTTP client so use cases don't depend on fetch/axios. - * Enables testing with mock API responses. - */ -export interface IBiometricApiClient { - /** Enroll a face with pre-validated image */ - enrollFace(params: EnrollFaceRequest): Promise; - - /** Verify a face against stored embedding */ - verifyFace(params: VerifyFaceRequest): Promise; - - /** Search for matching faces */ - searchFace(params: SearchFaceRequest): Promise; - - /** Server-side liveness check (for high-security flows) */ - checkLiveness(image: Blob): Promise; - - /** Server-side quality check (fallback) */ - assessQuality(image: Blob): Promise; -} -``` - -### 5.2 Entities (Value Objects) - -Immutable, validated value objects that mirror the Python dataclasses. - -#### FaceDetectionResult - -```typescript -// demo-ui/src/lib/biometric/domain/entities/face-detection-result.ts - -/** - * Immutable value object for face detection output. - * - * Mirrors: app/domain/entities/face_detection.py → FaceDetectionResult - * - * Invariants enforced at construction: - * - confidence ∈ [0, 1] - * - boundingBox dimensions > 0 when found = true - * - boundingBox required when found = true - */ -export class FaceDetectionResult { - readonly found: boolean; - readonly boundingBox: BoundingBox | null; - readonly landmarks: LandmarkPoint[] | null; - readonly confidence: number; - - private constructor(params: FaceDetectionParams) { /* validate + assign */ } - - static create(params: FaceDetectionParams): FaceDetectionResult { /* factory */ } - - getFaceRegion(canvas: HTMLCanvasElement): ImageData | null { /* crop */ } - getFaceCenter(): Point | null { /* compute center */ } -} -``` - -#### QualityAssessment - -```typescript -// demo-ui/src/lib/biometric/domain/entities/quality-assessment.ts - -/** - * Immutable value object for quality assessment output. - * - * Mirrors: app/domain/entities/quality_assessment.py → QualityAssessment - * - * Invariants: - * - score ∈ [0, 100] - * - blurScore ≥ 0 - * - faceSize ≥ 0 - */ -export class QualityAssessment { - readonly score: number; - readonly blurScore: number; - readonly lightingScore: number; - readonly faceSize: number; - readonly isAcceptable: boolean; - - private constructor(params: QualityParams) { /* validate + assign */ } - - static create(params: QualityParams): QualityAssessment { /* factory */ } - - getQualityLevel(): 'poor' | 'fair' | 'good' { /* thresholds */ } - isBlurry(threshold?: number): boolean { /* blur check */ } - isTooSmall(minSize?: number): boolean { /* size check */ } - getIssues(): QualityIssue[] { /* collect all issues */ } -} -``` - -#### LivenessResult - -```typescript -// demo-ui/src/lib/biometric/domain/entities/liveness-result.ts - -/** - * Immutable value object for liveness detection output. - * - * Mirrors: app/domain/entities/liveness_result.py → LivenessResult - * - * Invariants: - * - livenessScore ∈ [0, 100] - * - challenge is non-empty - */ -export class LivenessResult { - readonly isLive: boolean; - readonly livenessScore: number; - readonly challenge: string; - readonly challengeCompleted: boolean; - - private constructor(params: LivenessParams) { /* validate + assign */ } - - static create(params: LivenessParams): LivenessResult { /* factory */ } - - getConfidenceLevel(): 'low' | 'medium' | 'high' { /* thresholds */ } - isSpoofSuspected(threshold?: number): boolean { /* check */ } - requiresAdditionalVerification(threshold?: number): boolean { /* check */ } -} -``` - -### 5.3 Domain Errors - -```typescript -// demo-ui/src/lib/biometric/domain/errors.ts - -/** - * Domain error hierarchy. - * - * Mirrors: app/domain/exceptions/face_errors.py - */ -export abstract class BiometricError extends Error { - abstract readonly code: string; -} - -export class FaceNotDetectedError extends BiometricError { - readonly code = 'FACE_NOT_DETECTED'; -} - -export class MultipleFacesError extends BiometricError { - readonly code = 'MULTIPLE_FACES'; - constructor(readonly count: number) { super(`Expected 1 face, found ${count}`); } -} - -export class PoorImageQualityError extends BiometricError { - readonly code = 'POOR_IMAGE_QUALITY'; - constructor( - readonly qualityScore: number, - readonly minThreshold: number, - readonly issues: QualityIssue[], - ) { super(`Quality ${qualityScore} below threshold ${minThreshold}`); } -} - -export class LivenessCheckError extends BiometricError { - readonly code = 'LIVENESS_CHECK_FAILED'; -} - -export class ModelNotLoadedError extends BiometricError { - readonly code = 'MODEL_NOT_LOADED'; -} - -export class ModelLoadError extends BiometricError { - readonly code = 'MODEL_LOAD_FAILED'; - constructor(readonly modelName: string, cause?: Error) { - super(`Failed to load model: ${modelName}`); - } -} -``` - -### 5.4 Shared Types - -```typescript -// demo-ui/src/lib/biometric/domain/types.ts - -/** Input accepted by detector implementations */ -export type DetectorInput = ImageData | HTMLCanvasElement | HTMLVideoElement; - -/** Bounding box in pixel coordinates */ -export interface BoundingBox { - readonly x: number; - readonly y: number; - readonly width: number; - readonly height: number; -} - -/** 2D/3D landmark point */ -export interface LandmarkPoint { - readonly x: number; - readonly y: number; - readonly z?: number; - readonly label?: string; -} - -/** 2D point */ -export interface Point { - readonly x: number; - readonly y: number; -} - -/** Quality issue descriptor */ -export interface QualityIssue { - readonly type: 'blur' | 'lighting' | 'face_size' | 'occlusion' | 'pose'; - readonly description: string; - readonly score: number; - readonly threshold: number; -} - -/** Biometric processing configuration */ -export interface BiometricConfig { - readonly qualityThreshold: number; // 0-100, default 50 - readonly blurThreshold: number; // Laplacian variance, default 100 - readonly minFaceSize: number; // pixels, default 80 - readonly livenessThreshold: number; // 0-100, default 50 - readonly detectionConfidence: number; // 0-1, default 0.7 - readonly maxDetectionTimeMs: number; // timeout, default 500 - readonly enableClientLiveness: boolean; // default true - readonly enableClientQuality: boolean; // default true - readonly serverFallbackEnabled: boolean; // default true -} - -/** Default configuration values */ -export const DEFAULT_BIOMETRIC_CONFIG: BiometricConfig = { - qualityThreshold: 50, - blurThreshold: 100, - minFaceSize: 80, - livenessThreshold: 50, - detectionConfidence: 0.7, - maxDetectionTimeMs: 500, - enableClientLiveness: true, - enableClientQuality: true, - serverFallbackEnabled: true, -} as const; -``` - ---- - -## 6. Infrastructure Layer — ML Adapters - -### 6.1 MediaPipe Face Detector (WASM) - -```typescript -// demo-ui/src/lib/biometric/infrastructure/ml/mediapipe-face-detector.ts - -/** - * MediaPipe Face Detection adapter. - * - * Implements IFaceDetector using MediaPipe Face Detection WASM. - * - 468 landmarks at 30+ FPS - * - Runs entirely in browser (no server round-trip) - * - Lazy model loading with IModelLoader - * - * Liskov Substitution: Can replace ServerFaceDetector transparently. - */ -export class MediaPipeFaceDetector implements IFaceDetector { - constructor( - private readonly modelLoader: IModelLoader, - private readonly config: Pick, - ) {} - - async detect(image: DetectorInput): Promise { /* ... */ } - async dispose(): Promise { /* ... */ } -} -``` - -### 6.2 OpenCV.js Quality Assessor - -```typescript -// demo-ui/src/lib/biometric/infrastructure/ml/opencv-quality-assessor.ts - -/** - * OpenCV.js quality assessment adapter. - * - * Implements IQualityAssessor using OpenCV.js for: - * - Blur detection (Laplacian variance) - * - Lighting assessment (mean brightness, histogram analysis) - * - Face size validation - * - * Mirrors: app/infrastructure/ml/quality/quality_assessor.py - */ -export class OpenCVQualityAssessor implements IQualityAssessor { - constructor( - private readonly modelLoader: IModelLoader, - private readonly config: Pick, - ) {} - - async assess(faceImage: ImageData): Promise { /* ... */ } - getMinimumAcceptableScore(): number { return this.config.qualityThreshold; } -} -``` - -### 6.3 MediaPipe Passive Liveness Detector - -```typescript -// demo-ui/src/lib/biometric/infrastructure/ml/mediapipe-liveness-detector.ts - -/** - * Passive liveness detection using MediaPipe Face Mesh. - * - * Implements ILivenessDetector for client-side passive checks: - * - Depth estimation from 3D landmarks (z-coordinates) - * - Texture frequency analysis (moire pattern detection) - * - Edge sharpness analysis (print attack detection) - * - * Note: This is a PRE-CHECK only. High-security flows still - * require server-side liveness verification. - */ -export class MediaPipeLivenessDetector implements ILivenessDetector { - constructor( - private readonly modelLoader: IModelLoader, - private readonly config: Pick, - ) {} - - async checkLiveness(image: DetectorInput): Promise { /* ... */ } - getChallengeType(): string { return 'passive_depth'; } - getLivenessThreshold(): number { return this.config.livenessThreshold; } -} -``` - -### 6.4 Server Fallback Adapters - -```typescript -// demo-ui/src/lib/biometric/infrastructure/server/server-face-detector.ts - -/** - * Server-side face detection fallback adapter. - * - * Implements IFaceDetector by delegating to the backend API. - * Used when: - * - WASM is not supported - * - Client-side detection fails - * - Higher accuracy is required - * - * Open/Closed Principle: Added without changing existing code. - */ -export class ServerFaceDetector implements IFaceDetector { - constructor(private readonly apiClient: IBiometricApiClient) {} - - async detect(image: DetectorInput): Promise { /* ... */ } - async dispose(): Promise { /* no-op */ } -} -``` - -```typescript -// demo-ui/src/lib/biometric/infrastructure/server/server-quality-assessor.ts - -/** - * Server-side quality assessment fallback adapter. - */ -export class ServerQualityAssessor implements IQualityAssessor { - constructor(private readonly apiClient: IBiometricApiClient) {} - - async assess(faceImage: ImageData): Promise { /* ... */ } - getMinimumAcceptableScore(): number { return 50; } -} -``` - -### 6.5 Model Loaders - -```typescript -// demo-ui/src/lib/biometric/infrastructure/ml/loaders/mediapipe-model-loader.ts - -/** - * Lazy-loading model manager for MediaPipe WASM models. - * - * Implements IModelLoader with: - * - Lazy initialization (load on first use) - * - Singleton caching (load once, reuse) - * - Graceful error handling with ModelLoadError - * - Resource cleanup on dispose - * - * Single Responsibility: Only manages model lifecycle, not inference. - */ -export class MediaPipeModelLoader implements IModelLoader { - private model: TModel | null = null; - private loadPromise: Promise | null = null; - - constructor( - private readonly factory: () => Promise, - private readonly modelName: string, - ) {} - - async load(): Promise { - if (this.model) return this.model; - if (this.loadPromise) return this.loadPromise; - - this.loadPromise = this.factory() - .then(model => { this.model = model; return model; }) - .catch(err => { - this.loadPromise = null; - throw new ModelLoadError(this.modelName, err); - }); - - return this.loadPromise; - } - - isLoaded(): boolean { return this.model !== null; } - - async unload(): Promise { - if (this.model && typeof (this.model as any).close === 'function') { - (this.model as any).close(); - } - this.model = null; - this.loadPromise = null; - } -} -``` - -### 6.6 Resilient Adapter (Decorator Pattern) - -```typescript -// demo-ui/src/lib/biometric/infrastructure/resilience/fallback-face-detector.ts - -/** - * Resilient face detector with automatic fallback. - * - * Decorator Pattern: Wraps a primary IFaceDetector and falls back - * to a secondary implementation on failure. - * - * Example: MediaPipeFaceDetector (primary) → ServerFaceDetector (fallback) - * - * Open/Closed: Adds resilience without modifying existing detectors. - */ -export class FallbackFaceDetector implements IFaceDetector { - constructor( - private readonly primary: IFaceDetector, - private readonly fallback: IFaceDetector, - private readonly logger: ILogger, - ) {} - - async detect(image: DetectorInput): Promise { - try { - return await this.primary.detect(image); - } catch (error) { - this.logger.warn('Primary detector failed, using fallback', { error }); - return this.fallback.detect(image); - } - } - - async dispose(): Promise { - await Promise.all([this.primary.dispose(), this.fallback.dispose()]); - } -} -``` - ---- - -## 7. Application Layer — Use Cases - -### 7.1 ClientDetectFaceUseCase - -```typescript -// demo-ui/src/lib/biometric/application/use-cases/client-detect-face.ts - -/** - * Client-side face detection use case. - * - * Mirrors: app/application/use_cases/detect_multi_face.py (single face variant) - * - * Orchestrates: - * 1. Detect face via IFaceDetector - * 2. Validate single face found - * 3. Return structured result - * - * SRP: Only orchestrates detection, no quality or liveness logic. - * DIP: Depends on IFaceDetector interface, not MediaPipe directly. - */ -export class ClientDetectFaceUseCase { - constructor(private readonly detector: IFaceDetector) {} - - async execute(image: DetectorInput): Promise { - const result = await this.detector.detect(image); - - if (!result.found) { - throw new FaceNotDetectedError(); - } - - return result; - } -} -``` - -### 7.2 ClientAssessQualityUseCase - -```typescript -// demo-ui/src/lib/biometric/application/use-cases/client-assess-quality.ts - -/** - * Client-side quality assessment use case. - * - * Mirrors: app/application/use_cases/analyze_quality.py - * - * Orchestrates: - * 1. Detect face (to crop face region) - * 2. Assess quality on cropped region - * 3. Return quality assessment with issues - * - * SRP: Only orchestrates quality assessment pipeline. - */ -export class ClientAssessQualityUseCase { - constructor( - private readonly detector: IFaceDetector, - private readonly qualityAssessor: IQualityAssessor, - ) {} - - async execute(image: DetectorInput): Promise { - // Step 1: Detect face - const detection = await this.detector.detect(image); - if (!detection.found) throw new FaceNotDetectedError(); - - // Step 2: Crop face region - const faceImage = detection.getFaceRegion(/* canvas */); - if (!faceImage) throw new FaceNotDetectedError(); - - // Step 3: Assess quality - const quality = await this.qualityAssessor.assess(faceImage); - - return { detection, quality }; - } -} -``` - -### 7.3 HybridEnrollFaceUseCase - -```typescript -// demo-ui/src/lib/biometric/application/use-cases/hybrid-enroll-face.ts - -/** - * Hybrid enrollment use case — client pre-processing + server enrollment. - * - * This is the key use case that combines client and server capabilities: - * - * Client side: - * 1. Detect face (MediaPipe WASM) - * 2. Assess quality (OpenCV.js) - * 3. Passive liveness check (MediaPipe) - * 4. Reject poor images BEFORE upload - * - * Server side: - * 5. Extract embedding (DeepFace — requires GPU/CPU-intensive models) - * 6. Store in pgvector database - * 7. Return enrollment result - * - * This pattern ensures: - * - Instant feedback for quality/detection issues - * - Reduced server load (only good images reach the backend) - * - Security-critical operations stay server-side - * - * SRP: Orchestrates the full hybrid enrollment pipeline. - * DIP: All dependencies are interfaces. - */ -export class HybridEnrollFaceUseCase { - constructor( - private readonly detector: IFaceDetector, - private readonly qualityAssessor: IQualityAssessor, - private readonly livenessDetector: ILivenessDetector, - private readonly apiClient: IBiometricApiClient, - ) {} - - async execute(params: HybridEnrollParams): Promise { - // Phase 1 — Client-side pre-processing (instant feedback) - const detection = await this.detector.detect(params.image); - if (!detection.found) throw new FaceNotDetectedError(); - - const faceImage = detection.getFaceRegion(/* canvas */); - if (!faceImage) throw new FaceNotDetectedError(); - - const quality = await this.qualityAssessor.assess(faceImage); - if (!quality.isAcceptable) { - throw new PoorImageQualityError( - quality.score, - this.qualityAssessor.getMinimumAcceptableScore(), - quality.getIssues(), - ); - } - - const liveness = await this.livenessDetector.checkLiveness(params.image); - if (liveness.isSpoofSuspected()) { - throw new LivenessCheckError(); - } - - // Phase 2 — Server-side enrollment (security-critical) - const imageBlob = await canvasToBlob(params.image); - - const serverResult = await this.apiClient.enrollFace({ - personId: params.personId, - image: imageBlob, - clientQualityScore: quality.score, - clientLivenessScore: liveness.livenessScore, - metadata: params.metadata, - }); - - return { - ...serverResult, - clientSide: { detection, quality, liveness }, - }; - } -} -``` - -### 7.4 HybridVerifyFaceUseCase - -```typescript -// demo-ui/src/lib/biometric/application/use-cases/hybrid-verify-face.ts - -/** - * Hybrid verification use case — client pre-check + server verification. - * - * Client: detect + quality gate (reject early) - * Server: embedding extraction + similarity computation - */ -export class HybridVerifyFaceUseCase { - constructor( - private readonly detector: IFaceDetector, - private readonly qualityAssessor: IQualityAssessor, - private readonly apiClient: IBiometricApiClient, - ) {} - - async execute(params: HybridVerifyParams): Promise { /* ... */ } -} -``` - -### 7.5 RealTimeAnalysisUseCase - -```typescript -// demo-ui/src/lib/biometric/application/use-cases/realtime-analysis.ts - -/** - * Real-time video frame analysis use case. - * - * Replaces WebSocket-based live analysis with client-side processing. - * Runs detection + quality on each frame at target FPS. - * - * Used by: Live camera overlay, proctoring, enrollment guidance - */ -export class RealTimeAnalysisUseCase { - constructor( - private readonly detector: IFaceDetector, - private readonly qualityAssessor: IQualityAssessor, - private readonly config: Pick, - ) {} - - /** - * Analyze a single frame. Designed to be called from requestAnimationFrame. - * Returns null if processing exceeds time budget. - */ - async analyzeFrame(frame: DetectorInput): Promise { /* ... */ } -} -``` - ---- - -## 8. Presentation Layer — React Hooks & Components - -### 8.1 Hooks - -```typescript -// demo-ui/src/hooks/use-client-face-detection.ts - -/** - * React hook for client-side face detection. - * - * Wraps ClientDetectFaceUseCase with React state management. - * Uses the DI container to resolve the correct IFaceDetector. - */ -export function useClientFaceDetection() { - const useCase = useBiometricContainer().clientDetectFace; - // ... React Query mutation wrapping useCase.execute() -} -``` - -```typescript -// demo-ui/src/hooks/use-client-quality.ts - -/** - * React hook for client-side quality assessment. - * - * Provides real-time quality feedback with debouncing. - */ -export function useClientQuality() { /* ... */ } -``` - -```typescript -// demo-ui/src/hooks/use-hybrid-enrollment.ts - -/** - * React hook for hybrid enrollment. - * - * Orchestrates the full client + server enrollment flow - * with progress tracking and error handling. - */ -export function useHybridEnrollment() { /* ... */ } -``` - -```typescript -// demo-ui/src/hooks/use-realtime-analysis.ts - -/** - * React hook for real-time frame analysis. - * - * Replaces the existing use-live-camera-analysis.ts (WebSocket-based) - * with client-side processing using requestAnimationFrame. - */ -export function useRealtimeAnalysis( - videoRef: RefObject, - options?: { targetFps?: number; enabled?: boolean }, -) { /* ... */ } -``` - -### 8.2 Components - -```typescript -// demo-ui/src/components/biometric/quality-overlay.tsx - -/** - * Real-time quality feedback overlay. - * - * Renders on top of the camera feed: - * - Face bounding box (green/yellow/red based on quality) - * - Quality score badge - * - Issue descriptions ("Too blurry", "Face too small", etc.) - * - Liveness indicator - */ -export function QualityOverlay({ analysis }: { analysis: FrameAnalysis }) { /* ... */ } -``` - -```typescript -// demo-ui/src/components/biometric/guided-capture.tsx - -/** - * Guided face capture component. - * - * Combines camera feed + real-time analysis + capture trigger. - * Only enables the "Capture" button when quality is acceptable. - */ -export function GuidedCapture({ - onCapture, - minQuality, -}: GuidedCaptureProps) { /* ... */ } -``` - ---- - -## 9. Hybrid Client-Server Flow - -### 9.1 Enrollment Sequence - -``` -┌────────┐ ┌──────────┐ ┌────────────┐ ┌─────────────┐ -│ User │ │ React │ │ Use Case │ │ Backend │ -│ │ │ Hook │ │ (Client) │ │ API │ -└───┬────┘ └────┬─────┘ └─────┬──────┘ └──────┬──────┘ - │ │ │ │ - │ Click Capture │ │ │ - │───────────────→│ │ │ - │ │ execute(image) │ │ - │ │──────────────────→│ │ - │ │ │ │ - │ │ ┌──────────────┴─────────────┐ │ - │ │ │ 1. detect(image) │ │ - │ │ │ → MediaPipe WASM │ │ - │ │ │ 2. assess(faceRegion) │ │ - │ │ │ → OpenCV.js │ │ - │ │ │ 3. checkLiveness(image) │ │ - │ │ │ → MediaPipe passive │ │ - │ │ └──────────────┬─────────────┘ │ - │ │ │ │ - │ │ │ [quality OK] │ - │ │ │ enrollFace(blob) │ - │ │ │───────────────────→│ - │ │ │ │ - │ │ │ ┌───────────────┴────┐ - │ │ │ │ 4. detect(image) │ - │ │ │ │ 5. extract(face) │ - │ │ │ │ 6. save(embedding) │ - │ │ │ └───────────────┬────┘ - │ │ │ │ - │ │ │ EnrollResult │ - │ │ │←───────────────────│ - │ │ HybridResult │ │ - │ │←──────────────────│ │ - │ success + ID │ │ │ - │←───────────────│ │ │ -``` - -### 9.2 Real-Time Analysis (Replaces WebSocket) - -``` -┌───────────────────────────────────────────────────────────┐ -│ Browser — requestAnimationFrame loop │ -│ │ -│ Video Frame → MediaPipe Detect → OpenCV Quality │ -│ ↓ ↓ ↓ │ -│ 30 FPS < 15ms/frame < 5ms/frame │ -│ ↓ ↓ │ -│ FrameAnalysis { detection, quality } │ -│ ↓ │ -│ QualityOverlay (React render) │ -│ │ -│ NO NETWORK TRAFFIC for real-time analysis │ -└───────────────────────────────────────────────────────────┘ -``` - -### 9.3 Fallback Strategy - -``` - ┌─────────────────┐ - │ Feature Detect │ - │ (WASM Support?) │ - └────────┬────────┘ - │ - ┌──────────┴──────────┐ - │ │ - ┌────▼────┐ ┌─────▼─────┐ - │ YES │ │ NO │ - │ Client │ │ Server │ - │ Pipeline│ │ Fallback │ - └────┬────┘ └─────┬─────┘ - │ │ - ┌─────────▼──────────┐ ┌─────▼──────────┐ - │ MediaPipe Detect │ │ POST /detect │ - │ OpenCV Quality │ │ POST /quality │ - │ MediaPipe Liveness │ │ POST /liveness │ - └─────────┬──────────┘ └─────┬──────────┘ - │ │ - └──────────┬──────────┘ - │ - ┌────────▼────────┐ - │ Unified Result │ - │ (same types) │ - └─────────────────┘ -``` - ---- - -## 10. WebAuthn Integration - -### 10.1 Architecture - -WebAuthn replaces the always-failing fingerprint and voice stubs with -real device-native biometric authentication (Touch ID, Face ID, Windows Hello). - -``` -┌────────────────────────────────────────────────────────┐ -│ Browser │ -│ │ -│ ┌─────────────┐ ┌───────────────────────────────┐ │ -│ │ WebAuthn │ │ Platform Authenticator │ │ -│ │ Client │───→│ (Touch ID / Face ID / │ │ -│ │ (JS API) │ │ Windows Hello / PIN) │ │ -│ └──────┬──────┘ └───────────────────────────────┘ │ -│ │ │ -└─────────┼───────────────────────────────────────────────┘ - │ credential (public key + attestation) - ▼ -┌──────────────────────────────────────────────────────────┐ -│ Backend (biometric-processor) │ -│ │ -│ ┌────────────────────┐ ┌────────────────────────┐ │ -│ │ /api/v1/webauthn/ │ │ WebAuthn Credential │ │ -│ │ register │───→│ Repository │ │ -│ │ authenticate │ │ (PostgreSQL) │ │ -│ │ delete │ └────────────────────────┘ │ -│ └────────────────────┘ │ -│ │ -│ Replaces: fingerprint.py (stub) + voice.py (stub) │ -└──────────────────────────────────────────────────────────┘ -``` - -### 10.2 Domain Interface - -```typescript -// demo-ui/src/lib/biometric/domain/interfaces/device-authenticator.ts - -/** - * Port for device-native biometric authentication. - * - * Replaces fingerprint/voice stubs with WebAuthn/FIDO2. - * Abstracts the WebAuthn API so use cases don't depend on browser APIs. - */ -export interface IDeviceAuthenticator { - /** Check if device supports biometric authentication */ - isAvailable(): Promise; - - /** Register a new credential (enrollment equivalent) */ - register(params: DeviceRegisterParams): Promise; - - /** Authenticate with existing credential (verification equivalent) */ - authenticate(params: DeviceAuthParams): Promise; - - /** Remove a stored credential */ - removeCredential(credentialId: string): Promise; -} -``` - -### 10.3 Backend Route - -```python -# app/api/routes/webauthn.py - -""" -WebAuthn/FIDO2 endpoints. - -Replaces fingerprint and voice stubs with real device-native -biometric authentication. Maps to the same enrollment/verify/delete -contract that identity-core-api expects. - -Endpoints: - POST /api/v1/webauthn/register/begin → Generate challenge - POST /api/v1/webauthn/register/complete → Store credential - POST /api/v1/webauthn/authenticate/begin → Generate assertion - POST /api/v1/webauthn/authenticate/complete → Verify assertion - DELETE /api/v1/webauthn/{credential_id} → Remove credential -""" -``` - ---- - -## 11. DI Container - -### 11.1 Client-Side Container - -```typescript -// demo-ui/src/lib/biometric/container.ts - -/** - * Dependency injection container for browser biometric processing. - * - * Mirrors: app/core/container.py - * - * Wires together all interfaces and implementations: - * - Detects WASM capability and selects client vs. server adapters - * - Applies decorator pattern for fallback resilience - * - Provides singleton model loaders for memory efficiency - * - Exposes fully-configured use cases - * - * Usage: - * const container = createBiometricContainer(config); - * const result = await container.hybridEnroll.execute(params); - */ - -export interface BiometricContainer { - // Use cases (application layer) - readonly clientDetectFace: ClientDetectFaceUseCase; - readonly clientAssessQuality: ClientAssessQualityUseCase; - readonly hybridEnroll: HybridEnrollFaceUseCase; - readonly hybridVerify: HybridVerifyFaceUseCase; - readonly realtimeAnalysis: RealTimeAnalysisUseCase; - - // Lifecycle - dispose(): Promise; -} - -export function createBiometricContainer( - config: Partial = {}, - apiClient: IBiometricApiClient = createDefaultApiClient(), -): BiometricContainer { - const cfg = { ...DEFAULT_BIOMETRIC_CONFIG, ...config }; - - // Model loaders (singleton, lazy) - const mediapipeLoader = new MediaPipeModelLoader(/* ... */); - const opencvLoader = new OpenCVModelLoader(/* ... */); - - // Infrastructure — ML adapters - const clientDetector = new MediaPipeFaceDetector(mediapipeLoader, cfg); - const serverDetector = new ServerFaceDetector(apiClient); - const detector = new FallbackFaceDetector(clientDetector, serverDetector, logger); - - const clientQuality = new OpenCVQualityAssessor(opencvLoader, cfg); - const serverQuality = new ServerQualityAssessor(apiClient); - const quality = cfg.serverFallbackEnabled - ? new FallbackQualityAssessor(clientQuality, serverQuality, logger) - : clientQuality; - - const liveness = new MediaPipeLivenessDetector(mediapipeLoader, cfg); - - // Application — Use cases - return { - clientDetectFace: new ClientDetectFaceUseCase(detector), - clientAssessQuality: new ClientAssessQualityUseCase(detector, quality), - hybridEnroll: new HybridEnrollFaceUseCase(detector, quality, liveness, apiClient), - hybridVerify: new HybridVerifyFaceUseCase(detector, quality, apiClient), - realtimeAnalysis: new RealTimeAnalysisUseCase(detector, quality, cfg), - dispose: () => Promise.all([mediapipeLoader.unload(), opencvLoader.unload()]), - }; -} -``` - -### 11.2 React Context Provider - -```typescript -// demo-ui/src/lib/biometric/provider.tsx - -/** - * React context provider for the biometric DI container. - * - * Initializes the container once and provides it to all child components. - * Handles cleanup on unmount (model disposal). - */ -export function BiometricProvider({ - config, - children, -}: BiometricProviderProps) { - const container = useMemo(() => createBiometricContainer(config), [config]); - - useEffect(() => { - return () => { container.dispose(); }; - }, [container]); - - return ( - - {children} - - ); -} - -export function useBiometricContainer(): BiometricContainer { - const ctx = useContext(BiometricContext); - if (!ctx) throw new Error('useBiometricContainer must be used within BiometricProvider'); - return ctx; -} -``` - ---- - -## 12. Security Architecture - -### 12.1 Principles - -1. **Embeddings never leave the server** — client computes quality/detection only -2. **Server-side verification is authoritative** — client checks are UX optimization -3. **Client scores are advisory** — server re-validates quality before enrollment -4. **No client-side embedding extraction** — prevents template theft -5. **WebAuthn credentials are origin-bound** — cannot be phished - -### 12.2 Threat Model - -| Threat | Mitigation | -|--------|-----------| -| Spoofed client quality scores | Server re-validates all images independently | -| Injected pre-computed embeddings | Server extracts embeddings itself, never accepts client embeddings | -| WASM model tampering | Models loaded from same-origin CDN with SRI hashes | -| Camera feed interception | All processing in-memory, no disk writes | -| WebAuthn credential theft | Hardware-bound keys, non-exportable by spec | - -### 12.3 API Contract Extension - -The existing enrollment API gains optional client-side metadata: - -```python -# Server accepts but DOES NOT TRUST these values — they're for logging/analytics -class EnrollmentRequestExtended(BaseModel): - user_id: str - file: UploadFile - tenant_id: Optional[str] = None - # New — advisory fields from client - client_quality_score: Optional[float] = None # Client's quality assessment - client_liveness_score: Optional[float] = None # Client's liveness pre-check - client_detection_time_ms: Optional[int] = None # Client processing time -``` - ---- - -## 13. Testing Strategy - -### 13.1 Test Pyramid - -``` - ┌────────────┐ - │ E2E │ Playwright (3-5 tests) - │ Browser │ Full camera → enrollment flow - ┌┴────────────┴┐ - │ Integration │ Vitest (10-15 tests) - │ Hook + API │ Hooks with mock container - ┌┴──────────────┴┐ - │ Use Case │ Vitest (15-20 tests) - │ Unit Tests │ Use cases with mock interfaces - ┌┴────────────────┴┐ - │ Domain Entity │ Vitest (20-30 tests) - │ + Adapter Unit │ Entities, adapters, value objects - └──────────────────┘ -``` - -### 13.2 Test Locations - -| Layer | Directory | Framework | What's Tested | -|-------|-----------|-----------|---------------| -| Domain entities | `demo-ui/src/lib/biometric/domain/__tests__/` | Vitest | Value object creation, validation, invariants | -| Domain interfaces | — | — | Not tested directly (Protocols) | -| Infrastructure adapters | `demo-ui/src/lib/biometric/infrastructure/__tests__/` | Vitest | Each adapter in isolation with mock models | -| Use cases | `demo-ui/src/lib/biometric/application/__tests__/` | Vitest | Use cases with mock interface implementations | -| Container | `demo-ui/src/lib/biometric/__tests__/container.test.ts` | Vitest | Container wiring, fallback behavior | -| React hooks | `demo-ui/src/hooks/__tests__/` | Vitest + Testing Library | Hook state transitions, error handling | -| Components | `demo-ui/src/components/biometric/__tests__/` | Vitest + Testing Library | Render output, user interaction | -| E2E | `demo-ui/e2e/` | Playwright | Full browser flow with real camera mock | -| Backend WebAuthn | `tests/unit/api/routes/test_webauthn.py` | Pytest | WebAuthn endpoint behavior | -| Backend hybrid | `tests/integration/test_client_biometric.py` | Pytest | Hybrid endpoint with client metadata | - -### 13.3 Mock Strategy - -```typescript -// demo-ui/src/lib/biometric/testing/mocks.ts - -/** - * Mock implementations for testing. - * - * Each mock implements the domain interface and returns configurable results. - * Used in use case and hook tests. - */ -export function createMockFaceDetector( - overrides?: Partial, -): IFaceDetector { /* ... */ } - -export function createMockQualityAssessor( - overrides?: Partial, -): IQualityAssessor { /* ... */ } - -export function createMockLivenessDetector( - overrides?: Partial, -): ILivenessDetector { /* ... */ } - -export function createMockApiClient( - overrides?: Partial, -): IBiometricApiClient { /* ... */ } - -/** - * Create a fully-configured container with all mocks. - * Useful for integration testing hooks and components. - */ -export function createMockBiometricContainer( - overrides?: Partial, -): BiometricContainer { /* ... */ } -``` - ---- - -## 14. Directory Structure - -``` -demo-ui/src/lib/biometric/ -├── domain/ # Layer 0 — Pure types, zero deps -│ ├── interfaces/ -│ │ ├── face-detector.ts # IFaceDetector port -│ │ ├── quality-assessor.ts # IQualityAssessor port -│ │ ├── liveness-detector.ts # ILivenessDetector port -│ │ ├── model-loader.ts # IModelLoader port -│ │ ├── biometric-api-client.ts # IBiometricApiClient port -│ │ ├── device-authenticator.ts # IDeviceAuthenticator port (WebAuthn) -│ │ └── index.ts # Barrel export -│ ├── entities/ -│ │ ├── face-detection-result.ts # FaceDetectionResult value object -│ │ ├── quality-assessment.ts # QualityAssessment value object -│ │ ├── liveness-result.ts # LivenessResult value object -│ │ └── index.ts -│ ├── errors.ts # BiometricError hierarchy -│ ├── types.ts # Shared types (BoundingBox, etc.) -│ └── index.ts -│ -├── application/ # Layer 1 — Use cases, depends on domain only -│ ├── use-cases/ -│ │ ├── client-detect-face.ts -│ │ ├── client-assess-quality.ts -│ │ ├── hybrid-enroll-face.ts -│ │ ├── hybrid-verify-face.ts -│ │ ├── realtime-analysis.ts -│ │ └── index.ts -│ └── index.ts -│ -├── infrastructure/ # Layer 2 — Adapters, implements domain interfaces -│ ├── ml/ -│ │ ├── mediapipe-face-detector.ts -│ │ ├── opencv-quality-assessor.ts -│ │ ├── mediapipe-liveness-detector.ts -│ │ └── loaders/ -│ │ ├── mediapipe-model-loader.ts -│ │ └── opencv-model-loader.ts -│ ├── server/ -│ │ ├── server-face-detector.ts -│ │ ├── server-quality-assessor.ts -│ │ └── biometric-api-client-impl.ts -│ ├── webauthn/ -│ │ └── webauthn-device-authenticator.ts -│ ├── resilience/ -│ │ ├── fallback-face-detector.ts -│ │ └── fallback-quality-assessor.ts -│ └── index.ts -│ -├── testing/ # Test utilities -│ ├── mocks.ts # Mock implementations -│ ├── fixtures.ts # Test data factories -│ └── index.ts -│ -├── container.ts # DI container factory -├── provider.tsx # React context provider -└── index.ts # Public API barrel export - -# Backend additions: -app/api/routes/ -├── webauthn.py # WebAuthn endpoints (replaces stubs) -└── client_biometric.py # Extended enrollment with client metadata - -app/domain/interfaces/ -└── credential_repository.py # WebAuthn credential storage port - -app/infrastructure/persistence/ -└── webauthn_credential_repository.py # PostgreSQL WebAuthn storage -``` - ---- - -## 15. Migration Plan - -### Phase 1: Foundation (Week 1-2) - -- [ ] Domain layer: entities, interfaces, errors, types -- [ ] Infrastructure: MediaPipe face detector adapter -- [ ] Infrastructure: OpenCV quality assessor adapter -- [ ] Infrastructure: Model loaders with lazy initialization -- [ ] DI Container + React Provider -- [ ] Unit tests for all domain entities -- [ ] Unit tests for adapters with mock models - -### Phase 2: Use Cases + Hooks (Week 3) - -- [ ] ClientDetectFaceUseCase + useClientFaceDetection hook -- [ ] ClientAssessQualityUseCase + useClientQuality hook -- [ ] RealTimeAnalysisUseCase + useRealtimeAnalysis hook -- [ ] QualityOverlay component -- [ ] GuidedCapture component -- [ ] Integration tests for hooks - -### Phase 3: Hybrid Flow (Week 4) - -- [ ] Server fallback adapters (ServerFaceDetector, ServerQualityAssessor) -- [ ] FallbackFaceDetector decorator -- [ ] HybridEnrollFaceUseCase + useHybridEnrollment hook -- [ ] HybridVerifyFaceUseCase -- [ ] Backend: Extended enrollment endpoint with client metadata -- [ ] Integration tests for hybrid flow - -### Phase 4: WebAuthn (Week 5) - -- [ ] Backend: WebAuthn credential repository -- [ ] Backend: WebAuthn route endpoints -- [ ] Client: WebAuthnDeviceAuthenticator adapter -- [ ] Client: useDeviceAuth hook -- [ ] Update fingerprint/voice routes to delegate to WebAuthn -- [ ] E2E tests with Playwright - -### Phase 5: Polish + E2E (Week 6) - -- [ ] Performance benchmarking (target: < 15ms/frame) -- [ ] Bundle size optimization (tree-shaking WASM modules) -- [ ] Playwright E2E tests for full enrollment flow -- [ ] Documentation updates -- [ ] CLAUDE.md updates - ---- - -## Appendix A: Technology Versions - -| Technology | Version | Purpose | -|-----------|---------|---------| -| MediaPipe Face Detection | 0.4.x | Browser face detection (WASM) | -| MediaPipe Face Mesh | 0.4.x | 468 landmarks + passive liveness | -| OpenCV.js | 4.9.x | Quality assessment (Laplacian, histogram) | -| ONNX Runtime Web | 1.17.x | Future: client-side embedding (reserved) | -| @simplewebauthn/browser | 10.x | WebAuthn client-side API | -| @simplewebauthn/server | 10.x | WebAuthn server-side validation | -| py_webauthn | 2.x | Python WebAuthn (alternative to JS server) | - -## Appendix B: Performance Budget - -| Operation | Target | Fallback Threshold | -|-----------|--------|-------------------| -| Face detection (MediaPipe) | < 15ms | > 100ms → use server | -| Quality assessment (OpenCV) | < 5ms | > 50ms → use server | -| Passive liveness (MediaPipe) | < 20ms | > 150ms → use server | -| Full frame analysis | < 33ms (30 FPS) | > 50ms → drop frames | -| Model initial load | < 3s | > 10s → show warning | -| WASM bundle size | < 2MB gzipped | — | - -## Appendix C: Browser Support - -| Browser | WASM | MediaPipe | WebAuthn | Status | -|---------|------|-----------|----------|--------| -| Chrome 90+ | Yes | Yes | Yes | Full support | -| Firefox 89+ | Yes | Yes | Yes | Full support | -| Safari 15.2+ | Yes | Yes | Yes | Full support | -| Edge 90+ | Yes | Yes | Yes | Full support | -| Safari 14- | Partial | No | No | Server fallback only | -| IE 11 | No | No | No | Not supported | diff --git a/docs/design/PROCTORING_PHASE2_DESIGN.md b/docs/design/PROCTORING_PHASE2_DESIGN.md deleted file mode 100644 index 8a8e991..0000000 --- a/docs/design/PROCTORING_PHASE2_DESIGN.md +++ /dev/null @@ -1,1042 +0,0 @@ -# Proctoring Service Phase 2 - Production Readiness Design - -**Version**: 1.0 -**Date**: 2024-12-12 -**Status**: Draft -**Author**: Architecture Team - ---- - -## 1. Overview - -### 1.1 Purpose - -This document designs the production readiness features for the Proctoring Service: - -1. **Configuration Management** - Environment-based settings for proctoring -2. **PostgreSQL Repositories** - Production-grade data persistence -3. **Integration Tests** - Full API flow testing -4. **Performance Benchmarks** - ML pipeline latency validation - -### 1.2 Goals - -| Goal | Description | Success Metric | -|------|-------------|----------------| -| **Configurability** | All thresholds externalized | 100% settings in environment | -| **Persistence** | Production database support | PostgreSQL repositories working | -| **Quality Assurance** | Comprehensive test coverage | >80% code coverage on new code | -| **Performance** | Validated latency targets | <500ms p95 frame analysis | - -### 1.3 Non-Goals - -- WebSocket real-time API (future phase) -- Kubernetes deployment manifests (DevOps responsibility) -- Load testing infrastructure (separate initiative) - ---- - -## 2. Configuration Management Design - -### 2.1 Environment Variables - -Following the existing pattern in `app/core/config.py`: - -```python -# Proctoring Service Configuration -# Section: Session Management -PROCTOR_ENABLED: bool = True -PROCTOR_MAX_SESSIONS_PER_USER: int = 1 -PROCTOR_SESSION_TIMEOUT_MINUTES: int = 180 - -# Section: Verification Thresholds -PROCTOR_VERIFICATION_INTERVAL_SEC: int = 60 -PROCTOR_VERIFICATION_THRESHOLD: float = 0.6 -PROCTOR_LIVENESS_THRESHOLD: float = 0.7 - -# Section: Gaze Tracking -PROCTOR_GAZE_ENABLED: bool = True -PROCTOR_GAZE_THRESHOLD: float = 0.3 -PROCTOR_GAZE_AWAY_THRESHOLD_SEC: float = 5.0 - -# Section: Object Detection -PROCTOR_OBJECT_DETECTION_ENABLED: bool = True -PROCTOR_OBJECT_MODEL_SIZE: str = "nano" # nano, small, medium, large -PROCTOR_OBJECT_CONFIDENCE_THRESHOLD: float = 0.5 -PROCTOR_MAX_PERSONS_ALLOWED: int = 1 - -# Section: Deepfake Detection -PROCTOR_DEEPFAKE_ENABLED: bool = True -PROCTOR_DEEPFAKE_THRESHOLD: float = 0.6 -PROCTOR_DEEPFAKE_TEMPORAL_WINDOW: int = 10 - -# Section: Audio Analysis -PROCTOR_AUDIO_ENABLED: bool = False -PROCTOR_AUDIO_SAMPLE_RATE: int = 16000 -PROCTOR_AUDIO_VAD_THRESHOLD: float = 0.5 - -# Section: Risk Management -PROCTOR_RISK_THRESHOLD_WARNING: float = 0.5 -PROCTOR_RISK_THRESHOLD_CRITICAL: float = 0.8 -PROCTOR_AUTO_TERMINATE_ON_CRITICAL: bool = False - -# Section: Rate Limiting -PROCTOR_RATE_LIMIT_ENABLED: bool = True -PROCTOR_MAX_FRAMES_PER_SECOND: int = 5 -PROCTOR_MAX_FRAMES_PER_MINUTE: int = 120 -PROCTOR_RATE_LIMIT_BURST_ALLOWANCE: int = 10 - -# Section: Circuit Breaker -PROCTOR_CIRCUIT_BREAKER_ENABLED: bool = True -PROCTOR_CIRCUIT_BREAKER_FAILURE_THRESHOLD: int = 3 -PROCTOR_CIRCUIT_BREAKER_SUCCESS_THRESHOLD: int = 2 -PROCTOR_CIRCUIT_BREAKER_TIMEOUT_SEC: float = 30.0 - -# Section: Database -PROCTOR_DB_POOL_SIZE: int = 10 -PROCTOR_DB_MAX_OVERFLOW: int = 20 -``` - -### 2.2 Configuration Class Design - -```python -@dataclass -class ProctorSettings: - """Proctoring service configuration.""" - - # Feature flags - enabled: bool = True - gaze_enabled: bool = True - object_detection_enabled: bool = True - deepfake_enabled: bool = True - audio_enabled: bool = False - - # Session settings - max_sessions_per_user: int = 1 - session_timeout_minutes: int = 180 - - # Verification - verification_interval_sec: int = 60 - verification_threshold: float = 0.6 - liveness_threshold: float = 0.7 - - # Gaze tracking - gaze_threshold: float = 0.3 - gaze_away_threshold_sec: float = 5.0 - - # Object detection - object_model_size: str = "nano" - object_confidence_threshold: float = 0.5 - max_persons_allowed: int = 1 - - # Deepfake detection - deepfake_threshold: float = 0.6 - deepfake_temporal_window: int = 10 - - # Audio - audio_sample_rate: int = 16000 - audio_vad_threshold: float = 0.5 - - # Risk management - risk_threshold_warning: float = 0.5 - risk_threshold_critical: float = 0.8 - auto_terminate_on_critical: bool = False - - # Rate limiting - rate_limit_enabled: bool = True - max_frames_per_second: int = 5 - max_frames_per_minute: int = 120 - rate_limit_burst_allowance: int = 10 - - # Circuit breaker - circuit_breaker_enabled: bool = True - circuit_breaker_failure_threshold: int = 3 - circuit_breaker_success_threshold: int = 2 - circuit_breaker_timeout_sec: float = 30.0 - - def to_session_config(self) -> dict: - """Convert to SessionConfig dict for session creation.""" - return { - "verification_interval_sec": self.verification_interval_sec, - "verification_threshold": self.verification_threshold, - "liveness_threshold": self.liveness_threshold, - "gaze_away_threshold_sec": self.gaze_away_threshold_sec, - "risk_threshold_warning": self.risk_threshold_warning, - "risk_threshold_critical": self.risk_threshold_critical, - "enable_gaze_tracking": self.gaze_enabled, - "enable_object_detection": self.object_detection_enabled, - "enable_audio_monitoring": self.audio_enabled, - } -``` - -### 2.3 Configuration Validation - -```python -def __post_init__(self): - """Validate configuration values.""" - # Threshold validations - if not 0.0 <= self.verification_threshold <= 1.0: - raise ValueError("verification_threshold must be 0-1") - if not 0.0 <= self.liveness_threshold <= 1.0: - raise ValueError("liveness_threshold must be 0-1") - if not 0.0 <= self.risk_threshold_warning <= 1.0: - raise ValueError("risk_threshold_warning must be 0-1") - if self.risk_threshold_warning >= self.risk_threshold_critical: - raise ValueError("warning threshold must be less than critical") - - # Model validation - if self.object_model_size not in ["nano", "small", "medium", "large"]: - raise ValueError("Invalid object_model_size") - - # Rate limit validation - if self.max_frames_per_second > self.max_frames_per_minute / 60: - raise ValueError("frames_per_second exceeds frames_per_minute rate") -``` - ---- - -## 3. PostgreSQL Repository Design - -### 3.1 Connection Management - -Following existing patterns in the codebase: - -```python -from asyncpg import Pool, create_pool -from contextlib import asynccontextmanager - -class PostgresConnectionManager: - """Manages PostgreSQL connection pool for proctoring.""" - - def __init__( - self, - dsn: str, - min_size: int = 5, - max_size: int = 20, - ): - self._dsn = dsn - self._min_size = min_size - self._max_size = max_size - self._pool: Optional[Pool] = None - - async def initialize(self) -> None: - """Initialize connection pool.""" - self._pool = await create_pool( - self._dsn, - min_size=self._min_size, - max_size=self._max_size, - ) - - async def close(self) -> None: - """Close connection pool.""" - if self._pool: - await self._pool.close() - - @asynccontextmanager - async def connection(self): - """Get connection from pool.""" - async with self._pool.acquire() as conn: - yield conn - - @asynccontextmanager - async def transaction(self): - """Get connection with transaction.""" - async with self._pool.acquire() as conn: - async with conn.transaction(): - yield conn -``` - -### 3.2 Session Repository Implementation - -```python -class PostgresProctorSessionRepository(IProctorSessionRepository): - """PostgreSQL implementation of proctoring session repository.""" - - def __init__(self, connection_manager: PostgresConnectionManager): - self._conn_manager = connection_manager - - async def save(self, session: ProctorSession) -> None: - """Save or update a proctoring session.""" - query = """ - INSERT INTO proctor_sessions ( - id, exam_id, user_id, tenant_id, status, - config, baseline_embedding, risk_score, - verification_count, verification_failures, - created_at, started_at, ended_at, - termination_reason, metadata - ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, - $11, $12, $13, $14, $15 - ) - ON CONFLICT (id) DO UPDATE SET - status = EXCLUDED.status, - risk_score = EXCLUDED.risk_score, - verification_count = EXCLUDED.verification_count, - verification_failures = EXCLUDED.verification_failures, - ended_at = EXCLUDED.ended_at, - termination_reason = EXCLUDED.termination_reason - """ - async with self._conn_manager.connection() as conn: - await conn.execute( - query, - session.id, - session.exam_id, - session.user_id, - session.tenant_id, - session.status.value, - json.dumps(session.config.to_dict()) if session.config else None, - session.baseline_embedding.tolist() if session.baseline_embedding else None, - session.risk_score, - session.verification_count, - session.verification_failures, - session.created_at, - session.started_at, - session.ended_at, - session.termination_reason.value if session.termination_reason else None, - json.dumps(session.metadata) if session.metadata else None, - ) - - async def get_by_id( - self, - session_id: UUID, - tenant_id: str, - ) -> Optional[ProctorSession]: - """Get session by ID with tenant isolation.""" - query = """ - SELECT * FROM proctor_sessions - WHERE id = $1 AND tenant_id = $2 - """ - async with self._conn_manager.connection() as conn: - row = await conn.fetchrow(query, session_id, tenant_id) - if row: - return self._row_to_session(row) - return None - - async def get_active_sessions( - self, - tenant_id: str, - limit: int = 100, - offset: int = 0, - ) -> List[ProctorSession]: - """Get all active sessions for tenant.""" - query = """ - SELECT * FROM proctor_sessions - WHERE tenant_id = $1 AND status IN ('active', 'initializing', 'flagged') - ORDER BY created_at DESC - LIMIT $2 OFFSET $3 - """ - async with self._conn_manager.connection() as conn: - rows = await conn.fetch(query, tenant_id, limit, offset) - return [self._row_to_session(row) for row in rows] - - # ... additional methods following same pattern - - def _row_to_session(self, row: Record) -> ProctorSession: - """Convert database row to ProctorSession entity.""" - config = None - if row['config']: - config = SessionConfig.from_dict(json.loads(row['config'])) - - baseline = None - if row['baseline_embedding']: - baseline = np.array(row['baseline_embedding']) - - return ProctorSession( - id=row['id'], - exam_id=row['exam_id'], - user_id=row['user_id'], - tenant_id=row['tenant_id'], - status=SessionStatus(row['status']), - config=config, - baseline_embedding=baseline, - risk_score=row['risk_score'], - verification_count=row['verification_count'], - verification_failures=row['verification_failures'], - created_at=row['created_at'], - started_at=row['started_at'], - ended_at=row['ended_at'], - termination_reason=TerminationReason(row['termination_reason']) if row['termination_reason'] else None, - metadata=json.loads(row['metadata']) if row['metadata'] else None, - ) -``` - -### 3.3 Incident Repository Implementation - -```python -class PostgresProctorIncidentRepository(IProctorIncidentRepository): - """PostgreSQL implementation of proctoring incident repository.""" - - def __init__(self, connection_manager: PostgresConnectionManager): - self._conn_manager = connection_manager - - async def save(self, incident: ProctorIncident) -> None: - """Save or update an incident.""" - query = """ - INSERT INTO proctor_incidents ( - id, session_id, incident_type, severity, - confidence, timestamp, details, - reviewed, reviewed_by, reviewed_at, review_notes, review_action - ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 - ) - ON CONFLICT (id) DO UPDATE SET - reviewed = EXCLUDED.reviewed, - reviewed_by = EXCLUDED.reviewed_by, - reviewed_at = EXCLUDED.reviewed_at, - review_notes = EXCLUDED.review_notes, - review_action = EXCLUDED.review_action - """ - async with self._conn_manager.connection() as conn: - await conn.execute( - query, - incident.id, - incident.session_id, - incident.incident_type.value, - incident.severity.value, - incident.confidence, - incident.timestamp, - json.dumps(incident.details) if incident.details else None, - incident.reviewed, - incident.reviewed_by, - incident.reviewed_at, - incident.review_notes, - incident.review_action.value if incident.review_action else None, - ) - - async def get_by_session( - self, - session_id: UUID, - limit: int = 100, - offset: int = 0, - ) -> List[ProctorIncident]: - """Get all incidents for a session.""" - query = """ - SELECT * FROM proctor_incidents - WHERE session_id = $1 - ORDER BY timestamp DESC - LIMIT $2 OFFSET $3 - """ - async with self._conn_manager.connection() as conn: - rows = await conn.fetch(query, session_id, limit, offset) - return [self._row_to_incident(row) for row in rows] - - async def count_by_severity( - self, - session_id: UUID, - severity: IncidentSeverity, - ) -> int: - """Count incidents by severity.""" - query = """ - SELECT COUNT(*) FROM proctor_incidents - WHERE session_id = $1 AND severity = $2 - """ - async with self._conn_manager.connection() as conn: - return await conn.fetchval(query, session_id, severity.value) - - # ... additional methods - - def _row_to_incident(self, row: Record) -> ProctorIncident: - """Convert database row to ProctorIncident entity.""" - return ProctorIncident( - id=row['id'], - session_id=row['session_id'], - incident_type=IncidentType(row['incident_type']), - severity=IncidentSeverity(row['severity']), - confidence=row['confidence'], - timestamp=row['timestamp'], - details=json.loads(row['details']) if row['details'] else None, - reviewed=row['reviewed'], - reviewed_by=row['reviewed_by'], - reviewed_at=row['reviewed_at'], - review_notes=row['review_notes'], - review_action=ReviewAction(row['review_action']) if row['review_action'] else None, - ) -``` - -### 3.4 Repository Factory - -```python -class ProctorRepositoryFactory: - """Factory for creating proctoring repositories.""" - - @staticmethod - def create_session_repository( - storage_type: str = "memory", - connection_manager: Optional[PostgresConnectionManager] = None, - ) -> IProctorSessionRepository: - """Create session repository based on storage type.""" - if storage_type == "postgres": - if not connection_manager: - raise ValueError("connection_manager required for postgres") - return PostgresProctorSessionRepository(connection_manager) - else: - return InMemoryProctorSessionRepository() - - @staticmethod - def create_incident_repository( - storage_type: str = "memory", - connection_manager: Optional[PostgresConnectionManager] = None, - ) -> IProctorIncidentRepository: - """Create incident repository based on storage type.""" - if storage_type == "postgres": - if not connection_manager: - raise ValueError("connection_manager required for postgres") - return PostgresProctorIncidentRepository(connection_manager) - else: - return InMemoryProctorIncidentRepository() -``` - ---- - -## 4. Integration Tests Design - -### 4.1 Test Categories - -| Category | Description | Count | -|----------|-------------|-------| -| **Session Lifecycle** | Create, start, pause, resume, end | 8 tests | -| **Frame Analysis** | Submit frames with various conditions | 10 tests | -| **Incident Management** | Create, list, review incidents | 6 tests | -| **Reports** | Session reports and summaries | 4 tests | -| **Error Handling** | Invalid inputs, not found scenarios | 6 tests | -| **Rate Limiting** | Frame submission throttling | 4 tests | -| **Total** | | **38 tests** | - -### 4.2 Test Fixtures - -```python -@pytest.fixture -async def test_client(): - """Create test client with app.""" - from app.main import app - from httpx import AsyncClient - - async with AsyncClient(app=app, base_url="http://test") as client: - yield client - -@pytest.fixture -def tenant_headers(): - """Headers with tenant ID.""" - return {"X-Tenant-ID": "test-tenant"} - -@pytest.fixture -def sample_frame_base64(): - """Generate a valid test frame.""" - import cv2 - import base64 - - # Create a simple test image - img = np.zeros((480, 640, 3), dtype=np.uint8) - img[:] = (100, 100, 100) # Gray background - - # Encode to base64 - _, buffer = cv2.imencode('.jpg', img) - return base64.b64encode(buffer).decode('utf-8') - -@pytest.fixture -async def created_session(test_client, tenant_headers): - """Create a session for testing.""" - response = await test_client.post( - "/api/v1/proctoring/sessions", - headers=tenant_headers, - json={ - "exam_id": "exam-001", - "user_id": "user-001", - } - ) - return response.json() -``` - -### 4.3 Session Lifecycle Tests - -```python -class TestSessionLifecycle: - """Integration tests for session lifecycle.""" - - @pytest.mark.asyncio - async def test_create_session(self, test_client, tenant_headers): - """Test creating a new proctoring session.""" - response = await test_client.post( - "/api/v1/proctoring/sessions", - headers=tenant_headers, - json={ - "exam_id": "exam-001", - "user_id": "user-001", - } - ) - - assert response.status_code == 201 - data = response.json() - assert "session_id" in data - assert data["status"] == "created" - assert data["exam_id"] == "exam-001" - - @pytest.mark.asyncio - async def test_start_session(self, test_client, tenant_headers, created_session): - """Test starting a session.""" - session_id = created_session["session_id"] - - response = await test_client.post( - f"/api/v1/proctoring/sessions/{session_id}/start", - headers=tenant_headers, - ) - - assert response.status_code == 200 - data = response.json() - assert data["status"] == "active" - - @pytest.mark.asyncio - async def test_pause_resume_session(self, test_client, tenant_headers, created_session): - """Test pausing and resuming a session.""" - session_id = created_session["session_id"] - - # Start first - await test_client.post( - f"/api/v1/proctoring/sessions/{session_id}/start", - headers=tenant_headers, - ) - - # Pause - response = await test_client.post( - f"/api/v1/proctoring/sessions/{session_id}/pause", - headers=tenant_headers, - ) - assert response.status_code == 200 - assert response.json()["status"] == "paused" - - # Resume - response = await test_client.post( - f"/api/v1/proctoring/sessions/{session_id}/resume", - headers=tenant_headers, - ) - assert response.status_code == 200 - assert response.json()["status"] == "active" - - @pytest.mark.asyncio - async def test_end_session(self, test_client, tenant_headers, created_session): - """Test ending a session.""" - session_id = created_session["session_id"] - - # Start first - await test_client.post( - f"/api/v1/proctoring/sessions/{session_id}/start", - headers=tenant_headers, - ) - - # End - response = await test_client.post( - f"/api/v1/proctoring/sessions/{session_id}/end", - headers=tenant_headers, - json={"reason": "completed"} - ) - - assert response.status_code == 200 - data = response.json() - assert data["status"] == "completed" - assert "duration_seconds" in data -``` - -### 4.4 Frame Analysis Tests - -```python -class TestFrameAnalysis: - """Integration tests for frame analysis.""" - - @pytest.mark.asyncio - async def test_submit_frame_success( - self, test_client, tenant_headers, created_session, sample_frame_base64 - ): - """Test successful frame submission.""" - session_id = created_session["session_id"] - - # Start session - await test_client.post( - f"/api/v1/proctoring/sessions/{session_id}/start", - headers=tenant_headers, - ) - - # Submit frame - response = await test_client.post( - f"/api/v1/proctoring/sessions/{session_id}/frames", - headers=tenant_headers, - json={ - "frame_base64": sample_frame_base64, - "frame_number": 1, - } - ) - - assert response.status_code == 200 - data = response.json() - assert "risk_score" in data - assert "processing_time_ms" in data - - @pytest.mark.asyncio - async def test_submit_frame_invalid_session( - self, test_client, tenant_headers, sample_frame_base64 - ): - """Test frame submission with invalid session.""" - fake_session_id = str(uuid4()) - - response = await test_client.post( - f"/api/v1/proctoring/sessions/{fake_session_id}/frames", - headers=tenant_headers, - json={ - "frame_base64": sample_frame_base64, - "frame_number": 1, - } - ) - - assert response.status_code == 404 - - @pytest.mark.asyncio - async def test_submit_frame_rate_limited( - self, test_client, tenant_headers, created_session, sample_frame_base64 - ): - """Test frame submission rate limiting.""" - session_id = created_session["session_id"] - - # Start session - await test_client.post( - f"/api/v1/proctoring/sessions/{session_id}/start", - headers=tenant_headers, - ) - - # Submit many frames rapidly - for i in range(15): - response = await test_client.post( - f"/api/v1/proctoring/sessions/{session_id}/frames", - headers=tenant_headers, - json={ - "frame_base64": sample_frame_base64, - "frame_number": i, - } - ) - - # Should see rate limit info in response - data = response.json() - assert "rate_limit" in data -``` - -### 4.5 Incident Tests - -```python -class TestIncidentManagement: - """Integration tests for incident management.""" - - @pytest.mark.asyncio - async def test_create_incident(self, test_client, tenant_headers, created_session): - """Test creating an incident manually.""" - session_id = created_session["session_id"] - - # Start session - await test_client.post( - f"/api/v1/proctoring/sessions/{session_id}/start", - headers=tenant_headers, - ) - - # Create incident - response = await test_client.post( - f"/api/v1/proctoring/sessions/{session_id}/incidents", - headers=tenant_headers, - json={ - "incident_type": "FACE_NOT_DETECTED", - "confidence": 0.95, - "details": {"reason": "User looked away"}, - } - ) - - assert response.status_code == 201 - data = response.json() - assert "incident_id" in data - assert data["incident_type"] == "FACE_NOT_DETECTED" - - @pytest.mark.asyncio - async def test_list_incidents(self, test_client, tenant_headers, created_session): - """Test listing incidents for a session.""" - session_id = created_session["session_id"] - - response = await test_client.get( - f"/api/v1/proctoring/sessions/{session_id}/incidents", - headers=tenant_headers, - ) - - assert response.status_code == 200 - data = response.json() - assert "incidents" in data - assert "total" in data - - @pytest.mark.asyncio - async def test_review_incident(self, test_client, tenant_headers, created_session): - """Test reviewing an incident.""" - session_id = created_session["session_id"] - - # Start and create incident - await test_client.post( - f"/api/v1/proctoring/sessions/{session_id}/start", - headers=tenant_headers, - ) - - incident_response = await test_client.post( - f"/api/v1/proctoring/sessions/{session_id}/incidents", - headers=tenant_headers, - json={ - "incident_type": "FACE_NOT_DETECTED", - "confidence": 0.95, - } - ) - incident_id = incident_response.json()["incident_id"] - - # Review it - review_headers = {**tenant_headers, "X-Reviewer-ID": "admin-001"} - response = await test_client.post( - f"/api/v1/proctoring/incidents/{incident_id}/review", - headers=review_headers, - json={ - "action": "DISMISSED", - "notes": "False positive - user sneezed", - } - ) - - assert response.status_code == 200 -``` - ---- - -## 5. Performance Benchmarks Design - -### 5.1 Benchmark Categories - -| Benchmark | Target | Method | -|-----------|--------|--------| -| **Frame Analysis** | <500ms p95 | pytest-benchmark | -| **Session Create** | <100ms p95 | pytest-benchmark | -| **Database Query** | <50ms p95 | pytest-benchmark | -| **ML Pipeline** | <400ms p95 | Component timing | - -### 5.2 Benchmark Implementation - -```python -import pytest -from pytest_benchmark.fixture import BenchmarkFixture - -class TestPerformanceBenchmarks: - """Performance benchmarks for proctoring service.""" - - @pytest.mark.benchmark(group="session") - def test_session_create_performance(self, benchmark): - """Benchmark session creation.""" - from app.domain.entities.proctor_session import ProctorSession - - def create_session(): - return ProctorSession.create( - exam_id="exam-001", - user_id="user-001", - tenant_id="tenant-001", - ) - - result = benchmark(create_session) - assert result is not None - - @pytest.mark.benchmark(group="analysis") - def test_gaze_analysis_performance(self, benchmark, sample_image): - """Benchmark gaze tracking analysis.""" - from app.infrastructure.ml.proctoring import MediaPipeGazeTracker - - tracker = MediaPipeGazeTracker() - session_id = uuid4() - - async def analyze(): - return await tracker.analyze(sample_image, session_id) - - result = benchmark.pedantic( - lambda: asyncio.run(analyze()), - rounds=50, - warmup_rounds=5, - ) - # Target: <100ms per frame for gaze - - @pytest.mark.benchmark(group="analysis") - def test_object_detection_performance(self, benchmark, sample_image): - """Benchmark object detection.""" - from app.infrastructure.ml.proctoring import YOLOObjectDetector - - detector = YOLOObjectDetector(model_name="yolov8n.pt") - session_id = uuid4() - - async def detect(): - return await detector.detect(sample_image, session_id) - - result = benchmark.pedantic( - lambda: asyncio.run(detect()), - rounds=50, - warmup_rounds=5, - ) - # Target: <200ms per frame for YOLO nano - - @pytest.mark.benchmark(group="analysis") - def test_deepfake_detection_performance(self, benchmark, sample_image): - """Benchmark deepfake detection.""" - from app.infrastructure.ml.proctoring import TextureDeepfakeDetector - - detector = TextureDeepfakeDetector() - session_id = uuid4() - - async def detect(): - return await detector.detect(sample_image, session_id) - - result = benchmark.pedantic( - lambda: asyncio.run(detect()), - rounds=50, - warmup_rounds=5, - ) - # Target: <100ms per frame for texture analysis - - @pytest.mark.benchmark(group="database") - def test_session_save_performance(self, benchmark, session, db_connection): - """Benchmark session database save.""" - from app.infrastructure.persistence.repositories.postgres_session_repository import ( - PostgresProctorSessionRepository - ) - - repo = PostgresProctorSessionRepository(db_connection) - - async def save(): - await repo.save(session) - - result = benchmark.pedantic( - lambda: asyncio.run(save()), - rounds=100, - warmup_rounds=10, - ) - # Target: <20ms per save -``` - -### 5.3 Performance Metrics Collection - -```python -class PerformanceMetrics: - """Collect and report performance metrics.""" - - def __init__(self): - self.metrics = {} - - def record(self, name: str, duration_ms: float): - """Record a timing metric.""" - if name not in self.metrics: - self.metrics[name] = [] - self.metrics[name].append(duration_ms) - - def get_statistics(self, name: str) -> dict: - """Get statistics for a metric.""" - values = self.metrics.get(name, []) - if not values: - return {} - - import numpy as np - - return { - "count": len(values), - "min": np.min(values), - "max": np.max(values), - "mean": np.mean(values), - "p50": np.percentile(values, 50), - "p95": np.percentile(values, 95), - "p99": np.percentile(values, 99), - } - - def report(self) -> str: - """Generate performance report.""" - lines = ["Performance Report", "=" * 50] - - for name in sorted(self.metrics.keys()): - stats = self.get_statistics(name) - lines.append(f"\n{name}:") - lines.append(f" Count: {stats['count']}") - lines.append(f" Min: {stats['min']:.2f}ms") - lines.append(f" Mean: {stats['mean']:.2f}ms") - lines.append(f" P95: {stats['p95']:.2f}ms") - lines.append(f" P99: {stats['p99']:.2f}ms") - lines.append(f" Max: {stats['max']:.2f}ms") - - return "\n".join(lines) -``` - ---- - -## 6. Implementation Plan - -### 6.1 Phase Breakdown - -| Step | Task | Duration | -|------|------|----------| -| 1 | Add configuration to config.py | 30 min | -| 2 | Update dependencies with config | 30 min | -| 3 | Implement PostgreSQL session repository | 1 hour | -| 4 | Implement PostgreSQL incident repository | 45 min | -| 5 | Create repository factory | 30 min | -| 6 | Write integration tests | 1.5 hours | -| 7 | Write performance benchmarks | 45 min | -| 8 | Run tests and fix issues | 30 min | -| 9 | Commit and document | 15 min | - -### 6.2 Dependencies - -``` -# Additional test dependencies -pytest-benchmark>=4.0.0 -pytest-asyncio>=0.21.0 -httpx>=0.24.0 -``` - ---- - -## 7. SE Checklist Compliance Matrix - -| Category | Requirement | Status | Evidence | -|----------|-------------|--------|----------| -| **SOLID - SRP** | Each class single responsibility | ✅ | Config, Repository, Tests separate | -| **SOLID - OCP** | Open for extension | ✅ | Repository factory pattern | -| **SOLID - LSP** | Substitutable implementations | ✅ | Memory/Postgres repositories | -| **SOLID - ISP** | Focused interfaces | ✅ | IProctorSessionRepository, IProctorIncidentRepository | -| **SOLID - DIP** | Depend on abstractions | ✅ | Dependencies use interfaces | -| **DRY** | No duplicate logic | ✅ | Shared _row_to_entity methods | -| **KISS** | Simple solutions | ✅ | Standard pytest patterns | -| **YAGNI** | No premature features | ✅ | Only essential benchmarks | -| **Design Patterns** | Appropriate patterns | ✅ | Factory, Repository | -| **Error Handling** | Proper exceptions | ✅ | Validation with context | -| **Testing** | Comprehensive tests | ✅ | 38 integration tests planned | -| **Security** | Input validation | ✅ | Config validation | -| **Performance** | Measured targets | ✅ | Benchmarks with p95 targets | -| **Documentation** | Clear documentation | ✅ | This design document | - ---- - -## 8. Acceptance Criteria - -### 8.1 Configuration - -- [ ] All proctoring settings configurable via environment -- [ ] Validation on all threshold values -- [ ] Default values match design document - -### 8.2 Repositories - -- [ ] PostgreSQL session repository implements all interface methods -- [ ] PostgreSQL incident repository implements all interface methods -- [ ] Factory creates correct repository based on config - -### 8.3 Integration Tests - -- [ ] All 38 tests passing -- [ ] >80% code coverage on new code -- [ ] Tests run in CI pipeline - -### 8.4 Performance - -- [ ] Frame analysis <500ms p95 -- [ ] Session operations <100ms p95 -- [ ] Database operations <50ms p95 - ---- - -**Document Status**: Ready for implementation -**Approval**: Self-certified compliant with SE checklist diff --git a/docs/design/PROCTORING_PHASE3_DESIGN.md b/docs/design/PROCTORING_PHASE3_DESIGN.md deleted file mode 100644 index 20c17ca..0000000 --- a/docs/design/PROCTORING_PHASE3_DESIGN.md +++ /dev/null @@ -1,1438 +0,0 @@ -# Proctoring Service Phase 3 Design Document - -## Overview - -This document outlines the design for Phase 3 enhancements to the proctoring service, covering WebSocket streaming, PostgreSQL integration, observability, API documentation, E2E testing, and security hardening. - -## Table of Contents - -1. [WebSocket Streaming](#1-websocket-streaming) -2. [PostgreSQL Integration](#2-postgresql-integration) -3. [Observability](#3-observability) -4. [API Documentation](#4-api-documentation) -5. [E2E Tests](#5-e2e-tests) -6. [Security Hardening](#6-security-hardening) -7. [SE Checklist Compliance](#7-se-checklist-compliance) - ---- - -## 1. WebSocket Streaming - -### 1.1 Purpose - -Enable real-time bidirectional communication for frame submission and instant feedback, reducing latency compared to HTTP polling. - -### 1.2 Architecture - -``` -┌─────────────┐ WebSocket ┌─────────────────────┐ -│ Client │◄──────────────────►│ WebSocket Router │ -│ (Browser) │ Binary frames │ │ -└─────────────┘ └──────────┬──────────┘ - │ - ┌──────────▼──────────┐ - │ Connection Manager │ - │ - Session tracking │ - │ - Rate limiting │ - │ - Authentication │ - └──────────┬──────────┘ - │ - ┌──────────▼──────────┐ - │ Frame Processor │ - │ - Decode frames │ - │ - ML analysis │ - │ - Incident creation│ - └─────────────────────┘ -``` - -### 1.3 Message Protocol - -```python -# Client -> Server Messages -class ClientMessage: - type: Literal["frame", "audio", "ping", "config"] - session_id: UUID - payload: bytes | dict - timestamp: int # Unix ms - -# Server -> Client Messages -class ServerMessage: - type: Literal["result", "incident", "warning", "pong", "error"] - session_id: UUID - payload: dict - timestamp: int -``` - -### 1.4 Frame Message Format (Binary) - -``` -┌────────────┬────────────┬────────────┬─────────────────┐ -│ Header │ Session ID │ Frame # │ Image Data │ -│ (4 bytes) │ (16 bytes) │ (4 bytes) │ (variable) │ -└────────────┴────────────┴────────────┴─────────────────┘ -``` - -### 1.5 Components - -```python -# app/api/websocket/connection_manager.py -class ConnectionManager: - """Manage WebSocket connections per session.""" - - async def connect(self, websocket: WebSocket, session_id: UUID, tenant_id: str) -> bool - async def disconnect(self, session_id: UUID) -> None - async def send_result(self, session_id: UUID, result: FrameResult) -> None - async def send_incident(self, session_id: UUID, incident: dict) -> None - async def broadcast_to_monitors(self, session_id: UUID, message: dict) -> None - def get_connection_count(self) -> int - def get_session_connections(self, session_id: UUID) -> List[WebSocket] - -# app/api/websocket/frame_handler.py -class WebSocketFrameHandler: - """Handle incoming WebSocket frames.""" - - async def handle_binary_frame(self, data: bytes, session_id: UUID) -> FrameResult - async def handle_json_message(self, message: dict, session_id: UUID) -> dict - -# app/api/websocket/auth.py -class WebSocketAuthenticator: - """Authenticate WebSocket connections.""" - - async def authenticate(self, websocket: WebSocket) -> Tuple[str, str] # tenant_id, user_id - async def validate_session_access(self, session_id: UUID, tenant_id: str) -> bool -``` - -### 1.6 WebSocket Endpoint - -```python -# app/api/routes/proctor_ws.py -@router.websocket("/proctoring/sessions/{session_id}/stream") -async def websocket_stream( - websocket: WebSocket, - session_id: UUID, - token: str = Query(...), # JWT or API key -): - """ - WebSocket endpoint for real-time frame streaming. - - Connection flow: - 1. Client connects with session_id and auth token - 2. Server validates token and session access - 3. Client sends binary frames - 4. Server responds with analysis results - 5. Server pushes incidents in real-time - """ -``` - -### 1.7 Configuration - -```python -# New settings in config.py -PROCTOR_WS_ENABLED: bool = True -PROCTOR_WS_MAX_CONNECTIONS_PER_SESSION: int = 2 -PROCTOR_WS_HEARTBEAT_INTERVAL_SEC: int = 30 -PROCTOR_WS_MESSAGE_QUEUE_SIZE: int = 100 -PROCTOR_WS_MAX_FRAME_SIZE_BYTES: int = 5_000_000 # 5MB -PROCTOR_WS_AUTH_TIMEOUT_SEC: int = 10 -``` - ---- - -## 2. PostgreSQL Integration - -### 2.1 Purpose - -Production-ready persistent storage with connection pooling, migrations, and health checks. - -### 2.2 Architecture - -``` -┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ -│ Repository │────►│ Connection │────►│ PostgreSQL │ -│ Interface │ │ Pool (asyncpg) │ │ Database │ -└─────────────────┘ └─────────────────┘ └──────────────┘ - │ - ┌──────▼──────┐ - │ Health │ - │ Checker │ - └─────────────┘ -``` - -### 2.3 Database Schema - -```sql --- migrations/versions/001_create_proctor_tables.sql - --- Proctor Sessions Table -CREATE TABLE IF NOT EXISTS proctor_sessions ( - id UUID PRIMARY KEY, - exam_id VARCHAR(255) NOT NULL, - user_id VARCHAR(255) NOT NULL, - tenant_id VARCHAR(255) NOT NULL, - status VARCHAR(50) NOT NULL DEFAULT 'created', - risk_score FLOAT DEFAULT 0.0, - config JSONB DEFAULT '{}', - metadata JSONB DEFAULT '{}', - baseline_embedding JSONB, - verification_count INTEGER DEFAULT 0, - verification_failures INTEGER DEFAULT 0, - incident_count INTEGER DEFAULT 0, - total_gaze_away_sec FLOAT DEFAULT 0.0, - termination_reason VARCHAR(50), - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - started_at TIMESTAMP WITH TIME ZONE, - ended_at TIMESTAMP WITH TIME ZONE, - paused_at TIMESTAMP WITH TIME ZONE, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - - CONSTRAINT unique_exam_user_tenant UNIQUE (exam_id, user_id, tenant_id) -); - --- Indexes for common queries -CREATE INDEX idx_sessions_tenant_status ON proctor_sessions(tenant_id, status); -CREATE INDEX idx_sessions_exam ON proctor_sessions(exam_id); -CREATE INDEX idx_sessions_user ON proctor_sessions(user_id); -CREATE INDEX idx_sessions_created ON proctor_sessions(created_at); - --- Proctor Incidents Table -CREATE TABLE IF NOT EXISTS proctor_incidents ( - id UUID PRIMARY KEY, - session_id UUID NOT NULL REFERENCES proctor_sessions(id) ON DELETE CASCADE, - incident_type VARCHAR(50) NOT NULL, - severity VARCHAR(20) NOT NULL, - confidence FLOAT NOT NULL, - details JSONB DEFAULT '{}', - reviewed BOOLEAN DEFAULT FALSE, - reviewed_at TIMESTAMP WITH TIME ZONE, - reviewed_by VARCHAR(255), - review_action VARCHAR(50), - review_notes TEXT, - timestamp TIMESTAMP WITH TIME ZONE NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - -CREATE INDEX idx_incidents_session ON proctor_incidents(session_id); -CREATE INDEX idx_incidents_severity ON proctor_incidents(severity); -CREATE INDEX idx_incidents_reviewed ON proctor_incidents(reviewed); - --- Incident Evidence Table -CREATE TABLE IF NOT EXISTS incident_evidence ( - id UUID PRIMARY KEY, - incident_id UUID NOT NULL REFERENCES proctor_incidents(id) ON DELETE CASCADE, - evidence_type VARCHAR(50) NOT NULL, - storage_url TEXT NOT NULL, - thumbnail_url TEXT, - metadata JSONB DEFAULT '{}', - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - -CREATE INDEX idx_evidence_incident ON incident_evidence(incident_id); -``` - -### 2.4 Connection Pool Manager - -```python -# app/infrastructure/persistence/pool_manager.py -class DatabasePoolManager: - """Manage PostgreSQL connection pool lifecycle.""" - - def __init__(self, settings: Settings): - self._pool: Optional[asyncpg.Pool] = None - self._settings = settings - - async def initialize(self) -> None: - """Create connection pool on startup.""" - self._pool = await asyncpg.create_pool( - dsn=self._settings.DATABASE_URL, - min_size=self._settings.DB_POOL_MIN_SIZE, - max_size=self._settings.DB_POOL_MAX_SIZE, - max_inactive_connection_lifetime=300, - command_timeout=60, - ) - - async def close(self) -> None: - """Close pool on shutdown.""" - if self._pool: - await self._pool.close() - - async def health_check(self) -> bool: - """Check pool health.""" - try: - async with self._pool.acquire() as conn: - await conn.fetchval("SELECT 1") - return True - except Exception: - return False - - @property - def pool(self) -> asyncpg.Pool: - return self._pool -``` - -### 2.5 Repository Factory Enhancement - -```python -# app/infrastructure/persistence/repository_factory.py -class ProctorRepositoryFactory: - """Create proctoring repositories based on configuration.""" - - @staticmethod - def create_session_repository( - storage_type: str, - pool: Optional[asyncpg.Pool] = None, - ) -> IProctorSessionRepository: - if storage_type == "postgres" and pool: - return PostgresSessionRepository(pool) - return MemoryProctorSessionRepository() - - @staticmethod - def create_incident_repository( - storage_type: str, - pool: Optional[asyncpg.Pool] = None, - ) -> IProctorIncidentRepository: - if storage_type == "postgres" and pool: - return PostgresIncidentRepository(pool) - return MemoryProctorIncidentRepository() -``` - -### 2.6 Configuration - -```python -# Existing + new settings -DATABASE_URL: str = "postgresql://user:pass@localhost:5432/biometric" -DB_POOL_MIN_SIZE: int = 5 -DB_POOL_MAX_SIZE: int = 20 -DB_POOL_MAX_QUERIES: int = 50000 -DB_POOL_MAX_INACTIVE_LIFETIME: int = 300 -DB_COMMAND_TIMEOUT: int = 60 -``` - ---- - -## 3. Observability - -### 3.1 Purpose - -Production-grade monitoring with Prometheus metrics, structured logging, and distributed tracing. - -### 3.2 Architecture - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Application │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ -│ │ Metrics │ │ Logging │ │ Tracing │ │ -│ │ Collector │ │ Handler │ │ (OpenTelemetry) │ │ -│ └──────┬──────┘ └──────┬──────┘ └───────────┬─────────────┘ │ -└─────────┼────────────────┼─────────────────────┼────────────────┘ - │ │ │ - ▼ ▼ ▼ - ┌──────────┐ ┌──────────┐ ┌──────────┐ - │Prometheus│ │ stdout │ │ Jaeger │ - │ │ │ (JSON) │ │ /Zipkin │ - └──────────┘ └──────────┘ └──────────┘ -``` - -### 3.3 Proctoring-Specific Metrics - -```python -# app/core/metrics/proctoring.py -class ProctorMetrics: - """Proctoring-specific Prometheus metrics.""" - - # Session metrics - sessions_total: Counter # Total sessions created - sessions_active: Gauge # Currently active sessions - session_duration_seconds: Histogram - - # Frame processing metrics - frames_processed_total: Counter - frame_processing_duration_ms: Histogram - frame_processing_errors: Counter - - # ML analysis metrics - gaze_analysis_duration_ms: Histogram - object_detection_duration_ms: Histogram - deepfake_detection_duration_ms: Histogram - face_verification_duration_ms: Histogram - - # Incident metrics - incidents_total: Counter # By type and severity - incidents_reviewed_total: Counter - - # Risk score distribution - risk_score_histogram: Histogram - - # WebSocket metrics - ws_connections_active: Gauge - ws_messages_received: Counter - ws_messages_sent: Counter - ws_errors: Counter - - # Rate limiting metrics - rate_limit_violations: Counter - rate_limit_throttled_frames: Counter -``` - -### 3.4 Structured Logging - -```python -# app/core/logging/structured.py -import structlog -from typing import Any - -def configure_structured_logging( - service_name: str, - version: str, - environment: str, - log_level: str = "INFO", -) -> None: - """Configure structured JSON logging.""" - - structlog.configure( - processors=[ - structlog.contextvars.merge_contextvars, - structlog.processors.add_log_level, - structlog.processors.TimeStamper(fmt="iso"), - structlog.processors.StackInfoRenderer(), - structlog.processors.format_exc_info, - structlog.processors.JSONRenderer(), - ], - wrapper_class=structlog.make_filtering_bound_logger( - getattr(logging, log_level) - ), - context_class=dict, - logger_factory=structlog.PrintLoggerFactory(), - cache_logger_on_first_use=True, - ) - -def get_logger(name: str) -> structlog.BoundLogger: - """Get a structured logger with context.""" - return structlog.get_logger(name) - -# Context managers for request tracking -@contextmanager -def log_context(**kwargs): - """Add context to all log messages in scope.""" - token = structlog.contextvars.bind_contextvars(**kwargs) - try: - yield - finally: - structlog.contextvars.unbind_contextvars(*kwargs.keys()) -``` - -### 3.5 Distributed Tracing - -```python -# app/core/tracing/opentelemetry.py -from opentelemetry import trace -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor -from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor -from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor - -def configure_tracing( - service_name: str, - otlp_endpoint: str, - environment: str, -) -> None: - """Configure OpenTelemetry distributed tracing.""" - - provider = TracerProvider( - resource=Resource.create({ - "service.name": service_name, - "deployment.environment": environment, - }) - ) - - exporter = OTLPSpanExporter(endpoint=otlp_endpoint) - provider.add_span_processor(BatchSpanProcessor(exporter)) - trace.set_tracer_provider(provider) - - # Auto-instrument FastAPI - FastAPIInstrumentor.instrument() - - # Auto-instrument asyncpg - AsyncPGInstrumentor().instrument() - -def get_tracer(name: str) -> trace.Tracer: - """Get a tracer for manual instrumentation.""" - return trace.get_tracer(name) - -# Decorator for custom spans -def traced(name: str = None): - """Decorator to trace function execution.""" - def decorator(func): - @wraps(func) - async def wrapper(*args, **kwargs): - tracer = get_tracer(func.__module__) - span_name = name or func.__name__ - with tracer.start_as_current_span(span_name) as span: - span.set_attribute("function", func.__name__) - try: - result = await func(*args, **kwargs) - return result - except Exception as e: - span.record_exception(e) - raise - return wrapper - return decorator -``` - -### 3.6 Configuration - -```python -# Observability settings -TRACING_ENABLED: bool = False -TRACING_OTLP_ENDPOINT: str = "http://localhost:4317" -TRACING_SAMPLE_RATE: float = 0.1 # 10% sampling in prod - -LOG_FORMAT: Literal["json", "text"] = "json" -LOG_LEVEL: str = "INFO" -LOG_INCLUDE_TRACE_ID: bool = True - -METRICS_ENABLED: bool = True -METRICS_PATH: str = "/metrics" -METRICS_INCLUDE_PROCESS: bool = True -``` - ---- - -## 4. API Documentation - -### 4.1 Purpose - -Enhanced OpenAPI documentation with examples, detailed descriptions, and SDK generation support. - -### 4.2 OpenAPI Enhancements - -```python -# app/api/docs/proctoring.py -"""OpenAPI documentation enhancements for proctoring endpoints.""" - -PROCTORING_TAG = { - "name": "proctoring", - "description": """ -## Proctoring Service - -AI-powered exam monitoring with: -- **Real-time face verification** - Continuous identity checks -- **Gaze tracking** - Detect looking away from screen -- **Object detection** - Identify phones, notes, extra persons -- **Deepfake detection** - Prevent synthetic media attacks -- **Audio analysis** - Detect unauthorized voices - -### Session Lifecycle - -``` -Created → Started → Active ←→ Paused → Ended - ↓ - Flagged → Terminated -``` - -### Risk Scoring - -Risk scores range from 0.0 (no risk) to 1.0 (maximum risk): -- **0.0 - 0.3**: Low risk (green) -- **0.3 - 0.6**: Medium risk (yellow) -- **0.6 - 0.8**: High risk (orange) -- **0.8 - 1.0**: Critical risk (red) - -### Rate Limiting - -Frame submission is rate-limited per session: -- Maximum 5 frames/second burst -- Maximum 60 frames/minute sustained -- Violations may trigger session throttling -""", - "externalDocs": { - "description": "Proctoring API Guide", - "url": "https://docs.example.com/proctoring", - }, -} - -# Response examples -SESSION_CREATED_EXAMPLE = { - "session_id": "550e8400-e29b-41d4-a716-446655440000", - "exam_id": "midterm-2024-math", - "user_id": "student-12345", - "status": "created", - "config": { - "verification_interval_sec": 60, - "gaze_tracking_enabled": True, - "object_detection_enabled": True, - "deepfake_detection_enabled": True, - "audio_analysis_enabled": False, - }, -} - -FRAME_RESULT_EXAMPLE = { - "session_id": "550e8400-e29b-41d4-a716-446655440000", - "frame_number": 42, - "risk_score": 0.15, - "face_detected": True, - "face_matched": True, - "incidents_created": 0, - "processing_time_ms": 87, - "analysis": { - "gaze": {"on_screen": True, "confidence": 0.95}, - "objects": [], - "deepfake": {"is_fake": False, "confidence": 0.02}, - }, - "rate_limit": { - "frames_remaining": 58, - "reset_in_seconds": 45, - }, -} - -INCIDENT_EXAMPLE = { - "incident_id": "661e8400-e29b-41d4-a716-446655440001", - "session_id": "550e8400-e29b-41d4-a716-446655440000", - "incident_type": "gaze_away", - "severity": "low", - "confidence": 0.85, - "timestamp": "2024-01-15T10:30:00Z", - "details": { - "duration_seconds": 5.2, - "direction": "left", - }, - "reviewed": False, -} -``` - -### 4.3 Schema Enhancements - -```python -# app/api/schemas/proctor.py - Enhanced with examples and descriptions - -class CreateSessionRequest(BaseModel): - """Request to create a new proctoring session.""" - - exam_id: str = Field( - ..., - description="Unique identifier for the exam", - example="midterm-2024-math", - min_length=1, - max_length=255, - ) - user_id: str = Field( - ..., - description="Unique identifier for the exam-taker", - example="student-12345", - min_length=1, - max_length=255, - ) - config: Optional[SessionConfigSchema] = Field( - None, - description="Optional session configuration overrides", - ) - metadata: Optional[Dict[str, Any]] = Field( - None, - description="Custom metadata to attach to the session", - example={"course": "MATH101", "instructor": "Dr. Smith"}, - ) - - class Config: - json_schema_extra = { - "example": { - "exam_id": "midterm-2024-math", - "user_id": "student-12345", - "config": {"verification_interval_sec": 30}, - "metadata": {"course": "MATH101"}, - } - } - -class SubmitFrameRequest(BaseModel): - """Request to submit a video frame for analysis.""" - - frame_base64: str = Field( - ..., - description="Base64-encoded JPEG or PNG image", - min_length=100, - ) - frame_number: int = Field( - ..., - description="Sequential frame number (client-assigned)", - ge=0, - example=42, - ) - audio_base64: Optional[str] = Field( - None, - description="Base64-encoded audio data (float32 PCM)", - ) - audio_sample_rate: Optional[int] = Field( - None, - description="Audio sample rate in Hz", - example=16000, - ge=8000, - le=48000, - ) -``` - -### 4.4 Error Response Documentation - -```python -# app/api/docs/errors.py -ERROR_RESPONSES = { - 400: { - "description": "Bad Request - Invalid input data", - "content": { - "application/json": { - "examples": { - "invalid_image": { - "summary": "Invalid image data", - "value": { - "detail": "Invalid image data: could not decode base64", - "error_code": "INVALID_IMAGE", - }, - }, - "session_not_active": { - "summary": "Session not in correct state", - "value": { - "detail": "Session must be active to submit frames", - "error_code": "INVALID_SESSION_STATE", - }, - }, - }, - }, - }, - }, - 404: { - "description": "Not Found - Resource does not exist", - "content": { - "application/json": { - "example": { - "detail": "Session 550e8400-e29b-41d4-a716-446655440000 not found", - "error_code": "SESSION_NOT_FOUND", - }, - }, - }, - }, - 429: { - "description": "Too Many Requests - Rate limit exceeded", - "content": { - "application/json": { - "example": { - "detail": "Rate limit exceeded: 60 frames/minute", - "error_code": "RATE_LIMIT_EXCEEDED", - "retry_after": 30, - }, - }, - }, - }, -} -``` - ---- - -## 5. E2E Tests - -### 5.1 Purpose - -Comprehensive end-to-end tests validating complete workflows with real ML models. - -### 5.2 Test Architecture - -``` -tests/e2e/ -├── conftest.py # Fixtures and test app setup -├── test_session_workflow.py # Complete session lifecycle -├── test_frame_analysis.py # Frame processing with ML -├── test_incident_flow.py # Incident creation and review -├── test_websocket.py # WebSocket streaming -├── test_concurrent.py # Concurrent session handling -└── fixtures/ - ├── test_images/ # Sample face images - ├── test_audio/ # Sample audio clips - └── expected_results/ # Expected ML outputs -``` - -### 5.3 Test Fixtures - -```python -# tests/e2e/conftest.py -import pytest -from httpx import AsyncClient -from app.main import app - -@pytest.fixture -async def e2e_client(): - """Create async test client with real dependencies.""" - async with AsyncClient(app=app, base_url="http://test") as client: - yield client - -@pytest.fixture -def valid_face_image() -> bytes: - """Load a valid face image for testing.""" - with open("tests/e2e/fixtures/test_images/valid_face.jpg", "rb") as f: - return base64.b64encode(f.read()).decode() - -@pytest.fixture -def no_face_image() -> bytes: - """Load an image without faces.""" - with open("tests/e2e/fixtures/test_images/no_face.jpg", "rb") as f: - return base64.b64encode(f.read()).decode() - -@pytest.fixture -def multiple_faces_image() -> bytes: - """Load an image with multiple faces.""" - with open("tests/e2e/fixtures/test_images/multiple_faces.jpg", "rb") as f: - return base64.b64encode(f.read()).decode() - -@pytest.fixture -def phone_in_frame_image() -> bytes: - """Load an image with a phone visible.""" - with open("tests/e2e/fixtures/test_images/phone_visible.jpg", "rb") as f: - return base64.b64encode(f.read()).decode() -``` - -### 5.4 Session Workflow Tests - -```python -# tests/e2e/test_session_workflow.py -class TestCompleteSessionWorkflow: - """Test complete proctoring session from creation to report.""" - - async def test_full_exam_session( - self, - e2e_client: AsyncClient, - valid_face_image: str, - ): - """ - Test complete exam workflow: - 1. Create session - 2. Start with baseline - 3. Submit frames (simulating exam) - 4. Handle incidents - 5. End session - 6. Generate report - """ - headers = {"X-Tenant-ID": "test-tenant"} - - # 1. Create session - create_response = await e2e_client.post( - "/api/v1/proctoring/sessions", - headers=headers, - json={ - "exam_id": "e2e-test-exam", - "user_id": "e2e-test-user", - }, - ) - assert create_response.status_code == 201 - session_id = create_response.json()["session_id"] - - # 2. Start session with baseline - start_response = await e2e_client.post( - f"/api/v1/proctoring/sessions/{session_id}/start", - headers=headers, - json={"baseline_image_base64": valid_face_image}, - ) - assert start_response.status_code == 200 - assert start_response.json()["status"] == "active" - - # 3. Submit multiple frames - for frame_num in range(5): - frame_response = await e2e_client.post( - f"/api/v1/proctoring/sessions/{session_id}/frames", - headers=headers, - json={ - "frame_base64": valid_face_image, - "frame_number": frame_num, - }, - ) - assert frame_response.status_code == 200 - result = frame_response.json() - assert result["face_detected"] is True - assert result["face_matched"] is True - - # 4. End session - end_response = await e2e_client.post( - f"/api/v1/proctoring/sessions/{session_id}/end", - headers=headers, - json={"reason": "completed"}, - ) - assert end_response.status_code == 200 - assert end_response.json()["status"] == "ended" - - # 5. Get report - report_response = await e2e_client.get( - f"/api/v1/proctoring/sessions/{session_id}/report", - headers=headers, - ) - assert report_response.status_code == 200 - report = report_response.json() - assert report["verification_count"] >= 1 - assert report["risk_score"] < 0.5 # Should be low risk - - async def test_incident_detection_workflow( - self, - e2e_client: AsyncClient, - valid_face_image: str, - phone_in_frame_image: str, - ): - """Test that incidents are properly detected and can be reviewed.""" - headers = {"X-Tenant-ID": "test-tenant"} - - # Create and start session - create_resp = await e2e_client.post( - "/api/v1/proctoring/sessions", - headers=headers, - json={"exam_id": "incident-test", "user_id": "test-user"}, - ) - session_id = create_resp.json()["session_id"] - - await e2e_client.post( - f"/api/v1/proctoring/sessions/{session_id}/start", - headers=headers, - json={"baseline_image_base64": valid_face_image}, - ) - - # Submit frame with phone (should create incident) - frame_resp = await e2e_client.post( - f"/api/v1/proctoring/sessions/{session_id}/frames", - headers=headers, - json={ - "frame_base64": phone_in_frame_image, - "frame_number": 0, - }, - ) - - # Check for incident - incidents_resp = await e2e_client.get( - f"/api/v1/proctoring/sessions/{session_id}/incidents", - headers=headers, - ) - incidents = incidents_resp.json()["incidents"] - - # Should have detected phone - phone_incidents = [i for i in incidents if i["incident_type"] == "phone_detected"] - assert len(phone_incidents) > 0 - - # Review incident - incident_id = phone_incidents[0]["incident_id"] - review_resp = await e2e_client.post( - f"/api/v1/proctoring/incidents/{incident_id}/review", - headers={**headers, "X-Reviewer-ID": "proctor-001"}, - json={ - "action": "dismiss", - "notes": "False positive - was a calculator", - }, - ) - assert review_resp.status_code == 200 -``` - -### 5.5 WebSocket Tests - -```python -# tests/e2e/test_websocket.py -class TestWebSocketStreaming: - """Test WebSocket frame streaming.""" - - async def test_websocket_frame_submission( - self, - e2e_client: AsyncClient, - valid_face_image: str, - ): - """Test real-time frame submission via WebSocket.""" - # First create and start session via REST - headers = {"X-Tenant-ID": "test-tenant"} - - create_resp = await e2e_client.post( - "/api/v1/proctoring/sessions", - headers=headers, - json={"exam_id": "ws-test", "user_id": "ws-user"}, - ) - session_id = create_resp.json()["session_id"] - - # Start session - await e2e_client.post( - f"/api/v1/proctoring/sessions/{session_id}/start", - headers=headers, - json={"baseline_image_base64": valid_face_image}, - ) - - # Connect WebSocket - async with e2e_client.websocket_connect( - f"/api/v1/proctoring/sessions/{session_id}/stream?token=test-token" - ) as ws: - # Send frame - await ws.send_bytes(base64.b64decode(valid_face_image)) - - # Receive result - result = await ws.receive_json() - assert result["type"] == "result" - assert result["payload"]["face_detected"] is True - - async def test_websocket_incident_push( - self, - e2e_client: AsyncClient, - valid_face_image: str, - phone_in_frame_image: str, - ): - """Test that incidents are pushed via WebSocket.""" - # Setup session... - # Connect WebSocket... - # Send frame with phone... - # Assert incident message received - pass -``` - -### 5.6 Concurrent Session Tests - -```python -# tests/e2e/test_concurrent.py -class TestConcurrentSessions: - """Test concurrent session handling.""" - - async def test_multiple_concurrent_sessions(self, e2e_client: AsyncClient): - """Test handling multiple concurrent proctoring sessions.""" - num_sessions = 10 - headers = {"X-Tenant-ID": "concurrent-test"} - - # Create sessions concurrently - async def create_session(i: int) -> str: - resp = await e2e_client.post( - "/api/v1/proctoring/sessions", - headers=headers, - json={"exam_id": f"concurrent-exam-{i}", "user_id": f"user-{i}"}, - ) - return resp.json()["session_id"] - - session_ids = await asyncio.gather(*[ - create_session(i) for i in range(num_sessions) - ]) - - assert len(session_ids) == num_sessions - assert len(set(session_ids)) == num_sessions # All unique - - async def test_session_isolation(self, e2e_client: AsyncClient): - """Test that sessions are properly isolated between tenants.""" - # Create session for tenant A - resp_a = await e2e_client.post( - "/api/v1/proctoring/sessions", - headers={"X-Tenant-ID": "tenant-a"}, - json={"exam_id": "isolation-test", "user_id": "user-1"}, - ) - session_id = resp_a.json()["session_id"] - - # Try to access from tenant B (should fail) - resp_b = await e2e_client.get( - f"/api/v1/proctoring/sessions/{session_id}", - headers={"X-Tenant-ID": "tenant-b"}, - ) - assert resp_b.status_code == 404 -``` - ---- - -## 6. Security Hardening - -### 6.1 Purpose - -Enhance security with input validation, sanitization, and protection against common attacks. - -### 6.2 Input Validation - -```python -# app/api/validators/proctor.py -import re -from typing import Optional -from pydantic import validator, Field - -# Constants -MAX_IMAGE_SIZE_BYTES = 10 * 1024 * 1024 # 10MB -MAX_AUDIO_SIZE_BYTES = 5 * 1024 * 1024 # 5MB -ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/webp"} - -class ImageValidator: - """Validate image data for security and correctness.""" - - @staticmethod - def validate_base64_image(data: str) -> bytes: - """Validate and decode base64 image data.""" - try: - # Check length before decoding - if len(data) > MAX_IMAGE_SIZE_BYTES * 1.4: # Base64 overhead - raise ValueError("Image data too large") - - decoded = base64.b64decode(data) - - # Check decoded size - if len(decoded) > MAX_IMAGE_SIZE_BYTES: - raise ValueError(f"Image exceeds maximum size of {MAX_IMAGE_SIZE_BYTES} bytes") - - # Validate image magic bytes - if not ImageValidator._is_valid_image(decoded): - raise ValueError("Invalid image format") - - return decoded - - except Exception as e: - raise ValueError(f"Invalid image data: {e}") - - @staticmethod - def _is_valid_image(data: bytes) -> bool: - """Check image magic bytes.""" - # JPEG - if data[:2] == b'\xff\xd8': - return True - # PNG - if data[:8] == b'\x89PNG\r\n\x1a\n': - return True - # WebP - if data[:4] == b'RIFF' and data[8:12] == b'WEBP': - return True - return False - -class InputSanitizer: - """Sanitize user inputs to prevent injection attacks.""" - - # Patterns for dangerous inputs - SCRIPT_PATTERN = re.compile(r']*>.*?', re.IGNORECASE | re.DOTALL) - SQL_INJECTION_PATTERN = re.compile(r"(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|OR|AND)\b)", re.IGNORECASE) - - @staticmethod - def sanitize_string(value: str, max_length: int = 255) -> str: - """Sanitize a string input.""" - if not value: - return value - - # Truncate - value = value[:max_length] - - # Remove null bytes - value = value.replace('\x00', '') - - # Strip HTML tags for non-rich-text fields - value = re.sub(r'<[^>]+>', '', value) - - return value.strip() - - @staticmethod - def sanitize_metadata(metadata: dict, max_depth: int = 3) -> dict: - """Sanitize metadata dictionary.""" - if not metadata: - return {} - - def sanitize_value(val, depth=0): - if depth > max_depth: - return None - if isinstance(val, str): - return InputSanitizer.sanitize_string(val) - if isinstance(val, dict): - return {k: sanitize_value(v, depth + 1) for k, v in val.items()} - if isinstance(val, list): - return [sanitize_value(v, depth + 1) for v in val[:100]] # Limit list size - if isinstance(val, (int, float, bool, type(None))): - return val - return str(val)[:255] - - return sanitize_value(metadata) -``` - -### 6.3 Rate Limiting Enhancements - -```python -# app/infrastructure/resilience/advanced_rate_limiter.py -from dataclasses import dataclass -from typing import Dict, Optional -import time - -@dataclass -class RateLimitConfig: - """Rate limit configuration.""" - requests_per_second: int = 5 - requests_per_minute: int = 60 - burst_size: int = 10 - penalty_multiplier: float = 2.0 - max_violations_before_block: int = 5 - block_duration_seconds: int = 300 - -class AdvancedRateLimiter: - """Advanced rate limiter with adaptive throttling.""" - - def __init__(self, config: RateLimitConfig): - self._config = config - self._sessions: Dict[str, SessionRateState] = {} - - async def check_rate_limit(self, session_id: str) -> RateLimitResult: - """Check if request is allowed under rate limits.""" - state = self._get_or_create_state(session_id) - - # Check if blocked - if state.is_blocked(): - return RateLimitResult( - allowed=False, - reason="blocked", - retry_after=state.block_expires_in(), - ) - - # Check burst limit - if not state.check_burst(): - state.record_violation() - return RateLimitResult( - allowed=False, - reason="burst_exceeded", - retry_after=1, - ) - - # Check sustained limit - if not state.check_sustained(): - state.record_violation() - return RateLimitResult( - allowed=False, - reason="rate_exceeded", - retry_after=state.seconds_until_reset(), - ) - - # Record request - state.record_request() - - return RateLimitResult( - allowed=True, - remaining=state.remaining_requests(), - reset_in=state.seconds_until_reset(), - ) - - async def get_session_stats(self, session_id: str) -> dict: - """Get rate limit statistics for a session.""" - state = self._sessions.get(session_id) - if not state: - return { - "frames_last_minute": 0, - "remaining_this_minute": self._config.requests_per_minute, - "violation_count": 0, - "is_throttled": False, - } - - return { - "frames_last_minute": state.requests_last_minute(), - "remaining_this_minute": state.remaining_requests(), - "violation_count": state.violation_count, - "is_throttled": state.is_blocked(), - "throttle_expires_in": state.block_expires_in() if state.is_blocked() else None, - } -``` - -### 6.4 Authentication & Authorization - -```python -# app/api/middleware/auth.py -from fastapi import Request, HTTPException, status -from fastapi.security import APIKeyHeader -import hashlib -import hmac - -class ProctorAuthMiddleware: - """Authentication middleware for proctoring endpoints.""" - - def __init__(self, api_key_repository, settings): - self._repository = api_key_repository - self._settings = settings - - async def authenticate(self, request: Request) -> AuthContext: - """Authenticate request and return context.""" - # Extract API key - api_key = request.headers.get("X-API-Key") - if not api_key: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Missing API key", - ) - - # Validate key (constant-time comparison) - key_record = await self._repository.get_by_key_hash( - self._hash_key(api_key) - ) - - if not key_record or not key_record.is_active: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid API key", - ) - - # Check permissions - tenant_id = request.headers.get("X-Tenant-ID") - if tenant_id and tenant_id not in key_record.allowed_tenants: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Not authorized for this tenant", - ) - - return AuthContext( - api_key_id=key_record.id, - tenant_id=tenant_id or key_record.default_tenant, - permissions=key_record.permissions, - ) - - def _hash_key(self, key: str) -> str: - """Hash API key for storage comparison.""" - return hashlib.sha256(key.encode()).hexdigest() -``` - -### 6.5 Security Headers - -```python -# app/api/middleware/security_headers.py -from starlette.middleware.base import BaseHTTPMiddleware - -class SecurityHeadersMiddleware(BaseHTTPMiddleware): - """Add security headers to all responses.""" - - async def dispatch(self, request, call_next): - response = await call_next(request) - - # Prevent clickjacking - response.headers["X-Frame-Options"] = "DENY" - - # Prevent MIME type sniffing - response.headers["X-Content-Type-Options"] = "nosniff" - - # XSS protection - response.headers["X-XSS-Protection"] = "1; mode=block" - - # Referrer policy - response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" - - # Content Security Policy - response.headers["Content-Security-Policy"] = ( - "default-src 'self'; " - "img-src 'self' data:; " - "script-src 'self'; " - "style-src 'self' 'unsafe-inline'" - ) - - # HSTS (in production) - if not request.url.hostname == "localhost": - response.headers["Strict-Transport-Security"] = ( - "max-age=31536000; includeSubDomains" - ) - - return response -``` - -### 6.6 Configuration - -```python -# Security settings -SECURITY_API_KEY_MIN_LENGTH: int = 32 -SECURITY_ENABLE_RATE_LIMITING: bool = True -SECURITY_MAX_REQUEST_SIZE_MB: int = 50 -SECURITY_ALLOWED_HOSTS: List[str] = ["*"] -SECURITY_CSRF_ENABLED: bool = False # Not needed for API-only -SECURITY_AUDIT_LOG_ENABLED: bool = True -``` - ---- - -## 7. SE Checklist Compliance - -### 7.1 SOLID Principles - -| Principle | Implementation | -|-----------|----------------| -| **S**ingle Responsibility | Each class has one purpose (ConnectionManager, FrameHandler, Authenticator) | -| **O**pen/Closed | Extensible via interfaces (IProctorSessionRepository, metrics collectors) | -| **L**iskov Substitution | All repository implementations are interchangeable | -| **I**nterface Segregation | Small, focused interfaces (IProctorSessionRepository vs IProctorIncidentRepository) | -| **D**ependency Inversion | All dependencies injected via FastAPI DI | - -### 7.2 Design Patterns Used - -| Pattern | Usage | -|---------|-------| -| Repository | PostgresSessionRepository, MemoryProctorSessionRepository | -| Factory | ProctorRepositoryFactory, MLComponentFactory | -| Strategy | Different rate limiting strategies, authentication methods | -| Observer | WebSocket ConnectionManager (broadcast to monitors) | -| Decorator | @traced for adding tracing to functions | -| Singleton | Connection pool, metrics collector | - -### 7.3 Best Practices - -| Practice | Implementation | -|----------|----------------| -| **DRY** | Shared validators, common error handling, reusable components | -| **KISS** | Simple message protocol, clear API contracts | -| **YAGNI** | Only implemented features that are needed | -| **Separation of Concerns** | Clear layers (API, Application, Domain, Infrastructure) | -| **Defensive Programming** | Input validation, error handling, null checks | -| **Fail Fast** | Early validation, clear error messages | - -### 7.4 Security Checklist - -- [x] Input validation on all endpoints -- [x] Rate limiting with adaptive throttling -- [x] Authentication via API keys -- [x] Authorization per tenant -- [x] Security headers middleware -- [x] SQL injection prevention (parameterized queries) -- [x] XSS prevention (no user HTML rendering) -- [x] CSRF protection (API uses tokens, not cookies) -- [x] Audit logging for security events - -### 7.5 Testing Strategy - -| Test Type | Coverage Target | -|-----------|-----------------| -| Unit Tests | >80% code coverage | -| Integration Tests | All API endpoints | -| E2E Tests | Critical user workflows | -| Performance Tests | Response time < 100ms p95 | -| Security Tests | OWASP Top 10 | - ---- - -## Implementation Plan - -### Phase 3.1: WebSocket Streaming (Priority: High) -1. Create connection manager -2. Implement WebSocket endpoint -3. Add binary frame protocol -4. Add authentication -5. Write tests - -### Phase 3.2: PostgreSQL Integration (Priority: High) -1. Create database migrations -2. Implement pool manager -3. Update repository factory -4. Add health checks -5. Write tests - -### Phase 3.3: Observability (Priority: Medium) -1. Add proctoring metrics -2. Implement structured logging -3. Configure tracing -4. Update middleware -5. Write tests - -### Phase 3.4: API Documentation (Priority: Medium) -1. Enhance schemas with examples -2. Add OpenAPI tags -3. Document error responses -4. Generate SDK stubs - -### Phase 3.5: E2E Tests (Priority: Medium) -1. Set up test fixtures -2. Implement workflow tests -3. Add WebSocket tests -4. Add concurrent tests - -### Phase 3.6: Security Hardening (Priority: High) -1. Implement input validators -2. Enhance rate limiter -3. Add security headers -4. Add audit logging -5. Security testing - ---- - -## Acceptance Criteria - -1. **WebSocket**: Real-time frame streaming with <50ms latency -2. **PostgreSQL**: Persistent storage with connection pooling -3. **Observability**: Full metrics, logs, and traces available -4. **Documentation**: Complete OpenAPI spec with examples -5. **E2E Tests**: All critical workflows tested -6. **Security**: Pass security review, no critical vulnerabilities diff --git a/docs/design/PROCTORING_SERVICE_DESIGN.md b/docs/design/PROCTORING_SERVICE_DESIGN.md deleted file mode 100644 index 3f626c9..0000000 --- a/docs/design/PROCTORING_SERVICE_DESIGN.md +++ /dev/null @@ -1,3233 +0,0 @@ -# Proctoring Service - Technical Design Document - -**Version:** 1.1 -**Date:** December 2024 -**Status:** PROPOSED -**Author:** Architecture Team -**Updated:** Added deepfake detection, circuit breaker, per-session rate limiting - ---- - -## Table of Contents - -1. [Overview](#1-overview) -2. [Architecture](#2-architecture) -3. [Domain Model](#3-domain-model) -4. [Use Cases](#4-use-cases) -5. [API Specification](#5-api-specification) -6. [Infrastructure Components](#6-infrastructure-components) -7. [Data Flow](#7-data-flow) -8. [Configuration](#8-configuration) -9. [Security & Privacy](#9-security--privacy) -10. [Monitoring & Observability](#10-monitoring--observability) -11. [Implementation Phases](#11-implementation-phases) -12. [Validation Checklist](#12-validation-checklist) - ---- - -## 1. Overview - -### 1.1 Purpose - -Design a continuous identity verification and proctoring service that monitors exam-takers in real-time, detects suspicious behavior, and ensures exam integrity through multi-modal analysis. - -### 1.2 Goals - -| Goal | Description | Success Metric | -|------|-------------|----------------| -| **Continuous Identity** | Verify user remains the same throughout session | >99% true positive rate | -| **Cheating Detection** | Detect unauthorized materials, persons, behaviors | <5% false positive rate | -| **Real-time Alerts** | Notify on suspicious activity immediately | <2s alert latency | -| **Scalability** | Handle concurrent proctoring sessions | 10,000+ concurrent sessions | -| **Privacy Compliance** | Meet GDPR/CCPA requirements | Full compliance | - -### 1.3 Non-Goals (Out of Scope for MVP) - -- Browser lockdown (client-side responsibility) -- Live human proctor interface -- Secondary camera (360°) support -- Keystroke/mouse behavioral biometrics - -### 1.4 Design Principles - -1. **Clean Architecture** - Domain logic independent of frameworks -2. **SOLID Principles** - Single responsibility, open for extension -3. **Event-Driven** - Decouple components via events/webhooks -4. **Privacy-First** - Minimize data collection, maximize transparency -5. **Fail-Safe** - Graceful degradation, no false terminations - ---- - -## 2. Architecture - -### 2.1 High-Level Architecture - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ CLIENT │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ │ -│ │ Webcam │ │ Microphone │ │ Screen │ │ Exam Platform │ │ -│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └───────┬───────┘ │ -│ │ │ │ │ │ -│ └────────────────┴────────────────┴──────────────────┘ │ -│ │ │ -│ Frame Capture SDK │ -└───────────────────────────────────┼──────────────────────────────────────┘ - │ HTTPS/WebSocket - ▼ -┌───────────────────────────────────────────────────────────────────────────┐ -│ API GATEWAY │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ API Key │ │ Rate │ │ Request │ │ Routing │ │ -│ │ Auth │ │ Limiting │ │ Validation │ │ │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ -└───────────────────────────────────┼───────────────────────────────────────┘ - │ -┌───────────────────────────────────┼───────────────────────────────────────┐ -│ PROCTORING SERVICE │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ SESSION MANAGER │ │ -│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ -│ │ │ Create │ │ Start │ │ Monitor │ │ Pause │ │ End │ │ │ -│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ┌─────────────────────────────────┼───────────────────────────────────┐ │ -│ │ ANALYSIS PIPELINE │ │ -│ │ │ │ -│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ -│ │ │ Face │ │ Gaze │ │ Object │ │ Audio │ │ │ -│ │ │ Verifier │ │ Tracker │ │ Detector │ │ Analyzer │ │ │ -│ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ -│ │ │ │ │ │ │ │ -│ │ └───────────────┴───────────────┴───────────────┘ │ │ -│ │ │ │ │ -│ │ ┌──────────┴──────────┐ │ │ -│ │ │ FUSION ENGINE │ │ │ -│ │ │ (Risk Aggregator) │ │ │ -│ │ └──────────┬──────────┘ │ │ -│ └───────────────────────────────┼─────────────────────────────────────┘ │ -│ │ │ -│ ┌───────────────────────────────┼─────────────────────────────────────┐ │ -│ │ INCIDENT MANAGER │ │ -│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ -│ │ │ Create │ │ Classify│ │ Score │ │Evidence │ │ Notify │ │ │ -│ │ │Incident │ │Severity │ │ Risk │ │ Capture │ │(Webhook)│ │ │ -│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -└───────────────────────────────────┼───────────────────────────────────────┘ - │ -┌───────────────────────────────────┼───────────────────────────────────────┐ -│ DATA LAYER │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ PostgreSQL │ │ Redis │ │ S3 │ │ TimeSeries │ │ -│ │ (Sessions, │ │ (Cache, │ │ (Evidence │ │ (Metrics, │ │ -│ │ Incidents) │ │ State) │ │ Storage) │ │ Events) │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ -└────────────────────────────────────────────────────────────────────────────┘ -``` - -### 2.2 Component Responsibilities - -| Component | Responsibility | Technology | -|-----------|---------------|------------| -| **Session Manager** | Lifecycle management, state machine | Python/FastAPI | -| **Face Verifier** | Continuous identity verification | DeepFace, existing code | -| **Deepfake Detector** | Detect synthetic/manipulated faces | Custom CNN, frequency analysis | -| **Gaze Tracker** | Head pose & eye direction monitoring | MediaPipe | -| **Object Detector** | Phone/book/person detection | YOLOv8 | -| **Audio Analyzer** | Voice activity, multiple speakers | WebRTC VAD | -| **Fusion Engine** | Aggregate signals, compute risk | Custom Python | -| **Incident Manager** | Flag, classify, store incidents | Python | -| **Circuit Breaker** | ML failure resilience, graceful degradation | pybreaker | -| **Rate Limiter** | Per-session frame submission throttling | Redis + sliding window | - ---- - -## 3. Domain Model - -### 3.1 Entity Relationship Diagram - -``` -┌─────────────────────┐ ┌─────────────────────┐ -│ ProctorSession │ │ SessionConfig │ -├─────────────────────┤ ├─────────────────────┤ -│ id: UUID │──────▶│ id: UUID │ -│ exam_id: str │ │ verification_interval│ -│ user_id: str │ │ gaze_threshold │ -│ tenant_id: str │ │ object_detection │ -│ config_id: UUID │ │ audio_monitoring │ -│ status: SessionStatus│ │ risk_thresholds │ -│ risk_score: float │ └─────────────────────┘ -│ started_at: datetime│ -│ ended_at: datetime │ ┌─────────────────────┐ -│ baseline_embedding │──────▶│ FaceEmbedding │ -└─────────┬───────────┘ │ (existing) │ - │ └─────────────────────┘ - │ 1:N - ▼ -┌─────────────────────┐ ┌─────────────────────┐ -│ ProctorIncident │ │ IncidentEvidence │ -├─────────────────────┤ ├─────────────────────┤ -│ id: UUID │──────▶│ id: UUID │ -│ session_id: UUID │ │ incident_id: UUID │ -│ type: IncidentType │ │ type: EvidenceType │ -│ severity: Severity │ │ storage_url: str │ -│ confidence: float │ │ metadata: dict │ -│ timestamp: datetime │ │ created_at: datetime│ -│ details: dict │ └─────────────────────┘ -│ reviewed: bool │ -│ reviewer_action: str│ -└─────────────────────┘ - -┌─────────────────────┐ ┌─────────────────────┐ -│ VerificationEvent │ │ GazeEvent │ -├─────────────────────┤ ├─────────────────────┤ -│ id: UUID │ │ id: UUID │ -│ session_id: UUID │ │ session_id: UUID │ -│ timestamp: datetime │ │ timestamp: datetime │ -│ face_detected: bool │ │ head_pose: HeadPose │ -│ face_matched: bool │ │ gaze_direction: Vec │ -│ confidence: float │ │ on_screen: bool │ -│ liveness_score: float│ │ duration_off: float │ -│ quality_score: float│ └─────────────────────┘ -└─────────────────────┘ -``` - -### 3.2 Domain Entities - -#### 3.2.1 ProctorSession - -```python -# app/domain/entities/proctor_session.py - -from dataclasses import dataclass, field -from datetime import datetime -from enum import Enum -from typing import Any, Dict, List, Optional -from uuid import UUID, uuid4 - -import numpy as np - - -class SessionStatus(str, Enum): - """Proctor session status states.""" - - CREATED = "created" # Session created, not started - INITIALIZING = "initializing" # Initial verification in progress - ACTIVE = "active" # Session running, monitoring active - PAUSED = "paused" # Temporarily paused (bathroom, etc.) - FLAGGED = "flagged" # High risk, requires attention - COMPLETED = "completed" # Normal completion - TERMINATED = "terminated" # Forcibly ended (cheating detected) - EXPIRED = "expired" # Session timed out - - -class TerminationReason(str, Enum): - """Reasons for session termination.""" - - NORMAL_COMPLETION = "normal_completion" - USER_ENDED = "user_ended" - PROCTOR_ENDED = "proctor_ended" - IDENTITY_FAILURE = "identity_failure" - MULTIPLE_PERSONS = "multiple_persons" - CRITICAL_VIOLATION = "critical_violation" - TECHNICAL_FAILURE = "technical_failure" - TIMEOUT = "timeout" - - -@dataclass -class SessionConfig: - """Configuration for proctoring session behavior. - - Attributes: - verification_interval_sec: Seconds between face verifications - verification_threshold: Minimum confidence for successful verification - max_verification_failures: Failures before flagging - gaze_away_threshold_sec: Seconds looking away before incident - gaze_sensitivity: Sensitivity of gaze detection (0.0-1.0) - enable_object_detection: Whether to detect phones/books - enable_audio_monitoring: Whether to analyze audio - enable_multi_face_detection: Whether to detect additional persons - risk_threshold_warning: Risk score threshold for warning - risk_threshold_critical: Risk score threshold for critical - max_pause_duration_sec: Maximum allowed pause duration - session_timeout_sec: Maximum session duration - """ - - # Verification settings - verification_interval_sec: int = 60 - verification_threshold: float = 0.6 - max_verification_failures: int = 3 - - # Gaze tracking settings - gaze_away_threshold_sec: float = 5.0 - gaze_sensitivity: float = 0.7 - - # Detection toggles - enable_object_detection: bool = True - enable_audio_monitoring: bool = True - enable_multi_face_detection: bool = True - - # Risk thresholds - risk_threshold_warning: float = 0.5 - risk_threshold_critical: float = 0.8 - - # Session limits - max_pause_duration_sec: int = 300 # 5 minutes - session_timeout_sec: int = 14400 # 4 hours - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary.""" - return { - "verification_interval_sec": self.verification_interval_sec, - "verification_threshold": self.verification_threshold, - "max_verification_failures": self.max_verification_failures, - "gaze_away_threshold_sec": self.gaze_away_threshold_sec, - "gaze_sensitivity": self.gaze_sensitivity, - "enable_object_detection": self.enable_object_detection, - "enable_audio_monitoring": self.enable_audio_monitoring, - "enable_multi_face_detection": self.enable_multi_face_detection, - "risk_threshold_warning": self.risk_threshold_warning, - "risk_threshold_critical": self.risk_threshold_critical, - "max_pause_duration_sec": self.max_pause_duration_sec, - "session_timeout_sec": self.session_timeout_sec, - } - - -@dataclass -class ProctorSession: - """Proctoring session entity. - - Represents a single proctored exam session with all associated - state and metrics. Follows the Entity pattern from DDD. - - Attributes: - id: Unique session identifier - exam_id: External exam/assessment identifier - user_id: User being proctored - tenant_id: Multi-tenancy identifier - config: Session configuration - status: Current session status - risk_score: Aggregated risk score (0.0-1.0) - created_at: Session creation timestamp - started_at: When monitoring actually started - ended_at: When session ended - baseline_embedding: Reference face embedding for verification - verification_count: Total verifications performed - verification_failures: Count of failed verifications - incident_count: Total incidents recorded - total_gaze_away_sec: Cumulative time looking away - termination_reason: Reason if terminated - metadata: Additional session metadata - """ - - id: UUID - exam_id: str - user_id: str - tenant_id: str - config: SessionConfig - status: SessionStatus = SessionStatus.CREATED - risk_score: float = 0.0 - created_at: datetime = field(default_factory=datetime.utcnow) - started_at: Optional[datetime] = None - ended_at: Optional[datetime] = None - paused_at: Optional[datetime] = None - baseline_embedding: Optional[np.ndarray] = None - verification_count: int = 0 - verification_failures: int = 0 - incident_count: int = 0 - total_gaze_away_sec: float = 0.0 - termination_reason: Optional[TerminationReason] = None - metadata: Dict[str, Any] = field(default_factory=dict) - - def __post_init__(self) -> None: - """Validate session data.""" - if not self.exam_id: - raise ValueError("exam_id is required") - if not self.user_id: - raise ValueError("user_id is required") - if not 0.0 <= self.risk_score <= 1.0: - raise ValueError(f"risk_score must be 0-1, got {self.risk_score}") - - @classmethod - def create( - cls, - exam_id: str, - user_id: str, - tenant_id: str, - config: Optional[SessionConfig] = None, - metadata: Optional[Dict[str, Any]] = None, - ) -> "ProctorSession": - """Factory method to create a new session. - - Args: - exam_id: External exam identifier - user_id: User to proctor - tenant_id: Tenant identifier - config: Optional custom configuration - metadata: Optional additional metadata - - Returns: - New ProctorSession instance - """ - return cls( - id=uuid4(), - exam_id=exam_id, - user_id=user_id, - tenant_id=tenant_id, - config=config or SessionConfig(), - metadata=metadata or {}, - ) - - def can_start(self) -> bool: - """Check if session can be started.""" - return self.status == SessionStatus.CREATED - - def can_pause(self) -> bool: - """Check if session can be paused.""" - return self.status == SessionStatus.ACTIVE - - def can_resume(self) -> bool: - """Check if session can be resumed.""" - return self.status == SessionStatus.PAUSED - - def can_end(self) -> bool: - """Check if session can be ended normally.""" - return self.status in ( - SessionStatus.ACTIVE, - SessionStatus.PAUSED, - SessionStatus.FLAGGED, - ) - - def is_active(self) -> bool: - """Check if session is actively monitoring.""" - return self.status == SessionStatus.ACTIVE - - def is_terminal(self) -> bool: - """Check if session is in terminal state.""" - return self.status in ( - SessionStatus.COMPLETED, - SessionStatus.TERMINATED, - SessionStatus.EXPIRED, - ) - - def start(self, baseline_embedding: np.ndarray) -> None: - """Start the proctoring session. - - Args: - baseline_embedding: Initial face embedding for verification - - Raises: - ValueError: If session cannot be started - """ - if not self.can_start(): - raise ValueError(f"Cannot start session in status: {self.status}") - - self.status = SessionStatus.ACTIVE - self.started_at = datetime.utcnow() - self.baseline_embedding = baseline_embedding - - def pause(self) -> None: - """Pause the session.""" - if not self.can_pause(): - raise ValueError(f"Cannot pause session in status: {self.status}") - - self.status = SessionStatus.PAUSED - self.paused_at = datetime.utcnow() - - def resume(self) -> None: - """Resume a paused session.""" - if not self.can_resume(): - raise ValueError(f"Cannot resume session in status: {self.status}") - - self.status = SessionStatus.ACTIVE - self.paused_at = None - - def complete(self) -> None: - """Complete the session normally.""" - if not self.can_end(): - raise ValueError(f"Cannot complete session in status: {self.status}") - - self.status = SessionStatus.COMPLETED - self.ended_at = datetime.utcnow() - self.termination_reason = TerminationReason.NORMAL_COMPLETION - - def terminate(self, reason: TerminationReason) -> None: - """Terminate the session with reason. - - Args: - reason: Why session was terminated - """ - if self.is_terminal(): - return # Already terminated - - self.status = SessionStatus.TERMINATED - self.ended_at = datetime.utcnow() - self.termination_reason = reason - - def flag(self) -> None: - """Flag session as high risk.""" - if self.status == SessionStatus.ACTIVE: - self.status = SessionStatus.FLAGGED - - def update_risk_score(self, score: float) -> None: - """Update the aggregated risk score. - - Args: - score: New risk score (0.0-1.0) - """ - self.risk_score = max(0.0, min(1.0, score)) - - # Auto-flag if critical threshold exceeded - if self.risk_score >= self.config.risk_threshold_critical: - self.flag() - - def record_verification(self, success: bool) -> None: - """Record a verification attempt. - - Args: - success: Whether verification succeeded - """ - self.verification_count += 1 - if not success: - self.verification_failures += 1 - - def record_incident(self) -> None: - """Increment incident count.""" - self.incident_count += 1 - - def add_gaze_away_time(self, seconds: float) -> None: - """Add time spent looking away. - - Args: - seconds: Seconds spent looking away - """ - self.total_gaze_away_sec += seconds - - def get_duration_seconds(self) -> float: - """Get session duration in seconds.""" - if not self.started_at: - return 0.0 - - end = self.ended_at or datetime.utcnow() - return (end - self.started_at).total_seconds() - - def get_verification_success_rate(self) -> float: - """Get verification success rate.""" - if self.verification_count == 0: - return 1.0 - - successes = self.verification_count - self.verification_failures - return successes / self.verification_count - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary for serialization.""" - return { - "id": str(self.id), - "exam_id": self.exam_id, - "user_id": self.user_id, - "tenant_id": self.tenant_id, - "status": self.status.value, - "risk_score": self.risk_score, - "created_at": self.created_at.isoformat(), - "started_at": self.started_at.isoformat() if self.started_at else None, - "ended_at": self.ended_at.isoformat() if self.ended_at else None, - "verification_count": self.verification_count, - "verification_failures": self.verification_failures, - "incident_count": self.incident_count, - "total_gaze_away_sec": self.total_gaze_away_sec, - "termination_reason": self.termination_reason.value if self.termination_reason else None, - "duration_seconds": self.get_duration_seconds(), - "verification_success_rate": self.get_verification_success_rate(), - "config": self.config.to_dict(), - "metadata": self.metadata, - } -``` - -#### 3.2.2 ProctorIncident - -```python -# app/domain/entities/proctor_incident.py - -from dataclasses import dataclass, field -from datetime import datetime -from enum import Enum -from typing import Any, Dict, List, Optional -from uuid import UUID, uuid4 - - -class IncidentType(str, Enum): - """Types of proctoring incidents.""" - - # Identity incidents - FACE_NOT_DETECTED = "face_not_detected" - FACE_NOT_MATCHED = "face_not_matched" - MULTIPLE_FACES = "multiple_faces" - LIVENESS_FAILED = "liveness_failed" - DEEPFAKE_DETECTED = "deepfake_detected" # Synthetic/manipulated face detected - - # Attention incidents - GAZE_AWAY_PROLONGED = "gaze_away_prolonged" - HEAD_TURNED_AWAY = "head_turned_away" - USER_LEFT_FRAME = "user_left_frame" - - # Object incidents - PHONE_DETECTED = "phone_detected" - BOOK_DETECTED = "book_detected" - NOTES_DETECTED = "notes_detected" - ELECTRONIC_DEVICE = "electronic_device" - UNAUTHORIZED_OBJECT = "unauthorized_object" - - # Audio incidents - MULTIPLE_VOICES = "multiple_voices" - SUSPICIOUS_AUDIO = "suspicious_audio" - VOICE_ASSISTANT = "voice_assistant" - - # Environment incidents - PERSON_IN_BACKGROUND = "person_in_background" - SCREEN_SHARE_DETECTED = "screen_share_detected" - - # Technical incidents - CAMERA_BLOCKED = "camera_blocked" - CAMERA_SWITCHED = "camera_switched" - LOW_QUALITY_FEED = "low_quality_feed" - - # Session incidents - EXCESSIVE_PAUSES = "excessive_pauses" - SESSION_TIMEOUT = "session_timeout" - RATE_LIMIT_EXCEEDED = "rate_limit_exceeded" # Too many frame submissions - - -class IncidentSeverity(str, Enum): - """Severity levels for incidents.""" - - LOW = "low" # Minor, informational - MEDIUM = "medium" # Concerning, needs review - HIGH = "high" # Serious violation - CRITICAL = "critical" # Immediate action required - - -class ReviewAction(str, Enum): - """Actions taken after reviewing incident.""" - - DISMISSED = "dismissed" # False positive - ACKNOWLEDGED = "acknowledged" # Noted, no action - WARNING_ISSUED = "warning_issued" # Warning sent to user - SESSION_PAUSED = "session_paused" # Session paused for review - SESSION_TERMINATED = "session_terminated" # Session ended - ESCALATED = "escalated" # Escalated to human proctor - - -@dataclass -class IncidentEvidence: - """Evidence attached to an incident. - - Attributes: - id: Evidence identifier - incident_id: Parent incident - evidence_type: Type of evidence - storage_url: URL to stored evidence - thumbnail_url: Optional thumbnail - metadata: Additional metadata - created_at: When captured - """ - - id: UUID - incident_id: UUID - evidence_type: str # "image", "video_clip", "audio_clip", "screenshot" - storage_url: str - thumbnail_url: Optional[str] = None - metadata: Dict[str, Any] = field(default_factory=dict) - created_at: datetime = field(default_factory=datetime.utcnow) - - -@dataclass -class ProctorIncident: - """Proctoring incident entity. - - Represents a detected suspicious event during a proctoring session. - Immutable once created (except for review status). - - Attributes: - id: Unique incident identifier - session_id: Parent session - incident_type: Type of incident - severity: Severity level - confidence: Detection confidence (0.0-1.0) - timestamp: When incident occurred - details: Incident-specific details - evidence: List of evidence items - reviewed: Whether incident has been reviewed - reviewed_at: When reviewed - reviewed_by: Who reviewed - review_action: Action taken - review_notes: Reviewer notes - """ - - id: UUID - session_id: UUID - incident_type: IncidentType - severity: IncidentSeverity - confidence: float - timestamp: datetime - details: Dict[str, Any] = field(default_factory=dict) - evidence: List[IncidentEvidence] = field(default_factory=list) - reviewed: bool = False - reviewed_at: Optional[datetime] = None - reviewed_by: Optional[str] = None - review_action: Optional[ReviewAction] = None - review_notes: Optional[str] = None - - def __post_init__(self) -> None: - """Validate incident data.""" - if not 0.0 <= self.confidence <= 1.0: - raise ValueError(f"confidence must be 0-1, got {self.confidence}") - - @classmethod - def create( - cls, - session_id: UUID, - incident_type: IncidentType, - severity: IncidentSeverity, - confidence: float, - details: Optional[Dict[str, Any]] = None, - ) -> "ProctorIncident": - """Factory method to create incident. - - Args: - session_id: Parent session ID - incident_type: Type of incident - severity: Severity level - confidence: Detection confidence - details: Optional details - - Returns: - New ProctorIncident instance - """ - return cls( - id=uuid4(), - session_id=session_id, - incident_type=incident_type, - severity=severity, - confidence=confidence, - timestamp=datetime.utcnow(), - details=details or {}, - ) - - def add_evidence(self, evidence: IncidentEvidence) -> None: - """Add evidence to incident. - - Args: - evidence: Evidence to add - """ - self.evidence.append(evidence) - - def mark_reviewed( - self, - reviewer: str, - action: ReviewAction, - notes: Optional[str] = None, - ) -> None: - """Mark incident as reviewed. - - Args: - reviewer: Who reviewed - action: Action taken - notes: Optional notes - """ - self.reviewed = True - self.reviewed_at = datetime.utcnow() - self.reviewed_by = reviewer - self.review_action = action - self.review_notes = notes - - def get_risk_contribution(self) -> float: - """Calculate risk contribution based on severity and confidence. - - Returns: - Risk contribution (0.0-1.0) - """ - severity_weights = { - IncidentSeverity.LOW: 0.1, - IncidentSeverity.MEDIUM: 0.3, - IncidentSeverity.HIGH: 0.6, - IncidentSeverity.CRITICAL: 1.0, - } - - base_weight = severity_weights.get(self.severity, 0.1) - return base_weight * self.confidence - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary.""" - return { - "id": str(self.id), - "session_id": str(self.session_id), - "incident_type": self.incident_type.value, - "severity": self.severity.value, - "confidence": self.confidence, - "timestamp": self.timestamp.isoformat(), - "details": self.details, - "evidence_count": len(self.evidence), - "reviewed": self.reviewed, - "reviewed_at": self.reviewed_at.isoformat() if self.reviewed_at else None, - "reviewed_by": self.reviewed_by, - "review_action": self.review_action.value if self.review_action else None, - "review_notes": self.review_notes, - "risk_contribution": self.get_risk_contribution(), - } - - -# Severity mapping for incident types -INCIDENT_SEVERITY_MAP: Dict[IncidentType, IncidentSeverity] = { - # Critical - immediate action - IncidentType.MULTIPLE_FACES: IncidentSeverity.CRITICAL, - IncidentType.FACE_NOT_MATCHED: IncidentSeverity.CRITICAL, - IncidentType.PERSON_IN_BACKGROUND: IncidentSeverity.CRITICAL, - IncidentType.DEEPFAKE_DETECTED: IncidentSeverity.CRITICAL, # Highest priority - - # High - serious concern - IncidentType.PHONE_DETECTED: IncidentSeverity.HIGH, - IncidentType.LIVENESS_FAILED: IncidentSeverity.HIGH, - IncidentType.MULTIPLE_VOICES: IncidentSeverity.HIGH, - IncidentType.VOICE_ASSISTANT: IncidentSeverity.HIGH, - IncidentType.ELECTRONIC_DEVICE: IncidentSeverity.HIGH, - - # Medium - needs review - IncidentType.BOOK_DETECTED: IncidentSeverity.MEDIUM, - IncidentType.NOTES_DETECTED: IncidentSeverity.MEDIUM, - IncidentType.USER_LEFT_FRAME: IncidentSeverity.MEDIUM, - IncidentType.HEAD_TURNED_AWAY: IncidentSeverity.MEDIUM, - IncidentType.GAZE_AWAY_PROLONGED: IncidentSeverity.MEDIUM, - IncidentType.SUSPICIOUS_AUDIO: IncidentSeverity.MEDIUM, - IncidentType.RATE_LIMIT_EXCEEDED: IncidentSeverity.MEDIUM, # Suspicious behavior - - # Low - informational - IncidentType.FACE_NOT_DETECTED: IncidentSeverity.LOW, - IncidentType.CAMERA_BLOCKED: IncidentSeverity.LOW, - IncidentType.LOW_QUALITY_FEED: IncidentSeverity.LOW, - IncidentType.EXCESSIVE_PAUSES: IncidentSeverity.LOW, -} - - -def get_default_severity(incident_type: IncidentType) -> IncidentSeverity: - """Get default severity for incident type. - - Args: - incident_type: Type of incident - - Returns: - Default severity level - """ - return INCIDENT_SEVERITY_MAP.get(incident_type, IncidentSeverity.MEDIUM) -``` - -#### 3.2.3 Analysis Results - -```python -# app/domain/entities/proctor_analysis.py - -from dataclasses import dataclass, field -from datetime import datetime -from typing import Any, Dict, List, Optional, Tuple -from uuid import UUID - - -@dataclass(frozen=True) -class HeadPose: - """Head orientation in 3D space. - - Attributes: - pitch: Up/down rotation (degrees) - yaw: Left/right rotation (degrees) - roll: Tilt rotation (degrees) - """ - - pitch: float # Nodding: negative = down, positive = up - yaw: float # Shaking: negative = left, positive = right - roll: float # Tilting: negative = left, positive = right - - def is_facing_forward( - self, - pitch_threshold: float = 20.0, - yaw_threshold: float = 30.0, - ) -> bool: - """Check if head is facing approximately forward. - - Args: - pitch_threshold: Max pitch deviation - yaw_threshold: Max yaw deviation - - Returns: - True if facing forward within thresholds - """ - return abs(self.pitch) <= pitch_threshold and abs(self.yaw) <= yaw_threshold - - def to_dict(self) -> Dict[str, float]: - """Convert to dictionary.""" - return { - "pitch": self.pitch, - "yaw": self.yaw, - "roll": self.roll, - } - - -@dataclass(frozen=True) -class GazeDirection: - """Eye gaze direction estimation. - - Attributes: - x: Horizontal gaze (-1.0 to 1.0, negative = left) - y: Vertical gaze (-1.0 to 1.0, negative = down) - on_screen: Whether gaze is estimated to be on screen - confidence: Estimation confidence - """ - - x: float - y: float - on_screen: bool - confidence: float - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary.""" - return { - "x": self.x, - "y": self.y, - "on_screen": self.on_screen, - "confidence": self.confidence, - } - - -@dataclass -class GazeAnalysisResult: - """Result of gaze/attention analysis. - - Attributes: - session_id: Session being analyzed - timestamp: Analysis timestamp - head_pose: Detected head pose - gaze_direction: Estimated gaze direction - face_visible: Whether face is visible - looking_at_screen: Combined assessment - attention_score: Attention level (0.0-1.0) - off_screen_duration_sec: Current off-screen duration - """ - - session_id: UUID - timestamp: datetime - head_pose: Optional[HeadPose] - gaze_direction: Optional[GazeDirection] - face_visible: bool - looking_at_screen: bool - attention_score: float - off_screen_duration_sec: float = 0.0 - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary.""" - return { - "session_id": str(self.session_id), - "timestamp": self.timestamp.isoformat(), - "head_pose": self.head_pose.to_dict() if self.head_pose else None, - "gaze_direction": self.gaze_direction.to_dict() if self.gaze_direction else None, - "face_visible": self.face_visible, - "looking_at_screen": self.looking_at_screen, - "attention_score": self.attention_score, - "off_screen_duration_sec": self.off_screen_duration_sec, - } - - -@dataclass(frozen=True) -class DetectedObject: - """Detected object in frame. - - Attributes: - object_class: Classification (phone, book, person, etc.) - confidence: Detection confidence - bounding_box: (x, y, width, height) normalized 0-1 - is_prohibited: Whether object is prohibited - """ - - object_class: str - confidence: float - bounding_box: Tuple[float, float, float, float] - is_prohibited: bool = True - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary.""" - return { - "object_class": self.object_class, - "confidence": self.confidence, - "bounding_box": { - "x": self.bounding_box[0], - "y": self.bounding_box[1], - "width": self.bounding_box[2], - "height": self.bounding_box[3], - }, - "is_prohibited": self.is_prohibited, - } - - -@dataclass -class ObjectDetectionResult: - """Result of object detection analysis. - - Attributes: - session_id: Session being analyzed - timestamp: Analysis timestamp - objects: List of detected objects - prohibited_objects: Filtered list of prohibited objects - frame_clear: Whether frame is clear of prohibited objects - """ - - session_id: UUID - timestamp: datetime - objects: List[DetectedObject] = field(default_factory=list) - - @property - def prohibited_objects(self) -> List[DetectedObject]: - """Get only prohibited objects.""" - return [obj for obj in self.objects if obj.is_prohibited] - - @property - def frame_clear(self) -> bool: - """Check if frame is clear of prohibited objects.""" - return len(self.prohibited_objects) == 0 - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary.""" - return { - "session_id": str(self.session_id), - "timestamp": self.timestamp.isoformat(), - "objects": [obj.to_dict() for obj in self.objects], - "prohibited_count": len(self.prohibited_objects), - "frame_clear": self.frame_clear, - } - - -@dataclass -class AudioAnalysisResult: - """Result of audio analysis. - - Attributes: - session_id: Session being analyzed - timestamp: Analysis timestamp - voice_detected: Whether voice activity detected - speaker_count: Estimated number of speakers - background_noise_level: Background noise level (dB) - suspicious_keywords: Detected suspicious keywords - is_suspicious: Overall suspicion flag - """ - - session_id: UUID - timestamp: datetime - voice_detected: bool - speaker_count: int - background_noise_level: float - suspicious_keywords: List[str] = field(default_factory=list) - is_suspicious: bool = False - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary.""" - return { - "session_id": str(self.session_id), - "timestamp": self.timestamp.isoformat(), - "voice_detected": self.voice_detected, - "speaker_count": self.speaker_count, - "background_noise_level": self.background_noise_level, - "suspicious_keywords": self.suspicious_keywords, - "is_suspicious": self.is_suspicious, - } - - -@dataclass -class FrameAnalysisResult: - """Combined result of all frame analysis. - - Aggregates results from face verification, gaze tracking, - object detection, and audio analysis into a single result. - """ - - session_id: UUID - timestamp: datetime - frame_number: int - - # Face verification - face_detected: bool = True - face_matched: bool = True - face_confidence: float = 1.0 - face_count: int = 1 - liveness_score: float = 1.0 - quality_score: float = 1.0 - - # Gaze analysis - gaze_result: Optional[GazeAnalysisResult] = None - - # Object detection - object_result: Optional[ObjectDetectionResult] = None - - # Audio analysis - audio_result: Optional[AudioAnalysisResult] = None - - # Aggregated risk - frame_risk_score: float = 0.0 - incidents_generated: List[str] = field(default_factory=list) - - def calculate_risk_score(self) -> float: - """Calculate aggregated risk score for this frame. - - Returns: - Risk score (0.0-1.0) - """ - risks = [] - - # Identity risk - if not self.face_detected: - risks.append(0.3) - if not self.face_matched: - risks.append(0.9) - if self.face_count > 1: - risks.append(0.8) - if self.liveness_score < 0.5: - risks.append(0.7) - - # Attention risk - if self.gaze_result and not self.gaze_result.looking_at_screen: - risks.append(0.2 + (self.gaze_result.off_screen_duration_sec * 0.05)) - - # Object risk - if self.object_result and not self.object_result.frame_clear: - for obj in self.object_result.prohibited_objects: - if obj.object_class == "phone": - risks.append(0.7) - elif obj.object_class == "person": - risks.append(0.8) - else: - risks.append(0.5) - - # Audio risk - if self.audio_result and self.audio_result.is_suspicious: - if self.audio_result.speaker_count > 1: - risks.append(0.6) - else: - risks.append(0.3) - - # Aggregate (max with decay for multiple risks) - if not risks: - return 0.0 - - risks.sort(reverse=True) - total = risks[0] - for i, risk in enumerate(risks[1:], 1): - total += risk * (0.5 ** i) # Diminishing returns - - return min(1.0, total) - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary.""" - return { - "session_id": str(self.session_id), - "timestamp": self.timestamp.isoformat(), - "frame_number": self.frame_number, - "face_detected": self.face_detected, - "face_matched": self.face_matched, - "face_confidence": self.face_confidence, - "face_count": self.face_count, - "liveness_score": self.liveness_score, - "quality_score": self.quality_score, - "gaze": self.gaze_result.to_dict() if self.gaze_result else None, - "objects": self.object_result.to_dict() if self.object_result else None, - "audio": self.audio_result.to_dict() if self.audio_result else None, - "frame_risk_score": self.frame_risk_score, - "incidents_generated": self.incidents_generated, - } -``` - ---- - -## 4. Use Cases - -### 4.1 Use Case Diagram - -``` - ┌─────────────────────────────────────────┐ - │ PROCTORING SERVICE │ - │ │ - ┌───────┐ │ ┌─────────────────────────────────┐ │ - │ │ │ │ Session Management │ │ - │ │───────┼─▶│ - CreateSession │ │ - │ │ │ │ - StartSession │ │ - │ Exam │ │ │ - PauseSession │ │ - │Platform│ │ │ - ResumeSession │ │ - │ │ │ │ - EndSession │ │ - │ │ │ │ - GetSessionStatus │ │ - │ │ │ └─────────────────────────────────┘ │ - │ │ │ │ - │ │ │ ┌─────────────────────────────────┐ │ - │ │───────┼─▶│ Verification │ │ - │ │ │ │ - SubmitFrame │ │ - │ │ │ │ - VerifyIdentity │ │ - │ │ │ │ - CheckLiveness │ │ - └───────┘ │ └─────────────────────────────────┘ │ - │ │ - ┌───────┐ │ ┌─────────────────────────────────┐ │ - │ │ │ │ Analysis │ │ - │ │───────┼─▶│ - AnalyzeGaze │ │ - │ Test │ │ │ - DetectObjects │ │ - │ Taker │ │ │ - AnalyzeAudio │ │ - │ │ │ │ - AnalyzeFrame │ │ - │ │ │ └─────────────────────────────────┘ │ - └───────┘ │ │ - │ ┌─────────────────────────────────┐ │ - ┌───────┐ │ │ Incident Management │ │ ┌─────────┐ - │ │ │ │ - CreateIncident │───┼──────▶│ Webhook │ - │Proctor│───────┼─▶│ - ListIncidents │ │ │ System │ - │/Admin │ │ │ - ReviewIncident │ │ └─────────┘ - │ │ │ │ - GetSessionReport │ │ - │ │ │ └─────────────────────────────────┘ │ - └───────┘ │ │ - └─────────────────────────────────────────┘ -``` - -### 4.2 Use Case Specifications - -#### UC-01: Create Proctoring Session - -```yaml -Name: CreateProctorSession -Actor: Exam Platform -Preconditions: - - Valid API key with proctoring scope - - User exists and is enrolled (has face embedding) - -Input: - exam_id: string (required) - user_id: string (required) - config: SessionConfig (optional) - metadata: object (optional) - -Output: - session: ProctorSession - -Flow: - 1. Validate API key and permissions - 2. Verify user has enrolled face embedding - 3. Create session with CREATED status - 4. Store session in repository - 5. Return session details - -Exceptions: - - UserNotEnrolled: User has no face embedding - - InvalidConfiguration: Config values out of range - - QuotaExceeded: Tenant has reached session limit -``` - -#### UC-02: Start Proctoring Session - -```yaml -Name: StartProctorSession -Actor: Exam Platform -Preconditions: - - Session exists in CREATED status - - Initial frame provided for verification - -Input: - session_id: UUID (required) - initial_frame: Image (required) - -Output: - session: ProctorSession (updated) - verification: VerificationResult - -Flow: - 1. Load session by ID - 2. Validate session can be started - 3. Perform initial face verification against enrolled embedding - 4. Perform liveness check - 5. If verification passes: - a. Store baseline embedding from frame - b. Update session status to ACTIVE - c. Set started_at timestamp - 6. If verification fails: - a. Return failure with details - b. Session remains in CREATED status - 7. Trigger session.started webhook - 8. Return updated session and verification result - -Exceptions: - - SessionNotFound: Session ID not found - - InvalidSessionState: Session not in CREATED status - - VerificationFailed: Face doesn't match enrolled user - - LivenessCheckFailed: Liveness detection failed -``` - -#### UC-03: Submit Frame for Analysis - -```yaml -Name: SubmitFrame -Actor: Test Taker (via SDK) -Preconditions: - - Session is ACTIVE - - Frame meets quality requirements - -Input: - session_id: UUID (required) - frame: Image (required) - audio_chunk: bytes (optional) - client_timestamp: datetime (optional) - -Output: - analysis: FrameAnalysisResult - incidents: List[ProctorIncident] - session_status: SessionStatus - -Flow: - 1. Validate session is active - 2. Check frame quality (blur, lighting, size) - 3. Run analysis pipeline in parallel: - a. Face detection and verification - b. Multi-face detection - c. Gaze/head pose analysis - d. Object detection - e. Audio analysis (if provided) - 4. Aggregate results into FrameAnalysisResult - 5. Generate incidents for violations - 6. Update session risk score - 7. Check if session should be flagged/terminated - 8. Store analysis event (sampling) - 9. Return results - -Exceptions: - - SessionNotActive: Session not in active state - - LowQualityFrame: Frame doesn't meet requirements - - AnalysisTimeout: Analysis took too long -``` - -#### UC-04: Periodic Verification - -```yaml -Name: PeriodicVerification -Actor: System (scheduled) -Preconditions: - - Session is ACTIVE - - Verification interval elapsed - -Input: - session_id: UUID - frame: Image - -Output: - verified: bool - confidence: float - incidents: List[ProctorIncident] - -Flow: - 1. Load session and baseline embedding - 2. Detect face in frame - 3. Extract embedding from detected face - 4. Compare against baseline embedding - 5. Perform liveness check - 6. If verification fails: - a. Increment failure count - b. Create incident - c. If max failures exceeded, flag session - 7. Update session verification stats - 8. Return result - -Exceptions: - - NoFaceDetected: No face in frame - - VerificationFailed: Face doesn't match baseline -``` - -#### UC-05: Review Incident - -```yaml -Name: ReviewIncident -Actor: Proctor/Admin -Preconditions: - - Incident exists and is not reviewed - - User has review permissions - -Input: - incident_id: UUID (required) - action: ReviewAction (required) - notes: string (optional) - -Output: - incident: ProctorIncident (updated) - session: ProctorSession (if affected) - -Flow: - 1. Load incident by ID - 2. Validate incident not already reviewed - 3. Apply review action: - - DISMISSED: Mark as false positive - - ACKNOWLEDGED: Note and continue - - WARNING_ISSUED: Send warning to user - - SESSION_PAUSED: Pause the session - - SESSION_TERMINATED: End the session - - ESCALATED: Notify human proctor - 4. Update incident with review details - 5. If session affected, update session status - 6. Trigger incident.reviewed webhook - 7. Return updated incident - -Exceptions: - - IncidentNotFound: Incident ID not found - - AlreadyReviewed: Incident already reviewed - - InvalidAction: Action not appropriate for incident -``` - ---- - -## 5. API Specification - -### 5.1 REST Endpoints - -#### Session Management - -```yaml -# Create Session -POST /api/v1/proctor/sessions -Request: - Content-Type: application/json - X-API-Key: {api_key} - Body: - exam_id: string (required) - user_id: string (required) - config: - verification_interval_sec: integer (default: 60) - verification_threshold: number (default: 0.6) - gaze_away_threshold_sec: number (default: 5.0) - enable_object_detection: boolean (default: true) - enable_audio_monitoring: boolean (default: true) - metadata: object (optional) -Response: - 201 Created: - id: string (UUID) - exam_id: string - user_id: string - status: "created" - config: object - created_at: string (ISO 8601) - 400 Bad Request: - error: "validation_error" - details: [...] - 404 Not Found: - error: "user_not_enrolled" - message: "User has no enrolled face embedding" - -# Get Session -GET /api/v1/proctor/sessions/{session_id} -Response: - 200 OK: - id: string - exam_id: string - user_id: string - status: string - risk_score: number - verification_count: integer - verification_failures: integer - incident_count: integer - started_at: string | null - ended_at: string | null - duration_seconds: number - config: object - -# Start Session -POST /api/v1/proctor/sessions/{session_id}/start -Request: - Content-Type: multipart/form-data - Body: - frame: file (image/jpeg, image/png) -Response: - 200 OK: - session: - id: string - status: "active" - started_at: string - verification: - verified: boolean - confidence: number - liveness_score: number - 400 Bad Request: - error: "verification_failed" - details: - face_detected: boolean - face_matched: boolean - liveness_passed: boolean - -# Pause Session -POST /api/v1/proctor/sessions/{session_id}/pause -Response: - 200 OK: - session: - status: "paused" - paused_at: string - -# Resume Session -POST /api/v1/proctor/sessions/{session_id}/resume -Request: - Content-Type: multipart/form-data - Body: - frame: file (required for re-verification) -Response: - 200 OK: - session: - status: "active" - verification: - verified: boolean - confidence: number - -# End Session -POST /api/v1/proctor/sessions/{session_id}/end -Request: - Body: - reason: string (optional) -Response: - 200 OK: - session: - status: "completed" - ended_at: string - duration_seconds: number - verification_success_rate: number - incident_count: integer - risk_score: number -``` - -#### Frame Submission - -```yaml -# Submit Frame -POST /api/v1/proctor/sessions/{session_id}/frames -Request: - Content-Type: multipart/form-data - Body: - frame: file (required) - audio: file (optional, audio/wav) - timestamp: string (optional, ISO 8601) -Response: - 200 OK: - analysis: - face_detected: boolean - face_matched: boolean - face_confidence: number - face_count: integer - liveness_score: number - looking_at_screen: boolean - attention_score: number - objects_detected: [ - { class: string, confidence: number, prohibited: boolean } - ] - frame_risk_score: number - incidents: [ - { - id: string - type: string - severity: string - confidence: number - } - ] - session: - status: string - risk_score: number - 400 Bad Request: - error: "low_quality_frame" - details: - blur_score: number - brightness: number - face_size: number - -# Get Session Status (lightweight) -GET /api/v1/proctor/sessions/{session_id}/status -Response: - 200 OK: - status: string - risk_score: number - is_flagged: boolean - last_verification_at: string - incident_count: integer -``` - -#### Incident Management - -```yaml -# List Session Incidents -GET /api/v1/proctor/sessions/{session_id}/incidents -Query Parameters: - severity: string (filter by severity) - type: string (filter by type) - reviewed: boolean (filter by review status) - limit: integer (default: 50) - offset: integer (default: 0) -Response: - 200 OK: - incidents: [...] - total: integer - has_more: boolean - -# Get Incident Details -GET /api/v1/proctor/incidents/{incident_id} -Response: - 200 OK: - id: string - session_id: string - type: string - severity: string - confidence: number - timestamp: string - details: object - evidence: [ - { - id: string - type: string - url: string - thumbnail_url: string - } - ] - reviewed: boolean - review_action: string | null - review_notes: string | null - -# Review Incident -PATCH /api/v1/proctor/incidents/{incident_id} -Request: - Body: - action: string (required: dismissed, acknowledged, warning_issued, session_paused, session_terminated, escalated) - notes: string (optional) -Response: - 200 OK: - incident: - reviewed: true - reviewed_at: string - review_action: string - session: - status: string (if changed) - -# Get Incident Evidence -GET /api/v1/proctor/incidents/{incident_id}/evidence/{evidence_id} -Response: - 302 Redirect to signed URL -``` - -#### Reports - -```yaml -# Get Session Report -GET /api/v1/proctor/sessions/{session_id}/report -Response: - 200 OK: - session: - id: string - exam_id: string - user_id: string - status: string - duration_seconds: number - risk_score: number - verification: - total_count: integer - success_count: integer - failure_count: integer - success_rate: number - attention: - total_gaze_away_seconds: number - average_attention_score: number - incidents: - total: integer - by_severity: - critical: integer - high: integer - medium: integer - low: integer - by_type: object - timeline: [ - { - timestamp: string - event_type: string - details: object - } - ] - -# Get Session Timeline -GET /api/v1/proctor/sessions/{session_id}/timeline -Query Parameters: - start: string (ISO 8601) - end: string (ISO 8601) - include_frames: boolean (default: false) -Response: - 200 OK: - events: [ - { - timestamp: string - type: string - data: object - } - ] -``` - -### 5.2 WebSocket API (Real-time) - -```yaml -# Connect to Session Stream -WS /api/v1/proctor/sessions/{session_id}/stream - -# Client → Server Messages -frame: - type: "frame" - data: base64 (image) - timestamp: string - -audio: - type: "audio" - data: base64 (audio chunk) - timestamp: string - -# Server → Client Messages -analysis: - type: "analysis" - data: - face_detected: boolean - face_matched: boolean - looking_at_screen: boolean - risk_score: number - -incident: - type: "incident" - data: - id: string - type: string - severity: string - -status: - type: "status" - data: - status: string - message: string - -error: - type: "error" - code: string - message: string -``` - -### 5.3 Webhook Events - -```yaml -Events: - - proctor.session.created - - proctor.session.started - - proctor.session.paused - - proctor.session.resumed - - proctor.session.completed - - proctor.session.terminated - - proctor.session.flagged - - proctor.incident.created - - proctor.incident.reviewed - - proctor.verification.failed - -Payload Format: - event_id: string (UUID) - event_type: string - timestamp: string (ISO 8601) - tenant_id: string - data: - session_id: string - ... (event-specific data) - -Headers: - X-Event-ID: {event_id} - X-Event-Type: {event_type} - X-Webhook-Signature: sha256={hmac} - Content-Type: application/json -``` - ---- - -## 6. Infrastructure Components - -### 6.1 Repository Interfaces - -```python -# app/domain/interfaces/proctor_session_repository.py - -from datetime import datetime -from typing import List, Optional, Protocol -from uuid import UUID - -from app.domain.entities.proctor_session import ProctorSession, SessionStatus - - -class IProctorSessionRepository(Protocol): - """Repository interface for proctoring sessions.""" - - async def save(self, session: ProctorSession) -> None: - """Save or update a session.""" - ... - - async def find_by_id( - self, - session_id: UUID, - tenant_id: Optional[str] = None, - ) -> Optional[ProctorSession]: - """Find session by ID.""" - ... - - async def find_by_exam_id( - self, - exam_id: str, - tenant_id: Optional[str] = None, - ) -> List[ProctorSession]: - """Find sessions by exam ID.""" - ... - - async def find_by_user_id( - self, - user_id: str, - tenant_id: Optional[str] = None, - status: Optional[SessionStatus] = None, - ) -> List[ProctorSession]: - """Find sessions by user ID.""" - ... - - async def find_active( - self, - tenant_id: Optional[str] = None, - ) -> List[ProctorSession]: - """Find all active sessions.""" - ... - - async def delete( - self, - session_id: UUID, - tenant_id: Optional[str] = None, - ) -> bool: - """Delete a session.""" - ... - - async def count( - self, - tenant_id: Optional[str] = None, - status: Optional[SessionStatus] = None, - ) -> int: - """Count sessions.""" - ... - - -# app/domain/interfaces/proctor_incident_repository.py - -class IProctorIncidentRepository(Protocol): - """Repository interface for proctoring incidents.""" - - async def save(self, incident: ProctorIncident) -> None: - """Save an incident.""" - ... - - async def find_by_id(self, incident_id: UUID) -> Optional[ProctorIncident]: - """Find incident by ID.""" - ... - - async def find_by_session_id( - self, - session_id: UUID, - severity: Optional[IncidentSeverity] = None, - incident_type: Optional[IncidentType] = None, - reviewed: Optional[bool] = None, - limit: int = 100, - offset: int = 0, - ) -> List[ProctorIncident]: - """Find incidents by session ID.""" - ... - - async def count_by_session_id( - self, - session_id: UUID, - severity: Optional[IncidentSeverity] = None, - ) -> int: - """Count incidents for session.""" - ... - - async def update(self, incident: ProctorIncident) -> None: - """Update an incident.""" - ... -``` - -### 6.2 Analysis Interfaces - -```python -# app/domain/interfaces/gaze_tracker.py - -from typing import Optional, Protocol - -import numpy as np - -from app.domain.entities.proctor_analysis import GazeAnalysisResult, HeadPose - - -class IGazeTracker(Protocol): - """Interface for gaze and attention tracking.""" - - def analyze( - self, - image: np.ndarray, - session_id: UUID, - ) -> GazeAnalysisResult: - """Analyze gaze and head pose from image. - - Args: - image: BGR image array - session_id: Session being analyzed - - Returns: - GazeAnalysisResult with head pose and gaze direction - """ - ... - - def get_head_pose(self, image: np.ndarray) -> Optional[HeadPose]: - """Get head pose from image. - - Args: - image: BGR image array - - Returns: - HeadPose or None if face not detected - """ - ... - - -# app/domain/interfaces/object_detector.py - -class IObjectDetector(Protocol): - """Interface for object detection.""" - - def detect( - self, - image: np.ndarray, - session_id: UUID, - prohibited_classes: Optional[List[str]] = None, - ) -> ObjectDetectionResult: - """Detect objects in image. - - Args: - image: BGR image array - session_id: Session being analyzed - prohibited_classes: List of prohibited object classes - - Returns: - ObjectDetectionResult with detected objects - """ - ... - - -# app/domain/interfaces/audio_analyzer.py - -class IAudioAnalyzer(Protocol): - """Interface for audio analysis.""" - - def analyze( - self, - audio_data: bytes, - sample_rate: int, - session_id: UUID, - ) -> AudioAnalysisResult: - """Analyze audio chunk. - - Args: - audio_data: Raw audio bytes - sample_rate: Audio sample rate - session_id: Session being analyzed - - Returns: - AudioAnalysisResult with voice activity and speaker count - """ - ... - - -# app/domain/interfaces/deepfake_detector.py - -@dataclass -class DeepfakeAnalysisResult: - """Result of deepfake detection analysis. - - Attributes: - session_id: Session being analyzed - timestamp: Analysis timestamp - is_deepfake: Whether deepfake was detected - confidence: Detection confidence (0.0-1.0) - detection_method: Which method triggered detection - artifacts_found: List of detected artifacts - """ - session_id: UUID - timestamp: datetime - is_deepfake: bool - confidence: float - detection_method: str # "frequency", "texture", "temporal", "ensemble" - artifacts_found: List[str] = field(default_factory=list) - - def to_dict(self) -> Dict[str, Any]: - return { - "session_id": str(self.session_id), - "timestamp": self.timestamp.isoformat(), - "is_deepfake": self.is_deepfake, - "confidence": self.confidence, - "detection_method": self.detection_method, - "artifacts_found": self.artifacts_found, - } - - -class IDeepfakeDetector(Protocol): - """Interface for deepfake detection. - - Detects synthetic/manipulated faces using multiple techniques: - - Frequency analysis (DCT artifacts) - - Texture inconsistency detection - - Temporal coherence analysis (video) - - Ensemble model classification - """ - - def detect( - self, - image: np.ndarray, - session_id: UUID, - ) -> DeepfakeAnalysisResult: - """Analyze image for deepfake indicators. - - Args: - image: BGR image array - session_id: Session being analyzed - - Returns: - DeepfakeAnalysisResult with detection outcome - """ - ... - - def detect_video( - self, - frames: List[np.ndarray], - session_id: UUID, - ) -> DeepfakeAnalysisResult: - """Analyze video frames for temporal deepfake indicators. - - Args: - frames: List of BGR image arrays - session_id: Session being analyzed - - Returns: - DeepfakeAnalysisResult with temporal analysis - """ - ... -``` - -### 6.2.1 Circuit Breaker Pattern - -```python -# app/infrastructure/resilience/circuit_breaker.py - -from dataclasses import dataclass -from enum import Enum -from typing import Callable, Generic, TypeVar, Optional -import time -import threading - - -class CircuitState(str, Enum): - """Circuit breaker states.""" - CLOSED = "closed" # Normal operation, requests pass through - OPEN = "open" # Failures exceeded, requests blocked - HALF_OPEN = "half_open" # Testing if service recovered - - -@dataclass -class CircuitBreakerConfig: - """Circuit breaker configuration. - - Attributes: - failure_threshold: Failures before opening circuit - success_threshold: Successes in half-open before closing - timeout_seconds: Seconds before trying half-open - excluded_exceptions: Exceptions that don't count as failures - """ - failure_threshold: int = 5 - success_threshold: int = 2 - timeout_seconds: float = 30.0 - excluded_exceptions: tuple = () - - -T = TypeVar('T') - - -class CircuitBreaker(Generic[T]): - """Circuit breaker for ML service resilience. - - Prevents cascading failures by stopping requests to failing services. - Provides graceful degradation when ML models fail. - - Usage: - breaker = CircuitBreaker(config) - - @breaker - def call_ml_model(image): - return model.predict(image) - - # Or with fallback - result = breaker.call( - func=lambda: model.predict(image), - fallback=lambda: default_result - ) - """ - - def __init__(self, config: Optional[CircuitBreakerConfig] = None): - self.config = config or CircuitBreakerConfig() - self._state = CircuitState.CLOSED - self._failure_count = 0 - self._success_count = 0 - self._last_failure_time: Optional[float] = None - self._lock = threading.Lock() - - @property - def state(self) -> CircuitState: - """Get current circuit state.""" - with self._lock: - if self._state == CircuitState.OPEN: - if self._should_attempt_reset(): - self._state = CircuitState.HALF_OPEN - return self._state - - def _should_attempt_reset(self) -> bool: - """Check if enough time passed to try half-open.""" - if self._last_failure_time is None: - return True - return (time.time() - self._last_failure_time) >= self.config.timeout_seconds - - def call( - self, - func: Callable[[], T], - fallback: Optional[Callable[[], T]] = None, - ) -> T: - """Execute function with circuit breaker protection. - - Args: - func: Function to execute - fallback: Optional fallback if circuit is open - - Returns: - Function result or fallback result - - Raises: - CircuitBreakerOpenError: If circuit is open and no fallback - """ - state = self.state - - if state == CircuitState.OPEN: - if fallback: - return fallback() - raise CircuitBreakerOpenError("Circuit breaker is open") - - try: - result = func() - self._on_success() - return result - except Exception as e: - if isinstance(e, self.config.excluded_exceptions): - raise - self._on_failure() - if fallback: - return fallback() - raise - - def _on_success(self) -> None: - """Handle successful call.""" - with self._lock: - if self._state == CircuitState.HALF_OPEN: - self._success_count += 1 - if self._success_count >= self.config.success_threshold: - self._state = CircuitState.CLOSED - self._failure_count = 0 - self._success_count = 0 - elif self._state == CircuitState.CLOSED: - self._failure_count = 0 - - def _on_failure(self) -> None: - """Handle failed call.""" - with self._lock: - self._failure_count += 1 - self._last_failure_time = time.time() - - if self._state == CircuitState.HALF_OPEN: - self._state = CircuitState.OPEN - self._success_count = 0 - elif self._failure_count >= self.config.failure_threshold: - self._state = CircuitState.OPEN - - -class CircuitBreakerOpenError(Exception): - """Raised when circuit breaker is open.""" - pass - - -# Pre-configured circuit breakers for ML components -FACE_VERIFIER_BREAKER = CircuitBreaker(CircuitBreakerConfig( - failure_threshold=3, - success_threshold=2, - timeout_seconds=30.0, -)) - -DEEPFAKE_DETECTOR_BREAKER = CircuitBreaker(CircuitBreakerConfig( - failure_threshold=5, - success_threshold=2, - timeout_seconds=60.0, # Longer timeout for complex model -)) - -GAZE_TRACKER_BREAKER = CircuitBreaker(CircuitBreakerConfig( - failure_threshold=5, - success_threshold=2, - timeout_seconds=30.0, -)) - -OBJECT_DETECTOR_BREAKER = CircuitBreaker(CircuitBreakerConfig( - failure_threshold=5, - success_threshold=2, - timeout_seconds=30.0, -)) -``` - -### 6.2.2 Per-Session Rate Limiter - -```python -# app/infrastructure/resilience/session_rate_limiter.py - -from dataclasses import dataclass -from typing import Optional -from uuid import UUID -import time -import redis - - -@dataclass -class SessionRateLimitConfig: - """Per-session rate limiting configuration. - - Attributes: - max_frames_per_second: Maximum frames allowed per second - max_frames_per_minute: Maximum frames allowed per minute - burst_allowance: Extra frames allowed in burst - cooldown_seconds: Seconds to wait after limit exceeded - """ - max_frames_per_second: float = 2.0 # 2 FPS max - max_frames_per_minute: int = 60 # 60 frames/min max - burst_allowance: int = 5 # Allow 5 extra in burst - cooldown_seconds: int = 10 # 10s cooldown after violation - - -@dataclass -class RateLimitResult: - """Result of rate limit check. - - Attributes: - allowed: Whether request is allowed - remaining: Remaining requests in window - retry_after: Seconds until limit resets (if blocked) - is_suspicious: Whether pattern is suspicious - """ - allowed: bool - remaining: int - retry_after: Optional[float] = None - is_suspicious: bool = False - - -class SessionRateLimiter: - """Per-session rate limiter using sliding window algorithm. - - Prevents frame flooding attacks and detects suspicious submission patterns. - Uses Redis for distributed rate limiting across instances. - - Features: - - Sliding window rate limiting (accurate, no boundary issues) - - Burst allowance for legitimate traffic spikes - - Suspicious pattern detection - - Distributed via Redis - """ - - def __init__( - self, - redis_client: redis.Redis, - config: Optional[SessionRateLimitConfig] = None, - ): - self.redis = redis_client - self.config = config or SessionRateLimitConfig() - - def check(self, session_id: UUID) -> RateLimitResult: - """Check if frame submission is allowed for session. - - Args: - session_id: Session to check - - Returns: - RateLimitResult with allow/deny and metadata - """ - now = time.time() - key_second = f"proctor:rate:{session_id}:second" - key_minute = f"proctor:rate:{session_id}:minute" - key_violations = f"proctor:rate:{session_id}:violations" - - # Check cooldown from previous violations - violations = int(self.redis.get(key_violations) or 0) - if violations > 0: - ttl = self.redis.ttl(key_violations) - if ttl > 0: - return RateLimitResult( - allowed=False, - remaining=0, - retry_after=ttl, - is_suspicious=True, - ) - - # Sliding window check for per-second limit - second_count = self._sliding_window_count(key_second, now, 1.0) - if second_count >= self.config.max_frames_per_second + self.config.burst_allowance: - self._record_violation(session_id) - return RateLimitResult( - allowed=False, - remaining=0, - retry_after=1.0, - is_suspicious=second_count > self.config.max_frames_per_second * 2, - ) - - # Sliding window check for per-minute limit - minute_count = self._sliding_window_count(key_minute, now, 60.0) - if minute_count >= self.config.max_frames_per_minute + self.config.burst_allowance: - self._record_violation(session_id) - return RateLimitResult( - allowed=False, - remaining=0, - retry_after=60.0 - (now % 60), - is_suspicious=minute_count > self.config.max_frames_per_minute * 1.5, - ) - - # Record this request - pipe = self.redis.pipeline() - pipe.zadd(key_second, {str(now): now}) - pipe.expire(key_second, 2) - pipe.zadd(key_minute, {str(now): now}) - pipe.expire(key_minute, 120) - pipe.execute() - - return RateLimitResult( - allowed=True, - remaining=self.config.max_frames_per_minute - minute_count - 1, - is_suspicious=False, - ) - - def _sliding_window_count(self, key: str, now: float, window_seconds: float) -> int: - """Count requests in sliding window.""" - window_start = now - window_seconds - # Remove old entries and count current - pipe = self.redis.pipeline() - pipe.zremrangebyscore(key, 0, window_start) - pipe.zcard(key) - _, count = pipe.execute() - return count - - def _record_violation(self, session_id: UUID) -> None: - """Record rate limit violation.""" - key = f"proctor:rate:{session_id}:violations" - pipe = self.redis.pipeline() - pipe.incr(key) - pipe.expire(key, self.config.cooldown_seconds) - pipe.execute() - - def get_session_stats(self, session_id: UUID) -> dict: - """Get rate limiting stats for session.""" - now = time.time() - key_minute = f"proctor:rate:{session_id}:minute" - key_violations = f"proctor:rate:{session_id}:violations" - - minute_count = self._sliding_window_count(key_minute, now, 60.0) - violations = int(self.redis.get(key_violations) or 0) - - return { - "frames_last_minute": minute_count, - "remaining_this_minute": max(0, self.config.max_frames_per_minute - minute_count), - "violation_count": violations, - "is_throttled": violations > 0, - } -``` - -### 6.3 Database Schema - -```sql --- migrations/003_proctoring_tables.sql - --- Proctor Sessions -CREATE TABLE proctor_sessions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - exam_id VARCHAR(255) NOT NULL, - user_id VARCHAR(255) NOT NULL, - tenant_id VARCHAR(255) NOT NULL, - status VARCHAR(50) NOT NULL DEFAULT 'created', - risk_score FLOAT NOT NULL DEFAULT 0.0, - config JSONB NOT NULL DEFAULT '{}', - metadata JSONB NOT NULL DEFAULT '{}', - baseline_embedding vector(512), - verification_count INTEGER NOT NULL DEFAULT 0, - verification_failures INTEGER NOT NULL DEFAULT 0, - incident_count INTEGER NOT NULL DEFAULT 0, - total_gaze_away_sec FLOAT NOT NULL DEFAULT 0.0, - termination_reason VARCHAR(50), - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - started_at TIMESTAMP WITH TIME ZONE, - ended_at TIMESTAMP WITH TIME ZONE, - paused_at TIMESTAMP WITH TIME ZONE, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - - CONSTRAINT fk_user_embedding - FOREIGN KEY (user_id, tenant_id) - REFERENCES face_embeddings(user_id, tenant_id) -); - --- Indexes -CREATE INDEX idx_proctor_sessions_exam ON proctor_sessions(exam_id, tenant_id); -CREATE INDEX idx_proctor_sessions_user ON proctor_sessions(user_id, tenant_id); -CREATE INDEX idx_proctor_sessions_status ON proctor_sessions(status, tenant_id); -CREATE INDEX idx_proctor_sessions_active ON proctor_sessions(tenant_id) - WHERE status IN ('active', 'flagged'); - --- Proctor Incidents -CREATE TABLE proctor_incidents ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - session_id UUID NOT NULL REFERENCES proctor_sessions(id) ON DELETE CASCADE, - incident_type VARCHAR(50) NOT NULL, - severity VARCHAR(20) NOT NULL, - confidence FLOAT NOT NULL, - details JSONB NOT NULL DEFAULT '{}', - reviewed BOOLEAN NOT NULL DEFAULT FALSE, - reviewed_at TIMESTAMP WITH TIME ZONE, - reviewed_by VARCHAR(255), - review_action VARCHAR(50), - review_notes TEXT, - timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() -); - --- Indexes -CREATE INDEX idx_proctor_incidents_session ON proctor_incidents(session_id); -CREATE INDEX idx_proctor_incidents_severity ON proctor_incidents(session_id, severity); -CREATE INDEX idx_proctor_incidents_unreviewed ON proctor_incidents(session_id) - WHERE reviewed = FALSE; - --- Incident Evidence -CREATE TABLE incident_evidence ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - incident_id UUID NOT NULL REFERENCES proctor_incidents(id) ON DELETE CASCADE, - evidence_type VARCHAR(50) NOT NULL, - storage_url TEXT NOT NULL, - thumbnail_url TEXT, - metadata JSONB NOT NULL DEFAULT '{}', - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() -); - -CREATE INDEX idx_incident_evidence_incident ON incident_evidence(incident_id); - --- Verification Events (sampled, for analytics) -CREATE TABLE verification_events ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - session_id UUID NOT NULL REFERENCES proctor_sessions(id) ON DELETE CASCADE, - face_detected BOOLEAN NOT NULL, - face_matched BOOLEAN NOT NULL, - confidence FLOAT NOT NULL, - liveness_score FLOAT, - quality_score FLOAT, - face_count INTEGER NOT NULL DEFAULT 1, - timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() -); - -CREATE INDEX idx_verification_events_session ON verification_events(session_id, timestamp); - --- Partitioning for verification_events (by month) --- CREATE TABLE verification_events_y2024m12 PARTITION OF verification_events --- FOR VALUES FROM ('2024-12-01') TO ('2025-01-01'); - --- Session Config Templates -CREATE TABLE session_config_templates ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id VARCHAR(255) NOT NULL, - name VARCHAR(255) NOT NULL, - description TEXT, - config JSONB NOT NULL, - is_default BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - - UNIQUE(tenant_id, name) -); -``` - ---- - -## 7. Data Flow - -### 7.1 Frame Analysis Flow - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ FRAME SUBMISSION FLOW │ -└─────────────────────────────────────────────────────────────────────────────┘ - -Client API Service Storage - │ │ │ │ - │ POST /frames │ │ │ - │ {frame, audio} │ │ │ - │───────────────────▶│ │ │ - │ │ │ │ - │ │ Validate Session │ │ - │ │─────────────────────▶│ │ - │ │ │ │ - │ │ │ Load Session │ - │ │ │─────────────────────────▶│ - │ │ │◀─────────────────────────│ - │ │ │ │ - │ │ │ ┌─────────────────────┐ │ - │ │ │ │ PARALLEL ANALYSIS │ │ - │ │ │ │ │ │ - │ │ │ │ ┌─────────────────┐ │ │ - │ │ │ │ │ Face Verify │ │ │ - │ │ │ │ └─────────────────┘ │ │ - │ │ │ │ ┌─────────────────┐ │ │ - │ │ │ │ │ Gaze Track │ │ │ - │ │ │ │ └─────────────────┘ │ │ - │ │ │ │ ┌─────────────────┐ │ │ - │ │ │ │ │ Object Detect │ │ │ - │ │ │ │ └─────────────────┘ │ │ - │ │ │ │ ┌─────────────────┐ │ │ - │ │ │ │ │ Audio Analyze │ │ │ - │ │ │ │ └─────────────────┘ │ │ - │ │ │ └─────────────────────┘ │ - │ │ │ │ - │ │ │ Fuse Results │ - │ │ │ Calculate Risk │ - │ │ │ │ - │ │ │ Generate Incidents │ - │ │ │─────────────────────────▶│ - │ │ │ │ - │ │ │ Update Session │ - │ │ │─────────────────────────▶│ - │ │ │ │ - │ │ FrameAnalysisResult │ │ - │ │◀─────────────────────│ │ - │ │ │ │ - │ 200 OK │ │ │ - │ {analysis, │ │ │ - │ incidents, │ │ │ - │ session_status} │ │ │ - │◀───────────────────│ │ │ - │ │ │ │ - │ │ │ [If Critical Incident] │ - │ │ │ Send Webhook │ - │ │ │─────────────────────────▶│ Webhook - │ │ │ │ -``` - -### 7.2 Risk Score Calculation - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ RISK SCORE AGGREGATION │ -└─────────────────────────────────────────────────────────────────┘ - -Input Signals: -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Identity Risk │ │ Attention Risk │ │ Environment Risk│ -│ │ │ │ │ │ -│ • Face match │ │ • Gaze away │ │ • Objects │ -│ • Liveness │ │ • Head pose │ │ • Persons │ -│ • Face count │ │ • Duration │ │ • Audio │ -└────────┬────────┘ └────────┬────────┘ └────────┬────────┘ - │ │ │ - ▼ ▼ ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ FUSION ENGINE │ -│ │ -│ risk_identity = w1 * (1 - face_confidence) + │ -│ w2 * (1 - liveness_score) + │ -│ w3 * (face_count > 1 ? 0.8 : 0) │ -│ │ -│ risk_attention = w4 * gaze_away_ratio + │ -│ w5 * head_deviation_score │ -│ │ -│ risk_environment = w6 * max(object_risks) + │ -│ w7 * audio_risk │ -│ │ -│ total_risk = α * risk_identity + │ -│ β * risk_attention + │ -│ γ * risk_environment │ -│ │ -│ Weights: α=0.5, β=0.25, γ=0.25 (configurable) │ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ SESSION RISK UPDATE │ -│ │ -│ session.risk_score = EMA(session.risk_score, frame_risk) │ -│ │ -│ if session.risk_score > CRITICAL_THRESHOLD: │ -│ session.flag() │ -│ webhook.send('session.flagged') │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 8. Configuration - -### 8.1 Environment Variables - -```bash -# Proctoring Feature Flags -PROCTORING_ENABLED=true -PROCTORING_MAX_CONCURRENT_SESSIONS=10000 - -# Verification Settings -PROCTOR_VERIFICATION_INTERVAL_SEC=60 -PROCTOR_VERIFICATION_THRESHOLD=0.6 -PROCTOR_MAX_VERIFICATION_FAILURES=3 - -# Gaze Tracking -PROCTOR_GAZE_AWAY_THRESHOLD_SEC=5.0 -PROCTOR_GAZE_SENSITIVITY=0.7 -PROCTOR_HEAD_POSE_PITCH_THRESHOLD=20.0 -PROCTOR_HEAD_POSE_YAW_THRESHOLD=30.0 - -# Object Detection -PROCTOR_OBJECT_DETECTION_ENABLED=true -PROCTOR_OBJECT_DETECTION_MODEL=yolov8n -PROCTOR_OBJECT_CONFIDENCE_THRESHOLD=0.5 -PROCTOR_PROHIBITED_OBJECTS=phone,book,laptop,tablet,person - -# Audio Analysis -PROCTOR_AUDIO_MONITORING_ENABLED=true -PROCTOR_AUDIO_VAD_THRESHOLD=0.5 -PROCTOR_AUDIO_MULTI_SPEAKER_THRESHOLD=0.7 - -# Deepfake Detection (Market Differentiator) -PROCTOR_DEEPFAKE_DETECTION_ENABLED=true -PROCTOR_DEEPFAKE_CONFIDENCE_THRESHOLD=0.7 -PROCTOR_DEEPFAKE_DETECTION_MODEL=ensemble # ensemble, frequency, texture -PROCTOR_DEEPFAKE_TEMPORAL_ANALYSIS=true # Analyze video frames for temporal artifacts - -# Circuit Breaker (ML Resilience) -PROCTOR_CIRCUIT_BREAKER_ENABLED=true -PROCTOR_CB_FAILURE_THRESHOLD=5 -PROCTOR_CB_SUCCESS_THRESHOLD=2 -PROCTOR_CB_TIMEOUT_SECONDS=30 -PROCTOR_CB_FALLBACK_BEHAVIOR=skip # skip, default, error - -# Per-Session Rate Limiting -PROCTOR_SESSION_RATE_LIMIT_ENABLED=true -PROCTOR_SESSION_MAX_FPS=2.0 -PROCTOR_SESSION_MAX_FRAMES_PER_MINUTE=60 -PROCTOR_SESSION_BURST_ALLOWANCE=5 -PROCTOR_SESSION_RATE_COOLDOWN_SECONDS=10 - -# Risk Thresholds -PROCTOR_RISK_THRESHOLD_WARNING=0.5 -PROCTOR_RISK_THRESHOLD_CRITICAL=0.8 -PROCTOR_AUTO_TERMINATE_ON_CRITICAL=false - -# Session Limits -PROCTOR_MAX_PAUSE_DURATION_SEC=300 -PROCTOR_SESSION_TIMEOUT_SEC=14400 -PROCTOR_MAX_SESSIONS_PER_USER=1 - -# Evidence Storage -PROCTOR_EVIDENCE_STORAGE=s3 -PROCTOR_EVIDENCE_BUCKET=proctoring-evidence -PROCTOR_EVIDENCE_RETENTION_DAYS=90 - -# Webhooks -PROCTOR_WEBHOOK_EVENTS=session.flagged,incident.created,session.terminated -``` - -### 8.2 Default Session Config - -```python -DEFAULT_SESSION_CONFIG = SessionConfig( - verification_interval_sec=60, - verification_threshold=0.6, - max_verification_failures=3, - gaze_away_threshold_sec=5.0, - gaze_sensitivity=0.7, - enable_object_detection=True, - enable_audio_monitoring=True, - enable_multi_face_detection=True, - enable_deepfake_detection=True, # NEW: Market differentiator - enable_session_rate_limiting=True, # NEW: Prevent frame flooding - deepfake_confidence_threshold=0.7, # NEW: Deepfake detection threshold - max_frames_per_second=2.0, # NEW: Per-session rate limit - max_frames_per_minute=60, # NEW: Per-session rate limit - risk_threshold_warning=0.5, - risk_threshold_critical=0.8, - max_pause_duration_sec=300, - session_timeout_sec=14400, -) -``` - ---- - -## 9. Security & Privacy - -### 9.1 Data Classification - -| Data Type | Classification | Retention | Encryption | -|-----------|---------------|-----------|------------| -| Face Images | Biometric (Special) | Session only | AES-256 | -| Face Embeddings | Biometric (Special) | Until deletion | AES-256 | -| Session Records | PII | 1 year | AES-256 | -| Incident Evidence | Biometric (Special) | 90 days | AES-256 | -| Analytics | Anonymized | 2 years | Standard | - -### 9.2 Privacy Controls - -```yaml -Data Minimization: - - Process frames in memory, don't store - - Store only embeddings, not raw images - - Sample verification events (1 in 10) - - Delete evidence after retention period - -Consent Management: - - Explicit consent required before session - - Clear disclosure of data collection - - Right to withdraw consent - - Data export on request - -Access Controls: - - Role-based access to incidents - - Audit log for all data access - - Admin approval for exports - - Tenant isolation enforced -``` - -### 9.3 GDPR Compliance Checklist - -- [ ] Data Protection Impact Assessment (DPIA) completed -- [ ] Privacy notice updated for proctoring -- [ ] Consent mechanism implemented -- [ ] Data Subject Access Request (DSAR) process -- [ ] Right to erasure implemented -- [ ] Data retention policy enforced -- [ ] Cross-border transfer safeguards -- [ ] Data Processing Agreement (DPA) template - ---- - -## 10. Monitoring & Observability - -### 10.1 New Metrics - -```python -# Proctoring-specific Prometheus metrics - -# Session metrics -PROCTOR_SESSIONS_TOTAL = Counter( - "biometric_proctor_sessions_total", - "Total proctoring sessions", - ["tenant_id", "status"], -) - -PROCTOR_SESSIONS_ACTIVE = Gauge( - "biometric_proctor_sessions_active", - "Currently active proctoring sessions", - ["tenant_id"], -) - -PROCTOR_SESSION_DURATION = Histogram( - "biometric_proctor_session_duration_seconds", - "Session duration in seconds", - ["status"], - buckets=(60, 300, 600, 1800, 3600, 7200, 14400), -) - -# Verification metrics -PROCTOR_VERIFICATIONS_TOTAL = Counter( - "biometric_proctor_verifications_total", - "Total verification attempts", - ["result"], # success, failure -) - -PROCTOR_VERIFICATION_LATENCY = Histogram( - "biometric_proctor_verification_latency_seconds", - "Verification latency", - buckets=(0.1, 0.25, 0.5, 1.0, 2.0, 5.0), -) - -# Incident metrics -PROCTOR_INCIDENTS_TOTAL = Counter( - "biometric_proctor_incidents_total", - "Total incidents created", - ["type", "severity"], -) - -# Analysis metrics -PROCTOR_FRAME_ANALYSIS_LATENCY = Histogram( - "biometric_proctor_frame_analysis_latency_seconds", - "Frame analysis latency", - ["component"], # face, gaze, object, audio, total - buckets=(0.05, 0.1, 0.25, 0.5, 1.0), -) - -# Risk metrics -PROCTOR_RISK_SCORE = Histogram( - "biometric_proctor_risk_score", - "Session risk scores", - buckets=(0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0), -) - -# Deepfake detection metrics -PROCTOR_DEEPFAKE_DETECTIONS_TOTAL = Counter( - "biometric_proctor_deepfake_detections_total", - "Total deepfake detections", - ["result", "method"], # result: detected/clean, method: frequency/texture/temporal/ensemble -) - -PROCTOR_DEEPFAKE_LATENCY = Histogram( - "biometric_proctor_deepfake_latency_seconds", - "Deepfake detection latency", - buckets=(0.1, 0.25, 0.5, 1.0, 2.0, 5.0), -) - -# Circuit breaker metrics -PROCTOR_CIRCUIT_BREAKER_STATE = Gauge( - "biometric_proctor_circuit_breaker_state", - "Circuit breaker state (0=closed, 1=half-open, 2=open)", - ["component"], # face_verifier, deepfake_detector, gaze_tracker, object_detector -) - -PROCTOR_CIRCUIT_BREAKER_TRIPS = Counter( - "biometric_proctor_circuit_breaker_trips_total", - "Circuit breaker trip count", - ["component"], -) - -PROCTOR_CIRCUIT_BREAKER_FALLBACKS = Counter( - "biometric_proctor_circuit_breaker_fallbacks_total", - "Circuit breaker fallback invocations", - ["component"], -) - -# Rate limiting metrics -PROCTOR_RATE_LIMIT_CHECKS = Counter( - "biometric_proctor_rate_limit_checks_total", - "Rate limit check count", - ["result"], # allowed, denied -) - -PROCTOR_RATE_LIMIT_VIOLATIONS = Counter( - "biometric_proctor_rate_limit_violations_total", - "Rate limit violations", - ["session_status"], # suspicious, normal -) - -PROCTOR_FRAMES_PER_SESSION = Histogram( - "biometric_proctor_frames_per_session", - "Frames submitted per session per minute", - buckets=(10, 20, 30, 40, 50, 60, 80, 100, 150), -) -``` - -### 10.2 Alerts - -```yaml -# Add to monitoring/prometheus/alerts.yml - -- alert: ProctorHighIncidentRate - expr: | - sum(rate(biometric_proctor_incidents_total{severity="critical"}[5m])) > 1 - for: 5m - labels: - severity: warning - annotations: - summary: "High critical incident rate in proctoring" - -- alert: ProctorVerificationFailureRate - expr: | - sum(rate(biometric_proctor_verifications_total{result="failure"}[5m])) - / - sum(rate(biometric_proctor_verifications_total[5m])) > 0.1 - for: 10m - labels: - severity: warning - annotations: - summary: "High verification failure rate" - -- alert: ProctorAnalysisLatency - expr: | - histogram_quantile(0.95, rate(biometric_proctor_frame_analysis_latency_seconds_bucket{component="total"}[5m])) > 1 - for: 5m - labels: - severity: warning - annotations: - summary: "High frame analysis latency" - -- alert: ProctorDeepfakeDetectionSpike - expr: | - sum(rate(biometric_proctor_deepfake_detections_total{result="detected"}[10m])) > 0.5 - for: 5m - labels: - severity: critical - annotations: - summary: "Unusual spike in deepfake detections - possible coordinated attack" - -- alert: ProctorCircuitBreakerOpen - expr: | - biometric_proctor_circuit_breaker_state == 2 - for: 2m - labels: - severity: warning - annotations: - summary: "Circuit breaker open for {{ $labels.component }}" - description: "ML component {{ $labels.component }} is failing, circuit breaker is open" - -- alert: ProctorCircuitBreakerMultipleOpen - expr: | - count(biometric_proctor_circuit_breaker_state == 2) >= 2 - for: 1m - labels: - severity: critical - annotations: - summary: "Multiple circuit breakers are open - service degradation" - -- alert: ProctorHighRateLimitViolations - expr: | - sum(rate(biometric_proctor_rate_limit_violations_total[5m])) > 10 - for: 5m - labels: - severity: warning - annotations: - summary: "High rate of rate limit violations - possible abuse" - -- alert: ProctorSuspiciousRateLimitPattern - expr: | - sum(rate(biometric_proctor_rate_limit_violations_total{session_status="suspicious"}[5m])) > 1 - for: 2m - labels: - severity: critical - annotations: - summary: "Suspicious rate limit pattern detected - possible attack" -``` - ---- - -## 11. Implementation Phases - -### Phase 1: Core Session Management (Weeks 1-2) - -``` -Tasks: -├── Domain Entities -│ ├── ProctorSession entity -│ ├── SessionConfig value object -│ └── SessionStatus enum -├── Repository -│ ├── IProctorSessionRepository interface -│ └── PostgresSessionRepository implementation -├── Use Cases -│ ├── CreateProctorSession -│ ├── StartProctorSession -│ ├── PauseProctorSession -│ ├── ResumeProctorSession -│ └── EndProctorSession -├── API Routes -│ └── /api/v1/proctor/sessions/* -├── Database -│ └── Migration for proctor_sessions table -└── Tests - ├── Unit tests for entities - ├── Unit tests for use cases - └── Integration tests for API -``` - -### Phase 2: Continuous Verification (Weeks 3-4) - -``` -Tasks: -├── Analysis Pipeline -│ ├── FrameAnalysisResult entity -│ └── VerificationEvent entity -├── Use Cases -│ ├── SubmitFrame -│ ├── VerifyIdentity (continuous) -│ └── CheckSessionStatus -├── Risk Scoring -│ ├── RiskCalculator service -│ └── Risk aggregation logic -├── API Routes -│ └── POST /sessions/{id}/frames -├── Webhooks -│ └── session.flagged event -└── Tests -``` - -### Phase 3: Gaze & Attention Tracking (Weeks 5-6) - -``` -Tasks: -├── Domain -│ ├── HeadPose value object -│ ├── GazeDirection value object -│ └── GazeAnalysisResult entity -├── Infrastructure -│ ├── IGazeTracker interface -│ └── MediaPipeGazeTracker implementation -├── Integration -│ └── Add to frame analysis pipeline -└── Tests -``` - -### Phase 4: Incident Management (Weeks 7-8) - -``` -Tasks: -├── Domain -│ ├── ProctorIncident entity -│ ├── IncidentEvidence entity -│ └── IncidentType/Severity enums -├── Repository -│ ├── IProctorIncidentRepository interface -│ └── PostgresIncidentRepository implementation -├── Use Cases -│ ├── CreateIncident -│ ├── ListIncidents -│ ├── ReviewIncident -│ └── GetSessionReport -├── Evidence Storage -│ └── S3 evidence storage -├── API Routes -│ └── /api/v1/proctor/incidents/* -├── Webhooks -│ └── incident.created event -└── Tests -``` - -### Phase 5: Object Detection (Weeks 9-10) - -``` -Tasks: -├── Domain -│ ├── DetectedObject value object -│ └── ObjectDetectionResult entity -├── Infrastructure -│ ├── IObjectDetector interface -│ └── YOLOObjectDetector implementation -├── Integration -│ └── Add to frame analysis pipeline -└── Tests -``` - -### Phase 6: Audio Analysis (Weeks 11-12) - -``` -Tasks: -├── Domain -│ └── AudioAnalysisResult entity -├── Infrastructure -│ ├── IAudioAnalyzer interface -│ └── WebRTCVADAudioAnalyzer implementation -├── Integration -│ └── Add to frame analysis pipeline -└── Tests -``` - -### Phase 7: Deepfake Detection (Weeks 13-14) - MARKET DIFFERENTIATOR - -``` -Tasks: -├── Domain -│ ├── DeepfakeAnalysisResult entity -│ └── DEEPFAKE_DETECTED incident type (already added) -├── Infrastructure -│ ├── IDeepfakeDetector interface -│ ├── FrequencyAnalysisDetector (DCT artifact detection) -│ ├── TextureAnalysisDetector (skin texture inconsistencies) -│ ├── TemporalAnalysisDetector (video frame consistency) -│ └── EnsembleDeepfakeDetector (combines all methods) -├── ML Models -│ ├── Train/fine-tune deepfake classifier -│ ├── Frequency domain analysis (FFT/DCT) -│ └── Face texture analysis network -├── Integration -│ └── Add to frame analysis pipeline (parallel with face verification) -├── API -│ └── Deepfake detection results in frame response -└── Tests - ├── Unit tests with synthetic face datasets - ├── Integration tests with FaceForensics++ dataset - └── False positive rate testing with real faces -``` - -### Phase 8: Resilience & Rate Limiting (Weeks 15-16) - -``` -Tasks: -├── Infrastructure -│ ├── Circuit Breaker implementation -│ │ ├── CircuitBreaker generic class -│ │ ├── CircuitBreakerConfig -│ │ ├── Pre-configured breakers for each ML component -│ │ └── Fallback behavior handlers -│ └── Session Rate Limiter implementation -│ ├── SessionRateLimiter class -│ ├── Redis sliding window algorithm -│ ├── Burst allowance logic -│ └── Suspicious pattern detection -├── Integration -│ ├── Wrap all ML components with circuit breakers -│ ├── Add rate limiting to frame submission endpoint -│ └── Create RATE_LIMIT_EXCEEDED incidents -├── Monitoring -│ ├── Circuit breaker state metrics -│ ├── Rate limiting metrics -│ └── Alerts for open circuits -├── API -│ ├── Rate limit headers in responses (X-RateLimit-*) -│ └── 429 Too Many Requests responses -└── Tests - ├── Circuit breaker state transition tests - ├── Rate limiter accuracy tests - └── Load tests for rate limiting -``` - ---- - -## 12. Validation Checklist - -### Design Validation - -- [ ] **Single Responsibility**: Each entity/service has one clear purpose -- [ ] **Open/Closed**: Extensible via interfaces, not modification -- [ ] **Liskov Substitution**: Implementations are interchangeable -- [ ] **Interface Segregation**: Focused interfaces per capability -- [ ] **Dependency Inversion**: Depend on abstractions, not concretions - -### Architecture Validation - -- [ ] Clean Architecture layers maintained -- [ ] Domain layer has no external dependencies -- [ ] Use cases orchestrate domain operations -- [ ] Infrastructure implements interfaces -- [ ] API layer is thin translation layer - -### Security Validation - -- [ ] All endpoints authenticated -- [ ] Rate limiting configured -- [ ] Input validation comprehensive -- [ ] SQL injection prevented (parameterized queries) -- [ ] XSS prevention in responses -- [ ] Sensitive data encrypted - -### Privacy Validation - -- [ ] Data minimization implemented -- [ ] Retention policies enforced -- [ ] Consent tracked -- [ ] Audit logging complete -- [ ] GDPR requirements met - -### Performance Validation - -- [ ] Frame analysis < 500ms p95 -- [ ] API response < 200ms p95 -- [ ] Supports 10K concurrent sessions -- [ ] Database queries optimized -- [ ] Caching strategy defined - ---- - -## Appendix A: File Structure - -``` -app/ -├── domain/ -│ ├── entities/ -│ │ ├── proctor_session.py # NEW -│ │ ├── proctor_incident.py # NEW -│ │ ├── proctor_analysis.py # NEW -│ │ └── deepfake_analysis.py # NEW v1.1 -│ └── interfaces/ -│ ├── proctor_session_repository.py # NEW -│ ├── proctor_incident_repository.py # NEW -│ ├── gaze_tracker.py # NEW -│ ├── object_detector.py # NEW -│ ├── audio_analyzer.py # NEW -│ └── deepfake_detector.py # NEW v1.1 -├── application/ -│ └── use_cases/ -│ ├── proctor/ # NEW directory -│ │ ├── create_session.py -│ │ ├── start_session.py -│ │ ├── submit_frame.py -│ │ ├── end_session.py -│ │ ├── create_incident.py -│ │ ├── review_incident.py -│ │ └── get_session_report.py -│ └── ... -├── infrastructure/ -│ ├── persistence/ -│ │ └── repositories/ -│ │ ├── postgres_session_repository.py # NEW -│ │ └── postgres_incident_repository.py # NEW -│ ├── ml/ -│ │ ├── gaze/ # NEW directory -│ │ │ └── mediapipe_gaze_tracker.py -│ │ ├── detection/ # NEW directory -│ │ │ └── yolo_object_detector.py -│ │ ├── audio/ # NEW directory -│ │ │ └── vad_audio_analyzer.py -│ │ └── deepfake/ # NEW v1.1 - Market differentiator -│ │ ├── frequency_analyzer.py # DCT artifact detection -│ │ ├── texture_analyzer.py # Skin texture analysis -│ │ ├── temporal_analyzer.py # Video frame consistency -│ │ └── ensemble_detector.py # Combined detection -│ ├── resilience/ # NEW v1.1 - Reliability patterns -│ │ ├── circuit_breaker.py # Circuit breaker implementation -│ │ └── session_rate_limiter.py # Per-session rate limiting -│ └── storage/ -│ └── s3_evidence_storage.py # NEW -├── api/ -│ ├── routes/ -│ │ └── proctor.py # NEW -│ ├── middleware/ -│ │ └── session_rate_limit.py # NEW v1.1 -│ └── schemas/ -│ └── proctor.py # NEW -└── core/ - └── metrics/ - └── proctor_metrics.py # NEW (includes deepfake, circuit breaker, rate limit metrics) -``` - ---- - -## Appendix B: Glossary - -| Term | Definition | -|------|------------| -| **Baseline Embedding** | Reference face embedding captured at session start | -| **Circuit Breaker** | Pattern that stops requests to failing services to prevent cascading failures | -| **Deepfake** | Synthetic or manipulated face/video created using AI/ML techniques | -| **Frame** | Single image capture from webcam | -| **Gaze Direction** | Estimated direction where eyes are looking | -| **Head Pose** | 3D orientation of head (pitch, yaw, roll) | -| **Incident** | Detected suspicious event during session | -| **Liveness** | Verification that face is real, not photo/video | -| **Rate Limiting** | Controlling the rate of requests per session to prevent abuse | -| **Risk Score** | Aggregated suspicion level (0.0-1.0) | -| **Session** | Single proctored exam period | -| **Sliding Window** | Rate limiting algorithm that counts requests in a moving time window | -| **Verification** | Confirming identity matches baseline | - ---- - -**Document Status:** Ready for Review -**Version:** 1.1 (Added deepfake detection, circuit breaker, per-session rate limiting) -**Implementation Timeline:** 16 weeks (8 phases) -**Next Steps:** Stakeholder approval → Implementation kickoff diff --git a/docs/research/PROCTORING_SERVICE_RESEARCH.md b/docs/research/PROCTORING_SERVICE_RESEARCH.md deleted file mode 100644 index 85bf934..0000000 --- a/docs/research/PROCTORING_SERVICE_RESEARCH.md +++ /dev/null @@ -1,716 +0,0 @@ -# Proctoring Service Research Report - -**Date:** December 2024 -**Purpose:** Comprehensive research for implementing continuous identity verification and proctoring service -**Target:** Educational institutions, certification bodies, corporate training - ---- - -## Table of Contents - -1. [Executive Summary](#executive-summary) -2. [Market Landscape](#market-landscape) -3. [Current Codebase Capabilities](#current-codebase-capabilities) -4. [Threat Analysis: Cheating Methods](#threat-analysis-cheating-methods) -5. [Technical Requirements](#technical-requirements) -6. [Feature Comparison Matrix](#feature-comparison-matrix) -7. [Innovation Opportunities](#innovation-opportunities) -8. [Privacy & Compliance](#privacy--compliance) -9. [Implementation Recommendations](#implementation-recommendations) -10. [Technical Architecture Proposal](#technical-architecture-proposal) - ---- - -## Executive Summary - -### Market Opportunity -- Global online proctoring market: **$648M (2024)** → **$1.4B (2032)** -- CAGR: **11.3%** -- Key drivers: Remote education growth, certification demand, AI advancement -- Leaders: Examity, Proctorio (30%+ combined market share) - -### Key Findings -1. **Continuous verification is critical** - One-time authentication is easily bypassed -2. **Multi-modal approach required** - No single technology prevents all cheating -3. **Human review remains essential** - AI generates false positives requiring context -4. **Privacy is paramount** - GDPR/CCPA compliance mandatory for biometric data -5. **Deepfakes are emerging threat** - 900% increase in deepfake fraud (2022-2024) - -### Our Competitive Position -- **Strong foundation**: Face recognition, liveness detection, quality assessment already implemented -- **Gaps to fill**: Continuous monitoring, gaze tracking, audio analysis, session management -- **Innovation opportunity**: Real-time deepfake detection, behavioral biometrics integration - ---- - -## Market Landscape - -### Major Players & Features - -| Provider | Live Proctoring | AI Proctoring | ID Verification | Liveness | 360° View | Browser Lock | -|----------|----------------|---------------|-----------------|----------|-----------|--------------| -| **Proctorio** | ✓ | ✓ | ✓ | ✓ | - | ✓ | -| **ProctorU** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| **Examity** | ✓ | ✓ | ✓ | ✓ | - | ✓ | -| **Honorlock** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| **Talview** | ✓ | ✓ | ✓ | ✓ | ✓ | - | - -### Proctoring Types - -1. **Live Proctoring** - - Human proctor monitors via video in real-time - - Highest security, highest cost - - 1:1 or 1:many ratios - -2. **Automated/AI Proctoring** - - AI algorithms monitor for suspicious behavior - - Scalable, cost-effective - - Flags incidents for human review - -3. **Record & Review** - - Sessions recorded for post-exam review - - Lower real-time overhead - - Delayed detection - -4. **Hybrid** - - AI monitors continuously - - Human intervenes on flags - - Best balance of cost/effectiveness - -### Industry Trends (2024) -- 53% cloud-based adoption -- 49% mobile exam platforms -- 41% biometric integration -- AI-enhanced detection reducing false positives by 47% - ---- - -## Current Codebase Capabilities - -### Already Implemented ✓ - -| Category | Feature | File Location | Proctoring Relevance | -|----------|---------|---------------|---------------------| -| **Face Recognition** | 1:1 Verification | `verify_face.py` | Identity confirmation | -| **Face Recognition** | 1:N Search | `search_face.py` | Detect test-taker swaps | -| **Face Recognition** | Multi-face detection | `detect_multi_face.py` | Detect unauthorized persons | -| **Liveness** | Passive (texture analysis) | `texture_liveness_detector.py` | Anti-spoofing | -| **Liveness** | Active (blink/smile) | `active_liveness_detector.py` | Anti-spoofing | -| **Liveness** | Moiré pattern detection | `texture_liveness_detector.py` | Screen/photo detection | -| **Quality** | Blur detection | `quality_assessor.py` | Ensure clear images | -| **Quality** | Lighting assessment | `quality_assessor.py` | Prevent poor conditions | -| **Landmarks** | 468-point face mesh | `mediapipe_landmarks.py` | Head pose estimation | -| **Demographics** | Age/Gender estimation | `deepface_demographics.py` | Profile matching | -| **Batch** | Concurrent processing | `batch_process.py` | Scalable verification | -| **Webhooks** | Real-time notifications | `http_webhook_sender.py` | Alert systems | -| **Security** | API key auth | `api_key_auth.py` | Secure access | -| **Security** | Rate limiting | `rate_limit.py` | Abuse prevention | - -### Gaps to Fill ✗ - -| Category | Feature | Priority | Complexity | -|----------|---------|----------|------------| -| **Continuous Monitoring** | Session management | CRITICAL | Medium | -| **Continuous Monitoring** | Periodic verification | CRITICAL | Medium | -| **Gaze Tracking** | Eye direction estimation | HIGH | High | -| **Gaze Tracking** | Head pose monitoring | HIGH | Medium | -| **Audio** | Voice activity detection | HIGH | Medium | -| **Audio** | Background noise analysis | MEDIUM | Medium | -| **Object Detection** | Phone detection | HIGH | Medium | -| **Object Detection** | Book/notes detection | HIGH | Medium | -| **Object Detection** | Second person detection | HIGH | Low (exists) | -| **Environment** | Room scan verification | MEDIUM | Medium | -| **Browser** | Tab/window monitoring | HIGH | N/A (client-side) | -| **Anti-Deepfake** | Real-time deepfake detection | HIGH | High | -| **Behavior** | Keystroke dynamics | MEDIUM | Medium | -| **Behavior** | Mouse movement patterns | MEDIUM | Medium | - ---- - -## Threat Analysis: Cheating Methods - -### Category 1: Identity Fraud - -| Threat | Description | Detection Method | Current Capability | -|--------|-------------|------------------|-------------------| -| **Impersonation** | Someone else takes the exam | Continuous face verification | ✓ Face recognition | -| **Photo attack** | Static photo in front of camera | Liveness detection | ✓ Texture + Active | -| **Video replay** | Pre-recorded video playback | Moiré detection, temporal analysis | ✓ Partial | -| **3D mask** | Realistic face mask | Depth analysis, texture | △ Limited | -| **Deepfake** | Real-time face swap | Deepfake detection model | ✗ Not implemented | -| **Virtual camera** | Inject fake video feed | Camera authenticity check | ✗ Not implemented | - -### Category 2: Information Access - -| Threat | Description | Detection Method | Current Capability | -|--------|-------------|------------------|-------------------| -| **Secondary device** | Phone/tablet for answers | Object detection, 360° view | ✗ Not implemented | -| **Hidden notes** | Paper notes off-camera | Room scan, gaze tracking | ✗ Not implemented | -| **Second monitor** | Additional screen | Screen enumeration | ✗ Client-side | -| **Smart glasses** | AR glasses for info | Object detection | ✗ Not implemented | -| **Earpiece** | Audio assistance | Audio analysis | ✗ Not implemented | -| **ChatGPT/AI** | AI-generated answers | Behavioral analysis | ✗ Indirect | - -### Category 3: Collaboration - -| Threat | Description | Detection Method | Current Capability | -|--------|-------------|------------------|-------------------| -| **Second person present** | Helper in room | Multi-face detection | ✓ Implemented | -| **Remote assistance** | Screen sharing | Browser monitoring | ✗ Client-side | -| **Voice coaching** | Whispered answers | Audio analysis | ✗ Not implemented | -| **Chat/messaging** | Text-based help | Tab monitoring | ✗ Client-side | - -### Category 4: Technical Bypass - -| Threat | Description | Detection Method | Current Capability | -|--------|-------------|------------------|-------------------| -| **Virtual machine** | Run exam in VM | VM detection | ✗ Client-side | -| **Browser extensions** | Bypass tools | Extension detection | ✗ Client-side | -| **Network proxy** | Route through helper | Request analysis | ✗ Complex | -| **Time manipulation** | Pause exam, research | Server-side timing | ✓ Feasible | - -### Threat Severity Matrix - -``` - LIKELIHOOD - Low Medium High - High [Phone] [Deepfake][Imperson] -IMPACT Med [Mask] [Notes] [Helper] - Low [VM] [Extension][ChatGPT] -``` - ---- - -## Technical Requirements - -### Must-Have Features (MVP) - -#### 1. Session Management -``` -- Create proctoring session with expiry -- Track session state (pending, active, paused, completed, flagged) -- Store session metadata (exam ID, user ID, start time, duration) -- Handle session interruptions gracefully -``` - -#### 2. Continuous Identity Verification -``` -- Initial verification at session start -- Periodic re-verification (configurable: 30s-5min intervals) -- Confidence score tracking over time -- Alert on verification failure -- Grace period for brief camera blocks -``` - -#### 3. Gaze & Head Pose Tracking -``` -- Real-time head pose estimation (pitch, yaw, roll) -- Eye gaze direction estimation -- Track time spent looking away -- Configure thresholds for "suspicious" duration -- Aggregate statistics per session -``` - -#### 4. Multi-Face Detection -``` -- Continuous monitoring for additional faces -- Immediate alert on detection -- Capture evidence frame -- Track count over session -``` - -#### 5. Object Detection -``` -- Phone/mobile device detection -- Book/document detection -- Electronic device detection -- Configurable object whitelist (calculator, etc.) -``` - -#### 6. Audio Monitoring -``` -- Voice activity detection -- Multiple voice detection -- Keyword spotting (optional) -- Background noise baseline -- Suspicious audio flagging -``` - -#### 7. Incident Flagging & Scoring -``` -- Real-time incident creation -- Severity classification (low/medium/high) -- Risk score calculation -- Evidence attachment (image, timestamp) -- Export for human review -``` - -### Nice-to-Have Features (Phase 2) - -#### 8. Deepfake Detection -``` -- Real-time synthetic face detection -- Temporal consistency analysis -- Injection attack prevention -``` - -#### 9. Behavioral Biometrics -``` -- Keystroke dynamics profiling -- Mouse movement patterns -- Typing rhythm analysis -- Baseline establishment -``` - -#### 10. 360° Environment Monitoring -``` -- Secondary camera support -- Room scan verification -- Periodic environment checks -``` - -#### 11. Browser/Screen Monitoring -``` -- Active window detection -- Tab switching tracking -- Screen capture (with consent) -- Copy/paste detection -``` - ---- - -## Feature Comparison Matrix - -### Our Position vs Market Leaders - -| Feature | Proctorio | ProctorU | Examity | Honorlock | **Ours (Current)** | **Ours (Planned)** | -|---------|-----------|----------|---------|-----------|-------------------|-------------------| -| Face Verification | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Continuous Verification | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | -| Liveness Detection | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Multi-Face Detection | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Gaze Tracking | ✓ | ✓ | △ | ✓ | ✗ | ✓ | -| Head Pose Monitoring | ✓ | ✓ | △ | ✓ | △ | ✓ | -| Audio Analysis | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | -| Object Detection | ✓ | ✓ | △ | ✓ | ✗ | ✓ | -| Phone Detection | ✓ | ✓ | △ | ✓ | ✗ | ✓ | -| Browser Lockdown | ✓ | ✓ | ✓ | ✓ | ✗ | △ | -| Room Scan | △ | ✓ | △ | ✓ | ✗ | ✓ | -| 360° View | ✗ | ✓ | ✗ | ✓ | ✗ | △ | -| Live Proctoring | ✓ | ✓ | ✓ | ✓ | ✗ | △ | -| AI Risk Scoring | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | -| Deepfake Detection | △ | △ | △ | △ | ✗ | **✓** | -| Webhooks/API | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Multi-tenant | △ | △ | △ | △ | ✓ | ✓ | - -**Legend:** ✓ = Full, △ = Partial, ✗ = None - ---- - -## Innovation Opportunities - -### 1. Real-Time Deepfake Detection (Differentiator) - -**Market Gap:** Most proctoring solutions have limited deepfake detection. -**Opportunity:** Implement cutting-edge deepfake detection using: -- Temporal consistency analysis -- Physiological signal extraction (PPG from face) -- Frequency domain analysis -- Injection attack prevention - -**Technical Approach:** -```python -class DeepfakeDetector: - - analyze_temporal_consistency(frames) - - extract_physiological_signals(video) # PPG, subtle face movements - - detect_frequency_anomalies(frame) - - verify_camera_authenticity() -``` - -### 2. Adaptive Continuous Verification - -**Innovation:** Intelligent verification frequency based on: -- Risk score trending -- Session duration -- Previous incident count -- Behavioral baseline deviation - -```python -# Dynamic interval adjustment -if risk_score > 0.7: - verification_interval = 15 # seconds -elif risk_score > 0.4: - verification_interval = 30 -else: - verification_interval = 60 -``` - -### 3. Privacy-Preserving Proctoring - -**Innovation:** On-device processing where possible: -- Edge AI for initial analysis -- Only transmit incidents/flags -- Reduce data retention -- Provide transparency dashboard - -### 4. Behavioral Fingerprinting - -**Innovation:** Create unique behavioral profile: -- Typing cadence -- Mouse movement patterns -- Interaction timing -- Use as secondary authentication - -### 5. Explainable AI Flagging - -**Innovation:** Every flag includes: -- Visual evidence -- Confidence score -- Explanation of trigger -- Historical context -- Suggested action - ---- - -## Privacy & Compliance - -### GDPR Requirements - -| Requirement | Implementation | -|-------------|----------------| -| **Lawful Basis** | Explicit consent required for biometric processing | -| **Data Minimization** | Collect only necessary data | -| **Purpose Limitation** | Use only for stated proctoring purpose | -| **Storage Limitation** | Define retention period (e.g., 30 days post-exam) | -| **Security** | Encryption, access controls, audit logs | -| **DPIA** | Required for systematic monitoring | -| **Data Subject Rights** | Access, deletion, portability | - -### Biometric Data Handling - -```yaml -Biometric Data Policy: - collection: - - Face images: During session only - - Face embeddings: Stored encrypted - - Audio: Processed real-time, not stored - - retention: - - Session recordings: 30 days - - Incident evidence: 90 days - - Embeddings: Until user deletion - - encryption: - - At rest: AES-256 - - In transit: TLS 1.3 - - access: - - Role-based access control - - Audit logging - - Admin approval for exports -``` - -### Regional Compliance - -| Region | Law | Key Requirements | -|--------|-----|------------------| -| EU | GDPR | Explicit consent, DPIA, DPO | -| US (CA) | CCPA/CPRA | Right to know, delete, opt-out | -| US (IL) | BIPA | Written consent, retention schedule | -| US (TX) | CUBI | Consent before capture | -| Canada | PIPEDA | Meaningful consent | - -### Privacy-by-Design Features - -1. **Consent Management** - - Clear disclosure of data collection - - Granular consent options - - Easy withdrawal mechanism - -2. **Data Minimization** - - Process frames without storing - - Delete raw video after review period - - Anonymize analytics data - -3. **Transparency** - - Show when camera/mic active - - Explain each detection type - - Provide session report to user - ---- - -## Implementation Recommendations - -### Phase 1: Core Proctoring (MVP) - 6-8 weeks - -**Week 1-2: Session Management** -``` -- Session entity and repository -- Session lifecycle (create, start, pause, resume, end) -- Session configuration (duration, intervals, thresholds) -- REST API endpoints -``` - -**Week 3-4: Continuous Verification** -``` -- Verification scheduler -- Frame capture service -- Verification result aggregation -- Confidence tracking -- Webhook notifications -``` - -**Week 5-6: Gaze & Attention Tracking** -``` -- Head pose estimation using existing landmarks -- Gaze direction estimation -- "Looking away" detection -- Time-based threshold alerts -``` - -**Week 7-8: Incident System** -``` -- Incident entity and storage -- Risk score calculation -- Evidence capture -- Flag categorization -- Review API -``` - -### Phase 2: Enhanced Detection - 4-6 weeks - -**Week 9-10: Object Detection** -``` -- YOLO-based object detection -- Phone/book/device models -- Frame annotation -- Alert generation -``` - -**Week 11-12: Audio Analysis** -``` -- Audio stream processing -- Voice activity detection -- Multiple speaker detection -- Background noise baseline -``` - -**Week 13-14: Deepfake Detection** -``` -- Temporal analysis -- Frequency domain checks -- Injection detection -- Integration with liveness -``` - -### Phase 3: Advanced Features - 4+ weeks - -``` -- Behavioral biometrics -- Secondary camera support -- Live proctor integration -- Advanced analytics dashboard -``` - ---- - -## Technical Architecture Proposal - -### High-Level Architecture - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ CLIENT SIDE │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ -│ │ Webcam │ │ Mic │ │ Screen │ │ Browser Extension│ │ -│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │ -│ │ │ │ │ │ -│ └─────────────┴─────────────┴──────────────────┘ │ -│ │ │ -│ WebRTC / HTTP │ -└───────────────────────────┼───────────────────────────────────────┘ - │ -┌───────────────────────────┼───────────────────────────────────────┐ -│ API GATEWAY │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Auth/API Key│ │ Rate Limit │ │ Routing │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└───────────────────────────┼───────────────────────────────────────┘ - │ -┌───────────────────────────┼───────────────────────────────────────┐ -│ PROCTORING SERVICE │ -│ │ -│ ┌────────────────────────────────────────────────────────────┐ │ -│ │ Session Manager │ │ -│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │ │ -│ │ │ Create │ │ Start │ │ Monitor │ │ Close │ │ │ -│ │ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │ │ -│ └────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ Analysis Pipeline │ │ -│ │ │ │ -│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │ │ -│ │ │ Face │ │ Gaze │ │ Object │ │ Audio │ │ │ -│ │ │ Verify │ │ Track │ │ Detect │ │ Analyze │ │ │ -│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────────┬────────┘ │ │ -│ │ │ │ │ │ │ │ -│ │ └────────────┴────────────┴─────────────────┘ │ │ -│ │ │ │ │ -│ │ ┌──────┴──────┐ │ │ -│ │ │ Fusion │ │ │ -│ │ │ Engine │ │ │ -│ │ └──────┬──────┘ │ │ -│ └──────────────────────────┼────────────────────────────────────┘ │ -│ │ │ -│ ┌──────────────────────────┼────────────────────────────────────┐ │ -│ │ Incident Manager │ │ -│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ -│ │ │ Flag │ │ Score │ │ Evidence │ │ Webhook │ │ │ -│ │ │ Incidents│ │ Calculate│ │ Store │ │ Notify │ │ │ -│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -└───────────────────────────┼───────────────────────────────────────┘ - │ -┌───────────────────────────┼───────────────────────────────────────┐ -│ DATA LAYER │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ -│ │PostgreSQL│ │ Redis │ │ S3 │ │ TimescaleDB │ │ -│ │ Sessions │ │ Cache │ │ Evidence │ │ Metrics │ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────────────┘ │ -└────────────────────────────────────────────────────────────────────┘ -``` - -### New Domain Entities - -```python -# Session Entity -class ProctorSession: - id: UUID - exam_id: str - user_id: str - tenant_id: str - status: SessionStatus # pending, active, paused, completed, terminated - config: SessionConfig - started_at: Optional[datetime] - ended_at: Optional[datetime] - risk_score: float - incident_count: int - verification_count: int - verification_failures: int - -# Incident Entity -class ProctorIncident: - id: UUID - session_id: UUID - incident_type: IncidentType - severity: Severity # low, medium, high, critical - confidence: float - timestamp: datetime - evidence_url: Optional[str] - details: dict - reviewed: bool - reviewer_notes: Optional[str] - -# Verification Result -class VerificationResult: - session_id: UUID - timestamp: datetime - face_detected: bool - face_matched: bool - match_confidence: float - liveness_passed: bool - liveness_score: float - quality_score: float - gaze_on_screen: bool - head_pose: HeadPose - faces_detected: int - objects_detected: List[DetectedObject] - -# Risk Score Components -class RiskScore: - session_id: UUID - timestamp: datetime - overall_score: float # 0.0 - 1.0 - components: - identity_risk: float - attention_risk: float - environment_risk: float - behavior_risk: float -``` - -### API Endpoints (New) - -```yaml -# Session Management -POST /api/v1/proctor/sessions # Create session -GET /api/v1/proctor/sessions/{id} # Get session details -POST /api/v1/proctor/sessions/{id}/start # Start session -POST /api/v1/proctor/sessions/{id}/pause # Pause session -POST /api/v1/proctor/sessions/{id}/resume # Resume session -POST /api/v1/proctor/sessions/{id}/end # End session -DELETE /api/v1/proctor/sessions/{id} # Terminate session - -# Verification -POST /api/v1/proctor/sessions/{id}/verify # Submit frame for verification -GET /api/v1/proctor/sessions/{id}/status # Get current session status - -# Incidents -GET /api/v1/proctor/sessions/{id}/incidents # List incidents -GET /api/v1/proctor/incidents/{id} # Get incident details -PATCH /api/v1/proctor/incidents/{id} # Update incident (review) - -# Analytics -GET /api/v1/proctor/sessions/{id}/timeline # Get session timeline -GET /api/v1/proctor/sessions/{id}/report # Get session report -GET /api/v1/proctor/analytics # Aggregate analytics - -# Configuration -GET /api/v1/proctor/config # Get default config -POST /api/v1/proctor/config # Create custom config -``` - ---- - -## Appendix: Sources - -### Market Research -- [Global Growth Insights - Online Exam Proctoring Market](https://www.globalgrowthinsights.com/market-reports/online-exam-proctoring-market-101994) -- [Gartner Peer Insights - Remote Proctoring Services](https://www.gartner.com/reviews/market/remote-proctoring-services-for-higher-education) -- [Eklavvya - Best Proctoring Solutions 2024](https://www.eklavvya.com/blog/proctoring-solution-2025/) - -### Technical Research -- [IEEE - Identity Verification with Face Recognition and Eye Tracking](https://ieeexplore.ieee.org/document/10721819/) -- [SpringerOpen - Continuous User Identification in Distance Learning](https://slejournal.springeropen.com/articles/10.1186/s40561-023-00255-9) -- [arXiv - Principles of Designing Robust Remote Face Anti-Spoofing Systems](https://arxiv.org/abs/2406.03684) -- [PMC - AI-based Proctoring Systems: Systematic Review](https://pmc.ncbi.nlm.nih.gov/articles/PMC8220875/) - -### Security & Privacy -- [iProov - How Deepfakes Threaten Remote Identity Verification](https://www.iproov.com/blog/deepfakes-threaten-remote-identity-verification-systems) -- [Proctor360 - GDPR Compliance for Remote Proctoring](https://proctor360.com/blog/gdpr-compliance-remote-proctoring-essentials) -- [PSI Exams - AI Test Fraud and Online Proctoring Security](https://www.psiexams.com/knowledge-hub/ai-test-fraud-online-proctoring-security/) - -### Feature Analysis -- [Honorlock - Voice Detection](https://honorlock.com/voice-detection/) -- [Talview - Understanding Proctoring Flags](https://blog.talview.com/en/understanding-proctoring-flags) -- [Eklavvya - 360-Degree Proctoring](https://www.eklavvya.com/blog/360-degree-proctoring-exam/) - ---- - -## Conclusion - -The biometric-processor has a **strong foundation** for building a professional proctoring service. Key advantages: - -1. **Face Recognition**: Production-ready with multiple models -2. **Liveness Detection**: Multi-vector approach (passive + active) -3. **Infrastructure**: Webhooks, rate limiting, multi-tenancy already in place -4. **Architecture**: Clean architecture enables easy extension - -**Recommended approach:** -1. Start with **MVP** (session management + continuous verification) -2. Add **gaze tracking** using existing MediaPipe landmarks -3. Implement **object detection** with YOLO -4. Layer in **audio analysis** and **deepfake detection** -5. Differentiate with **privacy-first design** and **explainable AI** - -The proctoring market is growing rapidly, and our technology stack positions us well to capture this opportunity with a secure, privacy-compliant, and innovative solution. diff --git a/docs/research/SERVER_PROVIDERS_RESEARCH.md b/docs/research/SERVER_PROVIDERS_RESEARCH.md deleted file mode 100644 index 5788e97..0000000 --- a/docs/research/SERVER_PROVIDERS_RESEARCH.md +++ /dev/null @@ -1,196 +0,0 @@ -# Server Providers Research: GPU & CPU VPS for Biometric Processor - -> Research Date: February 2, 2026 - -## Table of Contents - -- [Architecture Context](#architecture-context) -- [CPU VPS Providers (Identity Core API)](#cpu-vps-providers-identity-core-api) -- [GPU VPS Providers (Biometric Processor)](#gpu-vps-providers-biometric-processor) -- [Recommendation Summary](#recommendation-summary) - ---- - -## Architecture Context - -The system consists of two microservices: - -``` -[Identity Core API (VPS)] → Standard web API, database-heavy - ↓ calls -[Biometric Processor (GPU)] → ML inference: face detection, embeddings, - liveness detection, quality assessment -``` - -**Identity Core API** — runs on a standard CPU VPS. Handles authentication, user management, and orchestrates biometric operations by calling the Biometric Processor API. - -**Biometric Processor** — the compute-heavy service. Runs DeepFace (TensorFlow), MediaPipe, YOLO models for: -- Face detection & embedding extraction (128–2622D vectors) -- Liveness detection (passive + active anti-spoofing) -- Quality assessment, demographics, 468-point landmarks -- Card/document type detection (YOLO) -- Real-time proctoring via WebSocket - -### Current Resource Requirements - -| Config | CPU | RAM | Notes | -|--------|-----|-----|-------| -| **Minimum (dev)** | 2 cores | 2 GB | Single-user, slow inference | -| **Production (CPU-only)** | 2–4 cores | 4 GB | 18–30 FPS, ~200–500ms/enrollment | -| **Production (GPU)** | 2 cores + 1 GPU | 4 GB + GPU VRAM | 50–80 FPS (est. 2.5–3x speedup) | -| **HA cluster (K8s)** | 3 nodes × 2 CPU | 3 × 2 GB | Auto-scales to 20 replicas | - ---- - -## CPU VPS Providers (Identity Core API) - -The Identity Core API is a standard REST API with PostgreSQL + Redis. A **2 vCPU / 4 GB RAM** VPS is sufficient. - -### Pricing Comparison - -| Provider | Plan | vCPU | RAM | Storage | Traffic | Price/mo | -|----------|------|------|-----|---------|---------|----------| -| **Hetzner** | CX22 | 2 (shared) | 4 GB | 40 GB NVMe | 20 TB | ~€3.79 (~$4–5) | -| **Contabo** | Cloud VPS 10 | 3–4 (shared) | 8 GB | 75–150 GB | Unlimited* | ~$3.96–$4.95 | -| **DigitalOcean** | Basic 2vCPU | 2 (shared) | 4 GB | 80 GB SSD | 4 TB | ~$24 | -| **DigitalOcean** | CPU-Optimized | 2 (dedicated) | 4 GB | 25 GB SSD | 4 TB | ~$42 | -| **Linode/Akamai** | Shared 4GB | 2 (shared) | 4 GB | 80 GB SSD | 4 TB | ~$24 | -| **Vultr** | Cloud Compute | 2 (shared) | 4 GB | 80 GB SSD | 3 TB | ~$24 | - -*\*Contabo: fair-use policy, 200 Mbit/s–1 Gbit/s port* - -### Assessment - -| Provider | Price | Performance | UX / Docs | Best For | -|----------|-------|-------------|-----------|----------| -| **Hetzner** | Excellent | Excellent | Minimal | Experienced devs, best value | -| **Contabo** | Excellent | Moderate | Basic | Max specs per dollar | -| **DigitalOcean** | Moderate | Good | Excellent | Dev experience, tutorials | - -**Recommendation for Identity Core API:** Hetzner CX22 (~$5/mo) or Contabo VPS 10 (~$4–5/mo). Both are sufficient for a REST API + PostgreSQL + Redis stack. - ---- - -## GPU VPS Providers (Biometric Processor) - -The biometric processor needs GPU acceleration for production-grade inference. Current CPU-only performance is 18–30 FPS; GPU is expected to deliver 50–80 FPS. - -### What GPU Does the Biometric Processor Need? - -The ML stack (TensorFlow 2.15 + DeepFace + MediaPipe + YOLO) requires: -- **Minimum VRAM:** 4–6 GB (fits all models concurrently) -- **Recommended VRAM:** 8–16 GB (comfortable headroom for batch processing) -- **CUDA:** 12.2+ with cuDNN 8 -- **Best fit:** NVIDIA T4 (16 GB, inference-optimized) or RTX A4000/A6000 - -### GPU VPS Pricing Comparison - -#### Budget Tier (< $1/hr) — Good for Dev/Staging & Light Production - -| Provider | GPU | VRAM | Price/hr | Price/mo (24/7) | Notes | -|----------|-----|------|----------|-----------------|-------| -| **Vast.ai** | RTX 3090 | 24 GB | $0.11–0.31 | $80–225 | Marketplace, variable availability | -| **Vast.ai** | T4 | 16 GB | ~$0.15–0.30 | $108–216 | Community GPUs | -| **Thunder Compute** | T4 | 16 GB | $0.29 | $209 | Simple UX | -| **RunPod** | RTX 4090 | 24 GB | $0.34 | $245 | Community cloud | -| **Hyperstack** | RTX A6000 | 48 GB | $0.50 | $360 | Good value for VRAM | -| **TensorDock** | A100 40GB | 40 GB | $0.75 | $540 | KVM isolation | -| **Thunder Compute** | A100 | 80 GB | $0.66 | $475 | On-demand | - -#### Mid Tier ($1–3/hr) — Production Workloads - -| Provider | GPU | VRAM | Price/hr | Price/mo (24/7) | Notes | -|----------|-----|------|----------|-----------------|-------| -| **GCP** | T4 | 16 GB | $0.35–0.95 | $252–684 | Attached to N1 VMs | -| **GCP** | A100 40GB | 40 GB | ~$1.15 | $828 | Spot pricing | -| **Lambda Labs** | A100 40GB | 40 GB | $1.29 | $929 | Dedicated instances | -| **Northflank** | A100 40GB | 40 GB | $1.42 | $1,022 | Managed platform | -| **GCP** | A100 80GB | 80 GB | ~$1.57 | $1,130 | On-demand | -| **RunPod** | H100 PCIe | 80 GB | $1.99 | $1,433 | On-demand | -| **TensorDock** | H100 SXM5 | 80 GB | $2.25 | $1,620 | No quotas | - -#### Enterprise / Hyperscaler Tier - -| Provider | GPU | VRAM | Price/hr | Notes | -|----------|-----|------|----------|-------| -| **AWS** | T4 (g4dn) | 16 GB | ~$0.53 | Ecosystem, compliance | -| **AWS** | A100 (p4d) | 40 GB | ~$4.10/GPU | 8-GPU minimum | -| **Azure** | T4 (spot) | 16 GB | ~$0.09 | Spot only, can be interrupted | -| **GCP** | L4 | 24 GB | ~$0.70 | New inference-optimized | - -### Serverless GPU Options (Pay-per-inference) - -For variable load, serverless GPU can be cost-effective: - -| Provider | GPU | Price Model | Notes | -|----------|-----|-------------|-------| -| **RunPod Serverless** | A100 80GB | $0.0008–0.0011/sec (~$2.88–3.96/hr active) | Pay only when running | -| **Modal** | T4/A100 | Per-second billing | Cold starts ~2–5s | -| **GCP Cloud Run + GPU** | L4/T4 | Per-request + GPU-seconds | Currently in preview | - ---- - -## Recommendation Summary - -### For Identity Core API (CPU VPS) - -| Scenario | Provider | Spec | Cost/mo | -|----------|----------|------|---------| -| **Budget / Dev** | Hetzner CX22 | 2 vCPU, 4 GB, 40 GB NVMe | ~$5 | -| **Budget + More RAM** | Contabo VPS 10 | 3–4 vCPU, 8 GB, 150 GB | ~$4–5 | -| **Production (DX)** | DigitalOcean | 2 vCPU, 4 GB, 80 GB | ~$24 | - -### For Biometric Processor (GPU) - -| Scenario | Provider | GPU | Cost/mo | Justification | -|----------|----------|-----|---------|---------------| -| **Dev/Testing** | Vast.ai | RTX 3090 | ~$80–150 | Cheapest, good for dev | -| **Staging** | Thunder Compute | T4 | ~$209 | Stable, simple | -| **Production (Budget)** | RunPod Community | RTX 4090 | ~$245 | Good perf/price | -| **Production (Reliable)** | TensorDock | A100 40GB | ~$540 | KVM isolation, reliable | -| **Production (Variable Load)** | RunPod Serverless | A100 | Pay-per-use | Best for bursty traffic | -| **Production (Compliance)** | GCP Cloud Run | T4/L4 | ~$252–684 | HIPAA/SOC2, current setup | -| **Production (Scale)** | Lambda Labs | A100 | ~$929 | Dedicated, consistent | - -### Architecture Decision: CPU-Only vs GPU - -The biometric processor **currently runs on CPU-only** (TensorFlow-CPU 2.15.0). For many use cases, this may be sufficient: - -| Metric | CPU-Only (2 cores) | GPU (T4) | GPU (A100) | -|--------|-------------------|----------|------------| -| Enrollment latency | 200–500ms | ~80–200ms | ~50–100ms | -| Throughput (FPS) | 18–30 | 50–80 | 100–150 | -| Cost/mo (24/7) | $5–24 | $108–540 | $475–929 | -| Best for | < 50 users/min | 50–500 users/min | 500+ users/min | - -**If your load is < 50 concurrent enrollments/minute, a CPU VPS ($5–24/mo) may be all you need.** GPU becomes valuable when you need real-time video processing (proctoring), batch operations, or high-throughput 1:N search. - -### Combined Stack Cost Estimates - -| Tier | Identity Core | Biometric Proc. | PostgreSQL | Redis | Total/mo | -|------|--------------|-----------------|------------|-------|----------| -| **Dev** | Hetzner $5 | CPU-only (same box) | Embedded | Embedded | **~$5** | -| **Staging** | Hetzner $5 | Vast.ai RTX 3090 $100 | Managed $15 | Managed $10 | **~$130** | -| **Prod (CPU)** | DO $24 | Hetzner $10 (4 CPU) | DO Managed $15 | DO Managed $15 | **~$64** | -| **Prod (GPU)** | DO $24 | RunPod 4090 $245 | DO Managed $15 | DO Managed $15 | **~$300** | -| **Prod (Scale)** | K8s cluster | TensorDock A100 $540 | CloudSQL $50 | Redis Cloud $30 | **~$700** | - ---- - -## Sources - -- [GPU Price Comparison 2026 — GetDeploying](https://getdeploying.com/gpus) -- [7 Cheapest Cloud GPU Providers 2026 — Northflank](https://northflank.com/blog/cheapest-cloud-gpu-providers) -- [Top 12 Cloud GPU Providers — RunPod](https://www.runpod.io/articles/guides/top-cloud-gpu-providers) -- [5 Best Cloud GPU Providers — Hyperstack](https://www.hyperstack.cloud/blog/case-study/best-cloud-gpu-providers-for-ai) -- [DigitalOcean GPU Droplets](https://www.digitalocean.com/resources/articles/cloud-gpu-provider) -- [HOSTKEY GPU VPS](https://hostkey.com/dedicated-servers/gpu-vps/) -- [Vast.ai GPU Marketplace](https://vast.ai/) -- [Lambda AI Pricing](https://lambda.ai/pricing) -- [RunPod Pricing](https://www.runpod.io/pricing) -- [Hetzner Cloud VPS](https://www.hetzner.com/cloud) -- [Contabo VPS](https://contabo.com/en-us/vps/) -- [DigitalOcean Droplet Pricing](https://www.digitalocean.com/pricing/droplets) -- [Top 10 Low-Cost VPS Providers 2026 — Nucamp](https://www.nucamp.co/blog/top-10-low-cost-vps-providers-in-2026-affordable-alternatives-to-aws-azure-gcp-and-vercel) -- [NVIDIA A100 Cloud Options — Fluence](https://www.fluence.network/blog/nvidia-a100/) -- [Thunder Compute Pricing](https://www.thundercompute.com/blog/cheapest-cloud-gpu-providers-in-2025) diff --git a/scripts/README_BENCHMARK.md b/scripts/README_BENCHMARK.md deleted file mode 100644 index fa6ac96..0000000 --- a/scripts/README_BENCHMARK.md +++ /dev/null @@ -1,199 +0,0 @@ -# Live Analysis Performance Benchmark - -This directory contains a performance benchmark tool to measure the processing speed and determine the maximum sustainable frame rate for each live analysis mode. - -## Prerequisites - -1. **Backend Server Running**: Ensure the backend server is running on `http://localhost:8000` - -```bash -# From the project root -docker-compose up -d -# OR if running locally -python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 -``` - -2. **Required Python Packages**: -```bash -pip install websockets rich opencv-python -``` - -## Usage - -### Basic Usage - -Run the benchmark with default settings (50 frames per mode): - -```bash -python scripts/benchmark_live_analysis.py -``` - -### Advanced Usage - -```bash -# Custom number of frames -python scripts/benchmark_live_analysis.py --frames 100 - -# Custom WebSocket URL -python scripts/benchmark_live_analysis.py --url ws://localhost:8000/ws/live-analysis - -# Use a specific test image -python scripts/benchmark_live_analysis.py --image path/to/test/face.jpg - -# Custom output file -python scripts/benchmark_live_analysis.py --output my_benchmark.json -``` - -### Full Example - -```bash -python scripts/benchmark_live_analysis.py \ - --frames 100 \ - --image tests/fixtures/sample_face.jpg \ - --output benchmark_$(date +%Y%m%d_%H%M%S).json -``` - -## Output - -The benchmark will: - -1. **Display real-time progress** for each analysis mode -2. **Print a formatted table** with performance metrics: - - Average processing time (ms) - - Min/Max processing time (ms) - - P95/P99 latency (ms) - - Maximum sustainable FPS - - Success rate - -3. **Show recommendations** for each mode based on performance -4. **Save detailed results** to a JSON file for further analysis - -### Sample Output - -``` -┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━┓ -┃ Mode ┃ Avg (ms) ┃ Min (ms) ┃ Max (ms) ┃ P95 (ms) ┃ Max FPS ┃ Success Rate ┃ -┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━┩ -│ face_detection │ 15.23 │ 12.45 │ 45.67 │ 18.90 │ 65.7 │ 100.0% │ -│ quality │ 45.67 │ 38.90 │ 89.12 │ 67.34 │ 21.9 │ 100.0% │ -│ demographics │ 234.56 │ 198.34 │ 456.78 │ 345.67 │ 4.3 │ 98.0% │ -│ liveness │ 123.45 │ 98.76 │ 234.56 │ 189.01 │ 8.1 │ 96.0% │ -│ enrollment_ready │ 67.89 │ 54.32 │ 123.45 │ 98.76 │ 14.7 │ 100.0% │ -│ landmarks │ 34.56 │ 28.90 │ 78.90 │ 56.78 │ 28.9 │ 100.0% │ -└────────────────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────────┘ - -Recommendations: - face_detection → 15-30 FPS (real-time video) - quality → 5-15 FPS (smooth interactive) - demographics → 2-5 FPS (interactive) - liveness → 5-15 FPS (smooth interactive) - enrollment_ready → 5-15 FPS (smooth interactive) - landmarks → 15-30 FPS (real-time video) -``` - -## Understanding the Results - -### Performance Metrics - -- **Avg (ms)**: Average processing time per frame -- **Min/Max (ms)**: Range of processing times -- **P95 (ms)**: 95th percentile latency (95% of frames process faster than this) -- **Max FPS**: Theoretical maximum frames per second (1000 / avg_ms) -- **Success Rate**: Percentage of frames processed without errors - -### FPS Recommendations - -Based on the results, the benchmark provides recommendations: - -- **30+ FPS**: Real-time video quality, suitable for smooth live streaming -- **15-30 FPS**: Smooth interactive experience -- **5-15 FPS**: Good interactive experience with slight delay -- **2-5 FPS**: Adequate for interactive use cases -- **<2 FPS**: Limited to slow/batch processing - -### Typical Performance Expectations - -For reference, here are approximate performance ranges on modern hardware: - -| Mode | Expected Avg (ms) | Expected Max FPS | Use Case | -|------|-------------------|------------------|----------| -| Face Detection | 10-30 | 30-100 | Very fast, suitable for real-time | -| Quality | 30-60 | 15-30 | Fast, good for live feedback | -| Demographics | 150-300 | 3-7 | Slower, use 2-5 FPS | -| Liveness | 80-150 | 7-12 | Moderate, use 5-10 FPS | -| Enrollment Ready | 50-100 | 10-20 | Fast, good for live guidance | -| Landmarks | 20-50 | 20-50 | Fast, suitable for real-time | -| Verification | 40-80 | 12-25 | Fast with database | -| Search | 100-500+ | 2-10 | Depends on database size | - -**Note**: Actual performance depends on: -- Hardware (CPU/GPU) -- Image resolution -- Database size (for verification/search) -- ML model complexity -- Network latency (WebSocket overhead) - -## Optimizing Performance - -If your results are slower than expected: - -1. **Reduce image resolution** in the frontend (640x480 vs 1920x1080) -2. **Use frame skipping** (`frame_skip: 1` or higher) -3. **Enable GPU acceleration** if available -4. **Optimize database** for verification/search modes -5. **Reduce JPEG quality** for faster encoding (trade-off: slight quality loss) -6. **Use dedicated hardware** (GPU inference, hardware encoders) - -## Troubleshooting - -### "Connection refused" error -- Ensure the backend server is running -- Check the WebSocket URL (default: `ws://localhost:8000/ws/live-analysis`) - -### "No module named..." errors -- Install required packages: `pip install websockets rich opencv-python` - -### Low FPS results -- Check if the server is running on adequate hardware -- Verify ML models are loaded correctly -- Check server logs for errors or warnings - -### High variance in processing times -- This is normal - first frames may be slower (model loading) -- Database lookups can vary (verification/search modes) -- Consider running with more frames (--frames 100) for stable averages - -## Integration with Frontend - -Based on the benchmark results, update the FPS selector in your frontend live streaming components: - -```typescript -// In LiveCameraStream component -const defaultFPS = { - quality: 10, // Adjust based on benchmark - demographics: 3, // Slower mode, lower FPS - liveness: 5, - enrollment_ready: 10, - verification: 8, - search: 3, // Depends on DB size - landmarks: 15, -}; -``` - -## Continuous Monitoring - -For production deployments: - -1. Run benchmarks regularly to monitor performance degradation -2. Compare results over time to identify trends -3. Set up alerts if processing times exceed thresholds -4. Use the P95/P99 metrics for SLA definitions - -## Next Steps - -After reviewing the benchmark results: - -1. Adjust the default FPS settings in the frontend components -2. Add frame skip options for slower modes -3. Consider implementing adaptive FPS based on processing time -4. Set up performance monitoring in production diff --git a/scripts/setup-database.md b/scripts/setup-database.md deleted file mode 100644 index 433ac93..0000000 --- a/scripts/setup-database.md +++ /dev/null @@ -1,89 +0,0 @@ -# Database Setup Instructions - -## Enable pgvector Extension - -The biometric API requires the pgvector extension for vector similarity search. Follow these steps to enable it: - -### Option 1: Google Cloud Console (Recommended) - -1. Go to [Cloud SQL Instances](https://console.cloud.google.com/sql/instances?project=fivucsas) -2. Click on `biometric-db` -3. Click "Cloud SQL Studio" in the left sidebar -4. Select database: `biometric` -5. Run the following SQL: - -```sql -CREATE EXTENSION IF NOT EXISTS vector CASCADE; -``` - -6. Verify with: -```sql -SELECT extname, extversion FROM pg_extension WHERE extname = 'vector'; -``` - -### Option 2: Using Cloud SQL Proxy (Local) - -1. Download Cloud SQL Proxy from [GitHub](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/releases) - -2. Start the proxy: -```bash -cloud-sql-proxy fivucsas:europe-west1:biometric-db --port=5432 -``` - -3. Connect with psql: -```bash -psql "host=127.0.0.1 port=5432 user=postgres dbname=biometric password=BiometricSecure2024!" -``` - -4. Run the init script: -```bash -psql "host=127.0.0.1 port=5432 user=postgres dbname=biometric" -f scripts/init-database.sql -``` - -### Option 3: Using gcloud (with authorized network) - -1. Add your IP to authorized networks: -```bash -gcloud sql instances patch biometric-db \ - --authorized-networks=$(curl -s ifconfig.me)/32 \ - --project=fivucsas -``` - -2. Connect: -```bash -gcloud sql connect biometric-db --database=biometric --user=postgres --project=fivucsas -``` - -3. Run the SQL commands from `scripts/init-database.sql` - -## Verify Setup - -After enabling the extension, test the API: - -```bash -curl https://biometric-api-902542798396.europe-west1.run.app/api/v1/health -``` - -The health check should return: -```json -{"status":"healthy","version":"1.0.0","model":"Facenet","detector":"opencv"} -``` - -## Troubleshooting - -### Error: "unknown type: public.vector" - -This means the pgvector extension is not installed. Run: -```sql -CREATE EXTENSION IF NOT EXISTS vector CASCADE; -``` - -### Error: "permission denied to create extension" - -Your database user needs superuser privileges. In Cloud SQL, the `postgres` user has these privileges by default. - -### Connection Issues - -1. Check that Cloud SQL instance is running -2. Verify VPC connector is configured -3. Check authorized networks include Cloud Run's egress IPs diff --git a/tests/TEST_RESULTS_REPORT.md b/tests/TEST_RESULTS_REPORT.md deleted file mode 100644 index acaaaa0..0000000 --- a/tests/TEST_RESULTS_REPORT.md +++ /dev/null @@ -1,252 +0,0 @@ -# Performance Optimization Test Results Report - -**Date:** 2025-12-15 -**Module:** biometric-processor -**Test Type:** Unit Tests, Integration Tests, Manual Tests - ---- - -## Executive Summary - -All performance optimization components have been thoroughly tested with **54 automated tests** (37 unit + 17 integration) and **6 manual test sections** covering real face images. All tests passed successfully. - -| Test Suite | Tests | Passed | Failed | Duration | -|------------|-------|--------|--------|----------| -| Unit Tests | 37 | 37 | 0 | 1.78s | -| Integration Tests | 17 | 17 | 0 | 1.93s | -| Manual Tests | 6 | 6 | 0 | ~5s | -| **Total** | **60** | **60** | **0** | - | - ---- - -## Test Data - -### Test Images Used -- **Location:** `tests/fixtures/images/` -- **Users:** 3 (afuat, aga, ahab) -- **Total Images:** 19 - -| User | Images | Formats | -|------|--------|---------| -| afuat | 10 | JPG, PNG | -| aga | 7 | JPG | -| ahab | 2 | JPG | - ---- - -## Unit Test Results - -**File:** `tests/unit/infrastructure/test_performance_optimizations.py` - -### TestThreadPoolManager (7 tests) -| Test | Status | Description | -|------|--------|-------------| -| test_init_default_workers | PASSED | Default worker count initialization | -| test_init_custom_workers | PASSED | Custom worker count initialization | -| test_run_blocking_simple | PASSED | Basic blocking function execution | -| test_run_blocking_with_kwargs | PASSED | Execution with keyword arguments | -| test_run_blocking_concurrent | PASSED | Concurrent task execution | -| test_run_blocking_after_shutdown | PASSED | Proper behavior after shutdown | -| test_shutdown_idempotent | PASSED | Multiple shutdowns are safe | - -### TestThreadSafeLRUCache (10 tests) -| Test | Status | Description | -|------|--------|-------------| -| test_init_valid | PASSED | Valid initialization parameters | -| test_init_invalid_size | PASSED | Rejects invalid size | -| test_put_and_get | PASSED | Basic put/get operations | -| test_get_missing_key | PASSED | Returns None for missing keys | -| test_lru_eviction | PASSED | Oldest entries evicted when full | -| test_ttl_expiration | PASSED | Entries expire after TTL | -| test_stats | PASSED | Statistics tracking (hits/misses) | -| test_invalidate | PASSED | Manual cache invalidation | -| test_clear | PASSED | Cache clearing | -| test_contains | PASSED | Contains/in operator | - -### TestImageHashing (7 tests) -| Test | Status | Description | -|------|--------|-------------| -| test_compute_image_hash_basic | PASSED | Basic hash computation | -| test_compute_image_hash_deterministic | PASSED | Same image = same hash | -| test_compute_image_hash_different_images | PASSED | Different images = different hashes | -| test_compute_image_hash_grayscale | PASSED | Grayscale image handling | -| test_compute_embedding_cache_key | PASSED | Cache key generation | -| test_compute_embedding_cache_key_with_params | PASSED | Cache key with extra params | -| test_compute_face_region_hash | PASSED | Face region hashing | - -### TestCachedEmbeddingExtractor (3 tests) -| Test | Status | Description | -|------|--------|-------------| -| test_sync_extraction_caches_result | PASSED | Results are cached correctly | -| test_cache_stats | PASSED | Cache statistics tracking | -| test_invalidate | PASSED | Cache entry invalidation | - -### TestAsyncWrappers (2 tests) -| Test | Status | Description | -|------|--------|-------------| -| test_async_face_detector | PASSED | Async face detection wrapper | -| test_async_embedding_extractor | PASSED | Async embedding extraction wrapper | - -### TestThreadSafeInMemoryRepository (4 tests) -| Test | Status | Description | -|------|--------|-------------| -| test_save_and_find | PASSED | Basic save/find operations | -| test_find_similar_vectorized | PASSED | Vectorized similarity search | -| test_lru_eviction | PASSED | LRU eviction at capacity | -| test_delete | PASSED | Deletion operations | - -### TestAutoCleaningMemoryStorage (4 tests) -| Test | Status | Description | -|------|--------|-------------| -| test_increment | PASSED | Rate limit increment | -| test_increment_exceeds_limit | PASSED | Behavior when limit exceeded | -| test_lru_eviction | PASSED | Auto-cleaning of old entries | -| test_reset | PASSED | Reset operations | - ---- - -## Integration Test Results - -**File:** `tests/integration/test_performance_with_real_images.py` - -### TestThreadPoolManagerWithRealImages (2 tests) -| Test | Status | Description | -|------|--------|-------------| -| test_concurrent_image_processing | PASSED | Process real images concurrently | -| test_thread_pool_exception_handling | PASSED | Exception propagation in threads | - -### TestImageHashingWithRealImages (4 tests) -| Test | Status | Description | -|------|--------|-------------| -| test_hash_consistency_same_image | PASSED | Same real image = same hash | -| test_hash_uniqueness_different_images | PASSED | All 19 images have unique hashes | -| test_hash_performance | PASSED | Hash computation under 10ms | -| test_cache_key_includes_model | PASSED | Model name in cache key | - -### TestLRUCacheWithRealEmbeddings (3 tests) -| Test | Status | Description | -|------|--------|-------------| -| test_cache_embeddings_from_real_images | PASSED | Cache embeddings from real images | -| test_cache_hit_rate_with_repeated_access | PASSED | Hit rate tracking | -| test_lru_eviction_preserves_recent | PASSED | Recent entries preserved | - -### TestThreadSafeRepositoryWithRealData (3 tests) -| Test | Status | Description | -|------|--------|-------------| -| test_save_and_search_real_users | PASSED | Save/search with real user data | -| test_concurrent_save_and_search | PASSED | Concurrent operations | -| test_vectorized_search_performance | PASSED | Vectorized search speed | - -### TestOptimizedLivenessWithRealImages (3 tests) -| Test | Status | Description | -|------|--------|-------------| -| test_liveness_detection_real_images | PASSED | Detect liveness on real faces | -| test_liveness_performance_optimized | PASSED | Detection under 200ms | -| test_sync_detection_available | PASSED | Sync method available | - -### TestEndToEndPerformance (2 tests) -| Test | Status | Description | -|------|--------|-------------| -| test_full_pipeline_with_caching | PASSED | Full processing pipeline | -| test_concurrent_user_enrollment | PASSED | Concurrent enrollments | - ---- - -## Manual Test Results - -**File:** `tests/manual/test_performance_manual.py` - -### Thread Pool Manager -- Basic execution: **PASSED** (0.0014s) -- Concurrent execution (5 tasks): **PASSED** (0.0018s) -- Exception handling: **PASSED** - -### Image Hashing -- Hash consistency: **PASSED** -- Hash uniqueness: **PASSED** (19/19 unique hashes) -- Performance: **PASSED** (0.06ms average per image) - -### LRU Cache -- Put/Get operations: **PASSED** -- Cache miss handling: **PASSED** -- LRU eviction: **PASSED** -- Hit rate: **66.67%** (100 hits, 50 misses) - -### Thread-Safe Repository -- Save and find: **PASSED** -- Similarity search: **PASSED** (0.10ms) -- Concurrent access: **PASSED** (100 saves in 3.97ms) -- Delete operation: **PASSED** - -### Optimized Liveness Detector -| Image | User | Status | Score | Time | -|-------|------|--------|-------|------| -| 3.jpg | afuat | SPOOF | 57.0 | 7.9ms | -| 4.jpg | afuat | LIVE | 61.2 | 4.5ms | -| 504494494_*.jpg | afuat | LIVE | 72.5 | 3.6ms | -| DSC_8476.jpg | aga | LIVE | 74.4 | 2.3ms | -| DSC_8681.jpg | aga | LIVE | 74.8 | 1.9ms | -| DSC_8693.jpg | aga | LIVE | 71.5 | 2.5ms | -| 1679744618228.jpg | ahab | LIVE | 74.2 | 104.7ms | -| foto.jpg | ahab | SPOOF | 36.3 | 316.7ms | - -- Average detection time: **4.46ms** -- P95 detection time: **5.65ms** -- Detection rate: **6/8 LIVE** (75%) - -### Full Pipeline -- Initial processing (19 images): **199.44ms** -- Cache lookup (19 images): **7.87ms** -- **Cache speedup: 25.3x** -- Similarity search: **0.07ms** - ---- - -## Performance Metrics Summary - -| Component | Metric | Value | Target | Status | -|-----------|--------|-------|--------|--------| -| Image Hashing | Average time | 0.06ms | <10ms | EXCEEDED | -| LRU Cache | Hit rate | 66.67% | >50% | PASSED | -| Similarity Search | Query time | 0.07-0.10ms | <10ms | EXCEEDED | -| Liveness Detection | Average time | 4.46ms | <100ms | EXCEEDED | -| Concurrent Saves | 100 embeddings | 3.97ms | <100ms | EXCEEDED | -| Cache Speedup | Ratio | 25.3x | >5x | EXCEEDED | - ---- - -## Test Commands - -```bash -# Run unit tests -python -m pytest tests/unit/infrastructure/test_performance_optimizations.py -v --no-cov - -# Run integration tests -python -m pytest tests/integration/test_performance_with_real_images.py -v --no-cov - -# Run manual tests -python -m tests.manual.test_performance_manual - -# Run specific manual test section -python -m tests.manual.test_performance_manual --section thread_pool -python -m tests.manual.test_performance_manual --section cache -python -m tests.manual.test_performance_manual --section hashing -python -m tests.manual.test_performance_manual --section repository -python -m tests.manual.test_performance_manual --section liveness -python -m tests.manual.test_performance_manual --section full_pipeline -``` - ---- - -## Conclusion - -All performance optimization components have been thoroughly validated: - -1. **ThreadPoolManager**: Correctly manages concurrent execution with proper exception handling -2. **ThreadSafeLRUCache**: Provides efficient caching with LRU eviction and TTL support -3. **Image Hashing**: Fast perceptual hashing (0.06ms) with 100% uniqueness for test images -4. **Thread-Safe Repository**: Handles concurrent operations safely with vectorized search -5. **Optimized Liveness Detector**: Achieves 4.46ms average detection time (3x improvement) -6. **Full Pipeline**: Demonstrates 25.3x speedup with caching enabled - -The performance optimization implementation is production-ready and meets all specified targets.