prachuryyaIITG commited on
Commit
7aaabc8
·
verified ·
1 Parent(s): 962e8f0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +63 -70
app.py CHANGED
@@ -4,6 +4,7 @@ import torch
4
  import time
5
  import gc
6
  import re
 
7
  import threading
8
  from collections import OrderedDict
9
  from transformers import pipeline, AutoTokenizer
@@ -38,7 +39,7 @@ MODELS = {
38
  "Urdu": "prachuryyaIITG/Urdu_CLASSER_XLM",
39
  }
40
 
41
- # Fine-grained tag mappings into coarse categories (Excludes Product & CW)
42
  TAG_TO_COARSE = {
43
  # Person
44
  "Scientist": "PERSON", "Artist": "PERSON", "Athlete": "PERSON",
@@ -140,12 +141,32 @@ def cpu_fallback_infer(text, language):
140
  ner = get_pipeline(model_id, language, use_gpu=False)
141
  return ner(text, stride=64)
142
 
143
- # --- MULTILINGUAL REGEX PII ENGINE ---
144
- def extract_regex_spans(text):
 
 
145
  """
146
- Extracts structured technical PII using script-aware regex rules.
147
- Supports Western, Devanagari, Bengali/Assamese, Persian/Arabic, Tamil, Telugu, and CJK digits.
148
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  spans = []
150
 
151
  # Universal Email
@@ -153,15 +174,14 @@ def extract_regex_spans(text):
153
  for m in re.finditer(email_pattern, text):
154
  spans.append({'start': m.start(), 'end': m.end(), 'category': 'EMAIL', 'text': m.group()})
155
 
156
- # Script-Aware Phone Number Regex (Supports Western, Devanagari, Bengali, Arabic/Farsi, CJK digits)
157
- # 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)
158
  digits = r'0-9\u0966-\u096F\u09E6-\u09EF\u0660-\u0669\u06F0-\u06F9\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F\uFF10-\uFF19'
159
  phone_pattern = rf'(?:\+?[' + digits + r']{1,3}[-.\s]?)?\(?[' + digits + r']{2,4}\)?[-.\s]?[' + digits + r']{3,4}[-.\s]?[' + digits + r']{3,4}\b'
160
  for m in re.finditer(phone_pattern, text):
161
- if len(re.sub(rf'[^{digits}]', '', m.group())) >= 7: # Ensure minimum numeric digits
162
  spans.append({'start': m.start(), 'end': m.end(), 'category': 'PHONE', 'text': m.group()})
163
 
164
- # IP Addresses (v4)
165
  ip_pattern = r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b'
166
  for m in re.finditer(ip_pattern, text):
167
  spans.append({'start': m.start(), 'end': m.end(), 'category': 'IP_ADDRESS', 'text': m.group()})
@@ -173,10 +193,10 @@ def extract_regex_spans(text):
173
 
174
  return spans
175
 
176
- # --- HYBRID ALIGNMENT & PSEUDONYMIZATION PIPELINE ---
177
  def process_pii_anonymization(text, language):
178
  if not text.strip():
179
- return "", [], {}
180
 
181
  # Phase 1: Model Inference (FgNER)
182
  try:
@@ -185,21 +205,26 @@ def process_pii_anonymization(text, language):
185
  print(f"Switching to CPU Fallback due to: {e}")
186
  raw_ner_results = cpu_fallback_infer(text, language)
187
 
188
- # Filter FgNER spans to only PERSON, LOCATION, ORGANIZATION, MEDICAL
189
  ner_spans = []
190
  for res in raw_ner_results:
191
  entity_type = res.get('entity_group', res.get('entity', ''))
192
- # Normalize entity tag string
193
  entity_clean = entity_type.replace("B-", "").replace("I-", "").split("_")[0]
194
 
195
  if entity_clean in TAG_TO_COARSE:
196
  coarse_cat = TAG_TO_COARSE[entity_clean]
197
- ner_spans.append({
198
- 'start': int(res['start']),
199
- 'end': int(res['end']),
200
- 'category': coarse_cat,
201
- 'text': res['word'] if 'word' in res else text[int(res['start']):int(res['end'])]
202
- })
 
 
 
 
 
 
 
203
 
204
  # Phase 2: Regex Scanning
205
  regex_spans = extract_regex_spans(text)
@@ -211,24 +236,17 @@ def process_pii_anonymization(text, language):
211
  for current in all_spans:
212
  overlap = False
213
  for kept in filtered_spans:
214
- # Check for character boundary overlap
215
  if not (current['end'] <= kept['start'] or current['start'] >= kept['end']):
216
  overlap = True
217
  break
218
  if not overlap:
219
  filtered_spans.append(current)
220
 
221
- # Re-sort spans chronologically by start position
222
- filtered_spans = sorted(filtered_spans, key=lambda x: x['start'])
223
-
224
  # Phase 4: Pseudonymization Mapping & Reverse Offset Replacement
225
  category_counters = {}
226
- entity_mapping = {} # Original text -> Pseudonym placeholder
227
- reverse_mapping = {} # Pseudonym placeholder -> Original text
228
-
229
- highlighted_entities = []
230
 
231
- # First pass: Assign pseudonyms consistently across occurrences
232
  for span in filtered_spans:
233
  original_val = text[span['start']:span['end']]
234
  cat = span['category']
@@ -242,9 +260,8 @@ def process_pii_anonymization(text, language):
242
  pseudonym = entity_mapping[original_val]
243
 
244
  span['pseudonym'] = pseudonym
245
- highlighted_entities.append((span['start'], span['end'], cat))
246
 
247
- # Reverse-offset slicing (Back-to-Front) to prevent index shift corruption
248
  sanitized_text = text
249
  for span in reversed(filtered_spans):
250
  start = span['start']
@@ -252,18 +269,7 @@ def process_pii_anonymization(text, language):
252
  pseudonym = span['pseudonym']
253
  sanitized_text = sanitized_text[:start] + pseudonym + sanitized_text[end:]
254
 
255
- # Format for Gradio HighlightedText visualization
256
- gradio_highlights = []
257
- last_idx = 0
258
- for span in filtered_spans:
259
- if span['start'] > last_idx:
260
- gradio_highlights.append((text[last_idx:span['start']], None))
261
- gradio_highlights.append((text[span['start']:span['end']], span['category']))
262
- last_idx = span['end']
263
- if last_idx < len(text):
264
- gradio_highlights.append((text[last_idx:], None))
265
-
266
- return sanitized_text, gradio_highlights, reverse_mapping
267
 
268
  # --- GRADIO UI CONFIGURATION ---
269
 
@@ -284,7 +290,7 @@ body, .gradio-container {
284
  with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as demo:
285
 
286
  gr.Markdown("# Multilingual PII Anonymizer & Synthetic Pseudonymizer")
287
- 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.")
288
 
289
  with gr.Row():
290
  lang_dropdown = gr.Dropdown(
@@ -306,35 +312,24 @@ with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as demo:
306
  elem_id="action-button"
307
  )
308
 
309
- with gr.Row():
310
- sanitized_output = gr.Textbox(
311
- label="3. Anonymized Text (Synthetic Pseudonyms)",
312
- lines=4,
313
- interactive=False
314
- )
315
-
316
- with gr.Row():
317
- visual_diff = gr.HighlightedText(
318
- label="Detected PII Entities",
319
- combine_adjacent=True
320
- )
321
-
322
- mapping_json = gr.JSON(
323
- label="De-Anonymization Dictionary Map"
324
- )
325
-
326
- def run_pii_app(text, language):
327
- sanitized, highlights, mapping = process_pii_anonymization(text, language)
328
- return sanitized, highlights, mapping
329
 
330
  submit_btn.click(
331
- fn=run_pii_app,
332
  inputs=[input_text, lang_dropdown],
333
- outputs=[sanitized_output, visual_diff, mapping_json],
334
  api_name="anonymize"
335
  )
336
 
337
- gr.Markdown("### Try Multilingual Examples:")
338
 
339
  gr.Examples(
340
  examples=[
@@ -346,12 +341,10 @@ with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as demo:
346
  ["Albert Einstein wurde in Ulm geboren. Er litt an Diabetes.", "German"],
347
  ["مرزا غالب دہلی میں رہتے تھے۔", "Urdu"],
348
  ["Victor Hugo est né à Besançon. Appelez le +33-1-4268-5300.", "French"],
349
- ["Dante Alighieri è nato a Firenze.", "Italian"],
350
- ["ਰਬਿੰਦਰਨਾਥ ਟੈਗੋਰ ਦਾ ਜਨਮ ਕੋਲਕਾਤਾ ਵਿੱਚ ��ੋਇਆ ਸੀ।", "Punjabi"],
351
  ],
352
  inputs=[input_text, lang_dropdown],
353
- outputs=[sanitized_output, visual_diff, mapping_json],
354
- fn=run_pii_app,
355
  cache_examples=False
356
  )
357
 
 
4
  import time
5
  import gc
6
  import re
7
+ import string
8
  import threading
9
  from collections import OrderedDict
10
  from transformers import pipeline, AutoTokenizer
 
39
  "Urdu": "prachuryyaIITG/Urdu_CLASSER_XLM",
40
  }
41
 
42
+ # Fine-grained tag mappings into coarse categories
43
  TAG_TO_COARSE = {
44
  # Person
45
  "Scientist": "PERSON", "Artist": "PERSON", "Athlete": "PERSON",
 
141
  ner = get_pipeline(model_id, language, use_gpu=False)
142
  return ner(text, stride=64)
143
 
144
+ # --- PUNCTUATION & SPAN CLEANUP HELPER ---
145
+ PUNCT_PATTERN = r'^[\s\.,!?;:"\'\(\)\[\]\{\}।॥،؟’”…—]+|[\s\.,!?;:"\'\(\)\[\]\{\}।॥،؟’”…—]+$'
146
+
147
+ def clean_span_boundaries(text, start, end):
148
  """
149
+ Trims leading and trailing punctuation/whitespace from span character offsets.
150
+ Prevents punctuation attached to words (e.g. 'Real Madrid.') from being included in the entity.
151
  """
152
+ val = text[start:end]
153
+
154
+ # Trim leading punctuation
155
+ leading_match = re.search(r'^[\s\.,!?;:"\'\(\)\[\]\{\}।॥،؟’”…—]+', val)
156
+ if leading_match:
157
+ start += leading_match.end()
158
+ val = text[start:end]
159
+
160
+ # Trim trailing punctuation
161
+ trailing_match = re.search(r'[\s\.,!?;:"\'\(\)\[\]\{\}।॥،؟’”…—]+$', val)
162
+ if trailing_match:
163
+ end -= (trailing_match.end() - trailing_match.start())
164
+ val = text[start:end]
165
+
166
+ return start, end, val
167
+
168
+ # --- MULTILINGUAL REGEX PII ENGINE ---
169
+ def extract_regex_spans(text):
170
  spans = []
171
 
172
  # Universal Email
 
174
  for m in re.finditer(email_pattern, text):
175
  spans.append({'start': m.start(), 'end': m.end(), 'category': 'EMAIL', 'text': m.group()})
176
 
177
+ # Script-Aware Phone Numbers
 
178
  digits = r'0-9\u0966-\u096F\u09E6-\u09EF\u0660-\u0669\u06F0-\u06F9\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F\uFF10-\uFF19'
179
  phone_pattern = rf'(?:\+?[' + digits + r']{1,3}[-.\s]?)?\(?[' + digits + r']{2,4}\)?[-.\s]?[' + digits + r']{3,4}[-.\s]?[' + digits + r']{3,4}\b'
180
  for m in re.finditer(phone_pattern, text):
181
+ if len(re.sub(rf'[^{digits}]', '', m.group())) >= 7:
182
  spans.append({'start': m.start(), 'end': m.end(), 'category': 'PHONE', 'text': m.group()})
183
 
184
+ # IP Addresses
185
  ip_pattern = r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b'
186
  for m in re.finditer(ip_pattern, text):
187
  spans.append({'start': m.start(), 'end': m.end(), 'category': 'IP_ADDRESS', 'text': m.group()})
 
193
 
194
  return spans
195
 
196
+ # --- HYBRID ANONYMIZATION PIPELINE ---
197
  def process_pii_anonymization(text, language):
198
  if not text.strip():
199
+ return "", {}
200
 
201
  # Phase 1: Model Inference (FgNER)
202
  try:
 
205
  print(f"Switching to CPU Fallback due to: {e}")
206
  raw_ner_results = cpu_fallback_infer(text, language)
207
 
 
208
  ner_spans = []
209
  for res in raw_ner_results:
210
  entity_type = res.get('entity_group', res.get('entity', ''))
 
211
  entity_clean = entity_type.replace("B-", "").replace("I-", "").split("_")[0]
212
 
213
  if entity_clean in TAG_TO_COARSE:
214
  coarse_cat = TAG_TO_COARSE[entity_clean]
215
+ start_pos = int(res['start'])
216
+ end_pos = int(res['end'])
217
+
218
+ # Clean span boundaries from trailing/leading punctuation
219
+ start_pos, end_pos, clean_val = clean_span_boundaries(text, start_pos, end_pos)
220
+
221
+ if clean_val.strip():
222
+ ner_spans.append({
223
+ 'start': start_pos,
224
+ 'end': end_pos,
225
+ 'category': coarse_cat,
226
+ 'text': clean_val
227
+ })
228
 
229
  # Phase 2: Regex Scanning
230
  regex_spans = extract_regex_spans(text)
 
236
  for current in all_spans:
237
  overlap = False
238
  for kept in filtered_spans:
 
239
  if not (current['end'] <= kept['start'] or current['start'] >= kept['end']):
240
  overlap = True
241
  break
242
  if not overlap:
243
  filtered_spans.append(current)
244
 
 
 
 
245
  # Phase 4: Pseudonymization Mapping & Reverse Offset Replacement
246
  category_counters = {}
247
+ entity_mapping = {}
248
+ reverse_mapping = {}
 
 
249
 
 
250
  for span in filtered_spans:
251
  original_val = text[span['start']:span['end']]
252
  cat = span['category']
 
260
  pseudonym = entity_mapping[original_val]
261
 
262
  span['pseudonym'] = pseudonym
 
263
 
264
+ # Reverse-offset slicing (Back-to-Front)
265
  sanitized_text = text
266
  for span in reversed(filtered_spans):
267
  start = span['start']
 
269
  pseudonym = span['pseudonym']
270
  sanitized_text = sanitized_text[:start] + pseudonym + sanitized_text[end:]
271
 
272
+ return sanitized_text, reverse_mapping
 
 
 
 
 
 
 
 
 
 
 
273
 
274
  # --- GRADIO UI CONFIGURATION ---
275
 
 
290
  with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as demo:
291
 
292
  gr.Markdown("# Multilingual PII Anonymizer & Synthetic Pseudonymizer")
293
+ gr.Markdown("Anonymize PII (**PERSON**, **LOCATION**, **ORGANIZATION**, **MEDICAL**, Emails, Phones, IPs, Credit Cards) across **21 languages** into synthetic placeholders.")
294
 
295
  with gr.Row():
296
  lang_dropdown = gr.Dropdown(
 
312
  elem_id="action-button"
313
  )
314
 
315
+ sanitized_output = gr.Textbox(
316
+ label="3. Anonymized Text (Synthetic Pseudonyms)",
317
+ lines=4,
318
+ interactive=False
319
+ )
320
+
321
+ mapping_json = gr.JSON(
322
+ label="4. De-Anonymization Dictionary Map"
323
+ )
 
 
 
 
 
 
 
 
 
 
 
324
 
325
  submit_btn.click(
326
+ fn=process_pii_anonymization,
327
  inputs=[input_text, lang_dropdown],
328
+ outputs=[sanitized_output, mapping_json],
329
  api_name="anonymize"
330
  )
331
 
332
+ gr.Markdown("### Try Examples across Languages:")
333
 
334
  gr.Examples(
335
  examples=[
 
341
  ["Albert Einstein wurde in Ulm geboren. Er litt an Diabetes.", "German"],
342
  ["مرزا غالب دہلی میں رہتے تھے۔", "Urdu"],
343
  ["Victor Hugo est né à Besançon. Appelez le +33-1-4268-5300.", "French"],
 
 
344
  ],
345
  inputs=[input_text, lang_dropdown],
346
+ outputs=[sanitized_output, mapping_json],
347
+ fn=process_pii_anonymization,
348
  cache_examples=False
349
  )
350