Spaces:
Running on Zero
Running on Zero
NER tab: render entities with gr.HighlightedText
Browse files
app.py
CHANGED
|
@@ -40,13 +40,82 @@ def _parse_labels(label_text: str):
|
|
| 40 |
return list(labels.keys())
|
| 41 |
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
@spaces.GPU(duration=15)
|
| 44 |
def extract_entities(
|
| 45 |
text: str,
|
| 46 |
labels: str,
|
| 47 |
threshold: float = 0.5,
|
| 48 |
include_confidence: bool = True,
|
| 49 |
-
include_spans: bool = True,
|
| 50 |
):
|
| 51 |
"""Extract named entities from text using a user-defined label schema.
|
| 52 |
|
|
@@ -55,13 +124,16 @@ def extract_entities(
|
|
| 55 |
labels: Entity types, one per line. Use ``label::description`` for
|
| 56 |
richer prompts (e.g. ``person::individual human``).
|
| 57 |
threshold: Confidence threshold (0–1). Lower finds more, noisier matches.
|
| 58 |
-
include_confidence:
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 60 |
"""
|
| 61 |
if not text.strip():
|
| 62 |
-
return
|
| 63 |
if not labels.strip():
|
| 64 |
-
return
|
| 65 |
|
| 66 |
try:
|
| 67 |
entity_types = _parse_labels(labels)
|
|
@@ -69,12 +141,12 @@ def extract_entities(
|
|
| 69 |
text,
|
| 70 |
entity_types,
|
| 71 |
threshold=threshold,
|
| 72 |
-
include_confidence=
|
| 73 |
-
include_spans=
|
| 74 |
)
|
| 75 |
-
return
|
| 76 |
except Exception as e:
|
| 77 |
-
return
|
| 78 |
|
| 79 |
|
| 80 |
@spaces.GPU(duration=15)
|
|
@@ -303,11 +375,14 @@ with gr.Blocks(
|
|
| 303 |
0.0, 1.0, value=0.5, step=0.05,
|
| 304 |
label="Confidence Threshold",
|
| 305 |
)
|
| 306 |
-
ner_conf = gr.Checkbox(value=True, label="
|
| 307 |
-
ner_spans = gr.Checkbox(value=True, label="Include spans")
|
| 308 |
ner_btn = gr.Button("Extract Entities", variant="primary")
|
| 309 |
with gr.Column(scale=2):
|
| 310 |
-
ner_out = gr.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
gr.Examples(
|
| 312 |
examples=NER_EXAMPLES,
|
| 313 |
inputs=[ner_text, ner_labels, ner_threshold],
|
|
@@ -318,7 +393,7 @@ with gr.Blocks(
|
|
| 318 |
)
|
| 319 |
ner_btn.click(
|
| 320 |
fn=extract_entities,
|
| 321 |
-
inputs=[ner_text, ner_labels, ner_threshold, ner_conf
|
| 322 |
outputs=ner_out,
|
| 323 |
api_name="/extract_entities",
|
| 324 |
)
|
|
|
|
| 40 |
return list(labels.keys())
|
| 41 |
|
| 42 |
|
| 43 |
+
def _to_highlighted(text: str, result: dict, include_confidence: bool = True):
|
| 44 |
+
"""Convert a GLiNER2 entity result into ``gr.HighlightedText`` spans.
|
| 45 |
+
|
| 46 |
+
Returns a list of ``(substring, label_or_None)`` tuples covering the whole
|
| 47 |
+
input text, so unmatched text is rendered plain and matches are highlighted
|
| 48 |
+
with their entity type.
|
| 49 |
+
"""
|
| 50 |
+
entities = (result or {}).get("entities") or {}
|
| 51 |
+
found = [] # (start, end, label, confidence)
|
| 52 |
+
|
| 53 |
+
for label, items in entities.items():
|
| 54 |
+
if not isinstance(items, list):
|
| 55 |
+
items = [items]
|
| 56 |
+
cursor = 0
|
| 57 |
+
for item in items:
|
| 58 |
+
if isinstance(item, dict):
|
| 59 |
+
surface = item.get("text", "")
|
| 60 |
+
start = item.get("start", item.get("char_start"))
|
| 61 |
+
end = item.get("end", item.get("char_end"))
|
| 62 |
+
conf = item.get("confidence")
|
| 63 |
+
else:
|
| 64 |
+
surface, start, end, conf = str(item), None, None, None
|
| 65 |
+
if start is not None and end is not None:
|
| 66 |
+
start, end = int(start), int(end)
|
| 67 |
+
# Trim whitespace that token boundaries may have swallowed.
|
| 68 |
+
while start < end and text[start].isspace():
|
| 69 |
+
start += 1
|
| 70 |
+
while end > start and text[end - 1].isspace():
|
| 71 |
+
end -= 1
|
| 72 |
+
if surface and text[start:end].strip() != surface.strip():
|
| 73 |
+
start = end = None # offsets don't match: fall back to search
|
| 74 |
+
if start is None or end is None:
|
| 75 |
+
# Fall back to locating the surface form in the text.
|
| 76 |
+
if not surface:
|
| 77 |
+
continue
|
| 78 |
+
start = text.find(surface, cursor)
|
| 79 |
+
if start == -1:
|
| 80 |
+
start = text.find(surface)
|
| 81 |
+
if start == -1:
|
| 82 |
+
continue
|
| 83 |
+
end = start + len(surface)
|
| 84 |
+
cursor = end
|
| 85 |
+
if end <= start:
|
| 86 |
+
continue
|
| 87 |
+
found.append((int(start), int(end), str(label), conf))
|
| 88 |
+
|
| 89 |
+
# Drop overlaps (keep the higher-confidence / longer span) so the text can
|
| 90 |
+
# be sliced into a clean sequence of segments.
|
| 91 |
+
found.sort(key=lambda s: (-(s[3] if s[3] is not None else 0.0), -(s[1] - s[0])))
|
| 92 |
+
kept = []
|
| 93 |
+
for span in found:
|
| 94 |
+
if all(span[1] <= k[0] or span[0] >= k[1] for k in kept):
|
| 95 |
+
kept.append(span)
|
| 96 |
+
kept.sort(key=lambda s: s[0])
|
| 97 |
+
|
| 98 |
+
segments = []
|
| 99 |
+
index = 0
|
| 100 |
+
for start, end, label, conf in kept:
|
| 101 |
+
if start > index:
|
| 102 |
+
segments.append((text[index:start], None))
|
| 103 |
+
tag = f"{label} ({conf:.2f})" if include_confidence and conf is not None else label
|
| 104 |
+
segments.append((text[start:end], tag))
|
| 105 |
+
index = end
|
| 106 |
+
if index < len(text):
|
| 107 |
+
segments.append((text[index:], None))
|
| 108 |
+
if not segments:
|
| 109 |
+
segments = [(text, None)]
|
| 110 |
+
return segments
|
| 111 |
+
|
| 112 |
+
|
| 113 |
@spaces.GPU(duration=15)
|
| 114 |
def extract_entities(
|
| 115 |
text: str,
|
| 116 |
labels: str,
|
| 117 |
threshold: float = 0.5,
|
| 118 |
include_confidence: bool = True,
|
|
|
|
| 119 |
):
|
| 120 |
"""Extract named entities from text using a user-defined label schema.
|
| 121 |
|
|
|
|
| 124 |
labels: Entity types, one per line. Use ``label::description`` for
|
| 125 |
richer prompts (e.g. ``person::individual human``).
|
| 126 |
threshold: Confidence threshold (0–1). Lower finds more, noisier matches.
|
| 127 |
+
include_confidence: Show confidence scores next to the entity labels.
|
| 128 |
+
|
| 129 |
+
Returns:
|
| 130 |
+
A list of ``(substring, entity_type_or_None)`` tuples for
|
| 131 |
+
``gr.HighlightedText``.
|
| 132 |
"""
|
| 133 |
if not text.strip():
|
| 134 |
+
return [("Please enter some text.", "error")]
|
| 135 |
if not labels.strip():
|
| 136 |
+
return [("Please specify at least one entity type.", "error")]
|
| 137 |
|
| 138 |
try:
|
| 139 |
entity_types = _parse_labels(labels)
|
|
|
|
| 141 |
text,
|
| 142 |
entity_types,
|
| 143 |
threshold=threshold,
|
| 144 |
+
include_confidence=True,
|
| 145 |
+
include_spans=True,
|
| 146 |
)
|
| 147 |
+
return _to_highlighted(text, result, include_confidence=include_confidence)
|
| 148 |
except Exception as e:
|
| 149 |
+
return [(f"Error: {e}", "error")]
|
| 150 |
|
| 151 |
|
| 152 |
@spaces.GPU(duration=15)
|
|
|
|
| 375 |
0.0, 1.0, value=0.5, step=0.05,
|
| 376 |
label="Confidence Threshold",
|
| 377 |
)
|
| 378 |
+
ner_conf = gr.Checkbox(value=True, label="Show confidence in labels")
|
|
|
|
| 379 |
ner_btn = gr.Button("Extract Entities", variant="primary")
|
| 380 |
with gr.Column(scale=2):
|
| 381 |
+
ner_out = gr.HighlightedText(
|
| 382 |
+
label="Extracted Entities",
|
| 383 |
+
combine_adjacent=True,
|
| 384 |
+
show_legend=True,
|
| 385 |
+
)
|
| 386 |
gr.Examples(
|
| 387 |
examples=NER_EXAMPLES,
|
| 388 |
inputs=[ner_text, ner_labels, ner_threshold],
|
|
|
|
| 393 |
)
|
| 394 |
ner_btn.click(
|
| 395 |
fn=extract_entities,
|
| 396 |
+
inputs=[ner_text, ner_labels, ner_threshold, ner_conf],
|
| 397 |
outputs=ner_out,
|
| 398 |
api_name="/extract_entities",
|
| 399 |
)
|