""" Pydantic schemas for KYC POC API. This module defines all request and response models for the API. """ from pydantic import BaseModel, Field from typing import Optional, Dict, Any # ============================================================================ # Common Models # ============================================================================ class BoundingBox(BaseModel): """Face bounding box coordinates.""" x: int = Field(..., description="X coordinate of top-left corner") y: int = Field(..., description="Y coordinate of top-left corner") width: int = Field(..., description="Width of bounding box") height: int = Field(..., description="Height of bounding box") class FacePose(BaseModel): """Face pose angles.""" yaw: float = Field(..., description="Yaw angle (left-right rotation)") pitch: float = Field(..., description="Pitch angle (up-down rotation)") roll: float = Field(..., description="Roll angle (tilt)") is_frontal: bool = Field(..., description="Whether face is frontal") class Demographics(BaseModel): """Demographic information extracted from face.""" age: Optional[int] = Field(None, description="Estimated age") gender: Optional[str] = Field(None, description="Estimated gender (Male/Female)") # ============================================================================ # Quality Models # ============================================================================ class BlurAnalysis(BaseModel): """Blur analysis results.""" blur_score: float = Field(..., description="Laplacian variance score (higher = sharper)") blur_threshold: float = Field(..., description="Threshold below which image is considered blurry") is_blurry: bool = Field(..., description="Whether image is too blurry") class BrightnessAnalysis(BaseModel): """Brightness analysis results.""" brightness: float = Field(..., description="Mean brightness (0-1)") brightness_min: float = Field(..., description="Minimum acceptable brightness") brightness_max: float = Field(..., description="Maximum acceptable brightness") is_too_dark: bool = Field(..., description="Whether image is too dark") is_too_bright: bool = Field(..., description="Whether image is too bright") class QualityAnalysis(BaseModel): """Complete quality analysis for an image.""" blur_score: float blur_threshold: float is_blurry: bool brightness: float brightness_min: float brightness_max: float is_too_dark: bool is_too_bright: bool pose: Optional[FacePose] = None is_good_quality: bool = Field(..., description="Overall quality assessment") # ============================================================================ # Face Match Models # ============================================================================ class FaceMatchResult(BaseModel): """Face matching result.""" is_match: bool = Field(..., description="Whether faces match") similarity_score: float = Field(..., description="Similarity score (0-1)") threshold: float = Field(..., description="Threshold used for matching") # ============================================================================ # Liveness Models # ============================================================================ class LivenessResult(BaseModel): """Liveness detection result.""" is_real: bool = Field(..., description="Whether face is from a real person") confidence: float = Field(..., description="Confidence score") label: str = Field(..., description="Classification label") prediction_class: Optional[int] = Field(None, description="Raw prediction class") models_used: Optional[int] = Field(None, description="Number of models used") error: Optional[str] = Field(None, description="Error message if any") # ============================================================================ # Request Models (Base64) # ============================================================================ class Base64VerifyRequest(BaseModel): """Request model for base64 verification endpoint.""" ktp_image: str = Field(..., description="Base64 encoded KTP image") selfie_image: str = Field(..., description="Base64 encoded selfie image") threshold: float = Field(default=0.5, ge=0.0, le=1.0, description="Similarity threshold") class Base64FaceMatchRequest(BaseModel): """Request model for base64 face match endpoint.""" image1: str = Field(..., description="Base64 encoded first image") image2: str = Field(..., description="Base64 encoded second image") threshold: float = Field(default=0.5, ge=0.0, le=1.0, description="Similarity threshold") class Base64SingleImageRequest(BaseModel): """Request model for single image base64 endpoints.""" image: str = Field(..., description="Base64 encoded image") # ============================================================================ # Response Models # ============================================================================ class ErrorResponse(BaseModel): """Error response model.""" error_code: str = Field(..., description="Error code") message: str = Field(..., description="Error message") detail: Optional[str] = Field(None, description="Additional details") class FaceInfo(BaseModel): """Information about a detected face.""" bbox: BoundingBox demographics: Optional[Demographics] = None det_score: Optional[float] = Field(None, description="Detection confidence") class QualityResponse(BaseModel): """Response for quality check endpoint.""" success: bool quality: QualityAnalysis face_box: Optional[BoundingBox] = None demographics: Optional[Demographics] = None message: str class LivenessResponse(BaseModel): """Response for liveness check endpoint.""" success: bool liveness: LivenessResult message: str class FaceMatchResponse(BaseModel): """Response for face match endpoint.""" success: bool face_match: FaceMatchResult face1: Optional[FaceInfo] = None face2: Optional[FaceInfo] = None message: str class VerifyResponse(BaseModel): """Response for full KYC verification endpoint.""" success: bool face_match: FaceMatchResult liveness: LivenessResult quality: Dict[str, QualityAnalysis] = Field( ..., description="Quality analysis for each image (ktp, selfie)" ) demographics: Dict[str, Demographics] = Field( ..., description="Demographics for each image (ktp, selfie)" ) face_boxes: Dict[str, BoundingBox] = Field( ..., description="Face bounding boxes for each image (ktp, selfie)" ) message: str class HealthResponse(BaseModel): """Health check response.""" status: str = Field(..., description="Service status") models_loaded: Dict[str, bool] = Field( ..., description="Status of each ML model" ) version: str = Field(..., description="API version") # ============================================================================ # OCR Models # ============================================================================ class OCRFieldResult(BaseModel): """Result for a single OCR field.""" value: str = Field(..., description="Extracted and sanitized value") confidence: float = Field(..., description="OCR confidence score (0-1)") raw_value: str = Field(..., description="Raw extracted text before sanitization") class NIKValidation(BaseModel): """NIK validation result with extracted information.""" is_valid: bool = Field(..., description="Whether NIK passes validation") errors: list = Field(default_factory=list, description="List of validation errors") extracted: Dict[str, Any] = Field( default_factory=dict, description="Information extracted from NIK (province code, city code, birth date, gender)" ) class KTPOCRData(BaseModel): """Structured KTP data extracted from OCR.""" provinsi: Optional[OCRFieldResult] = Field(None, description="Province name") kabupaten_kota: Optional[OCRFieldResult] = Field(None, description="City/Regency name") nik: Optional[OCRFieldResult] = Field(None, description="NIK (16-digit ID number)") nama: Optional[OCRFieldResult] = Field(None, description="Full name") tempat_lahir: Optional[OCRFieldResult] = Field(None, description="Place of birth") tanggal_lahir: Optional[OCRFieldResult] = Field(None, description="Date of birth (DD-MM-YYYY)") jenis_kelamin: Optional[OCRFieldResult] = Field(None, description="Gender (LAKI-LAKI/PEREMPUAN)") golongan_darah: Optional[OCRFieldResult] = Field(None, description="Blood type") alamat: Optional[OCRFieldResult] = Field(None, description="Address") rt_rw: Optional[OCRFieldResult] = Field(None, description="RT/RW") kelurahan_desa: Optional[OCRFieldResult] = Field(None, description="Village/District") kecamatan: Optional[OCRFieldResult] = Field(None, description="Sub-district") agama: Optional[OCRFieldResult] = Field(None, description="Religion") status_perkawinan: Optional[OCRFieldResult] = Field(None, description="Marital status") pekerjaan: Optional[OCRFieldResult] = Field(None, description="Occupation") kewarganegaraan: Optional[OCRFieldResult] = Field(None, description="Nationality (WNI/WNA)") berlaku_hingga: Optional[OCRFieldResult] = Field(None, description="Valid until (date or SEUMUR HIDUP)") class OCRTextBlock(BaseModel): """Raw OCR text block with position.""" text: str = Field(..., description="Extracted text") confidence: float = Field(..., description="OCR confidence score") bbox: list = Field(..., description="Bounding box coordinates [[x1,y1], [x2,y2], [x3,y3], [x4,y4]]") class KTPValidation(BaseModel): """KTP data validation results.""" nik: Optional[NIKValidation] = Field(None, description="NIK validation result") class Base64OCRRequest(BaseModel): """Request model for base64 OCR endpoint.""" image: str = Field(..., description="Base64 encoded KTP image") validate: bool = Field(default=True, description="Whether to validate extracted data (e.g., NIK)") class OCRResponse(BaseModel): """Response for KTP OCR endpoint.""" success: bool = Field(..., description="Whether OCR extraction was successful") data: KTPOCRData = Field(..., description="Structured KTP data") raw_text: list[OCRTextBlock] = Field( default_factory=list, description="Raw OCR results with bounding boxes" ) validation: Optional[KTPValidation] = Field(None, description="Data validation results") message: str = Field(..., description="Result message")