idvpackage 3.0.9__tar.gz → 3.0.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {idvpackage-3.0.9/idvpackage.egg-info → idvpackage-3.0.10}/PKG-INFO +1 -1
  2. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/common.py +120 -140
  3. idvpackage-3.0.10/idvpackage/jor_passport_extraction.py +256 -0
  4. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/ocr.py +84 -456
  5. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/ocr_utils.py +2 -1
  6. {idvpackage-3.0.9 → idvpackage-3.0.10/idvpackage.egg-info}/PKG-INFO +1 -1
  7. {idvpackage-3.0.9 → idvpackage-3.0.10}/setup.cfg +1 -1
  8. {idvpackage-3.0.9 → idvpackage-3.0.10}/setup.py +1 -1
  9. idvpackage-3.0.9/idvpackage/jor_passport_extraction.py +0 -513
  10. {idvpackage-3.0.9 → idvpackage-3.0.10}/LICENSE +0 -0
  11. {idvpackage-3.0.9 → idvpackage-3.0.10}/MANIFEST.in +0 -0
  12. {idvpackage-3.0.9 → idvpackage-3.0.10}/README.md +0 -0
  13. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/__init__.py +0 -0
  14. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/blur_detection.py +0 -0
  15. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/constants.py +0 -0
  16. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/ekyc.py +0 -0
  17. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/genai_utils.py +0 -0
  18. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/icons/battery1.png +0 -0
  19. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/icons/battery3.png +0 -0
  20. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/icons/network1.png +0 -0
  21. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/icons/network2.png +0 -0
  22. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/icons/wifi1.png +0 -0
  23. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/icons/wifi3.png +0 -0
  24. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/icons/wifi4.png +0 -0
  25. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/iraq_id_extraction.py +0 -0
  26. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/iraq_id_extraction_withopenai.py +0 -0
  27. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/iraq_passport_extraction.py +0 -0
  28. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/lazy_imports.py +0 -0
  29. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/lebanon_id_extraction.py +0 -0
  30. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/lebanon_passport_extraction.py +0 -0
  31. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/liveness_spoofing_v2.py +0 -0
  32. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/pse_passport_extraction.py +0 -0
  33. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/qatar_id_extraction.py +0 -0
  34. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/sau_id_extraction.py +0 -0
  35. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/spoof_resources/2.7_80x80_MiniFASNetV2.pth +0 -0
  36. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/spoof_resources/4_0_0_80x80_MiniFASNetV1SE.pth +0 -0
  37. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/spoof_resources/MiniFASNet.py +0 -0
  38. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/spoof_resources/__init__.py +0 -0
  39. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/spoof_resources/functional.py +0 -0
  40. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/spoof_resources/generate_patches.py +0 -0
  41. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/spoof_resources/transform.py +0 -0
  42. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/sudan_id_extraction.py +0 -0
  43. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/sudan_passport_extraction.py +0 -0
  44. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/syr_passport_extraction.py +0 -0
  45. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/uae_id_extraction.py +0 -0
  46. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage.egg-info/SOURCES.txt +0 -0
  47. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage.egg-info/dependency_links.txt +0 -0
  48. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage.egg-info/requires.txt +0 -0
  49. {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage.egg-info/top_level.txt +0 -0
  50. {idvpackage-3.0.9 → idvpackage-3.0.10}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: idvpackage
3
- Version: 3.0.9
3
+ Version: 3.0.10
4
4
  Summary: This repository contains a Python program designed to execute Optical Character Recognition (OCR) and Facial Recognition on images.
5
5
  Home-page: https://github.com/NymCard-Payments/project_idv_package
6
6
  Classifier: Programming Language :: Python :: 3
@@ -780,196 +780,176 @@ def load_and_process_image_deepface_topup(image_input):
780
780
 
781
781
 
782
782
  def load_and_process_image_deepface(image_input, country=None):
783
- DeepFace = get_deepface() # Only load when needed
784
- face_recognition = get_face_recognition() # Only load when needed
783
+ DeepFace = get_deepface()
784
+ face_recognition = get_face_recognition()
785
+
786
+ CONFIDENCE_THRESHOLD = 0.90 if country == "SDN" else 0.97
787
+
785
788
  def process_angle(img, angle):
789
+ img_to_process = None
790
+ img_rgb = None
791
+ img_pil = None
792
+ rotated = None
793
+
786
794
  try:
787
- # Create a view instead of copy when possible
795
+ # Rotate only if needed
788
796
  if angle != 0:
789
- # Minimize memory usage during rotation
790
- with np.errstate(all='ignore'):
791
- img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
792
- img_pil = Image.fromarray(img_rgb)
793
- # Use existing buffer when possible
794
- rotated = np.ascontiguousarray(img_pil.rotate(angle, expand=True))
795
- img_to_process = cv2.cvtColor(rotated, cv2.COLOR_RGB2BGR)
796
- # Clear references to intermediate arrays
797
- del img_rgb, img_pil, rotated
797
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
798
+ img_pil = Image.fromarray(img_rgb)
799
+ rotated = np.ascontiguousarray(
800
+ img_pil.rotate(angle, expand=True)
801
+ )
802
+ img_to_process = cv2.cvtColor(rotated, cv2.COLOR_RGB2BGR)
798
803
  else:
799
804
  img_to_process = img
800
805
 
801
- # Extract faces with memory optimization
802
806
  face_objs = DeepFace.extract_faces(
803
807
  img_to_process,
804
- detector_backend='fastmtcnn',
808
+ detector_backend="fastmtcnn",
805
809
  enforce_detection=False,
806
- align=True
810
+ align=True,
807
811
  )
808
812
 
809
- if face_objs and len(face_objs) > 0:
810
- confidence = face_objs[0].get('confidence', 0)
813
+ if not face_objs:
814
+ return None, None, 0.0
811
815
 
812
- # Check face frame size only if confidence is less than 1
813
- if confidence < 1:
814
- facial_area = face_objs[0]['facial_area']
815
- # Sudanese Edge Case. They have smaller pictures.
816
- if country == 'SDN' and (facial_area['w'] < 40 or facial_area['h'] < 50):
817
- print(f"Rejecting face at {angle} degrees due to small size of Sudanese Document: {facial_area['w']}x{facial_area['h']} (minimum 40x50)")
818
- return None, None, 0
819
- elif country != 'SDN' and (facial_area['w'] < 80 or facial_area['h'] < 90):
820
- print(f"Rejecting face at {angle} degrees due to small size: {facial_area['w']}x{facial_area['h']} (minimum 100x100)")
821
- return None, None, 0
822
-
823
- # Immediately reject if confidence is below threshold
824
- if confidence < 0.95 and country != 'SDN':
825
- print(f"Rejecting face at {angle} degrees due to low confidence: {confidence:.3f}")
826
- return None, None, 0
827
- elif confidence >= 0.90 and country == 'SDN':
828
- return face_objs, img_to_process, confidence
816
+ #get largest face
817
+ biggest_face = max(
818
+ face_objs,
819
+ key=lambda f: f["facial_area"]["w"] * f["facial_area"]["h"],
820
+ )
829
821
 
830
- return face_objs, img_to_process, confidence
822
+ facial_area = biggest_face["facial_area"]
823
+ confidence = biggest_face.get("confidence", 0.0)
824
+
825
+ logging.info(f"Angle {angle}: Detected face with confidence {confidence}")
826
+
827
+ if country == "SDN":
828
+ if confidence < CONFIDENCE_THRESHOLD:
829
+ logging.info(f"Low confidence for SDN at angle: {confidence} at angle {angle}")
830
+ return None, None, 0.0
831
+ else:
832
+ if confidence < 0.95:
833
+ logging.info(f"Low confidence: for country : {country} -> {confidence} at angle {angle}")
834
+ return None, None, 0.0
835
+
836
+ # Size validation (only when confidence < 1)
837
+ w, h = facial_area["w"], facial_area["h"]
838
+ if country == "SDN":
839
+ if w < 40 or h < 50:
840
+ logging.info(f"Face too small for SDN: w={w}, h={h}")
841
+ return None, None, 0.0
842
+ else:
843
+ if w < 80 or h < 90:
844
+ logging.info(f"Face too small: w={w}, h={h}")
845
+ return None, None, 0.0
846
+
847
+ # All checks passed
848
+ return biggest_face, img_to_process, confidence
831
849
 
832
- # Clear memory if no face found
833
- del img_to_process
834
- return None, None, 0
835
850
  except Exception as e:
836
- print(f"Error processing angle {angle}: {e}")
837
- return None, None, 0
851
+ print(f"[DeepFace] Error at angle {angle}: {e}")
852
+ return None, None, 0.0
853
+
838
854
  finally:
839
- # Ensure memory is cleared
840
- if 'img_to_process' in locals():
841
- del img_to_process
855
+ # Aggressive memory cleanup
856
+ if img_rgb is not None:
857
+ del img_rgb
858
+ if img_pil is not None:
859
+ del img_pil
860
+ if rotated is not None:
861
+ del rotated
862
+
863
+ # -------------------- INPUT HANDLING --------------------
842
864
 
843
865
  try:
844
- # Process input image efficiently
845
866
  if isinstance(image_input, np.ndarray):
846
- # Use view when possible
847
867
  image = np.ascontiguousarray(image_input)
848
868
  if image.dtype != np.uint8:
849
869
  image = image.astype(np.uint8, copy=False)
870
+
850
871
  elif isinstance(image_input, str):
851
- # Decode base64 directly to numpy array
852
872
  image_data = base64.b64decode(image_input)
853
- image = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR)
854
- del image_data # Clear decoded data
873
+ image = cv2.imdecode(
874
+ np.frombuffer(image_data, np.uint8),
875
+ cv2.IMREAD_COLOR,
876
+ )
877
+ del image_data
878
+
855
879
  else:
