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()