triflix commited on
Commit
c8b4f13
·
verified ·
1 Parent(s): 48e1fed

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +154 -76
app.py CHANGED
@@ -9,6 +9,7 @@ import re
9
  from dataclasses import dataclass
10
  from typing import List, Dict, Tuple
11
 
 
12
  import cv2
13
  import numpy as np
14
  import torch
@@ -16,8 +17,10 @@ from paddleocr import TextDetection
16
  from easyocr import Reader
17
  from rapidfuzz import fuzz
18
 
 
19
  import gradio as gr
20
 
 
21
  # ============ CORE VALIDATORS (UNCHANGED) ============
22
  class VerhoeffValidator:
23
  d_table = [[0,1,2,3,4,5,6,7,8,9],[1,2,3,4,0,6,7,8,9,5],[2,3,4,0,1,7,8,9,5,6],[3,4,0,1,2,8,9,5,6,7],[4,0,1,2,3,9,5,6,7,8],[5,9,8,7,6,0,4,3,2,1],[6,5,9,8,7,1,0,4,3,2],[7,6,5,9,8,2,1,0,4,3],[8,7,6,5,9,3,2,1,0,4],[9,8,7,6,5,4,3,2,1,0]]
@@ -29,6 +32,7 @@ class VerhoeffValidator:
29
  for i,ch in enumerate(reversed(n)): c=cls.d_table[c][cls.p_table[i%8][int(ch)]]
30
  return c==0
31
 
 
32
  class PatternValidator:
33
  @staticmethod
34
  def find_aadhaar(t: str) -> List[str]:
@@ -38,6 +42,7 @@ class PatternValidator:
38
  def find_pan(t: str) -> List[str]:
39
  return list(set(re.findall(r'\b[A-Z]{3}[PCHFATBLJG][A-Z]\d{4}[A-Z]\b', t.upper())))
40
 
 
41
  class TextNormalizer:
42
  OCR_CORRECTIONS = {'O':'0','o':'0','l':'1','I':'1','Z':'2','z':'2','S':'5','G':'6','b':'6','T':'7','B':'8','g':'9','q':'9'}
43
  @staticmethod
@@ -52,6 +57,7 @@ class TextNormalizer:
52
  text = re.sub(r'\b[0-9OolIZzSGbTBgq]{4,}\b', fix, text)
53
  return re.sub(r'\s+',' ',re.sub(r'[^\w\s\u0900-\u097F.,/-]','',text)).strip()
54
 
 
55
  # ============ CONFIGURATION ============
56
  @dataclass
57
  class Config:
@@ -71,6 +77,7 @@ class Config:
71
  "Ration_Card": ["ration","card","food","civil","supplies","apl","bpl","राशन","कार्ड","खाद्य","नागरी","पुरवठा"]
72
  }
73
 
 
74
  # ============ MAIN PIPELINE ============
75
  class DocumentOCRVerifier:
76
  def __init__(self, config: Config=None):
@@ -82,11 +89,13 @@ class DocumentOCRVerifier:
82
  self.detector = None
83
  self.reader = Reader(self.cfg.languages, gpu=torch.cuda.is_available())
84
 
 
85
  def _preprocess(self, img: np.ndarray) -> np.ndarray:
86
  img = self._resize(img)
87
  img = self._deskew(img)
88
  return self._enhance(img)
89
 
 
90
  def _resize(self, img: np.ndarray) -> np.ndarray:
91
  h,w = img.shape[:2]
92
  if max(h,w) > self.cfg.max_image_dim:
@@ -94,6 +103,7 @@ class DocumentOCRVerifier:
94
  img = cv2.resize(img, (int(w*scale), int(h*scale)), interpolation=cv2.INTER_AREA)
95
  return img
96
 
 
97
  def _deskew(self, img: np.ndarray) -> np.ndarray:
98
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
99
  _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
@@ -109,6 +119,7 @@ class DocumentOCRVerifier:
109
  img = cv2.warpAffine(img, M, (w,h), borderValue=(255,255,255))
110
  return img
