| import gradio as gr |
| import easyocr |
| import cv2 |
| import numpy as np |
| from PIL import Image |
|
|
| |
| reader = easyocr.Reader(['en']) |
|
|
| |
| def calculate_iou(boxA, boxB): |
| |
| 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 = [] |
|
|
| |
| try: |
| |
| 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 |
| |
| |
| 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 |
|
|
| |
| 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)}") |
|
|
| |
| |
| gray = cv2.cvtColor(image_np, cv2.COLOR_RGB2GRAY) |
| |
| blurred = cv2.GaussianBlur(gray, (5, 5), 0) |
| |
| thresh = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 25, 15) |
|
|
| |
| contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) |
|
|
| sticker_candidates = [] |
| min_sticker_size = width * 0.12 |
| max_sticker_size = width * 0.60 |
|
|
| for cnt in contours: |
| x, y, w, h = cv2.boundingRect(cnt) |
| |
| |
| if y < safe_chat_start_y: continue |
| |
| |
| if w < min_sticker_size or h < min_sticker_size: continue |
| if w > max_sticker_size or h > max_sticker_size: continue |
|
|
| |
| aspect = w / float(h) |
| if not (0.6 < aspect < 1.6): continue |
| |
| |
| sticker_candidates.append([x, y, x+w, y+h]) |
|
|
| |
| final_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 |
| |
| |
| 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 |
|
|
| |
| 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 |
| }) |
|
|
| |
| for s_box in sticker_candidates: |
| is_actually_text = False |
| |
| for t_box in ocr_boxes_formatted: |
| |
| if calculate_iou(s_box, t_box) > 0.15: |
| is_actually_text = True |
| break |
| |
| if not is_actually_text: |
| |
| 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}") |
|
|
| |
| |
| 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: |
| |
| if abs(obj['center'] - center_x) < (width * 0.10): |
| debug_log.append(f"Skipped Centered {obj['type']}") |
| continue |
|
|
| |
| 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 |
|
|
| |
| 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() |