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.
- {idvpackage-3.0.9/idvpackage.egg-info → idvpackage-3.0.10}/PKG-INFO +1 -1
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/common.py +120 -140
- idvpackage-3.0.10/idvpackage/jor_passport_extraction.py +256 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/ocr.py +84 -456
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/ocr_utils.py +2 -1
- {idvpackage-3.0.9 → idvpackage-3.0.10/idvpackage.egg-info}/PKG-INFO +1 -1
- {idvpackage-3.0.9 → idvpackage-3.0.10}/setup.cfg +1 -1
- {idvpackage-3.0.9 → idvpackage-3.0.10}/setup.py +1 -1
- idvpackage-3.0.9/idvpackage/jor_passport_extraction.py +0 -513
- {idvpackage-3.0.9 → idvpackage-3.0.10}/LICENSE +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/MANIFEST.in +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/README.md +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/__init__.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/blur_detection.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/constants.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/ekyc.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/genai_utils.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/icons/battery1.png +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/icons/battery3.png +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/icons/network1.png +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/icons/network2.png +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/icons/wifi1.png +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/icons/wifi3.png +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/icons/wifi4.png +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/iraq_id_extraction.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/iraq_id_extraction_withopenai.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/iraq_passport_extraction.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/lazy_imports.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/lebanon_id_extraction.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/lebanon_passport_extraction.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/liveness_spoofing_v2.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/pse_passport_extraction.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/qatar_id_extraction.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/sau_id_extraction.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/spoof_resources/2.7_80x80_MiniFASNetV2.pth +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/spoof_resources/4_0_0_80x80_MiniFASNetV1SE.pth +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/spoof_resources/MiniFASNet.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/spoof_resources/__init__.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/spoof_resources/functional.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/spoof_resources/generate_patches.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/spoof_resources/transform.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/sudan_id_extraction.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/sudan_passport_extraction.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/syr_passport_extraction.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage/uae_id_extraction.py +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage.egg-info/SOURCES.txt +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage.egg-info/dependency_links.txt +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage.egg-info/requires.txt +0 -0
- {idvpackage-3.0.9 → idvpackage-3.0.10}/idvpackage.egg-info/top_level.txt +0 -0
- {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.
|
|
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()
|
|
784
|
-
face_recognition = get_face_recognition()
|
|
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
|
-
#
|
|
795
|
+
# Rotate only if needed
|
|
788
796
|
if angle != 0:
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
img_pil =
|
|
793
|
-
|
|
794
|
-
|
|
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=
|
|
808
|
+
detector_backend="fastmtcnn",
|
|
805
809
|
enforce_detection=False,
|
|
806
|
-
align=True
|
|
810
|
+
align=True,
|
|
807
811
|
)
|
|
808
812
|
|
|
809
|
-
if
|
|
810
|
-
|
|
813
|
+
if not face_objs:
|
|
814
|
+
return None, None, 0.0
|
|
811
815
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
840
|
-
if
|
|
841
|
-
del
|
|
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(
|
|
854
|
-
|
|
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(
|
|
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
|
-
#
|
|
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
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
894
|
+
for angle in (0, 90, 180, 270):
|
|
895
|
+
face_objs, processed_image, confidence = process_angle(image, angle)
|
|
886
896
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
#
|
|
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
|
-
|
|
910
|
-
|
|
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
|
-
|
|
916
|
-
|
|
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
|
|
916
|
+
print(f"No valid face found (threshold={CONFIDENCE_THRESHOLD})")
|
|
934
917
|
return [], []
|
|
935
918
|
|
|
936
|
-
|
|
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
|
-
|
|
957
|
-
|
|
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
|
-
|
|
932
|
+
if not face_encodings:
|
|
960
933
|
return [], []
|
|
961
|
-
|
|
962
|
-
|
|
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"
|
|
938
|
+
print(f"[FacePipeline] Fatal error: {e}")
|
|
967
939
|
return [], []
|
|
940
|
+
|
|
968
941
|
finally:
|
|
969
|
-
#
|
|
970
|
-
if
|
|
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
|