ngupta2026 commited on
Commit
7f77d2e
Β·
verified Β·
1 Parent(s): 2d9c5d6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +132 -133
app.py CHANGED
@@ -5,11 +5,6 @@ import torch
5
  import re
6
  import requests
7
  import os
8
- import io
9
- import base64
10
-
11
- from reportlab.pdfgen import canvas
12
- from reportlab.lib.pagesizes import A4
13
 
14
  from transformers import LayoutLMTokenizerFast, LayoutLMForTokenClassification
15
 
@@ -18,8 +13,8 @@ from transformers import LayoutLMTokenizerFast, LayoutLMForTokenClassification
18
  # =====================================================
19
  RESEND_API_KEY = os.getenv("RESEND_API_KEY")
20
 
21
- # VERIFIED DOMAIN EMAIL (CHANGE THIS TO YOUR VERIFIED DOMAIN)
22
- FROM_EMAIL = "claims@send.yudham.com"
23
 
24
  MODEL_NAME = "ngupta2026/sroie-layoutlm"
25
 
@@ -44,7 +39,7 @@ model.to(device)
44
  model.eval()
45
 
46
  # =====================================================
47
- # NORMALIZE BOXES
48
  # =====================================================
49
  def normalize(box, width, height):
50
  return [
@@ -55,16 +50,26 @@ def normalize(box, width, height):
55
  ]
56
 
57
  # =====================================================
58
- # OCR + MODEL EXTRACTION
 
 
 
 
 
 
 
 
59
  # =====================================================
60
  def extract_receipt(image):
61
 
62
  try:
 
63
  image = image.convert("RGB")
64
 
65
  data = pytesseract.image_to_data(
66
  image,
67
- output_type=pytesseract.Output.DICT
 
68
  )
69
 
70
  words = []
@@ -72,234 +77,228 @@ def extract_receipt(image):
72
 
73
  for i in range(len(data["text"])):
74
 
75
- txt = data["text"][i].strip()
 
 
76
 
77
- if txt != "":
78
  x = data["left"][i]
79
  y = data["top"][i]
80
  w = data["width"][i]
81
  h = data["height"][i]
82
 
83
- words.append(txt)
84
- boxes.append([x, y, x+w, y+h])
85
 
86
  if len(words) == 0:
87
- return {"error": "No text found"}
88
 
89
  width, height = image.size
90
- boxes = [normalize(box, width, height) for box in boxes]
91
 
92
- # =================================================
93
- # TOKENIZER
94
- # =================================================
95
  encoding = tokenizer(
96
  words,
97
  boxes=boxes,
98
  return_tensors="pt",
99
  padding="max_length",
100
  truncation=True,
101
- max_length=512,
102
- is_split_into_words=True
103
  )
104
 
105
  encoding = {k: v.to(device) for k, v in encoding.items()}
106
 
107
- # =================================================
108
- # MODEL PREDICTION
109
- # =================================================
110
  with torch.no_grad():
111
  outputs = model(**encoding)
112
 
113
  probs = torch.softmax(outputs.logits, dim=2)
 
114
  preds = torch.argmax(probs, dim=2)[0][:len(words)]
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  # =================================================
117
- # EXTRACTION STORE
118
  # =================================================
119
- company_tokens = []
120
- totals = []
121
- dates = []
122
-
123
- for word, pred in zip(words, preds):
124
 
125
  label = id2label[pred.item()]
 
126
 
127
- # COMPANY
128
  if label == "COMPANY":
129
- company_tokens.append(word)
 
130
 
131
- # DATE via regex
132
  if re.search(r"\d{1,2}[/-]\d{1,2}[/-]\d{2,4}", word):
133
- dates.append(word)
 
 
 
 
134
 
135
- # MONEY
136
- if re.search(r"^\d+[.,]?\d*$", word):
137
  try:
138
- val = float(word.replace(",", ""))
139
- if val > 20:
140
- totals.append(val)
 
 
 
 
141
  except:
142
  pass
143
 
144
  # =================================================
145
  # FINAL CLEANUP
146
  # =================================================
147
- company = " ".join(company_tokens[:6]).strip()
 
 
148
  if company == "":
149
  company = "Not Found"
150
 
151
- date = dates[0] if len(dates) > 0 else "Not Found"
152
-
153
- total = str(max(totals)) if len(totals) > 0 else "Not Found"
154
 
155
- # =================================================
156
- # ADDRESS HEURISTIC
157
- # =================================================
158
- address_lines = []
159
 