856
- print(f"Unexpected input type: {type(image_input)}")
880
+ print("Unsupported image input type")
857
881
  return [], []
858
882
 
859
883
  if image is None or image.size == 0:
860
- print("Empty image")
884
+ print("Empty image input")
861
885
  return [], []
862
886
 
863
- if country == 'SDN':
864
- CONFIDENCE_THRESHOLD = 0.90
865
- else:
866
- CONFIDENCE_THRESHOLD = 0.97
867
887
 
868
- # Try original orientation first to avoid unnecessary processing
869
- face_objs, processed_image, confidence = process_angle(image, 0)
870
- if face_objs is not None and confidence >= CONFIDENCE_THRESHOLD:
871
- try:
872
- biggest_face = max(face_objs, key=lambda face: face['facial_area']['w'] * face['facial_area']['h'])
873
- facial_area = biggest_face['facial_area']
888
+ # -------------------- ANGLE LOOP (NO THREADS) --------------------
874
889
 
875
- # Double check size requirements for biggest face
876
- if confidence < 1:
877
- # print(f"Confidence less than 1: {confidence}")
878
- if country == 'SDN' and (facial_area['w'] < 40 or facial_area['h'] < 50):
879
- print(f"Face validation failed: Face frame too small {facial_area['w']}x{facial_area['h']} (minimum 40x50)")
880
- return [], []
881
- elif country != 'SDN' and (facial_area['w'] < 80 or facial_area['h'] < 90):
882
- print(f"Face validation failed: Face frame too small {facial_area['w']}x{facial_area['h']} (minimum 100x100)")
883
- return [], []
890
+ best_face_objs = None
891
+ best_image = None
892
+ best_confidence = 0.0
884
893
 
