multimodalart HF Staff commited on
Commit
04e6488
·
verified ·
1 Parent(s): c6c0fcb

NER tab: render entities with gr.HighlightedText

Browse files
Files changed (1) hide show
  1. app.py +88 -13
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: Include confidence scores in the output.
59
- include_spans: Include character offsets in the output.
 
 
 
60
  """
61
  if not text.strip():
62
- return json.dumps({"error": "Please enter some text."}, indent=2)
63
  if not labels.strip():
64
- return json.dumps({"error": "Please specify at least one entity type."}, indent=2)
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=include_confidence,
73
- include_spans=include_spans,
74
  )
75
- return json.dumps(result, indent=2, default=str)
76
  except Exception as e:
77
- return json.dumps({"error": str(e)}, indent=2)
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="Include confidence")
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.Code(label="Results (JSON)", language="json", lines=18)
 
 
 
 
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, ner_spans],
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
  )