multimodalart HF Staff commited on
Commit
105df6a
·
verified ·
1 Parent(s): 41567fa

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. README.md +21 -7
  2. app.py +220 -0
  3. gliner_config.json +147 -0
  4. requirements.txt +2 -0
README.md CHANGED
@@ -1,13 +1,27 @@
1
  ---
2
- title: Gliner Stream Pii Demo
3
- emoji: 📚
4
- colorFrom: pink
5
- colorTo: purple
6
  sdk: gradio
7
  sdk_version: 6.20.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: GLiNER Streaming PII Detector
3
+ emoji: 🔍
4
+ colorFrom: gray
5
+ colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.20.0
 
8
  app_file: app.py
9
+ short_description: Zero-shot PII detection with GLiNER streaming-span model
10
+ python_version: "3.12"
11
+ startup_duration_timeout: 30m
12
  ---
13
 
14
+ # GLiNER Streaming PII Detector
15
+
16
+ This Space demonstrates [knowledgator/gliner-stream-pii-v1.0](https://huggingface.co/knowledgator/gliner-stream-pii-v1.0),
17
+ a 0.6B-parameter zero-shot PII/NER model built on a Qwen3 backbone using the
18
+ GLiNER streaming-span architecture.
19
+
20
+ ## How it works
21
+
22
+ - Paste any text that may contain personally identifiable information (PII).
23
+ - Specify the entity types you want to detect (person, email, phone number, etc.).
24
+ - Adjust the confidence threshold to control precision vs. recall.
25
+ - The model highlights detected entities directly in the text and lists them in a table.
26
+
27
+ The model is **open-label** — you can use any entity type name, not just the defaults.
app.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spaces # MUST come before torch / any CUDA-touching import
2
+ import torch
3
+ import json
4
+ import re
5
+ import gradio as gr
6
+ from gliner import GLiNER
7
+
8
+ MODEL_ID = "knowledgator/gliner-stream-pii-v1.0"
9
+
10
+ # Load the model at module scope, .to("cuda") eagerly (ZeroGPU rule 2)
11
+ model = GLiNER.from_pretrained(
12
+ MODEL_ID,
13
+ load_tokenizer=True,
14
+ map_location="cuda",
15
+ dtype="bfloat16",
16
+ ).eval()
17
+
18
+ # Default PII labels from the model card
19
+ DEFAULT_LABELS = [
20
+ "person",
21
+ "email address",
22
+ "phone number",
23
+ "street address",
24
+ "credit card number",
25
+ "passport number",
26
+ "date of birth",
27
+ "social security number",
28
+ "bank account number",
29
+ "organization",
30
+ ]
31
+
32
+ # Pastel-ish colors per entity label for highlighting
33
+ LABEL_COLORS = [
34
+ "#FF6B6B", # red-ish
35
+ "#4ECDC4", # teal
36
+ "#95E1D3", # green
37
+ "#FFE66D", # yellow
38
+ "#FF8A5C", # orange
39
+ "#C7B8EA", # purple
40
+ "#6CB7FF", # blue
41
+ "#F4A4C0", # pink
42
+ "#B8E986", # lime
43
+ "#E0BBE4", # lavender
44
+ ]
45
+
46
+
47
+ def _color_for_label(label: str) -> str:
48
+ idx = hash(label) % len(LABEL_COLORS)
49
+ return LABEL_COLORS[idx]
50
+
51
+
52
+ def _highlight_html(text: str, entities: list) -> str:
53
+ """Build an HTML string that highlights detected entities in the input text."""
54
+ if not entities:
55
+ return text.replace("<", "&lt;").replace(">", "&gt;")
56
+
57
+ # Sort by start position; handle overlaps by taking longest-first
58
+ sorted_ents = sorted(entities, key=lambda e: (e.get("start", 0), -(e.get("end", 0) - e.get("start", 0))))
59
+ # De-overlap: greedy filter
60
+ filtered = []
61
+ last_end = -1
62
+ for ent in sorted_ents:
63
+ s = ent.get("start", 0)
64
+ e = ent.get("end", 0)
65
+ if s >= last_end:
66
+ filtered.append(ent)
67
+ last_end = e
68
+ # Re-sort by start for rendering
69
+ filtered.sort(key=lambda ent: ent.get("start", 0))
70
+
71
+ parts = []
72
+ pos = 0
73
+ for ent in filtered:
74
+ s = ent["start"]
75
+ e = ent["end"]
76
+ # Escaped plain text before this entity
77
+ parts.append(text[pos:s].replace("<", "&lt;").replace(">", "&gt;"))
78
+ ent_text = text[s:e].replace("<", "&lt;").replace(">", "&gt;")
79
+ label = ent["label"]
80
+ color = _color_for_label(label)
81
+ score = ent.get("score", 0)
82
+ tooltip = f"{label} (score: {score:.2f})"
83
+ parts.append(
84
+ f'<span style="background:{color}; border-radius:3px; '
85
+ f'padding:1px 3px; font-weight:600;" title="{tooltip}">{ent_text}</span>'
86
+ )
87
+ pos = e
88
+ parts.append(text[pos:].replace("<", "&lt;").replace(">", "&gt;"))
89
+ return "".join(parts)
90
+
91
+
92
+ @spaces.GPU(duration=30)
93
+ def detect_pii(text: str, labels_text: str, threshold: float):
94
+ """Detect PII entities in text using a zero-shot GLiNER model.
95
+
96
+ Args:
97
+ text: The input text to analyze for PII.
98
+ labels_text: Comma-separated list of entity types to detect.
99
+ threshold: Confidence threshold for entity detection (0-1).
100
+ """
101
+ if not text.strip():
102
+ return "<p style='color:gray;'>Enter some text to analyze…</p>", []
103
+
104
+ labels = [l.strip() for l in labels_text.split(",") if l.strip()]
105
+ if not labels:
106
+ labels = DEFAULT_LABELS
107
+
108
+ entities = model.predict_entities(text, labels, threshold=threshold)
109
+
110
+ # Build highlighted HTML
111
+ html = _highlight_html(text, entities)
112
+
113
+ # Build table data
114
+ table_rows = [
115
+ [ent["text"], ent["label"], f"{ent.get('score', 0):.3f}",
116
+ ent.get("start", 0), ent.get("end", 0)]
117
+ for ent in sorted(entities, key=lambda e: e.get("start", 0))
118
+ ]
119
+
120
+ return html, table_rows
121
+
122
+
123
+ CSS = """
124
+ #col-container { max-width: 1100px; margin: 0 auto; }
125
+ .dark .gradio-container { color: var(--body-text-color); }
126
+ """
127
+
128
+ EXAMPLES = [
129
+ [
130
+ "Jane Doe can be reached at jane.doe@example.com or at +1 (415) 555-0132. "
131
+ "Her credit card number is 4532-1234-5678-9010 and she lives at 123 Main St, San Francisco, CA.",
132
+ "person, email address, phone number, credit card number, street address",
133
+ 0.5,
134
+ ],
135
+ [
136
+ "Patient John Smith (DOB: 03/15/1980, SSN: 123-45-6789) visited Mercy Hospital on Jan 5, 2025. "
137
+ "Contact: john.smith@email.com, account 9988776655.",
138
+ "person, date of birth, social security number, organization, email address, bank account number",
139
+ 0.5,
140
+ ],
141
+ [
142
+ "Dear Sir, my name is Alice Chen and I would like to update my billing info. "
143
+ "My passport number is KL7890123 and my phone is 555-0199.",
144
+ "person, passport number, phone number",
145
+ 0.5,
146
+ ],
147
+ ]
148
+
149
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
150
+ with gr.Column(elem_id="col-container"):
151
+ gr.Markdown("# GLiNER Streaming PII Detector")
152
+ gr.Markdown(
153
+ "Zero-shot PII/NER detection with "
154
+ "[knowledgator/gliner-stream-pii-v1.0](https://huggingface.co/knowledgator/gliner-stream-pii-v1.0) — "
155
+ "a 0.6B streaming-span model built on a Qwen3 backbone. Enter any text and "
156
+ "the entity types you want to detect; the model highlights them in-place."
157
+ )
158
+
159
+ with gr.Row():
160
+ text_input = gr.Textbox(
161
+ label="Input text",
162
+ placeholder="Paste or type text that may contain PII…",
163
+ lines=8,
164
+ scale=4,
165
+ )
166
+ run_btn = gr.Button("Detect PII", variant="primary", scale=1)
167
+
168
+ with gr.Row():
169
+ labels_input = gr.Textbox(
170
+ label="Entity labels (comma-separated)",
171
+ value=", ".join(DEFAULT_LABELS),
172
+ scale=3,
173
+ )
174
+ threshold_slider = gr.Slider(
175
+ label="Confidence threshold",
176
+ minimum=0.0,
177
+ maximum=1.0,
178
+ value=0.5,
179
+ step=0.05,
180
+ scale=1,
181
+ )
182
+
183
+ gr.Markdown("### Highlighted output")
184
+ highlighted_output = gr.HTML(
185
+ value="<p style='color:gray;'>Enter some text and click Detect PII…</p>"
186
+ )
187
+
188
+ gr.Markdown("### Detected entities")
189
+ entities_table = gr.Dataframe(
190
+ headers=["Entity text", "Label", "Score", "Start", "End"],
191
+ datatype=["str", "str", "str", "number", "number"],
192
+ value=[],
193
+ interactive=False,
194
+ wrap=True,
195
+ )
196
+
197
+ run_btn.click(
198
+ fn=detect_pii,
199
+ inputs=[text_input, labels_input, threshold_slider],
200
+ outputs=[highlighted_output, entities_table],
201
+ api_name="detect_pii",
202
+ )
203
+
204
+ text_input.submit(
205
+ fn=detect_pii,
206
+ inputs=[text_input, labels_input, threshold_slider],
207
+ outputs=[highlighted_output, entities_table],
208
+ api_name="detect_pii_submit",
209
+ )
210
+
211
+ gr.Examples(
212
+ examples=EXAMPLES,
213
+ inputs=[text_input, labels_input, threshold_slider],
214
+ outputs=[highlighted_output, entities_table],
215
+ fn=detect_pii,
216
+ cache_examples=True,
217
+ cache_mode="lazy",
218
+ )
219
+
220
+ demo.launch(mcp_server=True)
gliner_config.json ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "class_token_index": 151669,
3
+ "decoder_config": {
4
+ "_name_or_path": "Qwen/Qwen3-0.6B",
5
+ "architectures": [
6
+ "Qwen3ForCausalLM"
7
+ ],
8
+ "attention_bias": false,
9
+ "attention_dropout": 0.0,
10
+ "bos_token_id": 151643,
11
+ "chunk_size_feed_forward": 0,
12
+ "dtype": "bfloat16",
13
+ "eos_token_id": 151645,
14
+ "head_dim": 128,
15
+ "hidden_act": "silu",
16
+ "hidden_size": 1024,
17
+ "id2label": {
18
+ "0": "LABEL_0",
19
+ "1": "LABEL_1"
20
+ },
21
+ "initializer_range": 0.02,
22
+ "intermediate_size": 3072,
23
+ "is_encoder_decoder": false,
24
+ "label2id": {
25
+ "LABEL_0": 0,
26
+ "LABEL_1": 1
27
+ },
28
+ "layer_types": [
29
+ "full_attention",
30
+ "full_attention",
31
+ "full_attention",
32
+ "full_attention",
33
+ "full_attention",
34
+ "full_attention",
35
+ "full_attention",
36
+ "full_attention",
37
+ "full_attention",
38
+ "full_attention",
39
+ "full_attention",
40
+ "full_attention",
41
+ "full_attention",
42
+ "full_attention",
43
+ "full_attention",
44
+ "full_attention",
45
+ "full_attention",
46
+ "full_attention",
47
+ "full_attention",
48
+ "full_attention",
49
+ "full_attention",
50
+ "full_attention",
51
+ "full_attention",
52
+ "full_attention",
53
+ "full_attention",
54
+ "full_attention",
55
+ "full_attention",
56
+ "full_attention"
57
+ ],
58
+ "max_position_embeddings": 40960,
59
+ "max_window_layers": 28,
60
+ "model_type": "qwen3",
61
+ "num_attention_heads": 16,
62
+ "num_hidden_layers": 28,
63
+ "num_key_value_heads": 8,
64
+ "output_attentions": false,
65
+ "output_hidden_states": false,
66
+ "pad_token_id": null,
67
+ "problem_type": null,
68
+ "return_dict": true,
69
+ "rms_norm_eps": 1e-06,
70
+ "rope_parameters": {
71
+ "rope_theta": 1000000,
72
+ "rope_type": "default"
73
+ },
74
+ "sliding_window": null,
75
+ "tie_word_embeddings": true,
76
+ "use_cache": true,
77
+ "use_sliding_window": false,
78
+ "vocab_size": 151671
79
+ },
80
+ "dropout": 0.3,
81
+ "embed_ent_token": true,
82
+ "encoder_config": null,
83
+ "ent_token": "<<ENT>>",
84
+ "eos_token_id": 151645,
85
+ "fine_tune": true,
86
+ "fuse_layers": false,
87
+ "hidden_size": 1024,
88
+ "id_to_classes": null,
89
+ "label_token": "<<LABEL>>",
90
+ "labels_encoder_config": {
91
+ "attention_probs_dropout_prob": 0.1,
92
+ "bos_token_id": null,
93
+ "eos_token_id": null,
94
+ "hidden_act": "gelu",
95
+ "hidden_dropout_prob": 0.1,
96
+ "hidden_size": 1024,
97
+ "initializer_range": 0.02,
98
+ "intermediate_size": 4096,
99
+ "layer_norm_eps": 1e-07,
100
+ "legacy": true,
101
+ "max_position_embeddings": 512,
102
+ "max_relative_positions": 512,
103
+ "model_type": "deberta-v2",
104
+ "num_attention_heads": 16,
105
+ "num_hidden_layers": 2,
106
+ "pad_token_id": 0,
107
+ "pooler_dropout": 0.0,
108
+ "pooler_hidden_act": "gelu",
109
+ "pooler_hidden_size": 1024,
110
+ "pos_att_type": [
111
+ "p2c",
112
+ "c2p"
113
+ ],
114
+ "position_biased_input": true,
115
+ "relative_attention": true,
116
+ "tie_word_embeddings": true,
117
+ "type_vocab_size": 0,
118
+ "vocab_size": 128100
119
+ },
120
+ "max_cache_length": null,
121
+ "max_len": 8192,
122
+ "max_neg_type_ratio": 1,
123
+ "max_types": 100,
124
+ "max_width": 12,
125
+ "model_name": "Qwen/Qwen3-0.6B",
126
+ "model_type": "gliner_streaming_span",
127
+ "name": "streaming span gliner",
128
+ "neg_spans_ratio": 1.0,
129
+ "num_post_fusion_layers": 1,
130
+ "num_rnn_layers": 0,
131
+ "pad_token_id": 151643,
132
+ "post_fusion_schema": "",
133
+ "precomputed_prompts_mode": null,
134
+ "represent_spans": false,
135
+ "right_context_width": 12,
136
+ "sep_token": "<<SEP>>",
137
+ "sep_token_index": 151670,
138
+ "span_encoder_config": null,
139
+ "span_loss_coef": 1.0,
140
+ "span_mode": "markerV2",
141
+ "subtoken_pooling": "first",
142
+ "token_loss_coef": 1.0,
143
+ "transformers_version": "5.6.2",
144
+ "use_cache": false,
145
+ "vocab_size": 151671,
146
+ "words_splitter_type": "whitespace"
147
+ }
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gliner>=0.2.28
2
+ torch