prachuryyaIITG commited on
Commit
1c7ad34
·
verified ·
1 Parent(s): fa049ce

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +373 -4
app.py CHANGED
@@ -1,7 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
1
+ Here is the complete, self-contained `app.py` script for your **Multilingual PII Anonymizer Space**.
2
+
3
+ It uses your **4-phase hybrid architecture** targeting the **21 selected languages**, filtering strictly for **PERSON, MEDICAL, LOCATION, and ORGANIZATION** entities alongside multilingual regex rules (emails, phone numbers with native script digits, IP addresses, credit cards, and postal codes).
4
+
5
+ ### Key Features Included:
6
+
7
+ 1. **Targeted Categories Only**: Filters strictly for fine-grained tags under `PERSON`, `LOCATION`, `ORGANIZATION`, and `MEDICAL` (ignoring `Product` and `Creative Work`).
8
+ 2. **Multilingual Script Regex**: Handles localized digits across Devanagari, Bengali/Assamese, Persian/Arabic, Tamil, Telugu, and CJK full-width scripts for phones and credit cards.
9
+ 3. **Synthetic Pseudonymization**: Maps repeated mentions of the same real-world entity to the same consistent placeholder (e.g., all occurrences of *"Jude Bellingham"* map to `[PERSON_1]`).
10
+ 4. **Reverse-Offset Slicing**: Replaces entity spans from back-to-front by character offset, completely preventing UTF-8 index shift corruption in non-Latin scripts.
11
+ 5. **Reversible Anonymization Map**: Displays a side-by-side redacted result along with an interactive JSON de-anonymization dictionary mapping keys to original values.
12
+
13
+ ---
14
+
15
+ ### Complete `app.py` Code
16
+
17
+ ```python
18
+ import spaces
19
  import gradio as gr
20
+ import torch
21
+ import time
22
+ import gc
23
+ import re
24
+ import threading
25
+ from collections import OrderedDict
26
+ from transformers import pipeline, AutoTokenizer
27
+
28
+ # ZeroGPU Configuration
29
+ MAX_MODELS_LOADED = 5
30
+ MODEL_IDLE_TIMEOUT = 15 * 60 # 15 minutes
31
+ CLEANUP_INTERVAL = 15 * 60 # check every 15 minutes
32
+
33
+ # Trimmed list: 21 target languages
34
+ MODELS = {
35
+ "Assamese": "prachuryyaIITG/CLASSER_Assamese_MuRIL",
36
+ "Bengali": "prachuryyaIITG/MultiCoNER2_Bengali_XLM",
37
+ "Bodo": "prachuryyaIITG/CLASSER_Bodo_MuRIL",
38
+ "Chinese": "prachuryyaIITG/MultiCoNER2_Chinese_XLM",
39
+ "English": "prachuryyaIITG/MultiCoNER2_English_XLM",
40
+ "Farsi": "prachuryyaIITG/MultiCoNER2_Farsi_XLM",
41
+ "French": "prachuryyaIITG/MultiCoNER2_French_XLM",
42
+ "German": "prachuryyaIITG/MultiCoNER2_German_XLM",
43
+ "Hindi": "prachuryyaIITG/MultiCoNER2_Hindi_XLM",
44
+ "Italian": "prachuryyaIITG/MultiCoNER2_Italian_XLM",
45
+ "Marathi": "prachuryyaIITG/CLASSER_Marathi_MuRIL",
46
+ "Mizo": "prachuryyaIITG/FiNERVINER_Mizo_XLM",
47
+ "Nepali": "prachuryyaIITG/CLASSER_Nepali_MuRIL",
48
+ "Portuguese": "prachuryyaIITG/MultiCoNER2_Portuguese_XLM",
49
+ "Sanskrit": "prachuryyaIITG/CLASSER_Sanskrit_MuRIL",
50
+ "Spanish": "prachuryyaIITG/MultiCoNER2_Spanish_XLM",
51
+ "Swedish": "prachuryyaIITG/MultiCoNER2_Swedish_XLM",
52
+ "Tamil": "prachuryyaIITG/APTFiNER_Tamil_MuRIL",
53
+ "Telugu": "prachuryyaIITG/APTFiNER_Telugu_MuRIL",
54
+ "Ukrainian": "prachuryyaIITG/MultiCoNER2_Ukrainian_XLM",
55
+ "Urdu": "prachuryyaIITG/Urdu_CLASSER_XLM",
56
+ }
57
+
58
+ # Fine-grained tag mappings into coarse categories (Excludes Product & CW)
59
+ TAG_TO_COARSE = {
60
+ # Person
61
+ "Scientist": "PERSON", "Artist": "PERSON", "Athlete": "PERSON",
62
+ "Politician": "PERSON", "Cleric": "PERSON", "SportsManager": "PERSON",
63
+ "OtherPER": "PERSON", "PER": "PERSON", "Person": "PERSON",
64
+
65
+ # Location
66
+ "Facility": "LOCATION", "OtherLOC": "LOCATION",
67
+ "HumanSettlement": "LOCATION", "Station": "LOCATION",
68
+ "LOC": "LOCATION", "Location": "LOCATION",
69
+
70
+ # Organization / Group
71
+ "MusicalGRP": "ORGANIZATION", "PublicCORP": "ORGANIZATION",
72
+ "PrivateCORP": "ORGANIZATION", "AerospaceManufacturer": "ORGANIZATION",
73
+ "SportsGRP": "ORGANIZATION", "CarManufacturer": "ORGANIZATION",
74
+ "ORG": "ORGANIZATION", "GRP": "ORGANIZATION", "Organization": "ORGANIZATION",
75
+
76
+ # Medical
77
+ "Medication/Vaccine": "MEDICAL", "MedicalProcedure": "MEDICAL",
78
+ "AnatomicalStructure": "MEDICAL", "Symptom": "MEDICAL",
79
+ "Disease": "MEDICAL", "MED": "MEDICAL", "Medical": "MEDICAL"
80
+ }
81
+
82
+ # Cache and locking
83
+ pipelines = OrderedDict()
84
+ last_used = {}
85
+ lock = threading.Lock()
86
+
87
+ def clear_memory():
88
+ gc.collect()
89
+ if torch.cuda.is_available():
90
+ torch.cuda.empty_cache()
91
+ torch.cuda.ipc_collect()
92
+
93
+ def load_pipeline(model_id, language, use_gpu=True):
94
+ strategy = "simple" if language == "Chinese" else "first"
95
+ tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True)
96
+
97
+ if use_gpu:
98
+ device = 0
99
+ current_dtype = torch.bfloat16
100
+ else:
101
+ device = -1
102
+ current_dtype = torch.float32
103
+
104
+ return pipeline(
105
+ "ner",
106
+ model=model_id,
107
+ tokenizer=tokenizer,
108
+ aggregation_strategy=strategy,
109
+ device=device,
110
+ torch_dtype=current_dtype
111
+ )
112
+
113
+ def get_pipeline(model_id, language, use_gpu=True):
114
+ cache_key = f"{model_id}_{language}"
115
+ with lock:
116
+ now = time.time()
117
+ if cache_key in pipelines:
118
+ pipelines.move_to_end(cache_key)
119
+ last_used[cache_key] = now
120
+ return pipelines[cache_key]
121
+
122
+ while len(pipelines) >= MAX_MODELS_LOADED:
123
+ old_key, old_pipe = pipelines.popitem(last=False)
124
+ del old_pipe
125
+ last_used.pop(old_key, None)
126
+ clear_memory()
127
+
128
+ ner = load_pipeline(model_id, language, use_gpu=use_gpu)
129
+ pipelines[cache_key] = ner
130
+ last_used[cache_key] = now
131
+ return ner
132
+
133
+ def cleanup_worker():
134
+ while True:
135
+ time.sleep(CLEANUP_INTERVAL)
136
+ with lock:
137
+ now = time.time()
138
+ to_remove = [k for k, v in last_used.items() if now - v > MODEL_IDLE_TIMEOUT]
139
+ for cache_key in to_remove:
140
+ if cache_key in pipelines:
141
+ pipe = pipelines.pop(cache_key)
142
+ del pipe
143
+ last_used.pop(cache_key, None)
144
+ if to_remove:
145
+ clear_memory()
146
+
147
+ threading.Thread(target=cleanup_worker, daemon=True).start()
148
+
149
+ @spaces.GPU
150
+ def try_gpu_infer(text, language):
151
+ model_id = MODELS[language]
152
+ ner = get_pipeline(model_id, language, use_gpu=True)
153
+ return ner(text, stride=64)
154
+
155
+ def cpu_fallback_infer(text, language):
156
+ model_id = MODELS[language]
157
+ ner = get_pipeline(model_id, language, use_gpu=False)
158
+ return ner(text, stride=64)
159
+
160
+ # --- MULTILINGUAL REGEX PII ENGINE ---
161
+ def extract_regex_spans(text):
162
+ """
163
+ Extracts structured technical PII using script-aware regex rules.
164
+ Supports Western, Devanagari, Bengali/Assamese, Persian/Arabic, Tamil, Telugu, and CJK digits.
165
+ """
166
+ spans = []
167
+
168
+ # Universal Email
169
+ email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
170
+ for m in re.finditer(email_pattern, text):
171
+ spans.append({'start': m.start(), 'end': m.end(), 'category': 'EMAIL', 'text': m.group()})
172
+
173
+ # Script-Aware Phone Number Regex (Supports Western, Devanagari, Bengali, Arabic/Farsi, CJK digits)
174
+ # Digits range: 0-9, \u0966-\u096F (Devanagari), \u09E6-\u09EF (Bengali/Assamese), \u0660-\u0669/\u06F0-\u06F9 (Arabic/Farsi), \u0B66-\u0B6F (Odia), \u0BE6-\u0BEF (Tamil), \u0C66-\u0C6F (Telugu), \uFF10-\uFF19 (Fullwidth CJK)
175
+ digits = r'0-9\u0966-\u096F\u09E6-\u09EF\u0660-\u0669\u06F0-\u06F9\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F\uFF10-\uFF19'
176
+ phone_pattern = rf'(?:\+?[' + digits + r']{1,3}[-.\s]?)?\(?[' + digits + r']{2,4}\)?[-.\s]?[' + digits + r']{3,4}[-.\s]?[' + digits + r']{3,4}\b'
177
+ for m in re.finditer(phone_pattern, text):
178
+ if len(re.sub(rf'[^{digits}]', '', m.group())) >= 7: # Ensure minimum numeric digits
179
+ spans.append({'start': m.start(), 'end': m.end(), 'category': 'PHONE', 'text': m.group()})
180
+
181
+ # IP Addresses (v4)
182
+ ip_pattern = r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b'
183
+ for m in re.finditer(ip_pattern, text):
184
+ spans.append({'start': m.start(), 'end': m.end(), 'category': 'IP_ADDRESS', 'text': m.group()})
185
+
186
+ # Credit Card Numbers
187
+ card_pattern = rf'\b(?:[' + digits + r']{4}[-\s]?){3}[' + digits + r']{4}\b'
188
+ for m in re.finditer(card_pattern, text):
189
+ spans.append({'start': m.start(), 'end': m.end(), 'category': 'CREDIT_CARD', 'text': m.group()})
190
+
191
+ return spans
192
+
193
+ # --- HYBRID ALIGNMENT & PSEUDONYMIZATION PIPELINE ---
194
+ def process_pii_anonymization(text, language):
195
+ if not text.strip():
196
+ return "", [], {}
197
+
198
+ # Phase 1: Model Inference (FgNER)
199
+ try:
200
+ raw_ner_results = try_gpu_infer(text, language)
201
+ except Exception as e:
202
+ print(f"Switching to CPU Fallback due to: {e}")
203
+ raw_ner_results = cpu_fallback_infer(text, language)
204
+
205
+ # Filter FgNER spans to only PERSON, LOCATION, ORGANIZATION, MEDICAL
206
+ ner_spans = []
207
+ for res in raw_ner_results:
208
+ entity_type = res.get('entity_group', res.get('entity', ''))
209
+ # Normalize entity tag string
210
+ entity_clean = entity_type.replace("B-", "").replace("I-", "").split("_")[0]
211
+
212
+ if entity_clean in TAG_TO_COARSE:
213
+ coarse_cat = TAG_TO_COARSE[entity_clean]
214
+ ner_spans.append({
215
+ 'start': int(res['start']),
216
+ 'end': int(res['end']),
217
+ 'category': coarse_cat,
218
+ 'text': res['word'] if 'word' in res else text[int(res['start']):int(res['end'])]
219
+ })
220
+
221
+ # Phase 2: Regex Scanning
222
+ regex_spans = extract_regex_spans(text)
223
+
224
+ # Phase 3: Conflict Resolution & Deduplication
225
+ all_spans = sorted(regex_spans + ner_spans, key=lambda x: (x['start'], -(x['end'] - x['start'])))
226
+ filtered_spans = []
227
+
228
+ for current in all_spans:
229
+ overlap = False
230
+ for kept in filtered_spans:
231
+ # Check for character boundary overlap
232
+ if not (current['end'] <= kept['start'] or current['start'] >= kept['end']):
233
+ overlap = True
234
+ break
235
+ if not overlap:
236
+ filtered_spans.append(current)
237
+
238
+ # Re-sort spans chronologically by start position
239
+ filtered_spans = sorted(filtered_spans, key=lambda x: x['start'])
240
+
241
+ # Phase 4: Pseudonymization Mapping & Reverse Offset Replacement
242
+ category_counters = {}
243
+ entity_mapping = {} # Original text -> Pseudonym placeholder
244
+ reverse_mapping = {} # Pseudonym placeholder -> Original text
245
+
246
+ highlighted_entities = []
247
+
248
+ # First pass: Assign pseudonyms consistently across occurrences
249
+ for span in filtered_spans:
250
+ original_val = text[span['start']:span['end']]
251
+ cat = span['category']
252
+
253
+ if original_val not in entity_mapping:
254
+ category_counters[cat] = category_counters.get(cat, 0) + 1
255
+ pseudonym = f"[{cat}_{category_counters[cat]}]"
256
+ entity_mapping[original_val] = pseudonym
257
+ reverse_mapping[pseudonym] = original_val
258
+ else:
259
+ pseudonym = entity_mapping[original_val]
260
+
261
+ span['pseudonym'] = pseudonym
262
+ highlighted_entities.append((span['start'], span['end'], cat))
263
+
264
+ # Reverse-offset slicing (Back-to-Front) to prevent index shift corruption
265
+ sanitized_text = text
266
+ for span in reversed(filtered_spans):
267
+ start = span['start']
268
+ end = span['end']
269
+ pseudonym = span['pseudonym']
270
+ sanitized_text = sanitized_text[:start] + pseudonym + sanitized_text[end:]
271
+
272
+ # Format for Gradio HighlightedText visualization
273
+ gradio_highlights = []
274
+ last_idx = 0
275
+ for span in filtered_spans:
276
+ if span['start'] > last_idx:
277
+ gradio_highlights.append((text[last_idx:span['start']], None))
278
+ gradio_highlights.append((text[span['start']:span['end']], span['category']))
279
+ last_idx = span['end']
280
+ if last_idx < len(text):
281
+ gradio_highlights.append((text[last_idx:], None))
282
+
283
+ return sanitized_text, gradio_highlights, reverse_mapping
284
+
285
+ # --- GRADIO UI CONFIGURATION ---
286
+
287
+ custom_css = """
288
+ body, .gradio-container {
289
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif !important;
290
+ }
291
+ #action-button {
292
+ background-color: #00568b !important;
293
+ color: white !important;
294
+ border: none !important;
295
+ }
296
+ #action-button:hover {
297
+ background-color: #00488b !important;
298
+ }
299
+ """
300
+
301
+ with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as demo:
302
+
303
+ gr.Markdown("# Multilingual PII Anonymizer & Synthetic Pseudonymizer")
304
+ gr.Markdown("Remove and anonymize sensitive PII (**PERSON**, **LOCATION**, **ORGANIZATION**, **MEDICAL**, Emails, Phone Numbers, IPs, Credit Cards) across **21 languages** using Fine-Grained NER + Hybrid Multilingual Regex Rules.")
305
+
306
+ with gr.Row():
307
+ lang_dropdown = gr.Dropdown(
308
+ choices=list(MODELS.keys()),
309
+ value="English",
310
+ label="1. Select Language"
311
+ )
312
+
313
+ input_text = gr.Textbox(
314
+ value="Jude Bellingham joined Real Madrid in 2023. You can reach him at jude@realmadrid.es or +1-555-0199.",
315
+ placeholder="Type or paste multilingual text here...",
316
+ label="2. Input Text",
317
+ lines=4
318
+ )
319
+
320
+ submit_btn = gr.Button(
321
+ "Anonymize PII",
322
+ variant="primary",
323
+ elem_id="action-button"
324
+ )
325
+
326
+ with gr.Row():
327
+ sanitized_output = gr.Textbox(
328
+ label="3. Anonymized Text (Synthetic Pseudonyms)",
329
+ lines=4,
330
+ interactive=False
331
+ )
332
+
333
+ with gr.Row():
334
+ visual_diff = gr.HighlightedText(
335
+ label="Detected PII Entities",
336
+ combine_adjacent=True
337
+ )
338
+
339
+ mapping_json = gr.JSON(
340
+ label="De-Anonymization Dictionary Map"
341
+ )
342
+
343
+ def run_pii_app(text, language):
344
+ sanitized, highlights, mapping = process_pii_anonymization(text, language)
345
+ return sanitized, highlights, mapping
346
+
347
+ submit_btn.click(
348
+ fn=run_pii_app,
349
+ inputs=[input_text, lang_dropdown],
350
+ outputs=[sanitized_output, visual_diff, mapping_json],
351
+ api_name="anonymize"
352
+ )
353
+
354
+ gr.Markdown("### Try Multilingual Examples:")
355
 
356
+ gr.Examples(
357
+ examples=[
358
+ ["Jude Bellingham joined Real Madrid in 2023. You can reach him at jude@realmadrid.es or +1-555-0199.", "English"],
359
+ ["姚明出生于上海。联系电话是 +86-13800138000。", "Chinese"],
360
+ ["अमिताभ बच्चन मुंबई में रहते हैं। उनका ईमेल contact@bachchan.com है।", "Hindi"],
361
+ ["Madrid es la capital de España. Contactar con Dr. Garcia al +34-912-345-678.", "Spanish"],
362
+ ["সকলোৱে ভাল পায় জুবিন গাৰ্গক। গুৱাহাটীত তেওঁৰ ঘৰ।", "Assamese"],
363
+ ["Albert Einstein wurde in Ulm geboren. Er litt an Diabetes.", "German"],
364
+ ["مرزا غالب دہلی میں رہتے تھے۔", "Urdu"],
365
+ ["Victor Hugo est né à Besançon. Appelez le +33-1-4268-5300.", "French"],
366
+ ["Dante Alighieri è nato a Firenze.", "Italian"],
367
+ ["ਰਬਿੰਦਰਨਾਥ ਟੈਗੋਰ ਦਾ ਜਨਮ ਕੋਲਕਾਤਾ ਵਿੱਚ ਹੋਇਆ ਸੀ।", "Punjabi"],
368
+ ],
369
+ inputs=[input_text, lang_dropdown],
370
+ outputs=[sanitized_output, visual_diff, mapping_json],
371
+ fn=run_pii_app,
372
+ cache_examples=False
373
+ )
374
 
375
+ if __name__ == "__main__":
376
+ demo.queue(max_size=20).launch(show_error=True)