111
 
 
112
  def _enhance(self, img: np.ndarray) -> np.ndarray:
113
  denoised = cv2.fastNlMeansDenoisingColored(img, None, 10, 10, 7, 21)
114
  lab = cv2.cvtColor(denoised, cv2.COLOR_BGR2LAB)
@@ -118,10 +129,12 @@ class DocumentOCRVerifier:
118
  kernel = np.array([[0,-1,0],[-1,5,-1],[0,-1,0]])
119
  return cv2.addWeighted(cv2.filter2D(enhanced, -1, kernel), 0.6, enhanced, 0.4, 0)
120
 
 
121
  def _extract_keywords(self, text: str) -> List[str]:
122
  if not text: return []
123
  return [t for t in re.split(r'\s+', text.strip()) if t]
124
 
 
125
  def _classify(self, text: str) -> Tuple[str, float, List[str]]:
126
  norm_text = TextNormalizer.normalize(text, aggressive=True)
127
  scores = {}
@@ -140,20 +153,24 @@ class DocumentOCRVerifier:
140
  scores[doc_type] = {"score": score, "matched": matched}
141
  winner = max(scores.items(), key=lambda x: x[1]["score"])
142
  if winner[1]["score"] >= self.cfg.min_keywords:
143
- conf = 0.95 if winner[1]["score"] == 100 else min(0.90, len(winner[1]["matched")]/len(self.cfg.doc_keywords[winner[0]]) + 0.3) if len(self.cfg.doc_keywords[winner[0]])>0 else 0.0
144
  return winner[0], conf, winner[1]["matched"]
145
  return "UNCLASSIFIED", 0.0, []
146
 
 
147
  def verify(self, image_path: str, user_keywords: List[str]) -> Dict:
148
  img = cv2.imread(image_path)
149
  if img is None: return {"error": "Image not found", "imagePath": image_path}
150
 
 
151
  img = self._preprocess(img)
152
 
 
153
  # Region-based OCR with word-level granularity
154
  ocr_keywords = []
155
  all_text = ""
156
 
 
157
  if self.detector:
158
  try:
159
  regions = self.detector.predict(input=image_path, batch_size=1)
@@ -162,6 +179,7 @@ class DocumentOCRVerifier:
162
  else:
163
  regions = []
164
 
 
165
  # If detector provided regions, use them; otherwise fallback to whole-image read
166
  if regions:
167
  for res in regions:
@@ -183,15 +201,20 @@ class DocumentOCRVerifier:
183
  ocr_keywords.extend(self._extract_keywords(t))
184
  all_text += " " + t
185
 
 
186
  # Classification
187
  doc_type, accuracy, matched_keywords = self._classify(all_text)
188
 
 
189
  # Verification - match against combined text for phrase support
 
190
  raw_input_keywords = user_keywords
 
191
  minimal_norm_user_keywords = [kw.strip() for kw in raw_input_keywords if kw is not None]
192
  exact_matches = list(set([kw for kw in minimal_norm_user_keywords if kw.lower() in all_text.lower()]))
193
  status = "verified" if exact_matches else "not_verified"
194
 
 
195
  return {
196
  "documentType": doc_type,
197
  "documentTypeAccuracy": round(accuracy, 4),
@@ -203,8 +226,10 @@ class DocumentOCRVerifier:
203
  "imagePath": image_path
204
  }
205
 
 
206
  # ============ APP ============
207
 
 
208
  verifier = DocumentOCRVerifier()
209
 
210
 
@@ -235,7 +260,7 @@ def save_upload_to_tmp(uploaded_file) -> str:
235
  with open(uploaded_file, "rb") as src, open(out_path, "wb") as dst:
236
  dst.write(src.read())
237
  except Exception:
238
- # last resort: try to read as PIL image (if provided)
239
  try:
240
  import PIL.Image as Image
241
  im = Image.open(uploaded_file).convert("RGB")
@@ -247,94 +272,147 @@ def save_upload_to_tmp(uploaded_file) -> str:
247
 
248
  def run_ocr(image, keywords_raw: str):
