ngupta2026 commited on
Commit
ac6dc07
Β·
verified Β·
1 Parent(s): d60e25f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +123 -112
app.py CHANGED
@@ -1,6 +1,12 @@
 
 
 
 
 
 
1
  import gradio as gr
2
  import pytesseract
3
- from PIL import Image, ImageFilter, ImageOps
4
  import torch
5
  import re
6
  import requests
@@ -12,7 +18,9 @@ from transformers import LayoutLMTokenizerFast, LayoutLMForTokenClassification
12
  # CONFIG
13
  # =====================================================
14
  RESEND_API_KEY = os.getenv("RESEND_API_KEY")
15
- FROM_EMAIL = "AI Claims <claims@yudham.com>" # verified sender
 
 
16
 
17
  MODEL_NAME = "ngupta2026/sroie-layoutlm"
18
 
@@ -37,7 +45,7 @@ model.to(device)
37
  model.eval()
38
 
39
  # =====================================================
40
- # NORMALIZE BOUNDING BOXES
41
  # =====================================================
42
  def normalize(box, width, height):
43
  return [
@@ -48,34 +56,72 @@ def normalize(box, width, height):
48
  ]
49
 
50
  # =====================================================
51
- # IMAGE PREPROCESSING (VERY IMPORTANT)
 
 
 
 
 
 
 
 
52
  # =====================================================
53
- def preprocess_image(image):
 
 
54
 
55
- image = image.convert("RGB")
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
- # upscale for OCR
58
- w, h = image.size
59
- image = image.resize((w * 2, h * 2))
 
 
 
 
 
60
 
61
- # grayscale
62
- image = image.convert("L")
 
 
63
 
64
- # sharpen
65
- image = image.filter(ImageFilter.SHARPEN)
66
 
67
- # auto contrast
68
- image = ImageOps.autocontrast(image)
69
 
70
- return image
71
 
72
  # =====================================================
73
- # CONFIDENCE AVG
74
  # =====================================================
75
- def avg_conf(lst):
76
- if len(lst) == 0:
77
- return 0
78
- return sum(lst) / len(lst)
 
 
 
 
 
 
 
 
79
 
80
  # =====================================================
81
  # OCR + EXTRACTION
@@ -83,13 +129,12 @@ def avg_conf(lst):
83
  def extract_receipt(image):
84
 
85
  try:
86
- image = preprocess_image(image)
 
87
 
88
- # Better OCR mode for receipts
89
  data = pytesseract.image_to_data(
90
  image,
91
- output_type=pytesseract.Output.DICT,
92
- config="--oem 3 --psm 4"
93
  )
94
 
95
  words = []
@@ -97,16 +142,16 @@ def extract_receipt(image):
97
 
98
  for i in range(len(data["text"])):
99
 
100
- txt = data["text"][i].strip()
101
 
102
- if txt != "" and txt != "|":
103
 
104
  x = data["left"][i]
105
  y = data["top"][i]
106
  w = data["width"][i]
107
  h = data["height"][i]
108
 
109
- words.append(txt)
110
  boxes.append([x, y, x + w, y + h])
111
 
112
  if len(words) == 0:
@@ -115,9 +160,9 @@ def extract_receipt(image):
115
  width, height = image.size
116
  boxes = [normalize(b, width, height) for b in boxes]
117
 
118
- # =================================================
119
  # TOKENIZER
