algoryn/validation
0
1"""2FastAPI endpoint for identity validation service.3 4This module provides the main API endpoint for identity validation,5accepting ID photos, user videos, and gesture requirements to perform6comprehensive identity verification.7"""8 9import os10import json11import tempfile12import time13import logging14from typing import Optional15from datetime import datetime, timezone16 17from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Depends18from fastapi.responses import ORJSONResponse19 20from .models import ValidationRequest, ValidationResponse, ValidationStatus21from .facial_validator import FacialValidator22from .gesture_validator import GestureValidator23from .config import config24 25logger = logging.getLogger(__name__)26 27# Create FastAPI app28app = FastAPI(29 title="Identity Validation API",30 description="API for identity verification using facial recognition and gesture validation",31 version="1.0.0",32 default_response_class=ORJSONResponse33)34 35# Initialize validators36facial_validator = FacialValidator()37gesture_validator = GestureValidator()38 39 40def get_validation_request(41 gestures: str = Form(...),42 # Gesture validation parameters (optional, fallback to env vars)43 error_margin: str = Form("default"),44 min_gesture_duration: str = Form("default"),45 require_all_gestures: str = Form("default"),46 confidence_threshold: str = Form("default"),47 # Facial recognition parameters (optional, fallback to env vars)48 similarity_threshold: str = Form("default"),49 frame_sample_rate: str = Form("default"),50 # Response parameters51 include_details: bool = Form(False)52) -> ValidationRequest:53 """54 Parse and validate the validation request from form data.55 56 All parameters are optional and will fall back to environment variable57 defaults if not provided. This allows for flexible configuration at both58 the request level and server level.59 60 Parameters61 ----------62 gestures : str63 JSON string containing the list of required gestures64 error_margin : Optional[float]65 Error margin for gesture validation (0.0-1.0). Uses DEFAULT_ERROR_MARGIN env var if None66 min_gesture_duration : Optional[int]67 Minimum duration for gesture detection. Uses MIN_GESTURE_DURATION env var if None68 require_all_gestures : Optional[bool]69 Whether all gestures must be present. Uses REQUIRE_ALL_GESTURES env var if None70 confidence_threshold : Optional[float]71 Minimum confidence threshold for gesture detection. Uses CONFIDENCE_THRESHOLD env var if None72 similarity_threshold : Optional[float]73 Minimum similarity threshold for facial matching. Uses SIMILARITY_THRESHOLD env var if None74 frame_sample_rate : Optional[int]75 Rate for sampling video frames for face detection. Uses FRAME_SAMPLE_RATE env var if None76 include_details : Optional[bool]77 Whether to include detailed results in response78 79 Returns80 -------81 ValidationRequest82 Parsed and validated request object with environment fallbacks83 84 Raises85 ------86 HTTPException87 If request validation fails88 """89 try:90 # Parse gestures JSON91 gesture_list = json.loads(gestures)92 if not isinstance(gesture_list, list):93 raise ValueError("gestures must be a list")94 if not gesture_list:95 raise ValueError("gestures list cannot be empty")96 97 # Validate gesture names (basic validation)98 for gesture in gesture_list:99 if not isinstance(gesture, str) or not gesture.strip():100 raise ValueError(f"Invalid gesture name: {gesture}")101 102 except json.JSONDecodeError as e:103 raise HTTPException(104 status_code=400,105 detail=f"Invalid JSON in gestures field: {str(e)}"106 )107 except ValueError as e:108 raise HTTPException(109 status_code=400,110 detail=f"Invalid gestures data: {str(e)}"111 )112 113 # Parse and convert parameters, using config defaults when "default" is provided114 def parse_param(value, default_value, value_type):115 """Parse parameter value, using default if 'default' string is provided."""116 # Handle FastAPI Form objects - extract the actual value from .default117 if hasattr(value, 'default'):118 actual_value = value.default119 else:120 actual_value = value121 122 if actual_value == "default" or actual_value is None:123 return default_value124 try:125 if value_type == float:126 return float(actual_value)127 elif value_type == int:128 return int(actual_value)129 elif value_type == bool:130 return str(actual_value).lower() in ('true', '1', 'yes', 'on')131 else:132 return actual_value133 except (ValueError, TypeError, AttributeError):134 raise HTTPException(135 status_code=400,136 detail=f"Invalid value for parameter: {actual_value}"137 )138 139 final_error_margin = parse_param(error_margin, config.default_error_margin, float)140 final_min_gesture_duration = parse_param(min_gesture_duration, config.min_gesture_duration, int)141 final_require_all_gestures = parse_param(require_all_gestures, config.require_all_gestures, bool)142 final_confidence_threshold = parse_param(confidence_threshold, config.confidence_threshold, float)143 final_similarity_threshold = parse_param(similarity_threshold, config.similarity_threshold, float)144 final_frame_sample_rate = parse_param(frame_sample_rate, config.frame_sample_rate, int)145 146 # Parse include_details parameter147 final_include_details = parse_param(include_details, False, bool)148 149 return ValidationRequest(150 asked_gestures=gesture_list,151 error_margin=final_error_margin,152 min_gesture_duration=final_min_gesture_duration,153 require_all_gestures=final_require_all_gestures,154 confidence_threshold=final_confidence_threshold,155 similarity_threshold=final_similarity_threshold,156 frame_sample_rate=final_frame_sample_rate,157 include_details=final_include_details158 )159 160 161@app.post("/", response_model=ValidationResponse)162async def validate_identity(163 photo: UploadFile = File(...),164 video: UploadFile = File(...),165 request: ValidationRequest = Depends(get_validation_request)166):167 """168 Validate user identity using facial recognition and gesture validation.169 170 This endpoint accepts an ID document photo, a user video containing171 the person's face and required gestures, and a list of gestures that172 must be performed. It returns validation results for both facial173 recognition and gesture compliance.174 175 Parameters176 ----------177 photo : UploadFile178 ID document photo file (image format)179 video : UploadFile180 User video file containing face and gestures (video format)181 request : ValidationRequest182 Validation configuration and gesture requirements183 184 Returns185 -------186 ValidationResponse187 Validation results with success indicators and optional details188 189 Raises190 ------191 HTTPException192 If validation fails or processing errors occur193 """194 start_time = time.time()195 logger.info(f"Identity validation request received for {request.asked_gestures}")196 197 # Validate file types198 if not photo.content_type or not photo.content_type.startswith(('image/', 'application/')):199 raise HTTPException(200 status_code=400,201 detail="Photo file must be an image"202 )203 204 if not video.content_type or not video.content_type.startswith('video/'):205 raise HTTPException(206 status_code=400,207 detail="Video file must be a video"208 )209 210 # Validate file sizes (basic check)211 MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB212 if photo.size and photo.size > MAX_FILE_SIZE:213 raise HTTPException(214 status_code=413,215 detail="Photo file too large (max 100MB)"216 )217 218 if video.size and video.size > MAX_FILE_SIZE:219 raise HTTPException(220 status_code=413,221 detail="Video file too large (max 100MB)"222 )223 224 # Create temporary files for processing225 temp_photo = None226 temp_video = None227 228 try:229 # Save uploaded files to temporary location230 with tempfile.NamedTemporaryFile(delete=False, suffix=f"_photo.{photo.filename.split('.')[-1] if '.' in photo.filename else 'jpg'}") as temp_photo_file:231 temp_photo = temp_photo_file.name232 photo_content = await photo.read()233 temp_photo_file.write(photo_content)234 235 with tempfile.NamedTemporaryFile(delete=False, suffix=f"_video.{video.filename.split('.')[-1] if '.' in video.filename else 'mp4'}") as temp_video_file:236 temp_video = temp_video_file.name237 video_content = await video.read()238 temp_video_file.write(video_content)239 240 logger.info(f"Files saved: photo={temp_photo}, video={temp_video}")241 242 # Perform facial validation243 logger.info("Starting facial validation")244 245 # Update facial validator with request-specific parameters if provided246 if request.similarity_threshold is not None:247 facial_validator.similarity_threshold = request.similarity_threshold248 if request.frame_sample_rate is not None:249 facial_validator.frame_sample_rate = request.frame_sample_rate250 251 face_result = facial_validator.validate_facial_match(temp_photo, temp_video)252 253 # Perform gesture validation254 logger.info("Starting gesture validation")255 gesture_result = gesture_validator.validate_gestures(256 temp_video,257 request.asked_gestures,258 error_margin=request.error_margin,259 require_all=request.require_all_gestures260 )261 262 # Update gesture validator with request-specific parameters if provided263 if request.confidence_threshold is not None:264 gesture_validator.confidence_threshold = request.confidence_threshold265 if request.min_gesture_duration is not None:266 gesture_validator.min_gesture_duration = request.min_gesture_duration267 268 # Determine overall result269 overall_success = face_result.success and gesture_result.success270 overall_status = ValidationStatus.SUCCESS if overall_success else ValidationStatus.PARTIAL271 272 # Calculate processing time273 processing_time_ms = int((time.time() - start_time) * 1000)274 275 # Build response276 response = ValidationResponse(277 face=face_result.success,278 gestures=gesture_result.success,279 overall=overall_success,280 status=overall_status,281 face_result=face_result if request.include_details else None,282 gesture_result=gesture_result if request.include_details else None,283 processing_time_ms=processing_time_ms,284 timestamp=datetime.now(timezone.utc).isoformat()285 )286 287 # Log results288 logger.info(289 "Identity validation completed",290 extra={291 "face_success": face_result.success,292 "gesture_success": gesture_result.success,293 "overall_success": overall_success,294 "processing_time_ms": processing_time_ms,295 "requested_gestures": request.asked_gestures296 }297 )298 299 return response300 301 except Exception as e:302 logger.error(f"Error during identity validation: {str(e)}", exc_info=True)303 raise HTTPException(304 status_code=500,305 detail=f"Internal server error during validation: {str(e)}"306 )307 308 finally:309 # Clean up temporary files310 for temp_file in [temp_photo, temp_video]:311 if temp_file and os.path.exists(temp_file):312 try:313 os.unlink(temp_file)314 logger.debug(f"Cleaned up temporary file: {temp_file}")315 except Exception as e:316 logger.warning(f"Failed to clean up temporary file {temp_file}: {e}")317 318 319@app.get("/health")320async def health_check():321 """322 Health check endpoint for the validation service.323 324 Returns325 -------326 dict327 Health status information328 """329 return {330 "status": "healthy",331 "service": "identity-validation",332 "version": "1.0.0",333 "timestamp": datetime.now(timezone.utc).isoformat(),334 "components": {335 "facial_validator": "initialized",336 "gesture_validator": "initialized"337 }338 }339 340 341@app.get("/")342async def root():343 """344 Root endpoint providing API information.345 346 Returns347 -------348 dict349 API information and usage instructions350 """351 return {352 "name": "Identity Validation API",353 "version": "1.0.0",354 "description": "Identity verification using facial recognition and gesture validation",355 "endpoints": {356 "POST /": "Perform identity validation",357 "GET /health": "Health check",358 "GET /": "API information"359 },360 "documentation": "/docs"361 }362 