249
  """
250
- image: uploaded file path or bytes (Gradio File component)
251
  keywords_raw: raw string entered by user. Split by comma EXACTLY to form keywords. Preserve internal spacing.
252
- Returns: image preview (path), HTML summary, parsed JSON (dict) for gr.JSON, raw JSON string
253
  """
254
- # Parse user keywords preserving internal spaces
 
 
 
255
  if keywords_raw is None:
256
  user_keywords = []
257
  else:
 
258
  user_keywords = [s if s is not None else "" for s in re.split(r',', keywords_raw)]
 
259
  user_keywords = [s.rstrip("\n\r\t ").lstrip("\n\r\t ") for s in user_keywords]
260
 
 
 
261
  image_path = save_upload_to_tmp(image)
262
  result = verifier.verify(image_path=image_path, user_keywords=user_keywords)
263
-
264
- # build a simple card-style HTML summary with color scheme for verification status
265
- status = result.get("verificationStatus", "not_verified")
266
- doc_type = result.get("documentType", "UNCLASSIFIED")
267
- accuracy = result.get("documentTypeAccuracy", 0.0)
268
- input_keys = result.get("inputUserKeywords", [])
269
-
270
- status_color = "#1a7f37" if status == "verified" else "#b22222"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  card_html = f"""
272
- <div style='border-radius:8px;padding:14px;box-shadow:0 4px 12px rgba(0,0,0,0.08);max-width:640px;font-family:system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial;'>
273
- <div style='display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;'>
274
- <div style='font-size:16px;font-weight:600'>Document Summary</div>
275
- <div style='padding:6px 10px;border-radius:14px;background:{status_color};color:white;font-weight:700'>{status.upper()}</div>
276
- </div>
277
- <div style='display:grid;grid-template-columns:1fr 1fr;gap:8px;'>
278
- <div style='background:#fafafa;padding:10px;border-radius:6px;'>
279
- <div style='font-size:12px;color:#555'>Document Type</div>
280
- <div style='font-size:15px;font-weight:600'>{doc_type}</div>
281
- </div>
282
- <div style='background:#fafafa;padding:10px;border-radius:6px;'>
283
- <div style='font-size:12px;color:#555'>Document Accuracy</div>
284
- <div style='font-size:15px;font-weight:600'>{accuracy*100:.2f}%</div>
285
  </div>
286
- <div style='grid-column:1 / -1;background:#fff8e6;padding:10px;border-radius:6px;'>
287
- <div style='font-size:12px;color:#555'>Input Keywords</div>
288
- <div style='font-size:14px;font-weight:600'>{', '.join(input_keys) if input_keys else '—'}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  </div>
290
- </div>
291
  </div>
292
  """
