sid570 commited on
Commit
2f38ad4
·
verified ·
1 Parent(s): b2b8689

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +105 -57
app.py CHANGED
@@ -11,7 +11,7 @@ with open(VOCAB_PATH, "r", encoding="utf-8") as f:
11
  VOCAB = {w.strip().lower() for w in f if w.strip()}
12
 
13
  # ============================================================
14
- # 🔢 CATEGORY PRIORITY
15
  # ============================================================
16
  CATEGORY_PRIORITY = {
17
  "DISTRICT": 1,
@@ -35,19 +35,26 @@ def tokenize(text):
35
  return re.findall(r"\w+|[.!?]", text)
36
 
37
  # ============================================================
38
- # 🔍 DICTIONARY MATCHER
39
  # ============================================================
40
  def find_dictionary_match(tokens, start_idx):
41
  for category, words in ENTITY_DICTIONARY.items():
42
  if category == "REGEX_PATTERNS":
43
  continue
 
44
  for w in words:
45
  w_tokens = w.split()
46
  span = tokens[start_idx:start_idx + len(w_tokens)]
 
47
  if len(span) != len(w_tokens):
48
  continue
 
49
  if [t.lower() for t in span] == [x.lower() for x in w_tokens]:
50
- return {"text": " ".join(span), "type": category, "length": len(w_tokens)}
 
 
 
 
51
  return None
52
 
53
  # ============================================================
@@ -56,86 +63,149 @@ def find_dictionary_match(tokens, start_idx):
56
  def detect_entities(text):
57
  tokens = tokenize(text)
58
  entities = []
 
59
  i = 0
60
  sentence_start = True
61
 
62
  while i < len(tokens):
63
  token = tokens[i]
 
 
64
  if token in [".", "!", "?"]:
65
  sentence_start = True
66
  i += 1
67
  continue
 
68
  if not token.isalpha():
69
  i += 1
70
  continue
71
 
72
- # First word logic
 
 
73
  if sentence_start:
74
  match = find_dictionary_match(tokens, i)
 
75
  if match:
76
- entities.append({"text": match["text"], "type": match["type"]})
 
 
 
77
  i += match["length"]
78
  else:
79
  if token.lower() not in VOCAB:
80
- entities.append({"text": token, "type": "UNKNOWN"})
 
 
 
81
  i += 1
 
82
  sentence_start = False
83
  continue
84
 
85
- # Dictionary priority
 
 
 
86
  match = find_dictionary_match(tokens, i)
87
  if match:
88
- entities.append({"text": match["text"], "type": match["type"]})
 
 
 
89
  i += match["length"]
90
  continue
91
 
92
- # Capital phrase grouping
93
  if token[0].isupper():
94
  phrase = [token]
95
  j = i + 1
96
- while j < len(tokens) and tokens[j].isalpha() and tokens[j][0].isupper():
 
 
 
 
 
97
  phrase.append(tokens[j])
98
  j += 1
 
99
  phrase_text = " ".join(phrase)
100
- entities.append({"text": phrase_text, "type": "UNKNOWN"})
 
 
 
 
 
101
  i = j
102
  continue
103
 
 
104
  i += 1
105
 
106
- # Regex patterns
 
 
107
  for etype, pattern in ENTITY_DICTIONARY.get("REGEX_PATTERNS", {}).items():
108
  for m in re.findall(pattern, text):
109
- entities.append({"text": m, "type": etype})
110
-
111
- # Deduplicate
112
- seen, uniq = set(), []
 
 
 
 
 
 
113
  for e in entities:
114
  key = (e["text"].lower(), e["type"])
115
  if key not in seen:
116
  seen.add(key)
117
  uniq.append(e)
 
118
  return uniq
119
 
120
  # ============================================================
121
- # 🎭 MASKING
122
  # ============================================================
123
  def detect_and_mask(text):
124
  detected = detect_entities(text)
125
- detected.sort(key=lambda e: (CATEGORY_PRIORITY.get(e["type"], 999), -len(e["text"])))
126
- masked, entity_map, counters = text, {}, {}
 
 
 
 
 
 
127
 
128
  for e in detected:
129
  cat = e["type"]
130
  counters[cat] = counters.get(cat, 0) + 1
131
  placeholder = f"<<{cat}_{counters[cat]}>>"
132
- masked = re.sub(rf"(?<!\w){re.escape(e['text'])}(?!\w)", placeholder, masked)
 
 
 
 
 
 
133
  entity_map[placeholder] = e["text"]
134
 
135
- return masked, entity_map, detected
 
 
 
 
 
 
 
 
 
 
136
 
137
  # ============================================================
138
- # 🔓 UNMASKING
139
  # ============================================================
140
  def unmask_entities(masked_text, entity_map):
141
  restored = masked_text
@@ -144,43 +214,21 @@ def unmask_entities(masked_text, entity_map):
144
  return restored
145
 
146
  # ============================================================
147
- # 🖥️ GRADIO UI (FIXED INTERFACE)
148
  # ============================================================
149
- def ui_mask_and_unmask(text):
150
- masked_text, entity_map, entities = detect_and_mask(text)
151
- unmasked_text = unmask_entities(masked_text, entity_map)
152
- return {
153
- "Masked Text": masked_text,
154
- "Entity Map": entity_map,
155
- "Entities": entities,
156
- "Unmasked Result": unmasked_text
157
- }
158
-
159
  with gr.Blocks() as demo:
160
- gr.Markdown("# 🧠 Entity Detection & Masking API (Fixed Interface)")
161
-
162
- with gr.Row():
163
- text_in = gr.Textbox(label="Input Text", lines=3, placeholder="Type sentence here...")
164
- run_btn = gr.Button("Run Masking + Unmasking")
165
-
166
- with gr.Row():
167
- masked_out = gr.Textbox(label="Masked Text", interactive=False)
168
- unmasked_out = gr.Textbox(label="Unmasked Output", interactive=False)
169
-
170
- with gr.Row():
171
- map_out = gr.JSON(label="Entity Map")
172
- ents_out = gr.JSON(label="Detected Entities")
173
-
174
- def process_pipeline(text):
175
- masked_text, entity_map, entities = detect_and_mask(text)
176
- unmasked_text = unmask_entities(masked_text, entity_map)
177
- return masked_text, unmasked_text, entity_map, entities
178
-
179
- run_btn.click(
180
- fn=process_pipeline,
181
- inputs=text_in,
182
- outputs=[masked_out, unmasked_out, map_out, ents_out]
183
- )
184
 
185
  demo.queue()
186
  demo.launch()
 
11
  VOCAB = {w.strip().lower() for w in f if w.strip()}
12
 
13
  # ============================================================
14
+ # 🔢 CATEGORY PRIORITY (lower = higher priority)
15
  # ============================================================
16
  CATEGORY_PRIORITY = {
17
  "DISTRICT": 1,
 
35
  return re.findall(r"\w+|[.!?]", text)
36
 
37
  # ============================================================
38
+ # 🔍 DICTIONARY MATCHER (multi-word, case-insensitive)
39
  # ============================================================
40
  def find_dictionary_match(tokens, start_idx):
41
  for category, words in ENTITY_DICTIONARY.items():
42
  if category == "REGEX_PATTERNS":
43
  continue
44
+
45
  for w in words:
46
  w_tokens = w.split()
47
  span = tokens[start_idx:start_idx + len(w_tokens)]
48
+
49
  if len(span) != len(w_tokens):
50
  continue
51
+
52
  if [t.lower() for t in span] == [x.lower() for x in w_tokens]:
53
+ return {
54
+ "text": " ".join(span),
55
+ "type": category,
56
+ "length": len(w_tokens)
57
+ }
58
  return None
59
 
60
  # ============================================================
 
63
  def detect_entities(text):
64
  tokens = tokenize(text)
65
  entities = []
66
+
67
  i = 0
68
  sentence_start = True
69
 
70
  while i < len(tokens):
71
  token = tokens[i]
72
+
73
+ # Sentence boundary
74
  if token in [".", "!", "?"]:
75
  sentence_start = True
76
  i += 1
77
  continue
78
+
79
  if not token.isalpha():
80
  i += 1
81
  continue
82
 
83
+ # ====================================================
84
+ # RULE 1 — FIRST WORD OF SENTENCE
85
+ # ====================================================
86
  if sentence_start:
87
  match = find_dictionary_match(tokens, i)
88
+
89
  if match:
90
+ entities.append({
91
+ "text": match["text"],
92
+ "type": match["type"]
93
+ })
94
  i += match["length"]
95
  else:
96
  if token.lower() not in VOCAB:
97
+ entities.append({
98
+ "text": token,
99
+ "type": "UNKNOWN"
100
+ })
101
  i += 1
102
+
103
  sentence_start = False
104
  continue
105
 
106
+ # ====================================================
107
+ # RULE 2 — SECOND WORD ONWARDS
108
+ # ====================================================
109
+ # Dictionary always has priority
110
  match = find_dictionary_match(tokens, i)
111
  if match:
112
+ entities.append({
113
+ "text": match["text"],
114
+ "type": match["type"]
115
+ })
116
  i += match["length"]
117
  continue
118
 
119
+ # Capital phrase grouping (e.g. Shopping Mall)
120
  if token[0].isupper():
121
  phrase = [token]
122
  j = i + 1
123
+
124
+ while (
125
+ j < len(tokens)
126
+ and tokens[j].isalpha()
127
+ and tokens[j][0].isupper()
128
+ ):
129
  phrase.append(tokens[j])
130
  j += 1
131
+
132
  phrase_text = " ".join(phrase)
133
+
134
+ entities.append({
135
+ "text": phrase_text,
136
+ "type": "UNKNOWN"
137
+ })
138
+
139
  i = j
140
  continue
141
 
142
+ # Normal word
143
  i += 1
144
 
145
+ # ====================================================
146
+ # REGEX RULES (UNCHANGED)
147
+ # ====================================================
148
  for etype, pattern in ENTITY_DICTIONARY.get("REGEX_PATTERNS", {}).items():
149
  for m in re.findall(pattern, text):
150
+ entities.append({
151
+ "text": m,
152
+ "type": etype
153
+ })
154
+
155
+ # ====================================================
156
+ # DEDUPLICATION
157
+ # ====================================================
158
+ seen = set()
159
+ uniq = []
160
  for e in entities:
161
  key = (e["text"].lower(), e["type"])
162
  if key not in seen:
163
  seen.add(key)
164
  uniq.append(e)
165
+
166
  return uniq
167
 
168
  # ============================================================
169
+ # 🎭 MASKING + AUTO-UNMASK (SAFE, BACKWARD-COMPATIBLE)
170
  # ============================================================
171
  def detect_and_mask(text):
172
  detected = detect_entities(text)
173
+
174
+ detected.sort(
175
+ key=lambda e: (CATEGORY_PRIORITY.get(e["type"], 999), -len(e["text"]))
176
+ )
177
+
178
+ masked = text
179
+ entity_map = {}
180
+ counters = {}
181
 
182
  for e in detected:
183
  cat = e["type"]
184
  counters[cat] = counters.get(cat, 0) + 1
185
  placeholder = f"<<{cat}_{counters[cat]}>>"
186
+
187
+ masked = re.sub(
188
+ rf"(?<!\w){re.escape(e['text'])}(?!\w)",
189
+ placeholder,
190
+ masked
191
+ )
192
+
193
  entity_map[placeholder] = e["text"]
194
 
195
+ # 🔑 AUTO-UNMASK (NEW, SAFE EXTENSION)
196
+ unmasked = masked
197
+ for ph in sorted(entity_map, key=len, reverse=True):
198
+ unmasked = unmasked.replace(ph, entity_map[ph])
199
+
200
+ return {
201
+ "masked_text": masked,
202
+ "entity_map": entity_map,
203
+ "entities": detected,
204
+ "unmasked_text": unmasked # ✅ NEW FIELD (non-breaking)
205
+ }
206
 
207
  # ============================================================
208
+ # 🔓 UNMASKING ENDPOINT (UNCHANGED)
209
  # ============================================================
210
  def unmask_entities(masked_text, entity_map):
211
  restored = masked_text
 
214
  return restored
215
 
216
  # ============================================================
217
+ # 🖥️ GRADIO UI
218
  # ============================================================
 
 
 
 
 
 
 
 
 
 
219
  with gr.Blocks() as demo:
220
+ gr.Markdown("# 🧠 Entity Detection & Masking API")
221
+
222
+ with gr.Tab("Detect & Mask"):
223
+ text_in = gr.Textbox(label="Input Text")
224
+ out = gr.JSON()
225
+ gr.Button("Run").click(detect_and_mask, text_in, out)
226
+
227
+ with gr.Tab("Unmask"):
228
+ masked = gr.Textbox(label="Masked Text")
229
+ entity_map = gr.JSON(label="Entity Map")
230
+ out = gr.Textbox(label="Unmasked Output")
231
+ gr.Button("Unmask").click(unmask_entities, [masked, entity_map], out)
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
  demo.queue()
234
  demo.launch()