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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +137 -136
app.py CHANGED
@@ -5,8 +5,11 @@ import torch
5
  import re
6
  import requests
7
  import os
 
8
  import base64
9
- import tempfile
 
 
10
 
11
  from transformers import LayoutLMTokenizerFast, LayoutLMForTokenClassification
12
 
@@ -15,12 +18,18 @@ from transformers import LayoutLMTokenizerFast, LayoutLMForTokenClassification
15
  # =====================================================
16
  RESEND_API_KEY = os.getenv("RESEND_API_KEY")
17
 
18
- # Use your VERIFIED sender email
19
- FROM_EMAIL = "AI Claims <claims@yudham.com>"
20
 
21
  MODEL_NAME = "ngupta2026/sroie-layoutlm"
22
 
23
- label2id = {"O": 0, "COMPANY": 1, "DATE": 2, "TOTAL": 3}
 
 
 
 
 
 
24
  id2label = {v: k for k, v in label2id.items()}
25
 
26
  # =====================================================
@@ -46,21 +55,12 @@ def normalize(box, width, height):
46
  ]
47
 
48
  # =====================================================
49
- # CONFIDENCE HELPER
50
- # =====================================================
51
- def avg_conf(lst):
52
- if len(lst) == 0:
53
- return 0
54
- return sum(lst) / len(lst)
55
-
56
- # =====================================================
57
- # OCR + EXTRACTION
58
  # =====================================================
59
  def extract_receipt(image):
60
 
61
  try:
62
  image = image.convert("RGB")
63
- image.thumbnail((1200, 1200))
64
 