293
-
294
- raw_json_str = json.dumps(result, indent=2, ensure_ascii=False)
295
-
296
- # return values: image preview path (Gradio Image accepts file path), summary HTML, parsed JSON, raw JSON string
297
- return image_path, card_html, result, raw_json_str
298
-
299
-
300
- with gr.Blocks(title="Document OCR Verifier") as demo:
301
- gr.Markdown("Upload an image and provide comma-separated keywords (preserve spacing inside keywords).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  with gr.Row():
303
- img_in = gr.File(label="Image (JPEG/PNG). The file will be stored in /tmp.")
304
- kws = gr.Textbox(label="User keywords (comma-separated). Example: ROHIT, KUMAR, SINGH")
305
- run_btn = gr.Button("Run OCR & Verify")
306
-
 
 
 
 
 
 
 
 
307
  with gr.Row():
308
- with gr.Column():
309
- card = gr.HTML("""
310
- <div style='border:1px solid #ccc;padding:16px;border-radius:12px;width:100%;'>
311
- <h3>Document Summary</h3>
312
- <p><strong>Document Type:</strong> <span id='doc_type'></span></p>
313
- <p><strong>Accuracy:</strong> <span id='doc_acc'></span></p>
314
- <p><strong>User Keywords:</strong> <span id='user_kws'></span></p>
315
- <p><strong>Status:</strong> <span id='verif_status' style='font-weight:bold;'></span></p>
316
- </div>
317
- """)
318
- out = gr.Textbox(label="Result (raw JSON)", lines=20)
319
-
320
- def update_card(json_str):
321
- try:
322
- data = json.loads(json_str)
323
- status_color = "green" if data.get("verificationStatus") == "verified" else "red"
324
- html = f"""
325
- <div style='border:1px solid #ccc;padding:16px;border-radius:12px;width:100%;'>
326
- <h3>Document Summary</h3>
327
- <p><strong>Document Type:</strong> {data.get('documentType')}</p>
328
- <p><strong>Accuracy:</strong> {data.get('documentTypeAccuracy')}</p>
329
- <p><strong>User Keywords:</strong> {', '.join(data.get('inputUserKeywords', []))}</p>
330
- <p><strong>Status:</strong> <span style='color:{status_color};font-weight:bold;'>{data.get('verificationStatus')}</span></p>
331
- </div>
332
- """
333
- return html
334
- except:
335
- return "<div>Invalid JSON</div>"
336
-
337
- run_btn.click(fn=run_ocr, inputs=[img_in, kws], outputs=[out])
338
- out.change(fn=update_card, inputs=[out], outputs=[card])
339
-
340
- demo.launch(server_name="0.0.0.0", server_port=7860)(server_name="0.0.0.0", server_port=7860)
 
9
  from dataclasses import dataclass
10
  from typing import List, Dict, Tuple
11
 
12
+
13
  import cv2
14
  import numpy as np
15
  import torch
 
17
  from easyocr import Reader
18
  from rapidfuzz import fuzz
19
 
20
+
21
  import gradio as gr
22
 
23
+
24
  # ============ CORE VALIDATORS (UNCHANGED) ============
25
  class VerhoeffValidator:
26
  d_table = [[0,1,2,3,4,5,6,7,8,9],[1,2,3,4,0,6,7,8,9,5],[2,3,4,0,1,7,8,9,5,6],[3,4,0,1,2,8,9,5,6,7],[4,0,1,2,3,9,5,6,7,8],[5,9,8,7,6,0,4,3,2,1],[6,5,9,8,7,1,0,4,3,2],[7,6,5,9,8,2,1,0,4,3],[8,7,6,5,9,3,2,1,0,4],[9,8,7,6,5,4,3,2,1,0]]
 
32
  for i,ch in enumerate(reversed(n)): c=cls.d_table[c][cls.p_table[i%8][int(ch)]]
33
  return c==0
34
 
35
+
36
  class PatternValidator:
37
  @staticmethod
38
  def find_aadhaar(t: str) -> List[str]:
 
42
  def find_pan(t: str) -> List[str]:
43
  return list(set(re.findall(r'\b[A-Z]{3}[PCHFATBLJG][A-Z]\d{4}[A-Z]\b', t.upper())))
44
 
45
+
46
  class TextNormalizer:
47
  OCR_CORRECTIONS = {'O':'0','o':'0','l':'1','I':'1','Z':'2','z':'2','S':'5','G':'6','b':'6','T':'7','B':'8','g':'9','q':'9'}
48
  @staticmethod
 
57
  text = re.sub(r'\b[0-9OolIZzSGbTBgq]{4,}\b', fix, text)
58
  return re.sub(r'\s+',' ',re.sub(r'[^\w\s\u0900-\u097F.,/-]','',text)).strip()
59
 
60
+
61
  # ============ CONFIGURATION ============
62
  @dataclass
63
  class Config:
 
77
  "Ration_Card": ["ration","card","food","civil","supplies","apl","bpl","राशन","कार्ड","खाद्य","नागरी","पुरवठा"]
78
  }
79
 
80
+
81
  # ============ MAIN PIPELINE ============
82
  class DocumentOCRVerifier:
83
  def __init__(self, config: Config=None):
 
89
  self.detector = None
90
  self.reader = Reader(self.cfg.languages, gpu=torch.cuda.is_available())
91
 
