ngupta2026 commited on
Commit
5f40dd2
Β·
verified Β·
1 Parent(s): 7b2550f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +153 -145
app.py CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import pytesseract
3
  from PIL import Image
@@ -10,7 +17,7 @@ import base64
10
 
11
  from transformers import LayoutLMTokenizerFast, LayoutLMForTokenClassification
12
 
13
- # PDF
14
  from reportlab.lib.pagesizes import A4
15
  from reportlab.pdfgen import canvas
16
 
@@ -19,9 +26,7 @@ from reportlab.pdfgen import canvas
19
  # =====================================================
20
  RESEND_API_KEY = os.getenv("RESEND_API_KEY")
21
 
22
- # Use your verified sender email/domain
23
  FROM_EMAIL = "AI Claims <claims@yudham.com>"
24
-
25
  MODEL_NAME = "ngupta2026/sroie-layoutlm"
26
 
27
  label2id = {
@@ -45,7 +50,7 @@ model.to(device)
45
  model.eval()
46
 
47
  # =====================================================
48
- # NORMALIZE BOXES
49
  # =====================================================
50
  def normalize(box, width, height):
51
  return [
@@ -55,21 +60,76 @@ def normalize(box, width, height):
55
  int(1000 * box[3] / height),
56
  ]
57
 
 
 
 
58
  # =====================================================
59
- # HELPERS
60
  # =====================================================
61
- def clean_text(txt):
62
- return txt.strip().replace("\n", " ")
63
 
64
- def avg(lst):
65
- if len(lst) == 0:
66
- return 0
67
- return sum(lst) / len(lst)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  # =====================================================
70
- # PDF CREATION
71
  # =====================================================
72
- def create_pdf(extracted):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  buffer = io.BytesIO()
75
 
@@ -79,7 +139,7 @@ def create_pdf(extracted):
79
  y = height - 60
80
 
81
  p.setFont("Helvetica-Bold", 18)
82
- p.drawString(50, y, "Insurance Claim Report")
83
 
84
  y -= 40
85
  p.setFont("Helvetica", 12)
@@ -87,17 +147,17 @@ def create_pdf(extracted):
87
  rows = [
88
  f"Provider Name : {extracted['company']}",
89
  f"Bill Date : {extracted['date']}",
90
- f"Claim Amount : Rs {extracted['total']}",
91
  f"Confidence : {extracted['confidence']}",
92
- f"Decision : {extracted['decision']}",
93
  ]
94
 
95
  for row in rows:
96
  p.drawString(50, y, row)
97
- y -= 25
98
 
99
  y -= 20
100
- p.drawString(50, y, "Generated by AI Insurance Claim System")
101
 
102
  p.showPage()
103
  p.save()
@@ -105,15 +165,16 @@ def create_pdf(extracted):
105
  pdf_bytes = buffer.getvalue()
106
  buffer.close()
107
 
108
- return base64.b64encode(pdf_bytes).decode("utf-8")
109
 
110
  # =====================================================
111
- # EXTRACTION ENGINE
112
  # =====================================================
113
  def extract_receipt(image):
114
 
115
  try:
116
  image = image.convert("RGB")
 
117
 
118
  data = pytesseract.image_to_data(
119
  image,
@@ -122,203 +183,148 @@ def extract_receipt(image):
122
 
123
  words = []
124
  boxes = []
125
- confs = []
126
 
127
- n = len(data["text"])
128
 
129
- for i in range(n):
130
 
131
- txt = clean_text(data["text"][i])
132
 
133
- if txt == "":
134
- continue
 
 
135
 
136
- x = data["left"][i]
137
- y = data["top"][i]
138
- w = data["width"][i]
139
- h = data["height"][i]
140
-
141
- words.append(txt)
142
- boxes.append([x, y, x + w, y + h])
143
 
144
  if len(words) == 0:
145
- return {"error": "No text found"}
146
 
147
  width, height = image.size
148
- boxes_norm = [normalize(b, width, height) for b in boxes]
149
 
150
- # =========================
151
- # TOKENIZER
152
- # =========================
153
  encoding = tokenizer(
154
  words,
155
- boxes=boxes_norm,
156
  return_tensors="pt",
157
  truncation=True,
158
  padding="max_length",
159
- is_split_into_words=True,
160
- max_length=256
161
  )
162
 
163
  encoding = {k: v.to(device) for k, v in encoding.items()}
164
 
165
- # =========================
166
- # MODEL
167
- # =========================
168
  with torch.no_grad():
169
  outputs = model(**encoding)
170
 
171
  probs = torch.softmax(outputs.logits, dim=2)
 
172
  preds = torch.argmax(probs, dim=2)[0][:len(words)]
173
- pred_conf = torch.max(probs, dim=2)[0][0][:len(words)]
174
 
175
- company_words = []
176
  company_scores = []
177
 
178
- # =========================
179
- # COMPANY FROM MODEL
180
- # =========================
181
- for word, pred, c in zip(words, preds, pred_conf):
182
 
183
  label = id2label[pred.item()]
184
 
185
  if label == "COMPANY":
186
- company_words.append(word)
187
- company_scores.append(c.item())
188
-
189
- company = " ".join(company_words).strip()
190
-
191
- if company == "":
192
- company = words[0]
193
-
194
- # =========================
195
- # DATE BY REGEX
196
- # =========================
197
- date = "Not Found"
198
 
199
- for w in words:
200
- if re.search(r"\d{2}[/-]\d{2}[/-]\d{2,4}", w):
201
- date = w
202
- break
203
-
204
- # =========================
205
- # TOTAL SMART LOGIC
206
- # =========================
207
- amount_candidates = []
208
-
209
- for w in words:
210
-
211
- t = w.replace(",", "").replace("RM", "").replace("Rs", "").replace("β‚Ή", "")
212
-
213
- if re.fullmatch(r"\d+(\.\d{2})?", t):
214
- try:
215
- val = float(t)
216
 
217
- if 1 <= val <= 100000:
218
- amount_candidates.append(val)
219
- except:
220
- pass
221
 
222
- total = "Not Found"
 
223
 
224
- if len(amount_candidates) > 0:
225
- total = f"{max(amount_candidates):.2f}"
226
 
227
- # =========================
228
- # CONFIDENCE
229
- # =========================
230
- company_conf = avg(company_scores)
231
 
232
  if total != "Not Found":
233
- total_conf = 0.90
234
- else:
235
- total_conf = 0.20
236
 
237
- if date != "Not Found":
238
- date_conf = 0.90
239
- else:
240
- date_conf = 0.20
241
 
242
- overall = round((company_conf + total_conf + date_conf) / 3, 3)
243
-
244
- result = {
245
  "company": company,
246
  "date": date,
247
  "total": total,
248
- "confidence": overall
249
  }
250
 
251
- return result
252
-
253
  except Exception as e:
254
  return {"error": str(e)}
255
 
256
  # =====================================================
257
- # DECISION ENGINE
258
  # =====================================================
259
  def decision_layer(conf):
260
 
261
  if conf >= 0.80:
262
  return "AUTO_SEND"
263
-
264
  elif conf >= 0.60:
265
  return "REVIEW"
266
-
267
- return "REJECT"
268
 
269
  # =====================================================
270
- # EMAIL SEND WITH PDF
271
  # =====================================================
272
  def send_claim_email(to_email, extracted):
273
 
274
  if not RESEND_API_KEY:
275
  return "❌ Missing RESEND_API_KEY"
276
 
277
- pdf_base64 = create_pdf(extracted)
278
 
279
- html_body = f"""
280
- <h2>Insurance Claim Request</h2>
281
-
282
- <p>Dear Claims Team,</p>
283
 
284
- <p>Please process reimbursement request.</p>
 
285
 
286
- <p><b>Provider Name:</b> {extracted['company']}</p>
287
- <p><b>Bill Date:</b> {extracted['date']}</p>
288
- <p><b>Claim Amount:</b> Rs {extracted['total']}</p>
289
- <p><b>Confidence:</b> {extracted['confidence']}</p>
290
 
291
- <p>Regards,<br>AI Claims System</p>
292
  """
293
 
294
- payload = {
295
- "from": FROM_EMAIL,
296
- "to": [to_email],
297
- "subject": "Insurance Claim Request",
298
- "html": html_body,
299
- "attachments": [
300
- {
301
- "filename": "Claim_Report.pdf",
302
- "content": pdf_base64
303
- }
304
- ]
305
- }
306
-
307
  try:
308
- response = requests.post(
309
  "https://api.resend.com/emails",
310
  headers={
311
  "Authorization": f"Bearer {RESEND_API_KEY}",
312
  "Content-Type": "application/json"
313
  },
314
- json=payload,
 
 
 
 
 
 
 
 
 
 
 
315
  timeout=20
316
  )
317
 
318
- if response.status_code in [200, 201]:
319
- return f"βœ… Email sent successfully to {to_email}"
320
 
321
- return f"❌ Email failed: {response.text}"
322
 
323
  except Exception as e:
324
  return f"❌ Email error: {str(e)}"
@@ -333,19 +339,21 @@ def process_and_send(image, email_id):
333
  if "error" in extracted:
334
  return extracted, extracted["error"]
335
 
336
- decision = decision_layer(extracted["confidence"])
 
 
337
  extracted["decision"] = decision
338
 
339
  if decision == "AUTO_SEND":
340
- email_status = send_claim_email(email_id, extracted)
341
 
342
  elif decision == "REVIEW":
343
- email_status = f"⚠️ Needs manual review (confidence={extracted['confidence']})"
344
 
345
  else:
346
- email_status = f"❌ Rejected (confidence={extracted['confidence']})"
347
 
348
- return extracted, email_status
349
 
350
  # =====================================================
351
  # UI
@@ -355,16 +363,16 @@ demo = gr.Interface(
355
 
356
  inputs=[
357
  gr.Image(type="pil", label="Upload Receipt"),
358
- gr.Textbox(label="Enter Destination Email")
359
  ],
360
 
361
  outputs=[
362
- gr.JSON(label="AI Extraction Result"),
363
  gr.Textbox(label="Email Status")
364
  ],
365
 
366
  title="πŸ“„ AI Insurance Claim Generator",
367
- description="Upload receipt β†’ Extract fields β†’ Confidence Check β†’ Auto Email + PDF"
368
  )
369
 
370
  demo.launch()
 
1
+ # =====================================================
2
+ # AI INSURANCE CLAIM GENERATOR
3
+ # FINAL VERSION
4
+ # Working Extraction Logic + Professional PDF + Email
5
+ # Hugging Face Space Ready
6
+ # =====================================================
7
+
8
  import gradio as gr
9
  import pytesseract
10
  from PIL import Image
 
17
 
18
  from transformers import LayoutLMTokenizerFast, LayoutLMForTokenClassification
19
 
20
+ # PDF LIBRARY
21
  from reportlab.lib.pagesizes import A4
22
  from reportlab.pdfgen import canvas
23
 
 
26
  # =====================================================
27
  RESEND_API_KEY = os.getenv("RESEND_API_KEY")
28
 
 
29
  FROM_EMAIL = "AI Claims <claims@yudham.com>"
 
30
  MODEL_NAME = "ngupta2026/sroie-layoutlm"
31
 
32
  label2id = {
 
50
  model.eval()
51
 
52
  # =====================================================
53
+ # HELPERS
54
  # =====================================================
55
  def normalize(box, width, height):
56
  return [
 
60
  int(1000 * box[3] / height),
61
  ]
62
 
63
+ def avg(lst):
64
+ return sum(lst) / len(lst) if len(lst) > 0 else 0
65
+
66
  # =====================================================
67
+ # CLEAN COMPANY
68
  # =====================================================
69
+ def clean_company(txt):
 
70
 
71
+ txt = txt.strip()
72
+ txt = re.sub(r"[^A-Za-z0-9&().,\- /]", "", txt)
73
+ txt = re.sub(r"\s+", " ", txt).strip()
74
+
75
+ if len(txt) < 2:
76
+ return "Not Found"
77
+
78
+ return txt.upper()
79
+
80
+ # =====================================================
81
+ # DATE EXTRACTION
82
+ # =====================================================
83
+ def extract_date(words):
84
+
85
+ for w in words:
86
+ if re.fullmatch(r"\d{1,2}[/-]\d{1,2}[/-]\d{2,4}", w):
87
+ return w
88
+
89
+ return "Not Found"
90
 
91
  # =====================================================
92
+ # TOTAL EXTRACTION
93
  # =====================================================
94
+ def clean_amount_token(txt):
95
+
96
+ txt = txt.upper()
97
+ txt = txt.replace("RM", "")
98
+ txt = txt.replace("MYR", "")
99
+ txt = txt.replace("RS", "")
100
+ txt = txt.replace("β‚Ή", "")
101
+ txt = txt.replace(",", "")
102
+ txt = txt.strip()
103
+
104
+ return txt
105
+
106
+ def extract_total(words):
107
+
108
+ vals = []
109
+
110
+ for w in words:
111
+
112
+ x = clean_amount_token(w)
113
+
114
+ if re.fullmatch(r"\d+\.\d{2}", x):
115
+ try:
116
+ v = float(x)
117
+
118
+ if 0.5 <= v <= 100000:
119
+ vals.append(v)
120
+
121
+ except:
122
+ pass
123
+
124
+ if vals:
125
+ return f"{max(vals):.2f}"
126
+
127
+ return "Not Found"
128
+
129
+ # =====================================================
130
+ # PROFESSIONAL PDF GENERATOR
131
+ # =====================================================
132
+ def create_pdf_base64(extracted):
133
 
134
  buffer = io.BytesIO()
135
 
 
139
  y = height - 60
140
 
141
  p.setFont("Helvetica-Bold", 18)
142
+ p.drawString(50, y, "AI Insurance Claim Report")
143
 
144
  y -= 40
145
  p.setFont("Helvetica", 12)
 
147
  rows = [
148
  f"Provider Name : {extracted['company']}",
149
  f"Bill Date : {extracted['date']}",
150
+ f"Claim Amount : {extracted['total']}",
151
  f"Confidence : {extracted['confidence']}",
152
+ f"Decision : {extracted['decision']}"
153
  ]
154
 
155
  for row in rows:
156
  p.drawString(50, y, row)
157
+ y -= 28
158
 
159
  y -= 20
160
+ p.drawString(50, y, "Generated by AI Insurance Claim Generator")
161
 
162
  p.showPage()
163
  p.save()
 
165
  pdf_bytes = buffer.getvalue()
166
  buffer.close()
167
 
168
+ return base64.b64encode(pdf_bytes).decode()
169
 
170
  # =====================================================
171
+ # MAIN EXTRACTION
172
  # =====================================================
173
  def extract_receipt(image):
174
 
175
  try:
176
  image = image.convert("RGB")
177
+ image.thumbnail((1500, 1500))
178
 
179
  data = pytesseract.image_to_data(
180
  image,
 
183
 
184
  words = []
185
  boxes = []
 
186
 
187
+ for i in range(len(data["text"])):
188
 
189
+ txt = data["text"][i].strip()
190
 
191
+ if txt != "" and len(txt) > 1:
192
 
193
+ x = data["left"][i]
194
+ y = data["top"][i]
195
+ w = data["width"][i]
196
+ h = data["height"][i]
197
 
198
+ words.append(txt)
199
+ boxes.append([x, y, x + w, y + h])
 
 
 
 
 
200
 
201
  if len(words) == 0:
202
+ return {"error": "No text detected"}
203
 
204
  width, height = image.size
205
+ boxes = [normalize(b, width, height) for b in boxes]
206
 
 
 
 
207
  encoding = tokenizer(
208
  words,
209
+ boxes=boxes,
210
  return_tensors="pt",
211
  truncation=True,
212
  padding="max_length",
213
+ max_length=512,
214
+ is_split_into_words=True
215
  )
216
 
217
  encoding = {k: v.to(device) for k, v in encoding.items()}
218
 
 
 
 
219
  with torch.no_grad():
220
  outputs = model(**encoding)
221
 
222
  probs = torch.softmax(outputs.logits, dim=2)
223
+
224
  preds = torch.argmax(probs, dim=2)[0][:len(words)]
225
+ confs = torch.max(probs, dim=2)[0][0][:len(words)]
226
 
227
+ company_tokens = []
228
  company_scores = []
229
 
230
+ for word, pred, conf in zip(words, preds, confs):
 
 
 
231
 
232
  label = id2label[pred.item()]
233
 
234
  if label == "COMPANY":
235
+ company_tokens.append(word)
236
+ company_scores.append(conf.item())
 
 
 
 
 
 
 
 
 
 
237
 
238
+ if company_tokens:
239
+ company = " ".join(company_tokens[:8])
240
+ else:
241
+ company = " ".join(words[:5])
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
+ company = clean_company(company)
 
 
 
244
 
245
+ date = extract_date(words)
246
+ total = extract_total(words)
247
 
248
+ score = avg(company_scores)
 
249
 
250
+ if date != "Not Found":
251
+ score += 0.12
 
 
252
 
253
  if total != "Not Found":
254
+ score += 0.18
 
 
255
 
256
+ score = min(score, 0.99)
 
 
 
257
 
258
+ return {
 
 
259
  "company": company,
260
  "date": date,
261
  "total": total,
262
+ "confidence": round(score, 3)
263
  }
264
 
 
 
265
  except Exception as e:
266
  return {"error": str(e)}
267
 
268
  # =====================================================
269
+ # DECISION
270
  # =====================================================
271
  def decision_layer(conf):
272
 
273
  if conf >= 0.80:
274
  return "AUTO_SEND"
 
275
  elif conf >= 0.60:
276
  return "REVIEW"
277
+ else:
278
+ return "REJECT"
279
 
280
  # =====================================================
281
+ # EMAIL SEND
282
  # =====================================================
283
  def send_claim_email(to_email, extracted):
284
 
285
  if not RESEND_API_KEY:
286
  return "❌ Missing RESEND_API_KEY"
287
 
288
+ pdf_b64 = create_pdf_base64(extracted)
289
 
290
+ subject = "Insurance Claim Request"
 
 
 
291
 
292
+ html = f"""
293
+ <h2>Insurance Claim Request</h2>
294
 
295
+ <p><b>Provider:</b> {extracted['company']}</p>
296
+ <p><b>Date:</b> {extracted['date']}</p>
297
+ <p><b>Amount:</b> β‚Ή{extracted['total']}</p>
 
298
 
299
+ <p>Attached: AI Claim Report PDF</p>
300
  """
301
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  try:
303
+ r = requests.post(
304
  "https://api.resend.com/emails",
305
  headers={
306
  "Authorization": f"Bearer {RESEND_API_KEY}",
307
  "Content-Type": "application/json"
308
  },
309
+ json={
310
+ "from": FROM_EMAIL,
311
+ "to": [to_email],
312
+ "subject": subject,
313
+ "html": html,
314
+ "attachments": [
315
+ {
316
+ "filename": "claim_report.pdf",
317
+ "content": pdf_b64
318
+ }
319
+ ]
320
+ },
321
  timeout=20
322
  )
323
 
324
+ if r.status_code in [200, 201]:
325
+ return f"βœ… Email + PDF sent to {to_email}"
326
 
327
+ return f"❌ Email failed: {r.text}"
328
 
329
  except Exception as e:
330
  return f"❌ Email error: {str(e)}"
 
339
  if "error" in extracted:
340
  return extracted, extracted["error"]
341
 
342
+ conf = extracted["confidence"]
343
+ decision = decision_layer(conf)
344
+
345
  extracted["decision"] = decision
346
 
347
  if decision == "AUTO_SEND":
348
+ status = send_claim_email(email_id, extracted)
349
 
350
  elif decision == "REVIEW":
351
+ status = f"⚠️ Human review required ({conf})"
352
 
353
  else:
354
+ status = f"❌ Rejected ({conf})"
355
 
356
+ return extracted, status
357
 
358
  # =====================================================
359
  # UI
 
363
 
364
  inputs=[
365
  gr.Image(type="pil", label="Upload Receipt"),
366
+ gr.Textbox(label="Destination Email")
367
  ],
368
 
369
  outputs=[
370
+ gr.JSON(label="AI Extraction"),
371
  gr.Textbox(label="Email Status")
372
  ],
373
 
374
  title="πŸ“„ AI Insurance Claim Generator",
375
+ description="Upload receipt β†’ Extract fields β†’ Generate PDF β†’ Auto Email"
376
  )
377
 
378
  demo.launch()