PrathameshRaut commited on
Commit
bc500dd
·
verified ·
1 Parent(s): e68edbe

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +337 -0
main.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import tempfile
5
+ import numpy as np
6
+ import tensorflow as tf
7
+ from tensorflow.keras.preprocessing import image
8
+ from pdf2image import convert_from_path
9
+ from fastapi import FastAPI, UploadFile, Form, HTTPException, Header, Depends
10
+ from fastapi.responses import JSONResponse
11
+ import pytesseract
12
+ from PIL import Image
13
+ import requests
14
+
15
+ # -----------------------------
16
+ # CONFIG
17
+ # -----------------------------
18
+ IMG_SIZE = (224, 224)
19
+ MODEL_PATH = "./final_model.keras"
20
+ class_names = ['Other', 'Aadhaar Card', 'Pan Card', 'Voter Id']
21
+
22
+ type_mapping = {
23
+ "aadhaar_card": "Aadhaar Card",
24
+ "pan_card": "Pan Card",
25
+ "voter_id": "Voter Id"
26
+ }
27
+
28
+ # Document types that support OCR extraction
29
+ EXTRACTABLE_TYPES = {"Aadhaar Card", "Pan Card", "Voter Id"}
30
+
31
+ # -----------------------------
32
+ # AUTH CONFIG (HF SECRETS)
33
+ # -----------------------------
34
+ MASTER_SECRET_KEY = os.getenv("MASTER_SECRET_KEY")
35
+ SARVAM_API_KEY = os.getenv("SARVAM_API_KEY")
36
+
37
+ PROJECT_KEYS_JSON = os.getenv("PROJECT_KEYS_JSON", "{}")
38
+ try:
39
+ PROJECT_KEYS = json.loads(PROJECT_KEYS_JSON)
40
+ except Exception:
41
+ PROJECT_KEYS = {}
42
+
43
+ # -----------------------------
44
+ # AUTH VALIDATION
45
+ # -----------------------------
46
+ def verify_keys(
47
+ x_secret_key: str = Header(None),
48
+ x_project_id: str = Header(None),
49
+ x_project_key: str = Header(None),
50
+ ):
51
+ if not MASTER_SECRET_KEY:
52
+ raise HTTPException(status_code=500, detail="MASTER_SECRET_KEY not configured in Space secrets")
53
+
54
+ if not x_secret_key or x_secret_key != MASTER_SECRET_KEY:
55
+ raise HTTPException(status_code=401, detail="Invalid Secret Key")
56
+
57
+ if not x_project_id or not x_project_key:
58
+ raise HTTPException(status_code=401, detail="Project Id and Project Key are required")
59
+
60
+ if x_project_id not in PROJECT_KEYS:
61
+ raise HTTPException(status_code=401, detail=f"Unknown project: {x_project_id}")
62
+
63
+ if PROJECT_KEYS.get(x_project_id) != x_project_key:
64
+ raise HTTPException(status_code=401, detail="Invalid Project Key")
65
+
66
+ return True
67
+
68
+ # -----------------------------
69
+ # LOAD MODEL
70
+ # -----------------------------
71
+ model = tf.keras.models.load_model(MODEL_PATH)
72
+
73
+ # -----------------------------
74
+ # Predict a single image array (H,W,C)
75
+ # -----------------------------
76
+ def predict_array(img_array):
77
+ img_array = tf.cast(img_array, tf.float32)
78
+ img_array = tf.image.resize(img_array, IMG_SIZE)
79
+ img_array = tf.expand_dims(img_array, axis=0)
80
+
81
+ pred = model.predict(img_array, verbose=0)[0]
82
+ class_id = int(np.argmax(pred))
83
+ conf = float(np.max(pred))
84
+
85
+ return class_names[class_id], conf, pred
86
+
87
+ # -----------------------------
88
+ # Predict from image path
89
+ # -----------------------------
90
+ def predict_image(img_path):
91
+ img = image.load_img(img_path, target_size=IMG_SIZE, color_mode="rgb")
92
+ img_array = image.img_to_array(img)
93
+ label, conf, _ = predict_array(img_array)
94
+ return {
95
+ "type": label,
96
+ "confidence": round(conf, 6)
97
+ }
98
+
99
+ # -----------------------------
100
+ # Predict from PDF (max 2 pages only)
101
+ # -----------------------------
102
+ def predict_pdf(pdf_path, dpi=200, max_pages=2):
103
+ pages = convert_from_path(pdf_path, dpi=dpi)
104
+
105
+ if len(pages) > max_pages:
106
+ return {
107
+ "error": "Maximum page limit reached",
108
+ "max_pages": max_pages,
109
+ "found_pages": len(pages)
110
+ }
111
+
112
+ results = []
113
+ labels = []
114
+
115
+ for page in pages:
116
+ page_np = np.array(page)
117
+
118
+ if page_np.shape[-1] == 4:
119
+ page_np = page_np[..., :3]
120
+
121
+ label, conf, _ = predict_array(page_np)
122
+ results.append((label, conf))
123
+ labels.append(label)
124
+
125
+ if len(labels) == 2:
126
+ if labels[0] != labels[1]:
127
+ return {
128
+ "error": "Invalid input",
129
+ "reason": "Pages belong to different document types",
130
+ "page1_type": labels[0],
131
+ "page2_type": labels[1]
132
+ }
133
+
134
+ final_label = labels[0]
135
+ final_conf = float(max(results[0][1], results[1][1]))
136
+
137
+ return {
138
+ "type": final_label,
139
+ "confidence": round(final_conf, 6)
140
+ }
141
+
142
+ return {
143
+ "type": labels[0],
144
+ "confidence": round(float(results[0][1]), 6)
145
+ }
146
+
147
+ # -----------------------------
148
+ # Main predict dispatcher
149
+ # -----------------------------
150
+ def predict_file(path):
151
+ if not os.path.exists(path):
152
+ return {"error": "File not found", "path": path}
153
+
154
+ ext = os.path.splitext(path)[1].lower()
155
+
156
+ if ext in [".png", ".jpg", ".jpeg", ".bmp", ".webp"]:
157
+ return predict_image(path)
158
+
159
+ if ext == ".pdf":
160
+ return predict_pdf(path)
161
+
162
+ return {"error": "Unsupported file type", "ext": ext}
163
+
164
+ # -----------------------------
165
+ # OCR: Extract raw text from file
166
+ # Uses eng+hin Tesseract langs to handle both English and Hindi text
167
+ # -----------------------------
168
+ def extract_text_from_file(path: str) -> str:
169
+ ext = os.path.splitext(path)[1].lower()
170
+ all_text = []
171
+
172
+ try:
173
+ if ext in [".png", ".jpg", ".jpeg", ".bmp", ".webp"]:
174
+ pil_img = Image.open(path).convert("RGB")
175
+ text = pytesseract.image_to_string(pil_img, lang="eng+hin")
176
+ all_text.append(text)
177
+
178
+ elif ext == ".pdf":
179
+ # Use higher DPI for better OCR accuracy on PDFs
180
+ pages = convert_from_path(path, dpi=250)
181
+ for page in pages:
182
+ text = pytesseract.image_to_string(page, lang="eng+hin")
183
+ all_text.append(text)
184
+
185
+ except Exception as e:
186
+ return f"OCR_ERROR: {str(e)}"
187
+
188
+ return "\n".join(all_text).strip()
189
+
190
+ # -----------------------------
191
+ # Sarvam AI: Extract structured fields from raw OCR text
192
+ # -----------------------------
193
+ def extract_fields_with_sarvam(raw_text: str, doc_type: str) -> dict:
194
+ empty_fields = {
195
+ "name": None,
196
+ "address": None,
197
+ "date_of_birth": None,
198
+ "gender": None,
199
+ "id_number": None,
200
+ "mobile_number": None,
201
+ }
202
+
203
+ if not SARVAM_API_KEY:
204
+ return {**empty_fields, "ocr_error": "SARVAM_API_KEY not configured in Space secrets"}
205
+
206
+ system_prompt = (
207
+ "You are a document field extractor for Indian identity documents. "
208
+ "Given raw OCR text, extract specific fields and return ONLY a valid JSON object. "
209
+ "No markdown, no explanation, no extra text — just the JSON.\n\n"
210
+ "Fields to extract:\n"
211
+ " - name: Full name of the document holder\n"
212
+ " - address: Full address (combine multi-line if needed)\n"
213
+ " - date_of_birth: In DD/MM/YYYY or YYYY format; use year_of_birth key if only year is present\n"
214
+ " - gender: Male / Female / Transgender\n"
215
+ " - id_number: Aadhaar=12-digit number, PAN=10-char alphanumeric, Voter ID=alphanumeric card number\n"
216
+ " - mobile_number: 10-digit mobile number if present\n\n"
217
+ "Use null for any field not found. Return a flat JSON object only."
218
+ )
219
+
220
+ user_prompt = (
221
+ f"Document Type: {doc_type}\n\n"
222
+ f"Raw OCR Text:\n{raw_text}\n\n"
223
+ "Extract the fields and return JSON."
224
+ )
225
+
226
+ try:
227
+ response = requests.post(
228
+ "https://api.sarvam.ai/v1/chat/completions",
229
+ headers={
230
+ "Authorization": f"Bearer {SARVAM_API_KEY}",
231
+ "Content-Type": "application/json"
232
+ },
233
+ json={
234
+ "model": "sarvam-m",
235
+ "messages": [
236
+ {"role": "system", "content": system_prompt},
237
+ {"role": "user", "content": user_prompt}
238
+ ],
239
+ "temperature": 0.0,
240
+ "max_tokens": 512
241
+ },
242
+ timeout=30
243
+ )
244
+
245
+ if response.status_code != 200:
246
+ return {
247
+ **empty_fields,
248
+ "sarvam_error": f"API returned {response.status_code}: {response.text[:300]}"
249
+ }
250
+
251
+ content = response.json()["choices"][0]["message"]["content"].strip()
252
+
253
+ # Strip markdown code fences if model wraps output
254
+ content = re.sub(r"^```(?:json)?\s*", "", content)
255
+ content = re.sub(r"\s*```$", "", content)
256
+
257
+ extracted = json.loads(content)
258
+
259
+ # Normalize keys — ensure all expected fields are present
260
+ fields = ["name", "address", "date_of_birth", "gender", "id_number", "mobile_number"]
261
+ normalized = {f: extracted.get(f) for f in fields}
262
+
263
+ # Handle year_of_birth fallback (if model returns it instead of date_of_birth)
264
+ if normalized["date_of_birth"] is None and "year_of_birth" in extracted:
265
+ normalized["date_of_birth"] = str(extracted["year_of_birth"])
266
+
267
+ return normalized
268
+
269
+ except json.JSONDecodeError as e:
270
+ return {**empty_fields, "sarvam_parse_error": f"JSON parse failed: {str(e)}"}
271
+ except requests.exceptions.Timeout:
272
+ return {**empty_fields, "sarvam_error": "Sarvam API request timed out"}
273
+ except Exception as e:
274
+ return {**empty_fields, "sarvam_error": str(e)}
275
+
276
+ # -----------------------------
277
+ # FastAPI App
278
+ # -----------------------------
279
+ app = FastAPI(
280
+ title="ID Document Validator",
281
+ description="Classify document type, authenticate, and extract structured fields via OCR + Sarvam AI."
282
+ )
283
+
284
+ @app.post("/predict")
285
+ async def predict(
286
+ file: UploadFile,
287
+ expected_type: str = Form(None),
288
+ auth: bool = Depends(verify_keys)
289
+ ):
290
+ if not file.filename:
291
+ raise HTTPException(status_code=400, detail="No file provided")
292
+
293
+ suffix = os.path.splitext(file.filename)[1]
294
+
295
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
296
+ temp_path = temp_file.name
297
+ temp_file.write(await file.read())
298
+
299
+ try:
300
+ # Step 1: Classify document type
301
+ result = predict_file(temp_path)
302
+
303
+ # Step 2: Authenticate against expected_type
304
+ if expected_type and expected_type in type_mapping and "type" in result:
305
+ if result["type"] == type_mapping[expected_type]:
306
+ result["Authentication"] = "Valid"
307
+ else:
308
+ result["Authentication"] = "Not valid"
309
+
310
+ # Step 3: OCR + Sarvam field extraction (only for recognized ID types)
311
+ doc_type = result.get("type")
312
+ if doc_type in EXTRACTABLE_TYPES:
313
+ raw_text = extract_text_from_file(temp_path)
314
+
315
+ if raw_text.startswith("OCR_ERROR"):
316
+ result["extracted_fields"] = {
317
+ "name": None,
318
+ "address": None,
319
+ "date_of_birth": None,
320
+ "gender": None,
321
+ "id_number": None,
322
+ "mobile_number": None,
323
+ "ocr_error": raw_text
324
+ }
325
+ else:
326
+ result["extracted_fields"] = extract_fields_with_sarvam(raw_text, doc_type)
327
+
328
+ return JSONResponse(content=result)
329
+
330
+ finally:
331
+ if os.path.exists(temp_path):
332
+ os.unlink(temp_path)
333
+
334
+
335
+ @app.get("/")
336
+ def read_root():
337
+ return {"message": "ID Validator API is running"}