92
+
93
  def _preprocess(self, img: np.ndarray) -> np.ndarray:
94
  img = self._resize(img)
95
  img = self._deskew(img)
96
  return self._enhance(img)
97
 
98
+
99
  def _resize(self, img: np.ndarray) -> np.ndarray:
100
  h,w = img.shape[:2]
101
  if max(h,w) > self.cfg.max_image_dim:
 
103
  img = cv2.resize(img, (int(w*scale), int(h*scale)), interpolation=cv2.INTER_AREA)
104
  return img
105
 
106
+
107
  def _deskew(self, img: np.ndarray) -> np.ndarray:
108
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
109
  _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
 
119
  img = cv2.warpAffine(img, M, (w,h), borderValue=(255,255,255))
120
  return img
121
 
122
+
123
  def _enhance(self, img: np.ndarray) -> np.ndarray:
124
  denoised = cv2.fastNlMeansDenoisingColored(img, None, 10, 10, 7, 21)
125
  lab = cv2.cvtColor(denoised, cv2.COLOR_BGR2LAB)
 
129
  kernel = np.array([[0,-1,0],[-1,5,-1],[0,-1,0]])
130
  return cv2.addWeighted(cv2.filter2D(enhanced, -1, kernel), 0.6, enhanced, 0.4, 0)
131
 
132
+
133
  def _extract_keywords(self, text: str) -> List[str]:
134
  if not text: return []
135
  return [t for t in re.split(r'\s+', text.strip()) if t]
136
 
137
+
138
  def _classify(self, text: str) -> Tuple[str, float, List[str]]:
139
  norm_text = TextNormalizer.normalize(text, aggressive=True)
140
  scores = {}
 
153
  scores[doc_type] = {"score": score, "matched": matched}
154
  winner = max(scores.items(), key=lambda x: x[1]["score"])
155
  if winner[1]["score"] >= self.cfg.min_keywords:
156
+ conf = 0.95 if winner[1]["score"] == 100 else min(0.90, len(winner[1]["matched"])/len(self.cfg.doc_keywords[winner[0]]) + 0.3)
157
  return winner[0], conf, winner[1]["matched"]
158
  return "UNCLASSIFIED", 0.0, []
159
 
160
+
161
  def verify(self, image_path: str, user_keywords: List[str]) -> Dict:
162
  img = cv2.imread(image_path)
163
  if img is None: return {"error": "Image not found", "imagePath": image_path}
164
 
165
+
166
  img = self._preprocess(img)
167
 
168
+
169
  # Region-based OCR with word-level granularity
170
  ocr_keywords = []
171
  all_text = ""
172
 
173
+
174
  if self.detector:
175
  try:
176
  regions = self.detector.predict(input=image_path, batch_size=1)
 
179
  else:
180
  regions = []
181
 
182
+
183
  # If detector provided regions, use them; otherwise fallback to whole-image read
184
  if regions:
185
  for res in regions:
 
201
  ocr_keywords.extend(self._extract_keywords(t))
202
  all_text += " " + t
203
 
204
+
205
  # Classification
206
  doc_type, accuracy, matched_keywords = self._classify(all_text)
207
 
208
+
209
  # Verification - match against combined text for phrase support
210
+ # Preserve raw input keywords (split externally) but perform exact matching on the combined OCR text without further altering user's internal spacing
211
  raw_input_keywords = user_keywords
212
+ # Do minimal trimming for matching (only strip outer whitespace)
213
  minimal_norm_user_keywords = [kw.strip() for kw in raw_input_keywords if kw is not None]
214
  exact_matches = list(set([kw for kw in minimal_norm_user_keywords if kw.lower() in all_text.lower()]))
215
  status = "verified" if exact_matches else "not_verified"
216
 
217
+
218
  return {
219
  "documentType": doc_type,
220
  "documentTypeAccuracy": round(accuracy, 4),
 
226
  "imagePath": image_path
227
  }
228
 
229
+
230
  # ============ APP ============
231
 
232
+
233
  verifier = DocumentOCRVerifier()
234
 