160
- for w in words:
161
- if (
162
- w not in company_tokens
163
- and w not in dates
164
- and not re.search(r"^\d+[.,]?\d*$", w)
165
- ):
166
- if len(w) > 2:
167
- address_lines.append(w)
168
 
169
- address = " ".join(address_lines[:10]).strip()
170
-
171
- if address == "":
172
- address = "Not Found"
173
 
174
  return {
175
  "company": company,
176
  "date": date,
177
  "total": total,
178
- "address": address
179
  }
180
 
181
  except Exception as e:
182
  return {"error": str(e)}
183
 
184
  # =====================================================
185
- # PDF GENERATOR
186
  # =====================================================
187
- def create_pdf(extracted):
188
-
189
- buffer = io.BytesIO()
190
-
191
- c = canvas.Canvas(buffer, pagesize=A4)
192
- width, height = A4
193
 
194
- y = height - 60
 
195
 
196
- c.setFont("Helvetica-Bold", 18)
197
- c.drawString(50, y, "Insurance Claim Summary")
198
-
199
- y -= 40
200
- c.setFont("Helvetica", 12)
201
-
202
- lines = [
203
- f"Provider Name : {extracted['company']}",
204
- f"Bill Date : {extracted['date']}",
205
- f"Claim Amount : β‚Ή{extracted['total']}",
206
- f"Address : {extracted['address']}",
207
- ]
208
 
209
- for line in lines:
210
- c.drawString(50, y, line)
211
- y -= 30
212
-
213
- c.save()
214
-
215
- pdf_bytes = buffer.getvalue()
216
- buffer.close()
217
-
218
- return pdf_bytes
219
 
220
  # =====================================================
221
- # EMAIL SEND VIA RESEND
222
  # =====================================================
223
- def send_email(to_email, extracted):
224
 
225
  if not RESEND_API_KEY:
226
- return "❌ RESEND_API_KEY missing"
227
 
228
- pdf_data = create_pdf(extracted)
229
- pdf_b64 = base64.b64encode(pdf_data).decode()
230
 
231
- html = f"""
232
  <h2>Insurance Claim Request</h2>
233
- <p><b>Provider:</b> {extracted['company']}</p>
234
- <p><b>Date:</b> {extracted['date']}</p>
235
- <p><b>Amount:</b> β‚Ή{extracted['total']}</p>
236
- <p><b>Address:</b> {extracted['address']}</p>
237
- <p>Please find attached PDF summary.</p>
238
- """
239
 
240
- payload = {
241
- "from": FROM_EMAIL,
242
- "to": [to_email],
243
- "subject": "Insurance Claim Request",
244
- "html": html,
245
- "attachments": [
246
- {
247
- "filename": "claim_summary.pdf",
248
- "content": pdf_b64
249
- }
250
- ]
251
- }
252
-
253
- headers = {
254
- "Authorization": f"Bearer {RESEND_API_KEY}",
255
- "Content-Type": "application/json"
256
- }
257
 
258
  try:
259
- r = requests.post(
260
  "https://api.resend.com/emails",
261
- json=payload,
262
- headers=headers,
 
 
 
 
 
 
 
 
263
  timeout=20
264
  )
265
 
266
- if r.status_code in [200, 201]:
267
- return f"βœ… Email sent to {to_email}"
268
 
269
- return f"❌ Email failed: {r.text}"
270
 
271
  except Exception as e:
272
  return f"❌ Email error: {str(e)}"
273
 
274
  # =====================================================
275
- # MAIN FUNCTION
276
  # =====================================================
277
- def process(image, email):
278
 
279
  extracted = extract_receipt(image)
280
 
281
  if "error" in extracted:
282
  return extracted, extracted["error"]
283
 
284
- status = send_email(email, extracted)
 
 
 
 
 
 
285
 
286
- return extracted, status
 
 
 
 
 
 
287
 
288
  # =====================================================
289
  # UI
290
  # =====================================================
291
  demo = gr.Interface(
292
- fn=process,
 
293
  inputs=[
294
  gr.Image(type="pil", label="Upload Receipt"),
295
- gr.Textbox(label="Enter Email ID")
296
  ],
 
297
  outputs=[
298
- gr.JSON(label="Extracted Output"),
299
  gr.Textbox(label="Email Status")
300
  ],
 
301
  title="πŸ“„ AI Insurance Claim Generator",
302
- description="Upload receipt β†’ Extract details β†’ Generate PDF β†’ Send Email"
303
  )
