import asyncio from fastapi import FastAPI, UploadFile, File, HTTPException from ocr.gemini_service import parse_pan from ocr.gemini_aadhar_service import parse_aadhar from ocr.gemini_face_extractor import detect_face from utils.crop import crop_face from utils.pdf import pdf_to_image_bytes app = FastAPI() SUPPORTED_TYPES = {"image/jpeg", "image/png", "image/webp", "application/pdf"} def _resolve_image(raw_bytes: bytes, content_type: str) -> bytes: if content_type == "application/pdf": return pdf_to_image_bytes(raw_bytes) return raw_bytes @app.post("/kyc/pan") async def process_pan(file: UploadFile = File(...)): if file.content_type not in SUPPORTED_TYPES: raise HTTPException( status_code=415, detail=f"Unsupported file type '{file.content_type}'. Upload a JPEG, PNG, WebP, or PDF.", ) raw_bytes = await file.read() try: image_bytes = _resolve_image(raw_bytes, file.content_type) except ValueError as e: raise HTTPException(status_code=422, detail=str(e)) try: pan_result, face_result = await asyncio.gather( asyncio.to_thread(parse_pan, image_bytes), asyncio.to_thread(detect_face, image_bytes), ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) face_bbox = face_result.get("box_2d") if face_bbox is None: raise HTTPException(status_code=422, detail="Could not detect face on the PAN card.") face_image = crop_face(image_bytes, face_bbox) return { "success": True, "parsed_data": pan_result.get("data"), "face_image": face_image, "engine_used": pan_result.get("engine_used"), } @app.post("/kyc/aadhar") async def process_aadhar(file: UploadFile = File(...)): if file.content_type not in SUPPORTED_TYPES: raise HTTPException( status_code=415, detail=f"Unsupported file type '{file.content_type}'. Upload a JPEG, PNG, WebP, or PDF.", ) raw_bytes = await file.read() try: image_bytes = _resolve_image(raw_bytes, file.content_type) except ValueError as e: raise HTTPException(status_code=422, detail=str(e)) # Face detection is best-effort — back side of Aadhaar has no photo. # return_exceptions=True prevents one failure from cancelling the other. ocr_result, face_result = await asyncio.gather( asyncio.to_thread(parse_aadhar, image_bytes), asyncio.to_thread(detect_face, image_bytes), return_exceptions=True, ) if isinstance(ocr_result, Exception): raise HTTPException(status_code=500, detail=str(ocr_result)) data = ocr_result.get("data", {}) address_found = data.get("address_found", False) # Attempt to crop face; silently skip if detection failed (e.g. back side) face_image = None if not isinstance(face_result, Exception): face_bbox = face_result.get("box_2d") if face_bbox: try: face_image = crop_face(image_bytes, face_bbox) except Exception: pass return { "success": True, "parsed_data": data, "face_image": face_image, "face_found": face_image is not None, "address_found": address_found, "engine_used": ocr_result.get("engine_used"), }