File size: 8,781 Bytes
e20644c
 
 
8a7ad0e
e20644c
 
 
 
 
 
 
 
 
 
8a7ad0e
e20644c
8a7ad0e
 
 
 
 
 
 
e20644c
 
8a7ad0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e20644c
 
 
 
 
 
 
 
 
8a7ad0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e20644c
 
8a7ad0e
 
e20644c
 
8a7ad0e
 
 
 
 
 
 
 
 
 
 
e20644c
8a7ad0e
 
 
 
e20644c
 
 
 
8a7ad0e
1da8f53
8a7ad0e
 
 
1da8f53
8a7ad0e
 
1da8f53
8a7ad0e
 
1da8f53
 
 
 
 
 
8a7ad0e
1da8f53
 
 
 
 
8a7ad0e
 
1da8f53
8a7ad0e
 
 
e20644c
 
 
 
 
 
1da8f53
e20644c
 
8a7ad0e
 
e20644c
1da8f53
e20644c
 
 
 
 
8a7ad0e
e20644c
8a7ad0e
e20644c
 
8a7ad0e
e20644c
 
 
8a7ad0e
e20644c
 
 
8a7ad0e
e20644c
 
 
8a7ad0e
e20644c
8a7ad0e
e20644c
 
 
 
 
8a7ad0e
e20644c
 
 
8a7ad0e
e20644c
 
 
 
 
8a7ad0e
 
 
e20644c
1da8f53
 
 
e20644c
 
 
 
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
import gradio as gr
import torch
import torch.nn.functional as F
import re
from transformers import AutoTokenizer, AutoModelForSequenceClassification

MODEL_ID = "gorkem371/pii-intent-classifier-xlmr-large"

