ngupta2026 commited on
Commit
8be9c6a
Β·
verified Β·
1 Parent(s): 95e66f9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +202 -112
app.py CHANGED
@@ -1,187 +1,262 @@
 
 
1
  from PIL import Image
2
  import torch
3
  import re
 
4
  import os
5
- import smtplib
6
-
7
- from email.mime.text import MIMEText
8
- from email.mime.multipart import MIMEMultipart
9
 
10
  from transformers import LayoutLMTokenizerFast, LayoutLMForTokenClassification
11
 
12
  # =====================================================
13
- # LABELS
14
  # =====================================================
15
- label2id = {"O": 0, "COMPANY": 1, "DATE": 2, "TOTAL": 3}
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  id2label = {v: k for k, v in label2id.items()}
17
 
18
  # =====================================================
19
  # LOAD MODEL
20
  # =====================================================
21
- MODEL_NAME = "ngupta2026/sroie-layoutlm"
22
 
23
  model = LayoutLMForTokenClassification.from_pretrained(MODEL_NAME)
24
  tokenizer = LayoutLMTokenizerFast.from_pretrained(MODEL_NAME)
 
25
  model.to(device)
26
  model.eval()
27
 
28
  # =====================================================
29
- # EMAIL CONFIG
30
- # Add these in Hugging Face Space Secrets:
31
- # EMAIL_USER = yourgmail@gmail.com
32
- # EMAIL_PASS = your_app_password
33
- # =====================================================
34
- EMAIL_USER = os.getenv("EMAIL_USER")
35
- EMAIL_PASS = os.getenv("EMAIL_PASS")
36
-
37
- # =====================================================
38
- # NORMALIZE BOXES
39
  # =====================================================
40
  def normalize(box, width, height):
41
  return [
42
  int(1000 * box[0] / width),
 
 
43
  int(1000 * box[3] / height),
44
  ]
45
 
46
  # =====================================================
47
- # EXTRACT DATA
 
 
 
 
 
 
 
 
48
  # =====================================================
49
  def extract_receipt(image):
50
 