304
 
305
  demo.launch()
 
5
  import re
6
  import requests
7
  import os
 
 
 
 
 
8
 
9
  from transformers import LayoutLMTokenizerFast, LayoutLMForTokenClassification
10
 
 
13
  # =====================================================
14
  RESEND_API_KEY = os.getenv("RESEND_API_KEY")
15
 
16
+ # Use verified sender from Resend
17
+ FROM_EMAIL = "AI Claims <claims@yudham.com>"
18
 
19
  MODEL_NAME = "ngupta2026/sroie-layoutlm"
20
 
 
39
  model.eval()
40
 
41
  # =====================================================
42
+ # NORMALIZE BOX
43
  # =====================================================
44
  def normalize(box, width, height):
45
  return [
 
50
  ]
51
 
52
  # =====================================================
53
+ # AVG CONFIDENCE
54
+ # =====================================================
55
+ def avg_conf(values):
56
+ if len(values) == 0:
57
+ return 0
58
+ return sum(values) / len(values)
59
+
60
+ # =====================================================
61
+ # OCR + EXTRACTION (IMPROVED ACCURACY)
62
  # =====================================================
63
  def extract_receipt(image):
64
 
65
  try:
66
+ # Keep quality high for OCR
67
  image = image.convert("RGB")
68
 
69
  data = pytesseract.image_to_data(
70
  image,
71
+ output_type=pytesseract.Output.DICT,
72
+ config="--oem 3 --psm 6"
73
  )
74
 
75
  words = []
 
77
 
78
  for i in range(len(data["text"])):
79
 
80
+ text = data["text"][i].strip()
81
+
82
+ if text != "" and text != "|":
83
 
 
84
  x = data["left"][i]
85
  y = data["top"][i]
86
  w = data["width"][i]
87
  h = data["height"][i]
88
 
89
+ words.append(text)
90
+ boxes.append([x, y, x + w, y + h])
91
 
92
  if len(words) == 0:
93
+ return {"error": "No text detected"}
94
 
95
  width, height = image.size
96
+ boxes = [normalize(b, width, height) for b in boxes]
97
 
98
+ # IMPORTANT: use 512 for better predictions
 
 
99
  encoding = tokenizer(
100
  words,
101
  boxes=boxes,
102
  return_tensors="pt",
103
  padding="max_length",
104
  truncation=True,
105
+ is_split_into_words=True,
106
+ max_length=512
107
  )
108
 
109
  encoding = {k: v.to(device) for k, v in encoding.items()}
110
 
 
 
 
111
  with torch.no_grad():
112
  outputs = model(**encoding)
113
 
114
  probs = torch.softmax(outputs.logits, dim=2)
115
+
116
  preds = torch.argmax(probs, dim=2)[0][:len(words)]
117
+ confs = torch.max(probs, dim=2)[0][0][:len(words)]
118
+
119
+ result = {
120
+ "company": [],
121
+ "date": [],
122
+ "total": []
123
+ }
124
+
125
+ conf_store = {
126
+ "company": [],
127
+ "date": [],
128
+ "total": []
129
+ }
130
 
131
  # =================================================
132
+ # TOKEN LEVEL EXTRACTION
133
  # =================================================
134
+ for word, pred, conf in zip(words, preds, confs):
 
 
 
 
135
 
136
  label = id2label[pred.item()]
137
+ c = conf.item()
138
 
139
+ # COMPANY from model
140
  if label == "COMPANY":
141
+ result["company"].append(word)
142
+ conf_store["company"].append(c)
143
 
144
+ # DATE regex
145
  if re.search(r"\d{1,2}[/-]\d{1,2}[/-]\d{2,4}", word):
146
+ result["date"].append(word)
147
+ conf_store["date"].append(c)
148
+
149
+ # TOTAL numeric values
150
+ cleaned = word.replace(",", "").replace("β‚Ή", "")
151
 
152
+ if re.fullmatch(r"\d+(\.\d{1,2})?", cleaned):
 
153
  try:
154
+ value = float(cleaned)
155
+
156
+ # Better range for totals
157
+ if value >= 10:
158
+ result["total"].append(value)
159
+ conf_store["total"].append(c)
160
+
161
  except:
162
  pass
163
 
164
  # =================================================
165
  # FINAL CLEANUP
166
  # =================================================
167
+
168
+ # COMPANY
169
+ company = " ".join(result["company"][:6]).strip()
170
  if company == "":
171
  company = "Not Found"
172
 