65
  data = pytesseract.image_to_data(
66
  image,
@@ -72,212 +72,216 @@ def extract_receipt(image):
72
 
73
  for i in range(len(data["text"])):
74
 
75
- text = data["text"][i].strip()
76
-
77
- if text != "" and len(text) > 1:
78
 
 
79
  x = data["left"][i]
80
  y = data["top"][i]
81
  w = data["width"][i]
82
  h = data["height"][i]
83
 
84
- words.append(text)
85
- boxes.append([x, y, x + w, y + h])
86
 
87
  if len(words) == 0:
88
- return {"error": "No text detected"}
89
 
90
  width, height = image.size
91
- boxes = [normalize(b, width, height) for b in boxes]
92
 
 
 
 
93
  encoding = tokenizer(
94
  words,
95
  boxes=boxes,
96
  return_tensors="pt",
97
  padding="max_length",
98
  truncation=True,
99
- is_split_into_words=True,
100
- max_length=256
101
  )
102
 
103
  encoding = {k: v.to(device) for k, v in encoding.items()}
104
 
 
 
 
105
  with torch.no_grad():
106
  outputs = model(**encoding)
107
 
108
  probs = torch.softmax(outputs.logits, dim=2)
109
-
110
  preds = torch.argmax(probs, dim=2)[0][:len(words)]
111
- confs = torch.max(probs, dim=2)[0][0][:len(words)]
112
-
113
- result = {
114
- "company": [],
115
- "date": [],
116
- "total": []
117
- }
118
 
119
- conf_store = {
120
- "company": [],
121
- "date": [],
122
- "total": []
123
- }
 
124
 
125
- for word, pred, conf in zip(words, preds, confs):
126
 
127
  label = id2label[pred.item()]
128
- c = conf.item()
129
 
 
130
  if label == "COMPANY":
131
- result["company"].append(word)
132
- conf_store["company"].append(c)
133
 
134
- if re.search(r"\d{2}[/-]\d{2}[/-]\d{2,4}", word):
135
- result["date"].append(word)
136
- conf_store["date"].append(c)
137
 
138
- if re.search(r"\d+(\.\d{2})?", word):
 
139
  try:
140
- value = float(word.replace(",", ""))
141
- if value > 50:
142
- result["total"].append(word)
143
- conf_store["total"].append(c)
144
  except:
145
  pass
146
 
147
- result["company"] = " ".join(result["company"]) if result["company"] else "Not Found"
148
- result["date"] = result["date"][0] if result["date"] else "Not Found"
149
- result["total"] = result["total"][-1] if result["total"] else "Not Found"
150
-
151
- overall = (
152
- avg_conf(conf_store["company"]) +
153
- avg_conf(conf_store["date"]) +
154
- avg_conf(conf_store["total"])
155
- ) / 3
156
-
157
- result["confidence"] = round(overall, 3)
158
-
159
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
  except Exception as e:
162
  return {"error": str(e)}
163
 
164
  # =====================================================
165
- # DECISION LAYER
166
  # =====================================================
167
- def decision_layer(conf):
168
 
169
- if conf >= 0.80:
170
- return "AUTO_SEND"
171
 
172
- elif conf >= 0.60:
173
- return "REVIEW"
174
 
175
- else:
176
- return "REJECT"
177
 
178
- # =====================================================
179
- # IMAGE TO PDF BASE64
180
- # =====================================================
181
- def create_pdf_base64(image):
182
 
183
- with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f:
 
184
 
185
- pdf_path = f.name
 
 
 
 
 
186
 
187
- image = image.convert("RGB")
188
- image.save(pdf_path, "PDF")
 
189
 
190
- with open(pdf_path, "rb") as file:
191
- pdf_bytes = file.read()
192
 
193
- encoded = base64.b64encode(pdf_bytes).decode("utf-8")
 
194
 
195
- return encoded
196
 
197
  # =====================================================
198
- # SEND EMAIL WITH PDF ATTACHMENT
199
  # =====================================================
200
- def send_claim_email(to_email, extracted, image):
201
 
202
  if not RESEND_API_KEY:
203
- return "❌ Missing RESEND_API_KEY"
204
-
205
- pdf_base64 = create_pdf_base64(image)
206
 
207
- subject = "Insurance Claim Request"
 
208
 
209
- html_body = f"""
210
  <h2>Insurance Claim Request</h2>
211
-
212
- <p>Dear Claims Team,</p>
213
-
214
- <p>Please process claim reimbursement request.</p>
215
-
216
  <p><b>Provider:</b> {extracted['company']}</p>
217
- <p><b>Bill Date:</b> {extracted['date']}</p>
218
- <p><b>Claim Amount:</b> β‚Ή{extracted['total']}</p>
219
-
220
- <p>Attached: Receipt PDF</p>
221
-
222
- <p>Regards,<br>AI Claims System</p>
223
  """
224
 
225
  payload = {
226
  "from": FROM_EMAIL,
227
  "to": [to_email],
228
- "subject": subject,
229
- "html": html_body,
230
  "attachments": [
231
  {
232
- "filename": "receipt.pdf",
233
- "content": pdf_base64
234
  }
235
  ]
236
  }
237
 
 
 
 
 
 
238
  try:
239
- response = requests.post(
240
  "https://api.resend.com/emails",
241
- headers={
242
- "Authorization": f"Bearer {RESEND_API_KEY}",
243
- "Content-Type": "application/json"
244
- },
245
  json=payload,
246
- timeout=30
 
247
  )
248
 
249
- if response.status_code in [200, 201]:
250
- return f"βœ… Email + PDF sent to {to_email}"
251
 
252
- else:
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
- status = send_claim_email(email_id, extracted, image)
275
-
276
- elif decision == "REVIEW":
277
- status = f"⚠️ Human review needed (confidence={conf})"
278
-
279
- else:
280
- status = f"❌ Rejected (confidence={conf})"
281
 
282
  return extracted, status
283
 
@@ -285,20 +289,17 @@ def process_and_send(image, email_id):
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="Extracted Fields"),
297
  gr.Textbox(label="Email Status")
298
  ],
299
-
300
- title="πŸ“„ AI Insurance Claim Generator + PDF",
301
- description="Upload receipt β†’ Extract data β†’ Auto email with PDF attachment"
302
  )
303
 
304
  demo.launch()
 
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
  # =====================================================
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
 
26
+ label2id = {
27
+ "O": 0,
28
+ "COMPANY": 1,
29
+ "DATE": 2,
30
+ "TOTAL": 3
31
+ }
32
+
33
  id2label = {v: k for k, v in label2id.items()}
34
 
35
  # =====================================================
 
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,
 
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
 
 
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()