ngupta2026 commited on
Commit
d46e315
Β·
verified Β·
1 Parent(s): d3c6e58

Update app.py

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