sunbal7 commited on
Commit
d6f9447
·
verified ·
1 Parent(s): adbb6e6

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -309
app.py DELETED
@@ -1,309 +0,0 @@
1
- # app.py
2
- import os
3
- import io
4
- import json
5
- import tempfile
6
- import base64
7
- import requests
8
- from PIL import Image, ImageChops, ImageOps, ExifTags
9
- import numpy as np
10
- import streamlit as st
11
- import cv2
12
- import easyocr
13
- import imagehash
14
-
15
- st.set_page_config(page_title="DocVerify - Prototype", layout="wide")
16
-
17
- # --- Config / Env ---
18
- GROQ_API_KEY = os.environ.get("GROQ_API_KEY") # REQUIRED
19
- GROQ_API_BASE = os.environ.get("GROQ_API_BASE", "https://api.groq.com/openai/v1") # default pattern (OpenAI-compatible)
20
- GROQ_MODEL = os.environ.get("GROQ_MODEL", "gpt-4o-mini") # change if your Groq model differs
21
-
22
- if not GROQ_API_KEY:
23
- st.warning("Set the GROQ_API_KEY environment variable before running (see README).")
24
-
25
- # Initialize OCR
26
- @st.cache_resource
27
- def get_ocr_reader(lang_list=["en","ur"]):
28
- # easyocr supports many languages; using english + urdu as default
29
- try:
30
- reader = easyocr.Reader(lang_list, gpu=False)
31
- except Exception as e:
32
- # fallback to english only
33
- reader = easyocr.Reader(["en"], gpu=False)
34
- return reader
35
-
36
- reader = get_ocr_reader()
37
-
38
- # ---------- Utility functions ----------
39
- def load_image(file):
40
- image = Image.open(file).convert("RGB")
41
- return image
42
-
43
- def pdf_to_images(file_bytes):
44
- # lightweight: use pdf2image if available, else ask user to upload images
45
- try:
46
- from pdf2image import convert_from_bytes
47
- images = convert_from_bytes(file_bytes)
48
- # convert to RGB PIL images
49
- return [img.convert("RGB") for img in images]
50
- except Exception:
51
- return []
52
-
53
- def image_to_cv2(img_pil):
54
- return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
55
-
56
- def compute_ela(img_pil, quality=90):
57
- """
58
- Error Level Analysis: save at lower quality and compute difference.
59
- Returns an image (PIL) and a scalar anomaly score (mean difference).
60
- """
61
- temp = io.BytesIO()
62
- img_pil.save(temp, format="JPEG", quality=quality)
63
- temp.seek(0)
64
- compressed = Image.open(temp).convert("RGB")
65
- diff = ImageChops.difference(img_pil, compressed)
66
- # amplify for visibility
67
- extrema = diff.getextrema()
68
- # numeric anomaly score
69
- diff_np = np.array(diff).astype(np.float32)
70
- score = float(diff_np.mean())
71
- # return difference image and score
72
- return diff, score
73
-
74
- def read_exif_info(img_pil):
75
- try:
76
- exif = img_pil._getexif()
77
- if not exif:
78
- return {}
79
- human = {}
80
- for tag, val in exif.items():
81
- decoded = ExifTags.TAGS.get(tag, tag)
82
- human[decoded] = val
83
- return human
84
- except Exception:
85
- return {}
86
-
87
- def ocr_image(img_pil):
88
- # returns list of results: [(bbox, text, confidence), ...]
89
- try:
90
- res = reader.readtext(np.array(img_pil))
91
- except Exception as e:
92
- # fallback: empty
93
- res = []
94
- extracted_text = "\n".join([r[1] for r in res])
95
- return res, extracted_text
96
-
97
- def signature_similarity(img_sig_pil, img_ref_pil):
98
- # compute perceptual hash difference (average_hash)
99
- try:
100
- h1 = imagehash.average_hash(img_sig_pil.convert("L").resize((300,100)))
101
- h2 = imagehash.average_hash(img_ref_pil.convert("L").resize((300,100)))
102
- dist = h1 - h2
103
- # transform to similarity score in [0,1]
104
- score = max(0.0, 1.0 - (dist / 20.0))
105
- return float(score), int(dist)
106
- except Exception:
107
- return None, None
108
-
109
- def call_groq_llm(prompt_text: str, model=GROQ_MODEL, base_url=GROQ_API_BASE, api_key=GROQ_API_KEY):
110
- """
111
- Calls a Groq OpenAI-compatible endpoint. Payload is minimal: model + input.
112
- Response parsing is tolerant of a few shapes.
113
- """
114
- if not api_key:
115
- raise ValueError("GROQ_API_KEY not provided")
116
- url = base_url.rstrip("/") + "/responses"
117
- headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
118
- payload = {"model": model, "input": prompt_text, "max_output_tokens": 512}
119
- # If the Groq endpoint you run differs, adjust base_url/model.
120
- r = requests.post(url, headers=headers, data=json.dumps(payload), timeout=60)
121
- r.raise_for_status()
122
- j = r.json()
123
- # Try a few common return shapes
124
- if "output_text" in j:
125
- return j["output_text"]
126
- # newer responses API: look into output -> [ { "content": [{"type":"output_text","text":"..."}]} ]
127
- try:
128
- out = j.get("output", [])
129
- if out and isinstance(out, list):
130
- c = out[0].get("content", [])
131
- for item in c:
132
- if item.get("type") == "output_text" and "text" in item:
133
- return item["text"]
134
- # fallback: string-join text fields
135
- texts = []
136
- for item in c:
137
- if "text" in item:
138
- texts.append(item["text"])
139
- if texts:
140
- return "\n".join(texts)
141
- except Exception:
142
- pass
143
- # final fallback: return pretty json
144
- return json.dumps(j, indent=2)
145
-
146
- # ---------- Streamlit UI ----------
147
- st.title("DocVerify — Prototype (OCR + ELA + Groq LLM)")
148
-
149
- with st.sidebar:
150
- st.header("Upload options")
151
- uploaded = st.file_uploader("Upload document (image or PDF)", type=["png","jpg","jpeg","pdf"], accept_multiple_files=False)
152
- ref_sig = st.file_uploader("(Optional) Reference signature image for comparison", type=["png","jpg","jpeg"])
153
- st.markdown("---")
154
- st.write("Settings:")
155
- st.slider("ELA quality (lower -> more difference shown)", 50, 98, 90, key="ela_q")
156
- st.checkbox("Show raw OCR result", value=True, key="show_ocr")
157
- st.checkbox("Run Groq LLM analysis (requires GROQ_API_KEY)", value=True, key="use_groq")
158
- st.markdown("---")
159
- st.info("This is a prototype. Do not rely on it as legal evidence. See README for details.")
160
-
161
- if not uploaded:
162
- st.info("Upload a document image or PDF to begin.")
163
- st.stop()
164
-
165
- # handle uploaded file
166
- file_bytes = uploaded.read()
167
- file_type = uploaded.type
168
- images = []
169
-
170
- if uploaded.type == "application/pdf" or uploaded.name.lower().endswith(".pdf"):
171
- imgs = pdf_to_images(file_bytes)
172
- if not imgs:
173
- st.error("PDF processing requires pdf2image; if unavailable, upload images instead.")
174
- st.stop()
175
- images = imgs
176
- else:
177
- images = [load_image(io.BytesIO(file_bytes))]
178
-
179
- # show first page
180
- page_idx = st.number_input("Page index", min_value=0, max_value=len(images)-1, value=0, step=1)
181
- img = images[page_idx]
182
- st.subheader("Document preview (page %d)" % page_idx)
183
- st.image(img, use_column_width=True)
184
-
185
- # EXIF
186
- exif = read_exif_info(img)
187
- if exif:
188
- st.write("Detected metadata (EXIF):", exif)
189
- else:
190
- st.write("No EXIF metadata detected.")
191
-
192
- # OCR
193
- with st.spinner("Running OCR..."):
194
- ocr_results, extracted_text = ocr_image(img)
195
- if st.session_state.show_ocr:
196
- st.subheader("OCR extracted text")
197
- st.text_area("Extracted text (raw)", value=extracted_text, height=200)
198
-
199
- # ELA
200
- with st.spinner("Running ELA..."):
201
- ela_img, ela_score = compute_ela(img, quality=st.session_state.ela_q)
202
- st.subheader("Error Level Analysis (ELA)")
203
- st.write(f"ELA mean diff score: {ela_score:.3f} (higher usually => more manipulated)")
204
- buf = io.BytesIO()
205
- ela_img.save(buf, format="PNG")
206
- st.image(buf.getvalue(), caption="ELA difference image — bright regions may indicate changes", use_column_width=True)
207
-
208
- # Signature similarity (if user provided)
209
- sig_score = None
210
- sig_dist = None
211
- if ref_sig:
212
- ref_img = load_image(ref_sig)
213
- # attempt to auto-crop signature region by heuristics: find largest dark connected component near bottom-right
214
- # For prototype, allow user to crop manually by simple resize
215
- st.subheader("Signature comparison (user-supplied reference)")
216
- st.write("Reference signature (uploaded):")
217
- st.image(ref_img, width=200)
218
- # let user optionally crop region from document for comparison
219
- st.write("Crop the signature region from the document preview for comparison.")
220
- col1, col2 = st.columns(2)
221
- with col1:
222
- st.write("Manual signature crop (enter bounding box in pixels):")
223
- x = st.number_input("x", min_value=0, max_value=img.width-1, value=int(img.width*0.6))
224
- y = st.number_input("y", min_value=0, max_value=img.height-1, value=int(img.height*0.7))
225
- w = st.number_input("w", min_value=10, max_value=img.width, value=int(img.width*0.35))
226
- h = st.number_input("h", min_value=10, max_value=img.height, value=int(img.height*0.15))
227
- with col2:
228
- crop_btn = st.button("Crop & Compare")
229
- if crop_btn:
230
- x2 = min(img.width, x + w)
231
- y2 = min(img.height, y + h)
232
- doc_sig = img.crop((x, y, x2, y2))
233
- st.image(doc_sig, caption="Cropped signature from document", width=300)
234
- sig_score, sig_dist = signature_similarity(doc_sig, ref_img)
235
- if sig_score is not None:
236
- st.write(f"Signature similarity score: {sig_score:.3f} (higher = more similar). Hash distance: {sig_dist}")
237
- else:
238
- st.write("Could not compute signature similarity.")
239
-
240
- # Simple heuristics summary
241
- heuristics = []
242
- heuristics.append({"name":"ela_score","value":ela_score,"interpretation":"higher may indicate manipulated areas"})
243
- if exif:
244
- heuristics.append({"name":"has_exif","value":True})
245
- else:
246
- heuristics.append({"name":"has_exif","value":False})
247
- if sig_score is not None:
248
- heuristics.append({"name":"signature_similarity","value":sig_score})
249
-
250
- st.subheader("Heuristic summary")
251
- st.json(heuristics)
252
-
253
- # Build evidence package
254
- evidence = {
255
- "file_name": uploaded.name,
256
- "page_index": page_idx,
257
- "ocr_text_snippet": extracted_text[:2000],
258
- "ocr_full_text": extracted_text,
259
- "ela_score": ela_score,
260
- "exif": exif,
261
- "signature_similarity": sig_score,
262
- "notes": []
263
- }
264
-
265
- # Add basic field extractions from OCR (naive searching for CNIC pattern)
266
- import re
267
- cnic_match = re.search(r"\d{5}-\d{7}-\d", extracted_text)
268
- if cnic_match:
269
- evidence["detected_cnic"] = cnic_match.group(0)
270
- evidence["notes"].append("Found CNIC-like pattern")
271
- else:
272
- evidence["notes"].append("No CNIC-like pattern found")
273
-
274
- # Prepare prompt for LLM
275
- prompt = f"""
276
- You are a document verification assistant. I will give you a JSON 'evidence' object with results from OCR, ELA, EXIF, signature comparison, and heuristics.
277
- Produce:
278
- 1) Short verdict (one sentence) with confidence (low/medium/high).
279
- 2) Bullet list of concrete findings (2-6 bullets).
280
- 3) Suggested next steps for verification (3-5 actionable things).
281
- 4) Caution / legal note to show the user.
282
-
283
- Evidence JSON:
284
- {json.dumps(evidence, indent=2)}
285
- """
286
-
287
- st.subheader("LLM Analysis / Report")
288
- if st.session_state.use_groq:
289
- try:
290
- with st.spinner("Calling Groq LLM for analysis..."):
291
- llm_out = call_groq_llm(prompt)
292
- st.text_area("LLM report", value=llm_out, height=320)
293
- except Exception as e:
294
- st.error(f"Error calling Groq LLM: {e}\nMake sure GROQ_API_KEY and GROQ_API_BASE are set and endpoint is reachable.")
295
- else:
296
- st.info("Groq LLM analysis disabled. Enable 'Run Groq LLM analysis' in sidebar to call the model.")
297
-
298
- # Audit / download
299
- st.subheader("Export evidence")
300
- if st.button("Download evidence JSON"):
301
- b = io.BytesIO()
302
- b.write(json.dumps(evidence, indent=2).encode("utf-8"))
303
- b.seek(0)
304
- b64 = base64.b64encode(b.read()).decode()
305
- href = f'<a href="data:application/json;base64,{b64}" download="evidence_{uploaded.name}.json">Download evidence JSON</a>'
306
- st.markdown(href, unsafe_allow_html=True)
307
-
308
- st.markdown("---")
309
- st.markdown("**Notes:** This prototype provides *indications* — not legally certified results. For high-stakes verification, involve certified forensic/document examiners and official government APIs.")