print("Loading model...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
model = AutoModelForSequenceClassification.from_pretrained(MODEL_ID)
model.eval()
print("Model loaded!")

# ── Auto-detect entities from text ──────────────────────────────

ENTITY_PATTERNS = [
    ("EMAIL", r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'),
    ("IBAN", r'\b[A-Z]{2}\d{2}[A-Z0-9]{10,30}\b'),
    ("PHONE", r'(?:\+?\d{1,3}[\s-]?)?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{3,4}\b'),
    ("SOCIAL_MEDIA", r'@[a-zA-Z0-9_.]{2,30}'),
    ("URL", r'(?:https?://)?(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(?:/\S*)?'),
    ("CRYPTO_ADDRESS", r'\b(?:0x[a-fA-F0-9]{40}|[13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-zA-HJ-NP-Z0-9]{39,59})\b'),
]

INTENT_KEYWORDS = {
    "PHONE": [
        "numaram", "numara", "whatsapp", "telefon", "ara beni", "bana ulaş",
        "رقمي", "واتساب", "اتصل", "كلمني",
        "my number", "call me", "whatsapp me", "reach me", "contact me",
        "send you my number", "numara atarim", "numara veririm",
        "بارسل لك رقمي", "بعطيك رقمي",
    ],
    "SOCIAL_MEDIA": [
        "instagram", "insta", "tiktok", "telegram", "snapchat", "snap",
        "twitter", "takip et", "follow me", "تابعني", "انستقرام", "سناب",
        "beni bul", "find me", "تلقاني", "add me",
    ],
    "EMAIL": ["mail", "email", "ايميل", "راسلني", "e-posta"],
    "OFF_PLATFORM_ATTEMPT": [
        "başka yerde", "platform dışı", "dışarıda", "oradan konuşalım",
        "let's talk on", "move to", "نروح", "بره", "خارج",
        "bio", "story", "profil", "بايو", "ستوري",
    ],
}


def detect_entities(text):
    """Auto-detect entities in text."""
    found = []

    for etype, pattern in ENTITY_PATTERNS:
        for match in re.finditer(pattern, text):
            found.append((match.group(), etype, match.start()))

    # Remove overlapping matches (keep longest)
    found.sort(key=lambda x: (-len(x[0]), x[2]))
    used_ranges = []
    filtered = []
    for entity, etype, start in found:
        end = start + len(entity)
        if not any(s <= start < e or s < end <= e for s, e in used_ranges):
            filtered.append((entity, etype))
            used_ranges.append((start, end))

    return filtered


def detect_intent_type(text):
    """If no entity found, detect intent type from keywords."""
    text_lower = text.lower()
    for etype, keywords in INTENT_KEYWORDS.items():
        if any(kw in text_lower for kw in keywords):
            return etype
    return "PHONE"


def predict_single(context, entity, entity_type):
    """Run model on a single entity."""
    text = f"{context} </s> {entity} | {entity_type}"
    inputs = tokenizer(text, max_length=256, padding="max_length", truncation=True, return_tensors="pt")

    with torch.no_grad():
        outputs = model(**inputs)
        probs = F.softmax(outputs.logits, dim=-1)

    pii_prob = probs[0][1].item()
    not_pii_prob = probs[0][0].item()
    return pii_prob, not_pii_prob


def classify(message: str):
    if not message.strip():
        return {}, "Please enter a message."

    entities = detect_entities(message)

    if not entities:
        # No explicit entity — check for intent
        intent_type = detect_intent_type(message)
        entities = [("NONE", intent_type)]

    # Run model on all detected entities, take the highest PII score
    best_pii = 0
    best_not_pii = 1
    best_entity = ""
    best_type = ""
    all_results = []

    for entity, etype in entities:
        pii_prob, not_pii_prob = predict_single(message, entity, etype)
        all_results.append((entity, etype, pii_prob))
        if pii_prob > best_pii:
            best_pii = pii_prob
            best_not_pii = not_pii_prob
            best_entity = entity
            best_type = etype

    label_scores = {
        "🚨 PII DETECTED": best_pii,
        "✅ NOT PII": best_not_pii,
    }

    # Build verdict
    if best_pii > best_not_pii:
        verdict = f"🚨 **PII DETECTED** — This message contains PII sharing intent.\n\n"
    else:
        verdict = f"✅ **NOT PII** — No PII sharing intent detected.\n\n"

    verdict += f"**Confidence:** {max(best_pii, best_not_pii):.1%}\n\n"

    # Show what was detected
    if len(all_results) == 1 and all_results[0][0] == "NONE":
        verdict += f"📋 No explicit entity found — analyzed message intent as `{best_type}`"
    else:
        verdict += "📋 **Detected entities:**\n"
        for ent, etype, prob in all_results:
            icon = "🚨" if prob > 0.5 else "✅"
            verdict += f"- {icon} `{ent}` ({etype}) → {'PII' if prob > 0.5 else 'NOT PII'} ({prob:.0%})\n"

    return label_scores, verdict


EXAMPLES = [
    # PII - Direct sharing
    ["my number is 05321234567 call me anytime"],
    ["numaram 05321234567 bana yazabilirsin"],
    ["رقمي 0501234567 تواصل معي"],
    # PII - Social media / email
    ["follow me on instagram @creator_official"],
    ["email me at creator@gmail.com"],
    # PII - Implicit intent
    ["i will send you my whatsapp tomorrow"],
    ["check my bio my number is right there"],
    ["if we agree on terms i will share my number"],
    # PII - Humor + real number (V11 improvement)
    ["numaram pizza siparişi gibi 05321234567 haha 😂"],
    # PII - Old number still visible
    ["my old number was 05321234567 it might still work"],
    # NOT PII - Order/tracking numbers
    ["your order number is ORD-784321"],
    ["siparis numaraniz SPN-78432156"],
    # NOT PII - Room number / postal code (V11 improvement)
    ["oda numaram 532 otelde buluşalım"],
    ["posta kodu 34720 Kadıköy İstanbul"],
    # NOT PII - Scam warning / non-contact
    ["05321234567 this number is a scammer do not call"],
    ["bake at 180 degrees celsius for 45 minutes"],
    # NOT PII - Sarcasm
    ["my number is 00000000000 lol just kidding"],
]

with gr.Blocks(
    title="PII Intent Classifier",
    theme=gr.themes.Soft(),
) as demo:
    gr.Markdown(
        """
        # 🔍 PII Intent Classifier (V11)
        ### Multilingual context-aware PII detection (Turkish, Arabic, English)

        Type any message below. The model will automatically detect entities (phone numbers, emails, handles, etc.)
        and determine if the message contains **intent to share personal contact information**.

        **Model:** [gorkem371/pii-intent-classifier-xlmr-large](https://huggingface.co/gorkem371/pii-intent-classifier-xlmr-large) — XLM-RoBERTa Large, 550M params | **F1: 99.3%** · **Conversation accuracy: 97.0%** · Trained on 41K samples
        """
    )

    with gr.Row():
        with gr.Column(scale=2):
            message_input = gr.Textbox(
                label="Message",
                placeholder="Type a message to analyze... (Turkish, Arabic, or English)",
                lines=3,
            )
            submit_btn = gr.Button("🔍 Analyze", variant="primary", size="lg")

        with gr.Column(scale=1):
            label_output = gr.Label(label="Classification", num_top_classes=2)
            verdict_output = gr.Markdown(label="Details")

    submit_btn.click(
        fn=classify,
        inputs=[message_input],
        outputs=[label_output, verdict_output],
    )

    message_input.submit(
        fn=classify,
        inputs=[message_input],
        outputs=[label_output, verdict_output],
    )

    gr.Examples(
        examples=EXAMPLES,
        inputs=[message_input],
        outputs=[label_output, verdict_output],
        fn=classify,
        cache_examples=False,
        label="💡 Try these examples",
    )

    gr.Markdown(
        """
        ---
        **How it works:** The model automatically detects entities (phone numbers, emails, @handles, URLs, IBANs) in your message
        using regex, then classifies each one for PII sharing intent using a fine-tuned XLM-RoBERTa Large model.
        If no entity is found, it analyzes the message for implicit intent (e.g., "I'll send my number later").

        **V11 improvements:** Understands humor + real numbers as PII, distinguishes room/postal/tracking numbers from real phone numbers, handles scam warnings with visible numbers, and 6 new NOT-PII categories.

        **Author:** [Gorkem Yildiz](https://gorkemyildiz.com) · **Dataset:** [pii-intent-detection-multilingual](https://huggingface.co/datasets/gorkem371/pii-intent-detection-multilingual) (41,427 samples)
        """
    )

demo.launch()