235
 
 
260
  with open(uploaded_file, "rb") as src, open(out_path, "wb") as dst:
261
  dst.write(src.read())
262
  except Exception:
263
+ # last resort: try to read as numpy array (if provided)
264
  try:
265
  import PIL.Image as Image
266
  im = Image.open(uploaded_file).convert("RGB")
 
272
 
273
  def run_ocr(image, keywords_raw: str):
274
  """
275
+ image: uploaded file path or bytes (Gradio Image component with type='file' or 'numpy')
276
  keywords_raw: raw string entered by user. Split by comma EXACTLY to form keywords. Preserve internal spacing.
 
277
  """
278
+ if image is None:
279
+ return None, "<div style='color: red; padding: 20px;'>⚠️ Please upload an image first!</div>", ""
280
+
281
+ # Split user keywords by comma only; do not auto-trim internal spaces (only strip ends)
282
  if keywords_raw is None:
283
  user_keywords = []
284
  else:
285
+ # Split on commas. Keep empty tokens if user left them intentionally.
286
  user_keywords = [s if s is not None else "" for s in re.split(r',', keywords_raw)]
287
+ # strip only leading/trailing newline and tabs, but preserve internal spacing and common spaces
288
  user_keywords = [s.rstrip("\n\r\t ").lstrip("\n\r\t ") for s in user_keywords]
289
 
290
+
291
+ # Save file to /tmp and call verifier
292
  image_path = save_upload_to_tmp(image)
293
  result = verifier.verify(image_path=image_path, user_keywords=user_keywords)
294
+
295
+ # Extract fields for card display
296
+ doc_type = result.get("documentType", "N/A")
297
+ doc_accuracy = result.get("documentTypeAccuracy", 0.0)
298
+ input_keywords = result.get("inputUserKeywords", [])
299
+ verification_status = result.get("verificationStatus", "not_verified")
300
+
301
+ # Format accuracy as percentage
302
+ accuracy_text = f"{doc_accuracy * 100:.2f}%"
303
+
304
+ # Format keywords as comma-separated string
305
+ keywords_text = ", ".join([f'"{kw}"' for kw in input_keywords]) if input_keywords else "None provided"
306
+
307
+ # Color-coded status
308
+ if verification_status == "verified":
309
+ status_html = '<span style="color: #16a34a; font-weight: bold; font-size: 22px;">✓ VERIFIED</span>'
310
+ status_bg = "#dcfce7"
311
+ status_border = "#16a34a"
312
+ else:
313
+ status_html = '<span style="color: #dc2626; font-weight: bold; font-size: 22px;">✗ NOT VERIFIED</span>'
314
+ status_bg = "#fee2e2"
315
+ status_border = "#dc2626"
316
+
317
+ # Create HTML card with improved styling
318
  card_html = f"""
319
+ <div style="border: 2px solid #e5e7eb; border-radius: 16px; padding: 28px; background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%); box-shadow: 0 10px 25px rgba(0,0,0,0.1); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">
320
+ <div style="display: flex; align-items: center; margin-bottom: 24px; border-bottom: 3px solid #3b82f6; padding-bottom: 16px;">
321
+ <span style="font-size: 32px; margin-right: 12px;">📄</span>
322
+ <h2 style="margin: 0; color: #1f2937; font-size: 26px; font-weight: 700;">Document Verification Results</h2>
 
 
 
 
 
 
 
 
 
323
  </div>
324
+
325
+ <div style="display: grid; gap: 16px;">
326
+ <div style="background: linear-gradient(135deg, #dbeafe 0%, #eff6ff 100%); padding: 20px; border-radius: 12px; border-left: 5px solid #3b82f6; box-shadow: 0 2px 8px rgba(59,130,246,0.2);">
327
+ <div style="color: #1e40af; font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;">📋 Document Type</div>
328
+ <div style="font-size: 24px; color: #1f2937; font-weight: 700;">{doc_type}</div>
329
+ </div>
330
+
331
+ <div style="background: linear-gradient(135deg, #d1fae5 0%, #ecfdf5 100%); padding: 20px; border-radius: 12px; border-left: 5px solid #10b981; box-shadow: 0 2px 8px rgba(16,185,129,0.2);">
332
+ <div style="color: #065f46; font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;">🎯 Type Detection Accuracy</div>
333
+ <div style="font-size: 24px; color: #1f2937; font-weight: 700;">{accuracy_text}</div>
334
+ </div>
335
+
336
+ <div style="background: linear-gradient(135deg, #fef3c7 0%, #fefce8 100%); padding: 20px; border-radius: 12px; border-left: 5px solid #f59e0b; box-shadow: 0 2px 8px rgba(245,158,11,0.2);">
337
+ <div style="color: #92400e; font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;">🔑 Input Keywords</div>
338
+ <div style="font-size: 17px; color: #451a03; font-weight: 500; line-height: 1.6;">{keywords_text}</div>
339
+ </div>
340
+
341
+ <div style="background: linear-gradient(135deg, {status_bg} 0%, {status_bg}dd 100%); padding: 24px; border-radius: 12px; border: 3px solid {status_border}; text-align: center; box-shadow: 0 4px 12px rgba(0,0,0,0.15);">
342
+ <div style="color: #374151; font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px;">🔍 Verification Status</div>
343
+ <div style="margin-top: 8px;">{status_html}</div>
344
+ </div>
345
  </div>
 
346
  </div>
347
  """