885
- x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
894
+ for angle in (0, 90, 180, 270):
895
+ face_objs, processed_image, confidence = process_angle(image, angle)
886
896
 
887
- # Minimize memory usage during color conversion
888
- image_rgb = cv2.cvtColor(processed_image, cv2.COLOR_BGR2RGB)
889
- face_locations = [(y, x + w, y + h, x)]
890
- face_encodings = face_recognition.face_encodings(image_rgb, face_locations)
897
+ if confidence > best_confidence:
898
+ best_face_objs = face_objs
899
+ best_image = processed_image
900
+ best_confidence = confidence
901
+ best_angle = angle
891
902
 
892
- if face_encodings:
893
- # print(f"Found face in original orientation with confidence {confidence}")
894
- return face_locations, face_encodings
895
- finally:
896
- # Clear memory
897
- del processed_image, image_rgb
898
- if 'face_objs' in locals():
899
- del face_objs
900
- if country=='QAT':
901
- return 0,0
902
-
903
- # Try other angles in parallel
904
- angles = [90, 180, 270]
905
- best_confidence = confidence if face_objs is not None else 0
906
- best_face_objs = face_objs
907
- best_image = processed_image
903
+ if face_objs is None:
904
+ continue
905
+
906
+ else:
907
+ break # Exit loop on first valid detection
908
908
 