120
- # =================================================
121
  encoding = tokenizer(
122
  words,
123
  boxes=boxes,
@@ -130,9 +175,9 @@ def extract_receipt(image):
130
 
131
  encoding = {k: v.to(device) for k, v in encoding.items()}
132
 
133
- # =================================================
134
- # MODEL PREDICTION
135
- # =================================================
136
  with torch.no_grad():
137
  outputs = model(**encoding)
138
 
@@ -141,92 +186,61 @@ def extract_receipt(image):
141
  preds = torch.argmax(probs, dim=2)[0][:len(words)]
142
  confs = torch.max(probs, dim=2)[0][0][:len(words)]
143
 
144
- result = {
145
- "company": [],
146
- "date": [],
147
- "total": []
148
- }
149
 
150
- conf_store = {
151
- "company": [],
152
- "date": [],
153
- "total": []
154
- }
155
-
156
- # =================================================
157
- # EXTRACT ENTITIES
158
- # =================================================
159
  for word, pred, conf in zip(words, preds, confs):
160
 
161
  label = id2label[pred.item()]
162
  c = conf.item()
163
 
164
- # -------------------------
165
- # COMPANY
166
- # -------------------------
167
  if label == "COMPANY":
168
- result["company"].append(word)
169
- conf_store["company"].append(c)
170
-
171
- # -------------------------
172
- # DATE
173
- # -------------------------
174
- if re.search(r"\d{1,2}[/-]\d{1,2}[/-]\d{2,4}", word):
175
- result["date"].append(word)
176
- conf_store["date"].append(c)
177
-
178
- # -------------------------
179
- # TOTAL
180
- # -------------------------
181
- cleaned = word.replace(",", "").replace("β‚Ή", "").replace("$", "")
182
-
183
- if re.fullmatch(r"\d+(\.\d{1,2})?", cleaned):
184
-
185
- try:
186
- value = float(cleaned)
187
-
188
- # realistic receipt range
189
- if 1 <= value <= 10000:
190
- result["total"].append(value)
191
- conf_store["total"].append(c)
192
-
193
- except:
194
- pass
195
-
196
- # =================================================
197
- # FINAL CLEANUP
198
- # =================================================
199
 
 
200
  # COMPANY
201
- company = " ".join(result["company"][:6]).strip()
202
-
203
- if company == "":
204
- # fallback top words
205
- company = " ".join(words[:3])
206
 
 
207
  # DATE
208
- date = result["date"][0] if result["date"] else "Not Found"
 
 
 
 
 
 
209
 
210
- # TOTAL = best realistic amount
211
- if result["total"]:
212
- total = f"{max(result['total']):.2f}"
213
- else:
214
- total = "Not Found"
215
 
 
216
  # CONFIDENCE
217
- company_conf = avg_conf(conf_store["company"])
218
- date_conf = avg_conf(conf_store["date"])
219
- total_conf = avg_conf(conf_store["total"])
 
 
220
 
221
- overall = (company_conf + date_conf + total_conf) / 3
222
 
223
- return {
224
  "company": company,
225
  "date": date,
226
  "total": total,
227
- "confidence": round(overall, 3)
228
  }
229
 
 
 
230
  except Exception as e:
231
  return {"error": str(e)}
232
 
@@ -250,22 +264,18 @@ def decision_layer(conf):
250
  def send_claim_email(to_email, extracted):
251
 
252
  if not RESEND_API_KEY:
253
- return "❌ Missing RESEND_API_KEY secret"
254
 
255
  subject = "Insurance Claim Request"
256
 
257
  html_body = f"""
258
  <h2>Insurance Claim Request</h2>
259
 
260
- <p>Dear Claims Team,</p>
261
-
262
- <p>Please process reimbursement request.</p>
263
 
264
- <p><b>Provider Name:</b> {extracted['company']}</p>
265
- <p><b>Bill Date:</b> {extracted['date']}</p>
266
- <p><b>Claim Amount:</b> β‚Ή{extracted['total']}</p>
267
-
268
- <p>Regards,<br>AI Claims System</p>
269
  """
270
 
271
  try:
@@ -285,7 +295,7 @@ def send_claim_email(to_email, extracted):
285
  )
286
 
287
  if response.status_code in [200, 201]:
288
- return f"βœ… Email sent successfully to {to_email}"
289
 
290
  return f"❌ Email failed: {response.text}"
291
 
@@ -303,6 +313,7 @@ def process_and_send(image, email_id):
303
  return extracted, extracted["error"]
304
 
305
  conf = extracted["confidence"]
 
306
  decision = decision_layer(conf)
307
 
308
  extracted["decision"] = decision
@@ -311,10 +322,10 @@ def process_and_send(image, email_id):
311
  email_status = send_claim_email(email_id, extracted)
312
 
313
  elif decision == "REVIEW":
314
- email_status = f"⚠️ Human review required (confidence={conf})"
315
 
316
  else:
317
- email_status = f"❌ Rejected (low confidence={conf})"
318
 
319
  return extracted, email_status
320
 
