GGUF
conversational
Sanjay1905 commited on
Commit
bba66c7
·
verified ·
1 Parent(s): 8344be8

Delete gradiosite.py

Browse files
Files changed (1) hide show
  1. gradiosite.py +0 -910
gradiosite.py DELETED
@@ -1,910 +0,0 @@
1
- import os
2
- import cv2
3
- import numpy as np
4
- import gradio as gr
5
- from ultralytics import YOLO
6
- from pdf2image import convert_from_path
7
- from PIL import Image
8
- import easyocr
9
- import uuid
10
- import re
11
- import difflib
12
- import math
13
-
14
- # Conditional import for LLM
15
- try:
16
- from llama_cpp import Llama
17
- LLAMA_AVAILABLE = True
18
- except ImportError:
19
- print("Warning: llama_cpp not available. LLM functionality will be disabled.")
20
- LLAMA_AVAILABLE = False
21
-
22
- # --- Configuration ---
23
- # Folders for temporary files and results
24
- UPLOAD_FOLDER = 'static/uploads/'
25
- RESULTS_FOLDER = 'static/results/'
26
-
27
- # --- Model Paths (Update these paths if necessary) ---
28
- CUSTOM_MODEL_PATH = 'best.pt'
29
- PRETRAINED_MODEL_PATH = 'yolov10s.pt'
30
- SIGNATURE_MODEL_PATH = 'yolov8s.pt'
31
- LLAMA_MODEL_PATH = "unsloth.F16.gguf"
32
-
33
- # Detection Parameters
34
- YOLO_CONFIDENCE_THRESHOLD = 0.5
35
- OCR_CONFIDENCE_THRESHOLD = 0.5
36
-
37
- # Create directories if they don't exist
38
- os.makedirs(UPLOAD_FOLDER, exist_ok=True)
39
- os.makedirs(RESULTS_FOLDER, exist_ok=True)
40
-
41
- # --- Global Model Placeholders ---
42
- custom_model, pretrained_model, signature_model, reader, llama_model = None, None, None, None, None
43
-
44
- def load_models():
45
- """
46
- Loads all AI models into the global scope. This function is called on the first
47
- analysis request to avoid startup conflicts. It ensures models are only loaded once.
48
- """
49
- global custom_model, pretrained_model, signature_model, reader, llama_model
50
-
51
- # If models are already loaded, do nothing.
52
- if reader is not None and (llama_model is not None or not LLAMA_AVAILABLE):
53
- print("Models already loaded.")
54
- return
55
- print("=== Loading Models (this may take a moment) ===")
56
-
57
- # Helper function to check for model files
58
- def check_model_path(path, name):
59
- if not os.path.exists(path):
60
- print(f"✗ WARNING: {name} model not found at '{path}'. The application may not function correctly.")
61
- return False
62
- return True
63
-
64
- # YOLO Models
65
- if check_model_path(CUSTOM_MODEL_PATH, "Custom YOLO"):
66
- try:
67
- custom_model = YOLO(CUSTOM_MODEL_PATH)
68
- print("✓ Custom YOLO model loaded.")
69
- except Exception as e:
70
- print(f"✗ Error loading custom model: {e}")
71
- if check_model_path(PRETRAINED_MODEL_PATH, "Pre-trained YOLO"):
72
- try:
73
- pretrained_model = YOLO(PRETRAINED_MODEL_PATH)
74
- print("✓ Pre-trained YOLO model loaded.")
75
- except Exception as e:
76
- print(f"✗ Error loading pre-trained model: {e}")
77
- if check_model_path(SIGNATURE_MODEL_PATH, "Signature YOLO"):
78
- try:
79
- signature_model = YOLO(SIGNATURE_MODEL_PATH)
80
- print("✓ Signature YOLO model loaded.")
81
- except Exception as e:
82
- print(f"✗ Error loading signature model: {e}")
83
-
84
- # OCR Model
85
- try:
86
- reader = easyocr.Reader(['en'], gpu=True)
87
- print("✓ EasyOCR model loaded.")
88
- except Exception as e:
89
- print(f"✗ Error loading EasyOCR: {e}. Text detection will be unavailable.")
90
-
91
- # LLM Model - Only load if available
92
- if LLAMA_AVAILABLE and check_model_path(LLAMA_MODEL_PATH, "LLM"):
93
- try:
94
- llama_model = Llama(
95
- model_path=LLAMA_MODEL_PATH,
96
- n_gpu_layers=-1, n_ctx=4096, chat_format="llama-3", verbose=False
97
- )
98
- print("✓ LLM model loaded.")
99
- except Exception as e:
100
- print(f"✗ Error loading LLM model: {e}. Text analysis will be unavailable.")
101
- print("=== All Models Initialized ===")
102
-
103
- # YOLO Class Mappings
104
- CUSTOM_CLASS_NAMES = {0: 'face', 1: 'qr', 2: 'signature'}
105
- PRETRAINED_CLASS_MAP = {0: 'face'}
106
-
107
- # --- Core Detection & Processing Functions ---
108
- def detect_visual_pii(image_data):
109
- """Runs the three-stage YOLO detection on a single image."""
110
- all_boxes = []
111
- all_classes = []
112
-
113
- if custom_model is None:
114
- print("Custom model not available for visual detection")
115
- return all_boxes, all_classes
116
-
117
- # Pass 1: Custom Model
118
- custom_results = custom_model.predict(source=image_data, conf=YOLO_CONFIDENCE_THRESHOLD, verbose=False)[0]
119
- detected_custom_classes = {CUSTOM_CLASS_NAMES[int(cls)] for cls in custom_results.boxes.cls}
120
-
121
- for box, cls in zip(custom_results.boxes.xyxy.cpu().numpy().astype(int), custom_results.boxes.cls):
122
- all_boxes.append(box)
123
- all_classes.append(CUSTOM_CLASS_NAMES[int(cls)])
124
-
125
- # Pass 2: Pre-trained Model (Face Fallback)
126
- if 'face' not in detected_custom_classes and pretrained_model is not None:
127
- print(" Custom model missed 'face'. Trying pre-trained model as fallback.")
128
- pretrained_results = pretrained_model.predict(source=image_data, conf=YOLO_CONFIDENCE_THRESHOLD, verbose=False)[0]
129
- for box in pretrained_results.boxes:
130
- if int(box.cls[0]) in PRETRAINED_CLASS_MAP:
131
- all_boxes.append(box.xyxy.cpu().numpy().astype(int)[0])
132
- all_classes.append("face (fallback)")
133
-
134
- # Pass 3: Specialized Model (Signature Fallback)
135
- if 'signature' not in detected_custom_classes and signature_model is not None:
136
- print(" Custom model missed 'signature'. Trying specialized signature model as fallback.")
137
- signature_results = signature_model.predict(source=image_data, conf=YOLO_CONFIDENCE_THRESHOLD, verbose=False)[0]
138
- for box in signature_results.boxes:
139
- all_boxes.append(box.xyxy.cpu().numpy().astype(int)[0])
140
- all_classes.append("signature (fallback)")
141
-
142
- return all_boxes, all_classes
143
-
144
- # --- OCR + LLM Functions ---
145
- def calculate_distance(bbox1, bbox2):
146
- """Calculates the Euclidean distance between the centers of two bounding boxes."""
147
- c1_x = (bbox1[0] + bbox1[2]) / 2
148
- c1_y = (bbox1[1] + bbox1[3]) / 2
149
- c2_x = (bbox2[0] + bbox2[2]) / 2
150
- c2_y = (bbox2[1] + bbox2[3]) / 2
151
- return math.sqrt((c2_x - c1_x)**2 + (c2_y - c1_y)**2)
152
-
153
- def refine_pii_flags(ocr_results, isolation_threshold=150):
154
- """Post-processing step to unmark short, isolated PII detections."""
155
- pii_indices = [i for i, result in enumerate(ocr_results) if result["is_pii"]]
156
-
157
- if len(pii_indices) <= 1:
158
- return ocr_results
159
- indices_to_unmark = []
160
- for i in pii_indices:
161
- current_result = ocr_results[i]
162
- normalized_text = re.sub(r'[^a-zA-Z0-9]', '', current_result["text"])
163
-
164
- if len(normalized_text) <= 3:
165
- min_dist_to_neighbor = float('inf')
166
-
167
- for j in pii_indices:
168
- if i == j:
169
- continue
170
-
171
- other_result = ocr_results[j]
172
- dist = calculate_distance(current_result["bbox"], other_result["bbox"])
173
- if dist < min_dist_to_neighbor:
174
- min_dist_to_neighbor = dist
175
-
176
- if min_dist_to_neighbor > isolation_threshold:
177
- print(f" - Refining PII: Unmarking short ('{current_result['text']}') and isolated (min_dist: {min_dist_to_neighbor:.2f}px) PII.")
178
- indices_to_unmark.append(i)
179
-
180
- for i in indices_to_unmark:
181
- ocr_results[i]["is_pii"] = False
182
-
183
- return ocr_results
184
-
185
- def parse_pii_output(generated_text):
186
- """Parse the new curly braces format PII output"""
187
- pii_list = []
188
- try:
189
- match = re.search(r'\{([^}]*)\}', generated_text)
190
- if match:
191
- content = match.group(1)
192
- items = re.findall(r'"([^"]*)"', content)
193
- pii_list = [item.strip() for item in items if item.strip()]
194
- except Exception as e:
195
- print(f"Error parsing PII output: {e}")
196
- pii_list = []
197
- return pii_list
198
-
199
- def normalize_text(text):
200
- """Comprehensive text normalization for better matching"""
201
- if not text:
202
- return ""
203
-
204
- normalized = re.sub(r'[.,;:!?()"\'\-_/\\]', '', text)
205
-
206
- ocr_corrections = {
207
- '0': 'o', 'O': '0', '1': 'l', 'l': '1', '5': 's', 'S': '5',
208
- '8': 'b', 'B': '8', 'rn': 'm', 'RN': 'M', 'vv': 'w', 'VV': 'W',
209
- 'cl': 'd', 'CL': 'D',
210
- }
211
-
212
- for wrong, correct in ocr_corrections.items():
213
- normalized = normalized.replace(wrong, correct)
214
-
215
- normalized = ' '.join(normalized.split()).lower()
216
- return normalized
217
-
218
- def fuzzy_match_score(text1, text2, threshold=0.8):
219
- """Calculate fuzzy matching score between two strings"""
220
- if not text1 or not text2:
221
- return False
222
- return difflib.SequenceMatcher(None, text1.lower(), text2.lower()).ratio() >= threshold
223
-
224
- def levenshtein_distance(s1, s2):
225
- """Calculate Levenshtein distance between two strings"""
226
- if len(s1) < len(s2):
227
- return levenshtein_distance(s2, s1)
228
-
229
- if len(s2) == 0:
230
- return len(s1)
231
-
232
- previous_row = list(range(len(s2) + 1))
233
- for i, c1 in enumerate(s1):
234
- current_row = [i + 1]
235
- for j, c2 in enumerate(s2):
236
- insertions = previous_row[j + 1] + 1
237
- deletions = current_row[j] + 1
238
- substitutions = previous_row[j] + (c1 != c2)
239
- current_row.append(min(insertions, deletions, substitutions))
240
- previous_row = current_row
241
-
242
- return previous_row[-1]
243
-
244
- def is_similar_by_edit_distance(text1, text2, max_distance=2):
245
- """Check if two texts are similar within edit distance threshold"""
246
- if not text1 or not text2:
247
- return False
248
-
249
- distance = levenshtein_distance(text1.lower(), text2.lower())
250
- max_len = max(len(text1), len(text2))
251
-
252
- if max_len <= 3:
253
- threshold = 1
254
- elif max_len <= 6:
255
- threshold = 2
256
- else:
257
- threshold = min(max_distance, max_len // 3)
258
-
259
- return distance <= threshold
260
-
261
- def extract_sentence_text(ocr_results):
262
- """Extract sentence-based text for LLM input"""
263
- paragraph_text = ""
264
- for item in ocr_results:
265
- if len(item) == 3:
266
- _, text, _ = item
267
- elif len(item) == 2:
268
- _, text = item
269
- else:
270
- print(f"Unexpected OCR result format: {item}")
271
- continue
272
- if text.strip():
273
- paragraph_text += text + " "
274
- return paragraph_text.strip()
275
-
276
- def extract_word_bboxes_improved(ocr_results):
277
- """Improved word extraction with better handling of punctuation and spacing"""
278
- word_bbox_map = []
279
-
280
- for item in ocr_results:
281
- if len(item) == 3:
282
- bbox, text, confidence = item
283
- elif len(item) == 2:
284
- bbox, text = item
285
- confidence = 1.0
286
- else:
287
- print(f"Unexpected OCR result format: {item}")
288
- continue
289
-
290
- original_text = text.strip()
291
- if not original_text:
292
- continue
293
-
294
- if isinstance(bbox[0], (list, tuple)):
295
- x_coords = [point[0] for point in bbox]
296
- y_coords = [point[1] for point in bbox]
297
- line_x1, line_y1 = min(x_coords), min(y_coords)
298
- line_x2, line_y2 = max(x_coords), max(y_coords)
299
- else:
300
- line_x1, line_y1, line_x2, line_y2 = bbox
301
-
302
- tokens = re.findall(r'\S+', original_text)
303
- if len(tokens) <= 1:
304
- padding = 1
305
- word_bbox_map.append({
306
- "word": original_text,
307
- "bbox": [
308
- max(0, int(line_x1 - padding)),
309
- max(0, int(line_y1 - padding)),
310
- int(line_x2 + padding),
311
- int(line_y2 + padding)
312
- ],
313
- "confidence": confidence,
314
- "original_line": original_text
315
- })
316
- continue
317
-
318
- full_width = line_x2 - line_x1
319
- full_height = line_y2 - line_y1
320
- text_without_spaces = original_text.replace(' ', '')
321
- total_chars = len(text_without_spaces)
322
-
323
- char_position = 0
324
- for i, token in enumerate(tokens):
325
- token_start_ratio = char_position / total_chars if total_chars > 0 else 0
326
- char_position += len(token)
327
- token_end_ratio = char_position / total_chars if total_chars > 0 else 1
328
-
329
- token_x1 = line_x1 + (full_width * token_start_ratio)
330
- token_x2 = line_x1 + (full_width * token_end_ratio)
331
-
332
- padding = 1
333
- word_bbox = [
334
- max(0, int(token_x1 - padding)),
335
- max(0, int(line_y1 - padding)),
336
- int(min(token_x2 + padding, line_x2)),
337
- int(line_y2 + padding)
338
- ]
339
-
340
- word_bbox_map.append({
341
- "word": token,
342
- "bbox": word_bbox,
343
- "confidence": confidence,
344
- "original_line": original_text
345
- })
346
-
347
- return sorted(word_bbox_map, key=lambda x: (x['bbox'][1], x['bbox'][0]))
348
-
349
- def advanced_match_pii_to_words(pii_list, word_bbox_map):
350
- """Advanced multi-strategy PII matching with comprehensive fallbacks"""
351
- ocr_results_for_template = []
352
- words = [info['word'] for info in word_bbox_map]
353
- bboxes = [info['bbox'] for info in word_bbox_map]
354
- is_pii_flags = [False] * len(words)
355
-
356
- # Pre-process all words with different normalization strategies
357
- normalized_words = [normalize_text(word) for word in words]
358
-
359
- print(f"Processing {len(pii_list)} PII items against {len(words)} OCR words")
360
-
361
- for pii_idx, pii_item in enumerate(pii_list):
362
- if not pii_item.strip():
363
- continue
364
-
365
- print(f"Processing PII item {pii_idx + 1}: '{pii_item}'")
366
-
367
- # Normalize the PII item
368
- normalized_pii = normalize_text(pii_item)
369
- pii_words = normalized_pii.split()
370
-
371
- if not pii_words:
372
- continue
373
-
374
- matched = False
375
-
376
- # Strategy 1: Exact matching after normalization
377
- if len(pii_words) == 1:
378
- pii_word = pii_words[0]
379
- for idx, norm_word in enumerate(normalized_words):
380
- if norm_word == pii_word and not is_pii_flags[idx]:
381
- is_pii_flags[idx] = True
382
- matched = True
383
- print(f" ✓ Exact match: '{words[idx]}' -> '{pii_item}'")
384
- else:
385
- # Multi-word exact matching
386
- pii_len = len(pii_words)
387
- start_idx = 0
388
- while start_idx < len(normalized_words) - pii_len + 1:
389
- exact_match = True
390
- for j in range(pii_len):
391
- if normalized_words[start_idx + j] != pii_words[j]:
392
- exact_match = False
393
- break
394
-
395
- if exact_match:
396
- # Check spatial proximity
397
- spatial_ok = True
398
- for j in range(1, pii_len):
399
- prev_bbox = bboxes[start_idx + j - 1]
400
- curr_bbox = bboxes[start_idx + j]
401
-
402
- horizontal_distance = curr_bbox[0] - prev_bbox[2]
403
- vertical_alignment = (abs(prev_bbox[1] - curr_bbox[1]) < 30 and
404
- abs(prev_bbox[3] - curr_bbox[3]) < 30)
405
-
406
- if not (vertical_alignment and horizontal_distance <= 150):
407
- spatial_ok = False
408
- break
409
-
410
- if spatial_ok:
411
- for j in range(pii_len):
412
- if not is_pii_flags[start_idx + j]:
413
- is_pii_flags[start_idx + j] = True
414
- matched = True
415
- matched_text = ' '.join(words[start_idx:start_idx + pii_len])
416
- print(f" ✓ Multi-word exact: '{matched_text}' -> '{pii_item}'")
417
- start_idx += pii_len
418
- continue
419
- start_idx += 1
420
-
421
- # Strategy 2: Fuzzy matching if exact matching failed
422
- if not matched:
423
- if len(pii_words) == 1:
424
- pii_word = pii_words[0]
425
- for idx, norm_word in enumerate(normalized_words):
426
- if (not is_pii_flags[idx] and
427
- (fuzzy_match_score(norm_word, pii_word, 0.9) or
428
- is_similar_by_edit_distance(norm_word, pii_word, 2))):
429
- is_pii_flags[idx] = True
430
- matched = True
431
- print(f" ✓ Fuzzy match: '{words[idx]}' -> '{pii_item}'")
432
- else:
433
- # Multi-word fuzzy matching
434
- pii_len = len(pii_words)
435
- start_idx = 0
436
- while start_idx < len(normalized_words) - pii_len + 1:
437
- fuzzy_match = True
438
- for j in range(pii_len):
439
- if not (fuzzy_match_score(normalized_words[start_idx + j], pii_words[j], 0.85) or
440
- is_similar_by_edit_distance(normalized_words[start_idx + j], pii_words[j], 2)):
441
- fuzzy_match = False
442
- break
443
-
444
- if fuzzy_match:
445
- # Check spatial proximity
446
- spatial_ok = True
447
- for j in range(1, pii_len):
448
- prev_bbox = bboxes[start_idx + j - 1]
449
- curr_bbox = bboxes[start_idx + j]
450
-
451
- horizontal_distance = curr_bbox[0] - prev_bbox[2]
452
- vertical_alignment = (abs(prev_bbox[1] - curr_bbox[1]) < 30 and
453
- abs(prev_bbox[3] - curr_bbox[3]) < 30)
454
-
455
- if not (vertical_alignment and horizontal_distance <= 150):
456
- spatial_ok = False
457
- break
458
-
459
- if spatial_ok:
460
- for j in range(pii_len):
461
- if not is_pii_flags[start_idx + j]:
462
- is_pii_flags[start_idx + j] = True
463
- matched = True
464
- matched_text = ' '.join(words[start_idx:start_idx + pii_len])
465
- print(f" ✓ Multi-word fuzzy: '{matched_text}' -> '{pii_item}'")
466
- start_idx += pii_len
467
- continue
468
- start_idx += 1
469
-
470
- # Strategy 3: Substring and partial matching
471
- if not matched:
472
- full_normalized_text = ' '.join(normalized_words)
473
-
474
- pos = 0
475
- while True:
476
- start_pos = full_normalized_text.find(normalized_pii, pos)
477
- if start_pos == -1:
478
- break
479
- end_pos = start_pos + len(normalized_pii)
480
-
481
- char_count = 0
482
- start_word_idx = None
483
- end_word_idx = None
484
-
485
- for idx, norm_word in enumerate(normalized_words):
486
- word_start = char_count
487
- word_end = char_count + len(norm_word)
488
-
489
- if start_word_idx is None and word_end > start_pos:
490
- start_word_idx = idx
491
-
492
- if word_start < end_pos:
493
- end_word_idx = idx
494
-
495
- char_count += len(norm_word) + 1
496
-
497
- if start_word_idx is not None and end_word_idx is not None and end_word_idx - start_word_idx + 1 >= len(pii_words):
498
- spatial_ok = True
499
- for j in range(start_word_idx, end_word_idx):
500
- if j + 1 <= end_word_idx:
501
- prev_bbox = bboxes[j]
502
- next_bbox = bboxes[j + 1]
503
-
504
- horizontal_distance = next_bbox[0] - prev_bbox[2]
505
- vertical_alignment = (abs(prev_bbox[1] - next_bbox[1]) < 30 and
506
- abs(prev_bbox[3] - next_bbox[3]) < 30)
507
-
508
- if not (vertical_alignment and horizontal_distance <= 200):
509
- spatial_ok = False
510
- break
511
-
512
- if spatial_ok:
513
- for j in range(start_word_idx, end_word_idx + 1):
514
- if not is_pii_flags[j]:
515
- is_pii_flags[j] = True
516
- matched = True
517
- matched_text = ' '.join(words[start_word_idx:end_word_idx + 1])
518
- print(f" ✓ Substring match: '{matched_text}' -> '{pii_item}'")
519
- pos = end_pos
520
-
521
- # Strategy 4: Individual word matching with relaxed criteria
522
- if not matched:
523
- for pii_word in pii_words:
524
- if len(pii_word) < 3:
525
- continue
526
-
527
- for idx, norm_word in enumerate(normalized_words):
528
- if not is_pii_flags[idx]:
529
- if (norm_word == pii_word or
530
- fuzzy_match_score(norm_word, pii_word, 0.8) or
531
- is_similar_by_edit_distance(norm_word, pii_word, 2) or
532
- (len(pii_word) > 5 and (pii_word in norm_word or norm_word in pii_word))):
533
- is_pii_flags[idx] = True
534
- print(f" ✓ Individual word match: '{words[idx]}' -> '{pii_word}' from '{pii_item}'")
535
-
536
- if not matched:
537
- print(f" ✗ No match found for: '{pii_item}'")
538
-
539
- for idx, word_info in enumerate(word_bbox_map):
540
- ocr_results_for_template.append({
541
- "text": word_info["word"],
542
- "bbox": word_info["bbox"],
543
- "is_pii": is_pii_flags[idx],
544
- "confidence": word_info.get("confidence", 1.0)
545
- })
546
-
547
- ocr_results_for_template = merge_horizontal_pii_boxes_improved(ocr_results_for_template)
548
-
549
- return ocr_results_for_template
550
-
551
- def merge_horizontal_pii_boxes_improved(ocr_results, merge_distance=50):
552
- """Improved merging with better spatial awareness and tighter boxes"""
553
- if not ocr_results:
554
- return ocr_results
555
-
556
- merged_results = []
557
- i = 0
558
-
559
- while i < len(ocr_results):
560
- current_word = ocr_results[i]
561
-
562
- if not current_word["is_pii"]:
563
- merged_results.append(current_word)
564
- i += 1
565
- continue
566
-
567
- merge_group = [current_word]
568
- j = i + 1
569
-
570
- while j < len(ocr_results):
571
- next_word = ocr_results[j]
572
-
573
- if not next_word["is_pii"]:
574
- break
575
-
576
- current_bbox = merge_group[-1]["bbox"]
577
- next_bbox = next_word["bbox"]
578
-
579
- y_center_current = (current_bbox[1] + current_bbox[3]) / 2
580
- y_center_next = (next_bbox[1] + next_bbox[3]) / 2
581
- y_overlap = abs(y_center_current - y_center_next) < 20
582
-
583
- horizontal_distance = next_bbox[0] - current_bbox[2]
584
-
585
- if y_overlap and horizontal_distance <= merge_distance:
586
- merge_group.append(next_word)
587
- j += 1
588
- else:
589
- break
590
-
591
- if len(merge_group) > 1:
592
- min_x = min(word["bbox"][0] for word in merge_group)
593
- min_y = min(word["bbox"][1] for word in merge_group)
594
- max_x = max(word["bbox"][2] for word in merge_group)
595
- max_y = max(word["bbox"][3] for word in merge_group)
596
-
597
- merged_text = " ".join(word["text"] for word in merge_group)
598
-
599
- merged_word = {
600
- "text": merged_text,
601
- "bbox": [min_x, min_y, max_x, max_y],
602
- "is_pii": True,
603
- "confidence": max(word.get("confidence", 1.0) for word in merge_group)
604
- }
605
- merged_results.append(merged_word)
606
- print(f" ✓ Merged PII box: '{merged_text}' at [{min_x},{min_y},{max_x},{max_y}]")
607
- else:
608
- merged_results.append(current_word)
609
-
610
- i = j
611
-
612
- return merged_results
613
-
614
- def post_process_pii_detection(ocr_results_for_template, pii_list):
615
- """Post-process to catch any missed PII using relaxed matching"""
616
- words = [result["text"] for result in ocr_results_for_template]
617
-
618
- for pii_item in pii_list:
619
- normalized_pii = normalize_text(pii_item)
620
- pii_words = normalized_pii.split()
621
-
622
- if not pii_words:
623
- continue
624
-
625
- pii_detected = False
626
- for result in ocr_results_for_template:
627
- if result["is_pii"]:
628
- result_normalized = normalize_text(result["text"])
629
- if (normalized_pii in result_normalized or
630
- result_normalized in normalized_pii or
631
- fuzzy_match_score(result_normalized, normalized_pii, 0.7)):
632
- pii_detected = True
633
- break
634
-
635
- if not pii_detected:
636
- print(f" ⚠ PII not detected, trying fallback matching: '{pii_item}'")
637
-
638
- for idx, result in enumerate(ocr_results_for_template):
639
- if result["is_pii"]:
640
- continue
641
-
642
- word_normalized = normalize_text(result["text"])
643
-
644
- for pii_word in pii_words:
645
- if (len(pii_word) > 3 and
646
- (pii_word in word_normalized or
647
- word_normalized in pii_word or
648
- fuzzy_match_score(word_normalized, pii_word, 0.6) or
649
- is_similar_by_edit_distance(word_normalized, pii_word, 3))):
650
-
651
- ocr_results_for_template[idx]["is_pii"] = True
652
- print(f" ✓ Fallback match: '{result['text']}' -> '{pii_word}' from '{pii_item}'")
653
- break
654
-
655
- return ocr_results_for_template
656
-
657
- def detect_pii_from_combined_text(combined_text):
658
- """Detect PII from combined multi-page text using LLM"""
659
- if llama_model is None:
660
- print("LLM model not available for PII detection")
661
- return [], "LLM model not available"
662
-
663
- instruction = (
664
- "Extract all Personally Identifiable Information (PII) of the main subject from the given text. "
665
- "Include data like Name, Date of Birth, Gender, Address, Phone Number, Email, Social Security Number (SSN), Member ID, Group Number, or any other PII data available. "
666
- "Ignore any information about doctors, staff, providers, colleagues, organizations, companies, hospitals, educational institutes, or facilities. "
667
- "Return the results strictly as a flat set of strings enclosed in { } without labels."
668
- )
669
- prompt_content = f"{instruction}\n{combined_text}"
670
-
671
- pii_list = []
672
- llama_raw_output = ""
673
- try:
674
- messages = [{"role": "user", "content": prompt_content}]
675
- response = llama_model.create_chat_completion(
676
- messages=messages,
677
- max_tokens=512,
678
- temperature=0.1,
679
- )
680
- llama_raw_output = response['choices'][0]['message']['content']
681
- pii_list = parse_pii_output(llama_raw_output)
682
- print(f"LLM detected {len(pii_list)} PII items from combined text: {pii_list}")
683
- except Exception as e:
684
- print(f"Error during Llama PII detection: {e}")
685
- llama_raw_output = f"Error: {str(e)}"
686
- pii_list = []
687
-
688
- return pii_list, llama_raw_output
689
-
690
- # --- Combined Processing Function ---
691
- def process_page_combined(img_cv, global_pii_list):
692
- """Process a single page with both YOLO and OCR+LLM detection"""
693
- all_detections = []
694
-
695
- # Step 1: YOLO Visual Detection
696
- print(" Running YOLO visual detection...")
697
- visual_boxes, visual_classes = detect_visual_pii(img_cv)
698
-
699
- for box, cls in zip(visual_boxes, visual_classes):
700
- all_detections.append({
701
- "text": cls,
702
- "bbox": box.tolist() if hasattr(box, 'tolist') else box,
703
- "is_pii": True,
704
- "confidence": 1.0,
705
- "detection_type": "visual"
706
- })
707
-
708
- print(f" YOLO detected {len(visual_boxes)} visual elements")
709
-
710
- # Step 2: OCR + LLM Text Detection
711
- if reader is not None:
712
- print(" Running OCR text extraction...")
713
- word_ocr_results = reader.readtext(img_cv, paragraph=False, width_ths=0.7, height_ths=0.7)
714
-
715
- if word_ocr_results:
716
- word_bbox_map = extract_word_bboxes_improved(word_ocr_results)
717
-
718
- ocr_results_for_template = advanced_match_pii_to_words(global_pii_list, word_bbox_map)
719
- ocr_results_for_template = post_process_pii_detection(ocr_results_for_template, global_pii_list)
720
- ocr_results_for_template = refine_pii_flags(ocr_results_for_template)
721
- ocr_results_for_template = merge_horizontal_pii_boxes_improved(ocr_results_for_template)
722
-
723
- for result in ocr_results_for_template:
724
- if result["is_pii"]:
725
- result["detection_type"] = "text"
726
- all_detections.append(result)
727
-
728
- print(f" OCR detected {sum(1 for r in ocr_results_for_template if r['is_pii'])} text PII elements")
729
-
730
- return all_detections
731
-
732
- # --- Main Gradio Processing Function ---
733
- def analyze_document(file, progress=gr.Progress()):
734
- """
735
- This function takes an uploaded file, processes it through the PII detection pipeline,
736
- and returns the annotated images, a redacted PDF, and a summary report.
737
- """
738
- load_models()
739
- if file is None:
740
- return None, None, "Please upload a document to begin."
741
-
742
- unique_id = uuid.uuid4().hex
743
-
744
- if hasattr(file, 'name'):
745
- filepath = file.name
746
- else:
747
- filepath = str(file)
748
-
749
- filename = os.path.basename(filepath)
750
- extension = os.path.splitext(filename)[1]
751
-
752
- progress(0, desc="Converting document to images...")
753
- images_to_process = []
754
- try:
755
- if extension.lower() == '.pdf':
756
- try:
757
- images_to_process = [cv2.cvtColor(np.array(page), cv2.COLOR_RGB2BGR) for page in convert_from_path(filepath, dpi=300)]
758
- except Exception as e:
759
- print(f"PDF conversion error: {e}. Trying fallback method...")
760
- try:
761
- images_to_process = [cv2.cvtColor(np.array(page), cv2.COLOR_RGB2BGR) for page in convert_from_path(filepath, dpi=150)]
762
- except Exception as e2:
763
- return None, None, f"🔴 **Error:** Could not process PDF. Please ensure Poppler is installed or try converting to images first.\nDetails: {e2}"
764
- else:
765
- img = cv2.imread(filepath)
766
- if img is not None:
767
- images_to_process.append(img)
768
- except Exception as e:
769
- return None, None, f"🔴 **Error:** Could not process file. Details: {e}"
770
-
771
- if not images_to_process:
772
- return None, None, "🔴 **Error:** No pages could be extracted from the document."
773
-
774
- total_pages = len(images_to_process)
775
- print(f"Processing {total_pages} pages for job {unique_id}...")
776
-
777
- progress(0.1, desc="Extracting text from all pages (OCR)...")
778
- combined_text, all_pages_data = "", []
779
- for i, img_cv in enumerate(images_to_process):
780
- page_text = ""
781
- if reader:
782
- page_text = extract_sentence_text(reader.readtext(img_cv, paragraph=True))
783
- combined_text += f"\n--- Page {i+1} ---\n{page_text}\n"
784
- all_pages_data.append({"img_cv": img_cv, "page_num": i + 1})
785
-
786
- progress(0.4, desc="Analyzing text for PII with LLM...")
787
- global_pii_list, llama_raw_output = detect_pii_from_combined_text(combined_text)
788
-
789
- annotated_paths, redacted_pils = [], []
790
- report = f"## 🔍 Analysis Report\n**Global PII Found:** `{', '.join(global_pii_list) if global_pii_list else 'None'}`\n\n---\n"
791
- for i, page_info in enumerate(all_pages_data):
792
- progress(0.5 + (i / total_pages * 0.4), desc=f"Processing Page {i+1}/{total_pages} (Visual & Text)...")
793
- img_cv, page_num = page_info["img_cv"], page_info["page_num"]
794
- detections = process_page_combined(img_cv, global_pii_list)
795
-
796
- annotated_img, redacted_img = img_cv.copy(), img_cv.copy()
797
- visual_count = sum(1 for d in detections if d["detection_type"] == "visual")
798
- text_count = sum(1 for d in detections if d.get("detection_type") == "text")
799
- for d in detections:
800
- bbox = d.get("bbox", [])
801
- if not bbox: continue
802
- x1, y1, x2, y2 = map(int, bbox)
803
- color = (0, 255, 0) if d.get("detection_type") == "visual" else (0, 0, 255)
804
- cv2.rectangle(annotated_img, (x1, y1), (x2, y2), color, 3)
805
- cv2.rectangle(redacted_img, (x1, y1), (x2, y2), (0, 0, 0), -1)
806
-
807
- path = os.path.join(RESULTS_FOLDER, f"annotated_{unique_id}_{page_num}.jpg")
808
- cv2.imwrite(path, annotated_img)
809
- annotated_paths.append(path)
810
- redacted_pils.append(Image.fromarray(cv2.cvtColor(redacted_img, cv2.COLOR_BGR2RGB)))
811
-
812
- report += f"### 📄 Page {page_num}\n- **Visual Detections (🟩 Green):** {visual_count}\n- **Text Detections (🟥 Red):** {text_count}\n"
813
-
814
- progress(0.9, desc="Generating final redacted PDF...")
815
- redacted_pdf_path = None
816
- if redacted_pils:
817
- pdf_path = os.path.join(RESULTS_FOLDER, f"redacted_{unique_id}.pdf")
818
- redacted_pils[0].save(pdf_path, "PDF", resolution=100.0, save_all=True, append_images=redacted_pils[1:])
819
- redacted_pdf_path = pdf_path
820
-
821
- progress(1, desc="Complete!")
822
- print("Processing Complete.")
823
- return annotated_paths, redacted_pdf_path, report
824
-
825
- # --- Gradio Interface Definition ---
826
- title = "🔒 Combined PII Detection System"
827
- description = """
828
- ### Advanced Multi-Modal PII Detection
829
- This system uses a combination of visual and textual analysis to detect and redact Personally Identifiable Information from your documents.
830
- - **🖼️ Visual Detection (YOLO):** Detects Faces, QR Codes, and Signatures.
831
- - **📝 Text Detection (OCR + LLM):** Detects Names, Addresses, Phone Numbers, IDs, and other contextual PII.
832
- **How to Use:**
833
- 1. Upload a document (PDF or image format).
834
- 2. The system will process each page and display annotated previews with colored boxes.
835
- 3. A fully redacted PDF with blacked-out PII is generated for you to download.
836
- 4. An analysis report summarizes the findings for each page.
837
- """
838
-
839
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
840
- gr.Markdown(f"<h1 style='margin-bottom: 0.25rem;'>{title}</h1>")
841
- gr.Markdown(
842
- "<p style='color:#475569; line-height:1.6;'>"
843
- "Upload a PDF or image to detect and redact PII using visual detectors (🟩) and text analysis (🟥). "
844
- "Fixed for local environment with proper YOLO support."
845
- "</p>"
846
- )
847
- with gr.Accordion("About this tool", open=False):
848
- gr.Markdown(description)
849
- with gr.Tabs():
850
- with gr.Tab("Run"):
851
- with gr.Row():
852
- with gr.Column(scale=1):
853
- file_input = gr.File(
854
- label="Upload Document",
855
- file_types=['.pdf', '.jpg', '.jpeg', '.png', '.bmp'],
856
- file_count="single",
857
- height=100
858
- )
859
- submit_btn = gr.Button("🚀 Analyze Document", variant="primary")
860
- with gr.Accordion("Tips", open=False):
861
- gr.Markdown(
862
- "- Prefer high-resolution files for better OCR results (300 DPI for PDFs).\n"
863
- "- For PDFs, ensure Poppler is installed on your system.\n"
864
- "- Visual detections are drawn in green; text-based detections are in red.\n"
865
- "- Use the Previews tab to inspect annotated pages and the Report tab to download the redacted PDF."
866
- )
867
- with gr.Column(scale=1):
868
- gr.Markdown("### What happens during analysis")
869
- gr.Markdown(
870
- "- Convert pages to images\n"
871
- "- Run global OCR to build combined text\n"
872
- "- Use LLM to extract possible PII strings\n"
873
- "- Match PII back to words and merge boxes\n"
874
- "- Render annotated previews and build a redacted PDF"
875
- )
876
- clear_btn = gr.Button("🧹 Clear Results", variant="secondary")
877
- with gr.Tab("Previews"):
878
- gr.Markdown("### Annotated Previews (🟩 Visual, 🟥 Text)")
879
- gallery_output = gr.Gallery(
880
- label="Annotated Pages",
881
- show_label=False,
882
- elem_id="gallery",
883
- columns=[2],
884
- rows=[1],
885
- object_fit="contain",
886
- height=480
887
- )
888
- with gr.Tab("Report & Download"):
889
- with gr.Row():
890
- with gr.Column(scale=1):
891
- gr.Markdown("### Download")
892
- file_output = gr.File(label="Redacted PDF")
893
- with gr.Column(scale=2):
894
- gr.Markdown("### Analysis Report")
895
- report_output = gr.Markdown(label="Analysis Report")
896
-
897
- submit_btn.click(
898
- fn=analyze_document,
899
- inputs=file_input,
900
- outputs=[gallery_output, file_output, report_output]
901
- )
902
-
903
- clear_btn.click(
904
- fn=lambda: ([], None, "Ready. Upload a document and click Analyze."),
905
- inputs=None,
906
- outputs=[gallery_output, file_output, report_output]
907
- )
908
-
909
- if __name__ == "__main__":
910
- demo.queue().launch(server_port=7860)