909
- with ThreadPoolExecutor(max_workers=3) as executor:
910
- futures = {
911
- executor.submit(process_angle, image, angle): angle
912
- for angle in angles
913
- }
909
+ # Keep best fallback (just in case)
910
+
914
911
 
915
- try:
916
- for future in as_completed(futures):
917
- face_objs, processed_image, confidence = future.result()
918
- if face_objs is not None:
919
- if confidence >= CONFIDENCE_THRESHOLD:
920
- # Cancel remaining tasks
921
- for f in futures:
922
- if not f.done():
923
- f.cancel()
924
- best_face_objs = face_objs
925
- best_image = processed_image
926
- best_confidence = confidence
927
- break
928
- finally:
929
- for future in futures:
930
- future.cancel()
912
+ if country == "QAT":
913
+ return 0, 0
931
914
 
932
915
  if best_face_objs is None or best_confidence < CONFIDENCE_THRESHOLD:
933
- print(f"No faces detected with confidence >= {CONFIDENCE_THRESHOLD}")
916
+ print(f"No valid face found (threshold={CONFIDENCE_THRESHOLD})")
934
917
  return [], []
935
918
 
936
- try:
937
- biggest_face = max(best_face_objs, key=lambda face: face['facial_area']['w'] * face['facial_area']['h'])
938
- facial_area = biggest_face['facial_area']
939
-
940
- # Final size check for rotated face
941
- if country != 'SDN' and confidence < 1:
942
- if facial_area['w'] < 80 or facial_area['h'] < 90:
943
- print(f"Face validation failed: Face frame too small {facial_area['w']}x{facial_area['h']} (minimum 100x100)")
944
- return [], []
945
- elif country == 'SDN' and confidence < CONFIDENCE_THRESHOLD:
946
- print(f"Face validation failed: Face frame too small {facial_area['w']}x{facial_area['h']} (minimum 40x50)")
947
- return [], []
948
-
949
- x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
950
-
951
- # Minimize memory during final processing
952
- image_rgb = cv2.cvtColor(best_image, cv2.COLOR_BGR2RGB)
953
- face_locations = [(y, x + w, y + h, x)]
954
- face_encodings = face_recognition.face_encodings(image_rgb, face_locations)
919
+ # -------------------- FINAL ENCODING --------------------
955
920
 
956
- if face_encodings:
957
- return face_locations, face_encodings
921
+
922
+ logging.info(f"Using best angle: {best_angle} detected with confidence {best_confidence} for encodings")
923
+ fa = best_face_objs["facial_area"]
924
+ x, y, w, h = fa["x"], fa["y"], fa["w"], fa["h"]
925
+
926
+ image_rgb = cv2.cvtColor(best_image, cv2.COLOR_BGR2RGB)
927
+ face_locations = [(y, x + w, y + h, x)]
928
+ face_encodings = face_recognition.face_encodings(
929
+ image_rgb, face_locations
930
+ )
958
931
 