@@ -326,7 +337,7 @@ demo = gr.Interface(
326
 
327
  inputs=[
328
  gr.Image(type="pil", label="Upload Receipt"),
329
- gr.Textbox(label="Enter Destination Email")
330
  ],
331
 
332
  outputs=[
@@ -335,7 +346,7 @@ demo = gr.Interface(
335
  ],
336
 
337
  title="πŸ“„ AI Insurance Claim Generator",
338
- description="Upload receipt β†’ Extract fields accurately β†’ Confidence Check β†’ Auto Email"
339
  )
340
 
341
  demo.launch()
 
1
+ # =====================================================
2
+ # AI INSURANCE CLAIM GENERATOR (FINAL HIGH-ACCURACY VERSION)
3
+ # Better TOTAL extraction + Better COMPANY extraction
4
+ # Hugging Face Space Ready
5
+ # =====================================================
6
+
7
  import gradio as gr
8
  import pytesseract
9
+ from PIL import Image
10
  import torch
11
  import re
12
  import requests
 
18
  # CONFIG
19
  # =====================================================
20
  RESEND_API_KEY = os.getenv("RESEND_API_KEY")
21
+
22
+ # VERIFIED DOMAIN EMAIL
23
+ FROM_EMAIL = "AI Claims <claims@yudham.com>"
24
 
25
  MODEL_NAME = "ngupta2026/sroie-layoutlm"
26
 
 
45
  model.eval()
46
 
47
  # =====================================================
48
+ # NORMALIZE BOX
49
  # =====================================================
50
  def normalize(box, width, height):
51
  return [
 
56
  ]
57
 
58
  # =====================================================
59
+ # AVG CONF
60
+ # =====================================================
61
+ def avg_conf(lst):
62
+ if len(lst) == 0:
63
+ return 0
64
+ return sum(lst) / len(lst)
65
+
66
+ # =====================================================
67
+ # CLEAN MONEY
68
  # =====================================================
69
+ def clean_amount(txt):
70
+ txt = txt.replace(",", "").replace("RM", "").replace("β‚Ή", "")
71
+ txt = txt.strip()
72
 
73
+ try:
74
+ val = float(txt)
75
+ return round(val, 2)
76
+ except:
77
+ return None
78
+
79
+ # =====================================================
80
+ # FIND BEST TOTAL (VERY IMPORTANT FIX)
81
+ # =====================================================
82
+ def extract_best_total(words):
83
+
84
+ candidates = []
85
+
86
+ for i, w in enumerate(words):
87
 
88
+ # match amounts like:
89
+ # 102.40
90
+ # 1,234.50
91
+ # RM102.40
92
+ if re.fullmatch(r"(RM)?\d+[.,]?\d*\.\d{2}", w):
93
+ amt = clean_amount(w)
94
+ if amt:
95
+ candidates.append(amt)
96
 
97
+ elif re.fullmatch(r"\d+\.\d{2}", w):
98
+ amt = clean_amount(w)
99
+ if amt:
100
+ candidates.append(amt)
101
 
102
+ # choose sensible max under 100000
103
+ candidates = [x for x in candidates if 1 <= x <= 100000]
104
 
105
+ if len(candidates) == 0:
106
+ return "Not Found"
107
 
108
+ return f"{max(candidates):.2f}"
109
 
110
  # =====================================================
111
+ # COMPANY CLEANER
112
  # =====================================================
113
+ def clean_company(txt):
114
+
115
+ txt = txt.strip()
116
+
117
+ # remove garbage symbols
118
+ txt = re.sub(r"[^A-Za-z0-9&().,\- ]", "", txt)
119
+
120
+ # remove too short
121
+ if len(txt) < 3:
122
+ return "Not Found"
123
+
124
+ return txt
125
 
126
  # =====================================================
127
  # OCR + EXTRACTION
 
129
  def extract_receipt(image):
130
 
131
  try:
132
+ image = image.convert("RGB")
133
+ image.thumbnail((1400, 1400))
134
 
 
135
  data = pytesseract.image_to_data(
136
  image,
137
+ output_type=pytesseract.Output.DICT
 
138
  )
139
 
140
  words = []
 
142
 
143
  for i in range(len(data["text"])):
144
 
145
+ text = data["text"][i].strip()
146
 
147
+ if text != "" and len(text) > 1:
148
 
149
  x = data["left"][i]
150
  y = data["top"][i]
151
  w = data["width"][i]
152
  h = data["height"][i]
153
 
154
+ words.append(text)
155
  boxes.append([x, y, x + w, y + h])
156
 
157
  if len(words) == 0:
 
160
  width, height = image.size
161
  boxes = [normalize(b, width, height) for b in boxes]
162
 
163
+ # -------------------------------------------------
164
  # TOKENIZER
165
+ # -------------------------------------------------
166
  encoding = tokenizer(
167
  words,
168
  boxes=boxes,
 
175
 
176
  encoding = {k: v.to(device) for k, v in encoding.items()}
177
 
178
+ # -------------------------------------------------
179
+ # MODEL
180
+ # -------------------------------------------------
181
  with torch.no_grad():
182
  outputs = model(**encoding)
183
 
 
186
  preds = torch.argmax(probs, dim=2)[0][:len(words)]
187
  confs = torch.max(probs, dim=2)[0][0][:len(words)]
188
 
189
+ company_words = []
190
+ company_conf = []
 
 
 
191
 
192
+ # -------------------------------------------------
193
+ # ENTITY EXTRACTION
194
+ # -------------------------------------------------
 
 
 
 
 
 
195
  for word, pred, conf in zip(words, preds, confs):
196
 
197
  label = id2label[pred.item()]
198
  c = conf.item()
199
 
 
 
 
200
  if label == "COMPANY":
201
+ company_words.append(word)
202
+ company_conf.append(c)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
+ # -------------------------------------------------
205
  # COMPANY
206
+ # -------------------------------------------------
207
+ company = " ".join(company_words[:6]) if company_words else words[0]
208
+ company = clean_company(company)
 
 
209
 
210
+ # -------------------------------------------------
211
  # DATE
212
+ # -------------------------------------------------
213
+ date = "Not Found"
214
+
215
+ for w in words:
216
+ if re.search(r"\d{2}[/-]\d{2}[/-]\d{2,4}", w):
217
+ date = w
218
+ break
219
 
220
+ # -------------------------------------------------
221
+ # TOTAL (NEW LOGIC)
222
+ # -------------------------------------------------
223
+ total = extract_best_total(words)
 
224
 
225
+ # -------------------------------------------------
226
  # CONFIDENCE
227
+ # -------------------------------------------------
228
+ conf = avg_conf(company_conf)
229
+
230
+ if total != "Not Found":
231
+ conf += 0.10
232
 
233
+ conf = min(conf, 0.99)
234
 
235
+ result = {
236
  "company": company,
237
  "date": date,
238
  "total": total,
239
+ "confidence": round(conf, 3)
240
  }
241
 
242
+ return result
243
+
244
  except Exception as e:
245
  return {"error": str(e)}
246
 
 
264
  def send_claim_email(to_email, extracted):
265
 
266
  if not RESEND_API_KEY:
267
+ return "❌ Missing RESEND_API_KEY"
268
 
269
  subject = "Insurance Claim Request"
270
 
271
  html_body = f"""
272
  <h2>Insurance Claim Request</h2>
273
 
274
+ <p><b>Provider:</b> {extracted['company']}</p>
275
+ <p><b>Date:</b> {extracted['date']}</p>
276
+ <p><b>Amount:</b> β‚Ή{extracted['total']}</p>
277
 
278
+ <p>Regards,<br>AI Claims Bot</p>
 
 
 
 
279
  """
280
 
281
  try:
 
295
  )
296
 
297
  if response.status_code in [200, 201]:
298
+ return f"βœ… Email sent to {to_email}"
299
 
300
  return f"❌ Email failed: {response.text}"
301
 
 
313
  return extracted, extracted["error"]
314
 
315
  conf = extracted["confidence"]
316
+
317
  decision = decision_layer(conf)
318
 
319
  extracted["decision"] = decision
 
322
  email_status = send_claim_email(email_id, extracted)
323
 
324
  elif decision == "REVIEW":
325
+ email_status = f"⚠️ Manual review required ({conf})"
326
 
327
  else:
328
+ email_status = f"❌ Rejected ({conf})"
329
 
330
  return extracted, email_status
331
 
 
337
 
338
  inputs=[
339
  gr.Image(type="pil", label="Upload Receipt"),
340
+ gr.Textbox(label="Destination Email")
341
  ],
342
 
343
  outputs=[
 
346
  ],
347
 
348
  title="πŸ“„ AI Insurance Claim Generator",
349
+ description="Upload receipt β†’ Better AI extraction β†’ Confidence β†’ Auto Email"
350
  )
351
 
352
  demo.launch()