sunbal7 commited on
Commit
b91bf94
·
verified ·
1 Parent(s): 12c16d9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +288 -333
app.py CHANGED
@@ -1,354 +1,309 @@
 
1
  import os
2
  import io
3
- import time
4
  import json
 
5
  import base64
6
- from pathlib import Path
7
-
8
- import torch
9
- import torchvision
10
  import numpy as np
11
- from PIL import Image
12
  import streamlit as st
 
 
 
13
 
14
- # Explainability
15
- from pytorch_grad_cam import GradCAM
16
- from pytorch_grad_cam.utils.image import show_cam_on_image
17
 
18
- # X-ray tools
19
- import torchxrayvision as xrv
 
 
20
 
21
- # Text-to-speech
22
- from gtts import gTTS
23
 
24
- # Groq API
25
- import requests
26
- import json as json_module
 
 
 
 
 
 
 
27
 
28
- # Utilities
29
- st.set_page_config(page_title="Rural Diagnostic Assistant (X-ray)", layout="wide")
30
 
31
- # --------------------
32
- # CONFIG / USER KEYS
33
- # --------------------
34
- # Hugging Face Secrets for API keys
35
- GROQ_API_KEY = st.secrets.get("GROQ_API_KEY", os.environ.get("GROQ_API_KEY", None))
36
 
37
- # --------------------
38
- # Helper functions
39
- # --------------------
40
- @st.cache_resource
41
- def load_xray_model():
42
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
43
- st.info("Loading pretrained chest X-ray model (torchxrayvision)...")
44
- # DenseNet pretrained on CheXpert-like weights — quick inference
45
- model = xrv.models.DenseNet(weights="densenet121-res224-chex")
46
- model = model.to(device)
47
- model.eval()
48
- return model, device
49
-
50
- def preprocess_for_model(pil_img):
51
- # Convert to L and resize in a way consistent with torchxrayvision transforms
52
- img = pil_img.convert("L")
53
- # We make a 224x224 tensor similar to the model expectation
54
- transform = torchvision.transforms.Compose([
55
- xrv.datasets.XRayCenterCrop(),
56
- xrv.datasets.XRayResizer(224),
57
- torchvision.transforms.ToTensor()
58
- ])
59
- t = transform(img)
60
- return t.unsqueeze(0) # 1xCxHxW
61
-
62
- def run_inference(model, device, pil_img):
63
- x = preprocess_for_model(pil_img).to(device)
64
- with torch.no_grad():
65
- out = model(x) # raw logits
66
- probs = torch.sigmoid(out).cpu().numpy().squeeze()
67
- labels = model.pathologies
68
- results = list(zip(labels, probs.tolist()))
69
- # sort descending by prob
70
- results = sorted(results, key=lambda x: x[1], reverse=True)
71
- return results, x
72
-
73
- def make_gradcam_overlay(model, input_tensor, target_index=None, use_cuda=False):
74
  try:
75
- # Choose a reasonable target layer
76
- target_layer = model.features.denseblock4.denselayer16
77
- cam = GradCAM(model=model, target_layers=[target_layer], use_cuda=use_cuda)
78
- grayscale_cam = cam(input_tensor=input_tensor, targets=None)[0]
79
- # convert tensor to image for overlay
80
- img_arr = input_tensor.cpu().squeeze().numpy()
81
- if len(img_arr.shape) == 3:
82
- img_arr = img_arr[0] # take first channel if 3 channels
83
-
84
- rgb_img = np.stack([img_arr]*3, axis=2)
85
- rgb_img = (rgb_img - rgb_img.min()) / (rgb_img.max() - rgb_img.min() + 1e-8)
86
- overlay = show_cam_on_image(rgb_img, grayscale_cam, use_rgb=True, image_weight=0.5)
87
- overlay_pil = Image.fromarray(overlay)
88
- return overlay_pil
89
- except Exception as e:
90
- st.error(f"Grad-CAM error: {e}")
91
- return None
92
-
93
- def call_groq_api(prompt, max_tokens=1024):
94
- """Call Groq API for medical explanation"""
95
- if not GROQ_API_KEY:
96
- return "Groq API key not configured. Please add GROQ_API_KEY to secrets."
97
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  try:
99
- url = "https://api.groq.com/openai/v1/chat/completions"
100
- headers = {
101
- "Authorization": f"Bearer {GROQ_API_KEY}",
102
- "Content-Type": "application/json"
103
- }
104
-
105
- # Medical-focused prompt with Pakistan context
106
- system_prompt = """You are a medical assistant specialized in radiology, trained specifically for Pakistani patients.
107
- Provide clear, accurate explanations in both English and Urdu. Be culturally sensitive and use terminology
108
- appropriate for rural healthcare settings in Pakistan."""
109
-
110
- data = {
111
- "messages": [
112
- {"role": "system", "content": system_prompt},
113
- {"role": "user", "content": prompt}
114
- ],
115
- "model": "llama3-8b-8192", # Using a model available on Groq
116
- "temperature": 0.3,
117
- "max_tokens": max_tokens,
118
- "top_p": 0.9
119
- }
120
-
121
- response = requests.post(url, headers=headers, json=data, timeout=30)
122
- response.raise_for_status()
123
-
124
- result = response.json()
125
- return result["choices"][0]["message"]["content"]
126
-
127
- except requests.exceptions.RequestException as e:
128
- return f"API Error: {str(e)}"
129
  except Exception as e:
130
- return f"Error: {str(e)}"
131
-
132
- def generate_medical_explanation(model_results, user_question=""):
133
- """Generate medical explanation using Groq API"""
134
-
135
- # Prepare model findings for the prompt
136
- findings_text = "Model findings:\n"
137
- for i, (label, prob) in enumerate(model_results[:8]):
138
- findings_text += f"{i+1}. {label}: {prob*100:.2f}%\n"
139
-
140
- prompt = f"""
141
- As a medical assistant for rural Pakistan, analyze these X-ray findings and provide explanations in both English and Urdu.
142
-
143
- User's question: {user_question}
144
-
145
- {findings_text}
146
-
147
- Please provide:
148
- 1. English Explanation: Clear medical interpretation
149
- 2. Urdu Explanation: Same content in Urdu script
150
- 3. Recommendations: Next steps for patient care
151
-
152
- Focus on accuracy and cultural appropriateness for Pakistani rural population.
153
  """
154
-
155
- return call_groq_api(prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
- def tts_save(text, lang, filename):
158
- """Generate TTS audio file"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  try:
160
- # Clean text for TTS
161
- clean_text = text.replace('*', '').replace('#', '').replace('```', '')
162
-
163
- # Language mapping
164
- lang_map = {"en": "en", "ur": "ur"}
165
- tts_lang = lang_map.get(lang, "en")
166
-
167
- tts = gTTS(text=clean_text[:500], lang=tts_lang, slow=False)
168
- audio_path = f"/tmp/{filename}"
169
- tts.save(audio_path)
170
- return audio_path
171
  except Exception as e:
172
- st.error(f"TTS Error: {e}")
173
- return None
174
-
175
- # --------------------
176
- # STREAMLIT UI
177
- # --------------------
178
- st.title("🏥 Rural Diagnostic Assistant — X-ray Analysis")
179
- st.markdown("**Medical AI Tool for X-ray Analysis with English/Urdu Support**")
180
-
181
- # Initialize session state
182
- if 'model_loaded' not in st.session_state:
183
- st.session_state.model_loaded = False
184
- if 'results' not in st.session_state:
185
- st.session_state.results = None
186
- if 'image_processed' not in st.session_state:
187
- st.session_state.image_processed = None
188
-
189
- # Sidebar
190
- st.sidebar.header("Configuration")
191
- use_groq = st.sidebar.checkbox("Use Groq AI for enhanced explanations", value=True)
192
- show_gradcam = st.sidebar.checkbox("Show Grad-CAM visualization", value=True)
193
-
194
- # Main columns
195
- col1, col2 = st.columns([1, 1])
196
-
197
- with col1:
198
- st.subheader("Upload X-ray Image")
199
- uploaded_file = st.file_uploader("Choose chest X-ray image:",
200
- type=['png','jpg','jpeg'],
201
- help="Upload a chest X-ray image for analysis")
202
-
203
- if uploaded_file and not st.session_state.model_loaded:
204
- with st.spinner("Loading AI model..."):
205
- model, device = load_xray_model()
206
- st.session_state.model = model
207
- st.session_state.device = device
208
- st.session_state.model_loaded = True
209
-
210
- if uploaded_file:
211
- # Display uploaded image
212
- image = Image.open(uploaded_file).convert("RGB")
213
- st.image(image, caption="Uploaded X-ray Image", use_column_width=True)
214
-
215
- # Process image if not already processed
216
- if st.button("Analyze X-ray") or st.session_state.image_processed != uploaded_file.name:
217
- with st.spinner("Analyzing X-ray image..."):
218
- try:
219
- results, input_tensor = run_inference(st.session_state.model,
220
- st.session_state.device,
221
- image)
222
- st.session_state.results = results
223
- st.session_state.input_tensor = input_tensor
224
- st.session_state.image_processed = uploaded_file.name
225
- st.success("Analysis complete!")
226
- except Exception as e:
227
- st.error(f"Analysis failed: {e}")
228
-
229
- # Display results if available
230
- if st.session_state.results:
231
- st.subheader("📊 Detection Results")
232
-
233
- # Top findings
234
- st.markdown("**Top Findings:**")
235
- for label, prob in st.session_state.results[:6]:
236
- if prob > 0.1: # Only show findings with >10% probability
237
- progress_val = min(prob, 1.0)
238
- st.write(f"**{label}**: {prob*100:.1f}%")
239
- st.progress(float(progress_val))
240
-
241
- # Grad-CAM visualization
242
- if show_gradcam:
243
- st.subheader("🔍 AI Attention Map (Grad-CAM)")
244
- try:
245
- overlay = make_gradcam_overlay(
246
- st.session_state.model,
247
- st.session_state.input_tensor,
248
- use_cuda=torch.cuda.is_available()
249
- )
250
- if overlay:
251
- st.image(overlay, caption="AI Attention Areas", use_column_width=True)
252
- except Exception as e:
253
- st.warning(f"Could not generate Grad-CAM: {e}")
254
-
255
- # User question input
256
- st.subheader("💬 Ask about your X-ray")
257
- user_question = st.text_input(
258
- "Ask a specific question about the findings:",
259
- placeholder="e.g., What do these results mean? Should I see a doctor?"
260
- )
261
-
262
- # Generate explanation
263
- if st.button("Get Medical Explanation") or user_question:
264
- with st.spinner("Generating medical explanation..."):
265
- if use_groq and GROQ_API_KEY:
266
- explanation = generate_medical_explanation(
267
- st.session_state.results,
268
- user_question
269
- )
270
- else:
271
- # Fallback explanation
272
- explanation = """
273
- **English Explanation:**\n
274
- Based on the AI analysis, this X-ray shows various potential findings. Please consult with a healthcare professional for accurate diagnosis.\n\n
275
- **Urdu Explanation (اردو وضاحت):**\n
276
- AI تجزیے کے مطابق، اس ایکس رے میں مختلف ممکنہ نتائج ہیں۔ درست تشخیص کے لیے براہ کرم ہیلتھ کیئر پیشہ ور سے مشورہ کریں۔\n\n
277
- **Recommendations:**\n
278
- - Consult a radiologist for professional interpretation\n
279
- - Share these results with your doctor\n
280
- - Follow up with recommended tests if needed
281
- """
282
-
283
- st.subheader("📋 Medical Explanation")
284
- st.markdown(explanation)
285
-
286
- # Audio generation
287
- st.subheader("🔊 Audio Explanation")
288
- col_audio1, col_audio2 = st.columns(2)
289
-
290
- with col_audio1:
291
- if st.button("Generate English Audio"):
292
- with st.spinner("Generating English audio..."):
293
- audio_path = tts_save(explanation, "en", "explanation_en.mp3")
294
- if audio_path:
295
- st.audio(audio_path, format="audio/mp3")
296
-
297
- with col_audio2:
298
- if st.button("Generate Urdu Audio"):
299
- with st.spinner("Generating Urdu audio..."):
300
- audio_path = tts_save(explanation, "ur", "explanation_ur.mp3")
301
- if audio_path:
302
- st.audio(audio_path, format="audio/mp3")
303
-
304
- with col2:
305
- st.subheader("ℹ️ About This Tool")
306
-
307
- st.markdown("""
308
- ### How to Use:
309
- 1. Upload a chest X-ray image (PNG/JPG)
310
- 2. Click 'Analyze X-ray' to process the image
311
- 3. Review the AI findings and probabilities
312
- 4. Ask specific questions about your results
313
- 5. Get explanations in English and Urdu
314
-
315
- ### Features:
316
- - 🏥 **Medical AI Analysis**: Uses torchxrayvision pretrained model
317
- - 🔍 **Visual Explanations**: Grad-CAM heatmaps show AI focus areas
318
- - 🌐 **Bilingual Support**: English and Urdu explanations
319
- - 🔊 **Audio Output**: Text-to-speech in both languages
320
- - 🎯 **Pakistan-Tuned**: Culturally appropriate for Pakistani patients
321
-
322
- ### Important Notes:
323
- - ⚠️ **This is a demonstration tool only**
324
- - ⚠️ **Not for clinical use or diagnosis**
325
- - ⚠️ **Always consult qualified healthcare professionals**
326
- - ⚠️ **Results should be verified by radiologists**
327
- """)
328
-
329
- # Technical details expander
330
- with st.expander("Technical Details"):
331
- st.markdown("""
332
- **AI Model:** torchxrayvision DenseNet-121
333
- **Training Data:** CheXpert, NIH Chest X-ray
334
- **Supported Findings:** 14 common chest conditions
335
- **Inference Framework:** PyTorch
336
- **Explanation Method:** Grad-CAM
337
- """)
338
-
339
- if st.session_state.results:
340
- st.markdown("**Raw Results (JSON):**")
341
- st.json({k: float(v) for k, v in dict(st.session_state.results).items()})
342
-
343
- # Footer
344
  st.markdown("---")
345
- st.markdown(
346
- """
347
- <div style='text-align: center; color: gray;'>
348
- <p>🚨 <strong>Disclaimer:</strong> This tool is for educational and demonstration purposes only.
349
- It is NOT a substitute for professional medical advice, diagnosis, or treatment.</p>
350
- <p>Always seek the advice of qualified healthcare providers with any medical questions.</p>
351
- </div>
352
- """,
353
- unsafe_allow_html=True
354
- )
 
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.")