51
- data = pytesseract.image_to_data(
52
- image,
53
- output_type=pytesseract.Output.DICT
54
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- words = []
57
- boxes = []
 
 
58
 
59
- for i in range(len(data["text"])):
 
60
 
61
- text = data["text"][i].strip()
 
62
 
63
- if text != "":
64
- h = data["height"][i]
65
 
66
- words.append(text)
67
- boxes.append([x, y, x + w, y + h])
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- if len(words) == 0:
70
- return {"error": "No text detected"}
71
 
 
 
72
 
73
- width, height = image.size
74
- boxes = [normalize(box, width, height) for box in boxes]
 
 
 
75
 
 
 
 
 
 
76
 
77
- encoding = tokenizer(
78
- words,
79
- boxes=boxes,
80
- max_length=512
81
- )
82
 
83
- encoding = {k: v.to(device) for k, v in encoding.items()}
 
84
 
 
 
 
 
85
 
86
- with torch.no_grad():
87
- outputs = model(**encoding)
 
 
88
 
89
- predictions = torch.argmax(outputs.logits, dim=2)[0][:len(words)]
 
90
 
91
- result = {
92
- "company": [],
93
- "date": [],
94
- "total": []
95
- }
96
 
97
- for word, pred in zip(words, predictions):
 
 
 
98
 
99
- label = id2label[pred.item()]
 
100
 
101
- # company from model
102
- if label == "COMPANY":
103
- result["company"].append(word)
104
 
105
- # date from regex
106
- if re.search(r"\d{2}[/-]\d{2}[/-]\d{2,4}", word):
107
- result["date"].append(word)
 
108
 
109
- # total from regex
110
- if re.search(r"\d+(\.\d{2})?", word):
111
- try:
112
- value = float(word.replace(",", ""))
113
- if value > 50:
114
- result["total"].append(word)
115
- except:
116
- pass
117
 
118
- result["company"] = (
119
- " ".join(result["company"])
120
- if result["company"] else "Not Found"
121
- )
122
 
123
- result["date"] = (
124
- result["date"][0]
125
- if result["date"] else "Not Found"
126
- )
127
 
128
- result["total"] = (
129
- result["total"][-1]
130
- if result["total"] else "Not Found"
131
- )
132
 
133
- return result
 
 
 
 
 
 
 
 
134
 
135
  # =====================================================
136
- # SEND EMAIL
137
  # =====================================================
138
- def send_claim_email(to_email, extracted):
139
 
140
- if not EMAIL_USER or not EMAIL_PASS:
141
- return "Email secrets not configured."
142
 
143
- subject = "Insurance Claim Request"
 
144
 
145
- body = f"""
146
- Dear Claims Team,
147
 
148
- I would like to request reimbursement for an eligible expense.
 
 
 
149
 
150
- Provider Name: {extracted['company']}
151
- Bill Date: {extracted['date']}
152
- Claim Amount: β‚Ή{extracted['total']}
153
 
154
- Please process the claim.
155
 
156
- Regards
157
- Customer
158
- """
159
 
160
- msg = MIMEMultipart()
161
- msg["From"] = EMAIL_USER
162
- msg["To"] = to_email
163
- msg["Subject"] = subject
164
 
165
- msg.attach(MIMEText(body, "plain"))
 
 
 
 
 
 
 
166
 
167
  try:
168
- server = smtplib.SMTP("smtp.gmail.com", 587)
169
- server.starttls()
170
- server.login(EMAIL_USER, EMAIL_PASS)
171
- server.sendmail(
172
- EMAIL_USER,
173
- to_email,
174
- msg.as_string()
 
 
 
 
 
 
175
  )
176
- server.quit()
177
 
178
- return f"βœ… Email sent successfully to {to_email}"
 
 
 
179
 
180
  except Exception as e:
181
- return f"❌ Email failed: {str(e)}"
182
 
183
  # =====================================================
184
- # MAIN UI FUNCTION
185
  # =====================================================
186
  def process_and_send(image, email_id):
187
 
@@ -190,7 +265,19 @@ def process_and_send(image, email_id):
190
  if "error" in extracted:
191
  return extracted, extracted["error"]
192
 
193
- email_status = send_claim_email(email_id, extracted)
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
  return extracted, email_status
196
 
@@ -199,16 +286,19 @@ def process_and_send(image, email_id):
199
  # =====================================================
200
  demo = gr.Interface(
201
  fn=process_and_send,
 
202
  inputs=[
203
  gr.Image(type="pil", label="Upload Receipt"),
204
- gr.Textbox(label="Insurance Email ID")
205
  ],
 
206
  outputs=[
207
- gr.JSON(label="Extracted Data"),
208
  gr.Textbox(label="Email Status")
209
  ],
 
210
  title="πŸ“„ AI Insurance Claim Generator",
211
- description="Upload receipt β†’ Extract details β†’ Auto send claim email"
212
  )
213
 
214
  demo.launch()
 
1
+ import gradio as gr
2
+ import pytesseract
3
  from PIL import Image
4
  import torch
5
  import re
6
+ import requests
7
  import os
 
 
 
 
8
 
9
  from transformers import LayoutLMTokenizerFast, LayoutLMForTokenClassification
10
 
11
  # =====================================================
12
+ # CONFIG
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
+
21
+ label2id = {
22
+ "O": 0,
23
+ "COMPANY": 1,
24
+ "DATE": 2,
25
+ "TOTAL": 3
26
+ }
27
+
28
  id2label = {v: k for k, v in label2id.items()}
29
 
30
  # =====================================================
31
  # LOAD MODEL
32
  # =====================================================
33
+ device = "cuda" if torch.cuda.is_available() else "cpu"
34
 
35
  model = LayoutLMForTokenClassification.from_pretrained(MODEL_NAME)
36
  tokenizer = LayoutLMTokenizerFast.from_pretrained(MODEL_NAME)
37
+
38
  model.to(device)
39
  model.eval()
40
 
41
  # =====================================================
42
+ # NORMALIZE BOX
 
 
 
 
 
 
 
 
 
43
  # =====================================================
44
  def normalize(box, width, height):
45
  return [
46
  int(1000 * box[0] / width),
47
+ int(1000 * box[1] / height),
48
+ int(1000 * box[2] / width),
49
  int(1000 * box[3] / height),
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 = []
76
+ boxes = []
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
 
 
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
 
 
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()