File size: 6,825 Bytes
545bf64 eb22246 e49d260 545bf64 14ce970 eb22246 14ce970 545bf64 eb22246 14ce970 eb22246 14ce970 545bf64 14ce970 545bf64 14ce970 545bf64 eb22246 545bf64 14ce970 545bf64 eb22246 14ce970 eb22246 14ce970 eb22246 14ce970 eb22246 14ce970 da51c76 eb22246 14ce970 e49d260 14ce970 e49d260 14ce970 eb22246 14ce970 b7faecc 14ce970 b7faecc 14ce970 e49d260 14ce970 e49d260 14ce970 e49d260 14ce970 e49d260 545bf64 14ce970 39fcf38 14ce970 39fcf38 14ce970 545bf64 14ce970 545bf64 14ce970 5559b60 a8a1267 14ce970 545bf64 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | import gradio as gr
import easyocr
import cv2
import numpy as np
from PIL import Image
# Initialize Deep Learning OCR once
reader = easyocr.Reader(['en'])
# Helper to calculate Intersection over Union (Overlap)
def calculate_iou(boxA, boxB):
# box = [x_left, y_top, x_right, y_bottom]
xA = max(boxA[0], boxB[0])
yA = max(boxA[1], boxB[1])
xB = min(boxA[2], boxB[2])
yB = min(boxA[3], boxB[3])
interArea = max(0, xB - xA) * max(0, yB - yA)
if interArea == 0: return 0.0
boxAArea = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1])
boxBArea = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1])
iou = interArea / float(boxAArea + boxBArea - interArea)
return iou
def validate_whatsapp_hybrid(image):
if image is None:
return {"valid": False, "reason": "No image provided"}
image_np = np.array(image.convert('RGB'))
height, width, _ = image_np.shape
center_x = width / 2
debug_log = []
# --- PHASE 1: DEEP OCR & HEADER BARRIER ---
try:
# detail=1 returns [ [[tl,tr,br,bl]], text, conf ]
ocr_results = reader.readtext(image_np, detail=1, paragraph=False)
except Exception as e:
return {"valid": False, "error": f"OCR Failed: {str(e)}"}
header_keywords = ["block", "add to contacts", "report", "business account",
"joined in", "not a contact", "encrypted", "end-to-end"]
safe_chat_start_y = 0
# Find the lowest header element
for (bbox, text, conf) in ocr_results:
if conf < 0.3: continue
bottom_y = bbox[2][1]
if any(k in text.lower() for k in header_keywords):
if bottom_y > safe_chat_start_y:
safe_chat_start_y = bottom_y
# Add buffer or set default
if safe_chat_start_y > 0:
safe_chat_start_y += 30
debug_log.append(f"Header barrier set at Y={int(safe_chat_start_y)}")
else:
safe_chat_start_y = height * 0.18
debug_log.append(f"No header keywords. Using default barrier Y={int(safe_chat_start_y)}")
# --- PHASE 2: OPENCV STICKER CANDIDATES ---
# Convert to grayscale
gray = cv2.cvtColor(image_np, cv2.COLOR_RGB2GRAY)
# Blur slightly to remove image noise
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# Adaptive Threshold (handles different background brightness)
thresh = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 25, 15)
# Find contours (blobs)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
sticker_candidates = []
min_sticker_size = width * 0.12 # Sticker must be at least 12% of screen width
max_sticker_size = width * 0.60
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
# Filter A: Must be below header
if y < safe_chat_start_y: continue
# Filter B: Size criteria (Stickers aren't tiny dots or massive screens)
if w < min_sticker_size or h < min_sticker_size: continue
if w > max_sticker_size or h > max_sticker_size: continue
# Filter C: Aspect Ratio (Stickers are roughly square-ish, 0.6 to 1.6 ratio)
aspect = w / float(h)
if not (0.6 < aspect < 1.6): continue
# This is a potential sticker or a text bubble
sticker_candidates.append([x, y, x+w, y+h]) # Format: [left, top, right, bottom]
# --- PHASE 3: DE-DUPLICATION (Separating Text vs. Stickers) ---
final_objects = []
# 3a. Process OCR Text Objects
ocr_boxes_formatted = []
system_words = ["today", "yesterday", "messages", "calls", "unread"]
for (bbox, text, conf) in ocr_results:
top_y = bbox[0][1]
if top_y < safe_chat_start_y: continue
if conf < 0.4 or len(text) < 2: continue
# Center check for system dates
text_center = bbox[0][0] + ((bbox[1][0] - bbox[0][0]) / 2)
if abs(text_center - center_x) < (width * 0.12): continue
if any(s in text.lower() for s in system_words): continue
# Format: [left, top, right, bottom]
l, t, r, b = bbox[0][0], bbox[0][1], bbox[2][0], bbox[2][1]
ocr_boxes_formatted.append([l, t, r, b])
final_objects.append({
'type': 'text', 'text_content': text, 'left': l, 'top': t,
'center': l + (r-l)/2
})
# 3b. Process Sticker Candidates against Text Boxes
for s_box in sticker_candidates:
is_actually_text = False
# Check if this sticker candidate overlaps significantly with any text box
for t_box in ocr_boxes_formatted:
# If IoU > 15%, assume this blob is just the container for the text
if calculate_iou(s_box, t_box) > 0.15:
is_actually_text = True
break
if not is_actually_text:
# It's a blob with no text inside! Probably a sticker.
l, t, r, b = s_box
final_objects.append({
'type': 'sticker', 'text_content': 'STICKER/IMAGE', 'left': l, 'top': t,
'center': l + (r-l)/2
})
debug_log.append(f"Found Sticker Candidate at Y={t}, X={l}")
# --- PHASE 4: UNIFIED DECISION LOOP ---
# Sort everything by top Y position
final_objects.sort(key=lambda x: x['top'])
final_decision = None
stranger_limit = width * 0.20
me_limit = width * 0.40
for obj in final_objects:
# Double-check center alignment (redundant safety check)
if abs(obj['center'] - center_x) < (width * 0.10):
debug_log.append(f"Skipped Centered {obj['type']}")
continue
# --- ALIGNMENT CHECK ---
if obj['left'] < stranger_limit:
final_decision = True
debug_log.append(f"✅ VALID (Stranger): Found Left-Aligned {obj['type']} at X={int(obj['left'])}")
break
elif obj['left'] > me_limit:
final_decision = False
debug_log.append(f"❌ INVALID (Me): Found Right-Aligned {obj['type']} at X={int(obj['left'])}")
break
# --- RETURN ---
if final_decision is True:
return {"valid": True, "reason": "First message/sticker is from stranger.", "debug": debug_log}
elif final_decision is False:
return {"valid": False, "reason": "First message/sticker is from you.", "debug": debug_log}
else:
return {"valid": False, "reason": "No valid messages or stickers detected.", "debug": debug_log}
iface = gr.Interface(
fn=validate_whatsapp_hybrid,
inputs=gr.Image(type="pil"),
outputs=gr.JSON(),
title="WhatsApp Validator V13 (Hybrid Text + Sticker Detection)",
description="Uses Deep Learning OCR for text and OpenCV Blob Detection for stickers."
)
if __name__ == "__main__":
iface.launch() |