173
+ # DATE
174
+ date = result["date"][0] if result["date"] else "Not Found"
 
175
 
176
+ # TOTAL = highest amount (better than last token)
177
+ total = str(max(result["total"])) if result["total"] else "Not Found"
 
 
178
 
179
+ # CONFIDENCE
180
+ company_conf = avg_conf(conf_store["company"])
181
+ date_conf = avg_conf(conf_store["date"])
182
+ total_conf = avg_conf(conf_store["total"])
 
 
 
 
183
 
184
+ overall = (company_conf + date_conf + total_conf) / 3
 
 
 
185
 
186
  return {
187
  "company": company,
188
  "date": date,
189
  "total": total,
190
+ "confidence": round(overall, 3)
191
  }
192
 
193
  except Exception as e:
194
  return {"error": str(e)}
195
 
196
  # =====================================================
197
+ # DECISION ENGINE
198
  # =====================================================
199
+ def decision_layer(conf):
 
 
 
 
 
200
 
201
+ if conf >= 0.80:
202
+ return "AUTO_SEND"
203
 
204
+ elif conf >= 0.60:
205
+ return "REVIEW"
 
 
 
 
 
 
 
 
 
 
206
 
207
+ else:
208
+ return "REJECT"
 
 
 
 
 
 
 
 
209
 
210
  # =====================================================
211
+ # EMAIL SEND
212
  # =====================================================
213
+ def send_claim_email(to_email, extracted):
214
 
215
  if not RESEND_API_KEY:
216
+ return "❌ Missing RESEND_API_KEY secret"
217
 
218
+ subject = "Insurance Claim Request"
 
219
 
220
+ html_body = f"""
221
  <h2>Insurance Claim Request</h2>
 
 
 
 
 
 
222
 
223
+ <p>Dear Claims Team,</p>
224
+
225
+ <p>Please process reimbursement request.</p>
226
+
227
+ <p><b>Provider Name:</b> {extracted['company']}</p>
228
+ <p><b>Bill Date:</b> {extracted['date']}</p>
229
+ <p><b>Claim Amount:</b> β‚Ή{extracted['total']}</p>
230
+
231
+ <p>Regards,<br>AI Claims System</p>
232
+ """
 
 
 
 
 
 
 
233
 
234
  try:
235
+ response = requests.post(
236
  "https://api.resend.com/emails",
237
+ headers={
238
+ "Authorization": f"Bearer {RESEND_API_KEY}",
239
+ "Content-Type": "application/json"
240
+ },
241
+ json={
242
+ "from": FROM_EMAIL,
243
+ "to": [to_email],
244
+ "subject": subject,
245
+ "html": html_body
246
+ },
247
  timeout=20
248
  )
249
 
250
+ if response.status_code in [200, 201]:
251
+ return f"βœ… Email sent successfully to {to_email}"
252
 
253
+ return f"❌ Email failed: {response.text}"
254
 
255
  except Exception as e:
256
  return f"❌ Email error: {str(e)}"
257
 
258
  # =====================================================
259
+ # MAIN PIPELINE
260
  # =====================================================
261
+ def process_and_send(image, email_id):
262
 
263
  extracted = extract_receipt(image)
264
 
265
  if "error" in extracted:
266
  return extracted, extracted["error"]
267
 
268
+ conf = extracted["confidence"]
269
+ decision = decision_layer(conf)
270
+
271
+ extracted["decision"] = decision
272
+
273
+ if decision == "AUTO_SEND":
274
+ email_status = send_claim_email(email_id, extracted)
275
 
276
+ elif decision == "REVIEW":
277
+ email_status = f"⚠️ Human review required (confidence={conf})"
278
+
279
+ else:
280
+ email_status = f"❌ Rejected (low confidence={conf})"
281
+
282
+ return extracted, email_status
283
 
284
  # =====================================================
285
  # UI
286
  # =====================================================
287
  demo = gr.Interface(
288
+ fn=process_and_send,
289
+
290
  inputs=[
291
  gr.Image(type="pil", label="Upload Receipt"),
292
+ gr.Textbox(label="Enter Destination Email")
293
  ],
294
+
295
  outputs=[
296
+ gr.JSON(label="AI Extraction"),
297
  gr.Textbox(label="Email Status")
298
  ],
299
+
300
  title="πŸ“„ AI Insurance Claim Generator",
301
+ description="Upload receipt β†’ Better extraction β†’ Confidence check β†’ Auto Email"
302
  )
303
 
304
  demo.launch()