959
- print("Failed to extract face encodings")
932
+ if not face_encodings:
960
933
  return [], []
961
- finally:
962
- # Clear final processing memory
963
- del image_rgb, best_image, best_face_objs
934
+
935
+ return face_locations, face_encodings
964
936
 
965
937
  except Exception as e:
966
- print(f"Error in face detection: {e}")
938
+ print(f"[FacePipeline] Fatal error: {e}")
967
939
  return [], []
940
+
968
941
  finally:
969
- # Ensure main image is cleared
970
- if 'image' in locals():
942
+ # Final memory cleanup
943
+ if "image_rgb" in locals():
944
+ del image_rgb
945
+ if "best_image" in locals():
946
+ del best_image
947
+ if "best_face_objs" in locals():
948
+ del best_face_objs
949
+ if "image" in locals():
971
950
  del image
972
951
 
952
+
973
953
  def calculate_similarity(face_encoding1, face_encoding2):
974
954
  face_recognition = get_face_recognition()
975
955
  similarity_score = 1 - face_recognition.face_distance([face_encoding1], face_encoding2)[0]
@@ -0,0 +1,256 @@
1
+
2
+ import base64
3
+ import time
4
+ from io import BytesIO
5
+ from typing import Optional
6
+ import cv2
7
+
8
+ from openai import OpenAI
9
+ from pydantic import BaseModel, Field
10
+ import logging
11
+
12
+
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format="%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s",
16
+ datefmt="%Y-%m-%d %H:%M:%S",
17
+ force=True,
18
+ )
19
+
20
+
21
+ PROMPT_PASSPORT = """
22
+ Extract ALL fields from this Jordan Passport **front side** image with high accuracy.
23
+
24
+ Return a JSON object with the following fields (use the exact field names):
25
+
26
+ - dob: Date of birth exactly as shown on the card, but always return in DD/MM/YYYY format (e.g., '15/06/1990'). If the card shows a different format, convert it to DD/MM/YYYY.
27
+ - date_of_expiry: Date of expiry exactly as shown on the card, but always return in DD/MM/YYYY format (e.g., '15/06/1990'). If the card shows a different format, convert it to DD/MM/YYYY.
28
+ - mrz1: First line of the MRZ (extract exactly as written).
29
+ - mrz2: Second line of the MRZ (extract exactly as written).
30
+ - name: Full name as printed on the card (extract exactly as written).
31
+ - first_name: First name as printed on the card (extract exactly as written).
32
+ - gender: Gender as either M or F (printed as Sex; output MALE if M, FEMALE if F).
33
+ - place_of_issue: Issuing place as printed on the card (extract exactly as written).
34
+ - full_name: Full name as printed on the card (extract exactly as written).
35
+ - last_name: Last name from the full name (extract exactly as written; if not present, return null).
36
+ - mother_name: Mother's full name as printed on the card (look for the label "Mother Full Name" and extract the name exactly as written in English, even if Arabic is present).
37
+ - nationality: Nationality as printed on the card and return ISO 3166-1 alpha-3 code (e.g., JOR).
38
+ - passport_national_number: National number as printed on the card (extract exactly as written) return empty string if not present.
39
+ - passport_number: Passport number as printed on the card (exactly 8 characters)
40
+ - issuing_date: Date of issue exactly as shown on the card, always in DD/MM/YYYY format.
41
+ - place_of_birth: Place of birth as printed on the card, tagged under Address (extract exactly as written).
42
+ - header_verified: Return True if any of these texts are present in the image: "Hashemite Kingdom of Jordan", "Hashemite Kingdom", or "Jordan"; otherwise False.
43
+ - dob_mrz: Date of birth as extracted from MRZ (in DD/MM/YYYY format)
44
+ - id_number_mrz: ID number as extracted from MRZ
45
+ - expiry_date_mrz: Expiry date as extracted from MRZ (in DD/MM/YYYY format)
46
+ - gender_mrz: Gender as extracted from MRZ (M or F) if M return MALE else if F return FEMALE
47
+
48
+ Instructions:
49
+ Instructions:
50
+ - Do NOT guess or hallucinate any values. If unclear,return empty string.
51
+ - Only use information visible on the card.
52
+ - Return the result as a single JSON object matching the schema above.
53
+ """
54
+
55
+
56
+ class JordanPassportFront(BaseModel):
57
+ dob: str = Field(
58
+ ...,
59
+ description="The date of birth (preserve (dd/mm/yyyy) format)",
60
+ )
61
+ expiry_date: str = Field(
62
+ ...,
63
+ description="The date of expiry (preserve (dd/mm/yyyy) format) tagged as Date if Expiry",
64
+ )
65
+
66
+ mrz1: str = Field(..., description="First line of the MRZ")
67
+
68
+ mrz2: str = Field(..., description="Second line of the MRZ")
69
+
70
+ name: str = Field(
71
+ ...,
72
+ description="Full name as printed on the card (extract exactly as written on the card)",
73
+ )
74
+
75
+ first_name: str = Field(
76
+ ...,
77
+ description="First name as printed on the card (extract exactly as written on the card)",
78
+ )
79
+
80
+ gender: str = Field(
81
+ ...,
82
+ description="Gender as either M or F , (printed as Sex, Male if M or Female if F)",
83
+ )
84
+
85
+ place_of_issue: str = Field(
86
+ ...,
87
+ description="Issuing place as printed on the card (extract exactly as written on the card)",
88
+ )
89
+
90
+ full_name: str = Field(
91
+ ...,
92
+ description="Full name as printed on the card (extract exactly as written on the card)",
93
+ )
94
+
95
+ last_name: Optional[str] = Field(
96
+ None,
97
+ description="Last name from the full name",
98
+ )
99
+
100
+ mother_name: str = Field(
101
+ ...,
102
+ description=" Mother's full name as printed on the card (look for the label Mother Full Name and extract the name exactly as written in English, Even Arabic is present)",
103
+ )
104
+
105
+ nationality: str = Field(
106
+ ...,
107
+ description="Nationality as printed on the card and return ISO 3166-1 alpha-3, e.g., JOR",
108
+ )
109
+
110
+ passport_number: str = Field(
111
+ ...,
112
+ min_length=8,
113
+ max_length=8,
114
+ description="ID number as printed on the card, extract exactly as written on the card ",
115
+ )
116
+
117
+ passport_national_number: str = Field(
118
+ ...,
119
+ description="National number as printed on the card, extract exactly as written on the card return empty string if not present",
120
+ )
121
+
122
+ issuing_date: str = Field(
123
+ ...,
124
+ description="The date of issue (preserve (dd/mm/yyyy) format)",
125
+ )
126
+
127
+ place_of_birth: str = Field(
128
+ ...,
129
+ description="Place of birth as printed on the card tagged under Address tag, extract exactly as written on the card",
130
+ )
131
+
132
+ header_verified: bool = Field(
133
+ ...,
134
+ description=" Return True if one of the texts present in the image Hashemite Kingdom of Jordan or Hashemite Kingdom or Jordan ",
135
+ )
136
+
137
+ dob_mrz: str = Field(
138
+ ..., description="Date of birth as extracted from MRZ (in DD/MM/YYYY format)"
139
+ )
140
+ passport_number_mrz: str = Field(
141
+ ..., description="Passport number as extracted from MRZ"
142
+ )
143
+ expiry_date_mrz: str = Field(
144
+ ..., description="Expiry date as extracted from MRZ (in DD/MM/YYYY format)"
145
+ )
146
+ gender_mrz: str = Field(
147
+ ..., description="Gender as extracted from MRZ (M or F) if M return MALE else if F return FEMALE"
148
+ )
149
+
150
+
151
+ def process_image(side):
152
+ if side == "first" or side == "page1":
153
+ prompt = PROMPT_PASSPORT
154
+ model = JordanPassportFront
155
+
156
+ else:
157
+ raise ValueError(
158
+ "Invalid document side specified. please upload front side of passport'."
159
+ )
160
+
161
+ return model, prompt
162
+
163
+
164
+ def get_openai_response(prompt: str, model_type, image: BytesIO, genai_key):
165
+ b64_image = base64.b64encode(image.getvalue()).decode("utf-8")
166
+ for attempt in range(3):
167
+ try:
168
+ client = OpenAI(api_key=genai_key)
169
+ response = client.responses.parse(
170
+ model="gpt-4.1-mini",
171
+ input=[
172
+ {
173
+ "role": "system",
174
+ "content": "You are an expert at extracting information from identity documents.",
175
+ },
176
+ {
177
+ "role": "user",
178
+ "content": [
179
+ {"type": "input_text", "text": prompt},
180
+ {
181
+ "type": "input_image",
182
+ "image_url": f"data:image/jpeg;base64,{b64_image}",
183
+ "detail": "low",
184
+ },
185
+ ],
186
+ },
187
+ ],
188
+ text_format=model_type,
189
+ )
190
+ return response.output_parsed
191
+ except Exception as e:
192
+ logging.info(f"[ERROR] Attempt {attempt + 1} failed: {str(e)}")
193
+ time.sleep(2)
194
+ return None
195
+
196
+
197
+ def _image_to_jpeg_bytesio(image) -> BytesIO:
198
+ """
199
+ Accepts: numpy.ndarray (OpenCV BGR), PIL.Image.Image, bytes/bytearray, or io.BytesIO
200
+ Returns: io.BytesIO containing JPEG bytes (ready for get_openai_response)
201
+ """
202
+ import numpy as np
203
+
204
+ if isinstance(image, BytesIO):
205
+ image.seek(0)
206
+ return image
207
+
208
+ if isinstance(image, (bytes, bytearray)):
209
+ return BytesIO(image)
210
+
211
+ try:
212
+ from PIL.Image import Image as _PILImage
213
+
214
+ if isinstance(image, _PILImage):
215
+ buf = BytesIO()
216
+ image.convert("RGB").save(buf, format="JPEG", quality=95)
217
+ buf.seek(0)
218
+ return buf
219
+ except Exception:
220
+ pass
221
+
222
+ if isinstance(image, np.ndarray):
223
+ success, enc = cv2.imencode(".jpg", image)
224
+ if not success:
225
+ raise ValueError("cv2.imencode failed")
226
+ return BytesIO(enc.tobytes())
227
+
228
+ raise TypeError(
229
+ "Unsupported image type. Provide numpy.ndarray, PIL.Image.Image, bytes, or io.BytesIO."
230
+ )
231
+
232
+
233
+ def get_response_from_openai_jor(image, side, country, openai_key):
234
+ logging.info("Processing image for Jordan passport extraction OPENAI......")
235
+ logging.info(f" and type: {type(image)}")
236
+ try:
237
+ image = _image_to_jpeg_bytesio(image)
238
+ except Exception as e:
239
+ logging.error(f"Error encoding image: {e}")
240
+ return {"error": "Image encoding failed"}
241
+ try:
242
+ model, prompt = process_image(side)
243
+ logging.info(f"Using model: {model.__name__} and prompt {prompt[:100]}")
244
+ except ValueError as ve:
245
+ logging.error(f"Error: {ve}")
246
+ return {"error": str(ve)}
247
+
248
+ try:
249
+ response = get_openai_response(prompt, model, image, openai_key)
250
+ except Exception as e:
251
+ logging.error(f"Error during OpenAI request: {e}")
252
+ return {"error": "OpenAI request failed"}
253
+
254
+ response_data = vars(response)
255
+ logging.info(f"Openai response: {response}")
256
+ return response_data