348
+
349
+ # Return JSON string exactly as produced
350
+ json_output = json.dumps(result, indent=2, ensure_ascii=False)
351
+
352
+ return image_path, card_html, json_output
353
+
354
+
355
+ # Custom CSS for better styling
356
+ custom_css = """
357
+ .gradio-container {
358
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
359
+ }
360
+
361
+ .gr-button-primary {
362
+ background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%) !important;
363
+ border: none !important;
364
+ font-weight: 600 !important;
365
+ font-size: 16px !important;
366
+ padding: 12px 24px !important;
367
+ transition: all 0.3s ease !important;
368
+ }
369
+
370
+ .gr-button-primary:hover {
371
+ transform: translateY(-2px) !important;
372
+ box-shadow: 0 8px 16px rgba(59, 130, 246, 0.3) !important;
373
+ }
374
+
375
+ .gr-box {
376
+ border-radius: 12px !important;
377
+ }
378
+ """
379
+
380
+ with gr.Blocks(title="Document OCR Verifier", css=custom_css) as demo:
381
+ gr.Markdown("""
382
+ # 🔍 Document OCR Verifier
383
+ ### Upload a document image and provide comma-separated keywords to verify the document authenticity.
384
+ """)
385
+
386
  with gr.Row():
387
+ with gr.Column(scale=1):
388
+ img_in = gr.File(label="📤 Upload Document Image (JPEG/PNG)")
389
+ kws = gr.Textbox(
390
+ label="🔑 Verification Keywords (comma-separated)",
391
+ placeholder="Example: ROHIT, KUMAR, SINGH, Date of Birth",
392
+ lines=3
393
+ )
394
+ run_btn = gr.Button("🚀 Run OCR & Verify", variant="primary", size="lg")
395
+
396
+ with gr.Column(scale=1):
397
+ img_out = gr.Image(label="📸 Uploaded Document", type="filepath", height=400)
398
+
399
  with gr.Row():
400
+ card_out = gr.HTML(label="📊 Verification Summary")
401
+
402
+ with gr.Row():
403
+ json_out = gr.Textbox(label="📋 Complete JSON Response", lines=18, max_lines=25)
404
+
405
+ run_btn.click(
406
+ fn=run_ocr,
407
+ inputs=[img_in, kws],
408
+ outputs=[img_out, card_out, json_out]
409
+ )
410
+
411
+ gr.Markdown("""
412
+ ---
413
+ **Note:** The document will be stored in `/tmp/ocr_app/` directory. Supported formats: JPEG, PNG, JPG.
414
+ """)
415
+
416
+
417
+ if __name__ == "__main__":
418
+ demo.launch(server_name="0.0.0.0", server_port=7860)