m-newhauser commited on
Commit
d538019
·
verified ·
1 Parent(s): 0a06cc6

Upload folder using huggingface_hub

Browse files
__pycache__/model.cpython-312.pyc CHANGED
Binary files a/__pycache__/model.cpython-312.pyc and b/__pycache__/model.cpython-312.pyc differ
 
fixtures/clinical.json CHANGED
@@ -1,64 +1,181 @@
1
  {
2
  "spans": [
3
  {
4
- "start": 15,
5
- "end": 25,
6
  "text": "chest pain",
7
  "label": "condition",
8
- "score": 0.94,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  "attributes": {
10
  "assertion": [
11
  {
12
  "label": "absent",
13
- "score": 0.93
14
  }
15
  ]
16
  }
17
  },
18
  {
19
- "start": 42,
20
- "end": 51,
21
- "text": "pneumonia",
22
  "label": "condition",
23
- "score": 0.96,
24
  "attributes": {
25
  "assertion": [
26
  {
27
  "label": "absent",
28
- "score": 0.95
29
  }
30
  ]
31
  }
32
  },
33
  {
34
- "start": 62,
35
- "end": 73,
36
- "text": "mild anemia",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  "label": "condition",
38
- "score": 0.87,
39
  "attributes": {
40
  "assertion": [
41
  {
42
- "label": "possible",
43
- "score": 0.81
44
  }
45
  ]
46
  }
47
  },
48
  {
49
- "start": 86,
50
- "end": 98,
51
- "text": "hypertension",
52
  "label": "condition",
53
- "score": 0.95,
54
  "attributes": {
55
  "assertion": [
56
  {
57
- "label": "historical",
58
- "score": 0.92
59
  }
60
  ]
61
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  }
63
  ]
64
  }
 
1
  {
2
  "spans": [
3
  {
4
+ "start": 107,
5
+ "end": 117,
6
  "text": "chest pain",
7
  "label": "condition",
8
+ "score": 0.9160624146461487,
9
+ "attributes": {
10
+ "assertion": [
11
+ {
12
+ "label": "present",
13
+ "score": 0.772571325302124
14
+ }
15
+ ]
16
+ }
17
+ },
18
+ {
19
+ "start": 219,
20
+ "end": 231,
21
+ "text": "hypertension",
22
+ "label": "condition",
23
+ "score": 0.9371581077575684,
24
+ "attributes": {
25
+ "assertion": [
26
+ {
27
+ "label": "present",
28
+ "score": 0.6551124453544617
29
+ }
30
+ ]
31
+ }
32
+ },
33
+ {
34
+ "start": 273,
35
+ "end": 283,
36
+ "text": "amlodipine",
37
+ "label": "medication",
38
+ "score": 0.9881898164749146,
39
+ "attributes": {}
40
+ },
41
+ {
42
+ "start": 324,
43
+ "end": 345,
44
+ "text": "myocardial infarction",
45
+ "label": "condition",
46
+ "score": 0.9385595917701721,
47
+ "attributes": {
48
+ "assertion": [
49
+ {
50
+ "label": "present",
51
+ "score": 0.3682917058467865
52
+ }
53
+ ]
54
+ }
55
+ },
56
+ {
57
+ "start": 428,
58
+ "end": 453,
59
+ "text": "jugular venous distention",
60
+ "label": "condition",
61
+ "score": 0.7140679955482483,
62
  "attributes": {
63
  "assertion": [
64
  {
65
  "label": "absent",
66
+ "score": 0.9634988307952881
67
  }
68
  ]
69
  }
70
  },
71
  {
72
+ "start": 499,
73
+ "end": 515,
74
+ "text": "peripheral edema",
75
  "label": "condition",
76
+ "score": 0.8489705920219421,
77
  "attributes": {
78
  "assertion": [
79
  {
80
  "label": "absent",
81
+ "score": 0.9043235182762146
82
  }
83
  ]
84
  }
85
  },
86
  {
87
+ "start": 517,
88
+ "end": 520,
89
+ "text": "ECG",
90
+ "label": "procedure",
91
+ "score": 0.5720260143280029,
92
+ "attributes": {}
93
+ },
94
+ {
95
+ "start": 551,
96
+ "end": 559,
97
+ "text": "troponin",
98
+ "label": "medication",
99
+ "score": 0.8206470608711243,
100
+ "attributes": {}
101
+ },
102
+ {
103
+ "start": 598,
104
+ "end": 609,
105
+ "text": "Chest x-ray",
106
+ "label": "procedure",
107
+ "score": 0.5801931619644165,
108
+ "attributes": {}
109
+ },
110
+ {
111
+ "start": 631,
112
+ "end": 640,
113
+ "text": "pneumonia",
114
  "label": "condition",
115
+ "score": 0.8876052498817444,
116
  "attributes": {
117
  "assertion": [
118
  {
119
+ "label": "absent",
120
+ "score": 0.5739515423774719
121
  }
122
  ]
123
  }
124
  },
125
  {
126
+ "start": 695,
127
+ "end": 710,
128
+ "text": "unstable angina",
129
  "label": "condition",
130
+ "score": 0.8392333388328552,
131
  "attributes": {
132
  "assertion": [
133
  {
134
+ "label": "possible",
135
+ "score": 0.982353687286377
136
  }
137
  ]
138
  }
139
+ },
140
+ {
141
+ "start": 712,
142
+ "end": 722,
143
+ "text": "Cardiology",
144
+ "label": "procedure",
145
+ "score": 0.5568299889564514,
146
+ "attributes": {}
147
+ },
148
+ {
149
+ "start": 771,
150
+ "end": 778,
151
+ "text": "Aspirin",
152
+ "label": "medication",
153
+ "score": 0.9122529029846191,
154
+ "attributes": {}
155
+ },
156
+ {
157
+ "start": 913,
158
+ "end": 931,
159
+ "text": "Discharge planning",
160
+ "label": "procedure",
161
+ "score": 0.6805453896522522,
162
+ "attributes": {}
163
+ },
164
+ {
165
+ "start": 943,
166
+ "end": 953,
167
+ "text": "amlodipine",
168
+ "label": "medication",
169
+ "score": 0.9821869134902954,
170
+ "attributes": {}
171
+ },
172
+ {
173
+ "start": 971,
174
+ "end": 1002,
175
+ "text": "outpatient cardiology follow-up",
176
+ "label": "procedure",
177
+ "score": 0.5434748530387878,
178
+ "attributes": {}
179
  }
180
  ]
181
  }
fixtures/clinical.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ CHART NOTE - Internal Medicine - Admission Day 1
2
+
3
+ Patient is a 58-year-old male admitted for evaluation of chest pain that began two days ago while climbing stairs. He denies any radiation to the jaw or arm. History of hypertension, diagnosed eight years ago, managed with amlodipine. Family history notable for father with myocardial infarction at age 61.
4
+
5
+ On examination, blood pressure 148 over 92, heart rate 88 regular, no jugular venous distention. Lungs clear to auscultation bilaterally. No peripheral edema. ECG shows nonspecific ST changes; troponin within normal limits, repeat pending. Chest x-ray shows no evidence of pneumonia or congestive failure.
6
+
7
+ Assessment and plan: Possible unstable angina. Cardiology consulted for possible stress testing tomorrow. Aspirin started. Patient counseled on smoking cessation; he reports smoking one pack per day for thirty years. Follow-up lipid panel ordered. Discharge planning to include amlodipine continuation and outpatient cardiology follow-up within one week.
fixtures/review.json CHANGED
@@ -1,61 +1,61 @@
1
  {
2
  "spans": [
3
  {
4
- "start": 4,
5
- "end": 10,
6
- "text": "screen",
7
  "label": "product",
8
- "score": 0.96,
9
  "attributes": {
10
  "sentiment": [
11
  {
12
- "label": "positive",
13
- "score": 0.95
14
  }
15
  ]
16
  }
17
  },
18
  {
19
- "start": 31,
20
- "end": 39,
21
- "text": "keyboard",
22
  "label": "product",
23
- "score": 0.93,
24
  "attributes": {
25
  "sentiment": [
26
  {
27
- "label": "positive",
28
- "score": 0.89
29
  }
30
  ]
31
  }
32
  },
33
  {
34
- "start": 61,
35
- "end": 68,
36
- "text": "battery",
37
  "label": "product",
38
- "score": 0.95,
39
  "attributes": {
40
  "sentiment": [
41
  {
42
  "label": "negative",
43
- "score": 0.94
44
  }
45
  ]
46
  }
47
  },
48
  {
49
- "start": 94,
50
- "end": 103,
51
- "text": "fan noise",
52
  "label": "product",
53
- "score": 0.88,
54
  "attributes": {
55
  "sentiment": [
56
  {
57
  "label": "negative",
58
- "score": 0.92
59
  }
60
  ]
61
  }
 
1
  {
2
  "spans": [
3
  {
4
+ "start": 25,
5
+ "end": 41,
6
+ "text": "Nimbus 14 laptop",
7
  "label": "product",
8
+ "score": 0.9600220918655396,
9
  "attributes": {
10
  "sentiment": [
11
  {
12
+ "label": "negative",
13
+ "score": 0.975648820400238
14
  }
15
  ]
16
  }
17
  },
18
  {
19
+ "start": 224,
20
+ "end": 233,
21
+ "text": "Nimbus 14",
22
  "label": "product",
23
+ "score": 0.906394898891449,
24
  "attributes": {
25
  "sentiment": [
26
  {
27
+ "label": "negative",
28
+ "score": 0.9998642206192017
29
  }
30
  ]
31
  }
32
  },
33
  {
34
+ "start": 408,
35
+ "end": 419,
36
+ "text": "USB-C ports",
37
  "label": "product",
38
+ "score": 0.5385769605636597,
39
  "attributes": {
40
  "sentiment": [
41
  {
42
  "label": "negative",
43
+ "score": 0.8142373561859131
44
  }
45
  ]
46
  }
47
  },
48
  {
49
+ "start": 510,
50
+ "end": 525,
51
+ "text": "docking station",
52
  "label": "product",
53
+ "score": 0.8726967573165894,
54
  "attributes": {
55
  "sentiment": [
56
  {
57
  "label": "negative",
58
+ "score": 0.9747726917266846
59
  }
60
  ]
61
  }
fixtures/review.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ We have been running the Nimbus 14 laptop as the primary machine for our design team for six months now, and the experience has been genuinely mixed.
2
+
3
+ The screen is gorgeous and the keyboard feels great. Battery life on the Nimbus 14 is disappointing, and the fan noise is unacceptable during rendering sessions. Our studio lead praised the trackpad and the hinge, but two junior designers reported that the USB-C ports feel loose after a few weeks.
4
+
5
+ Support has been responsive on software questions, but the docking station we ordered never arrived and nobody could tell us where it went.
model.py CHANGED
@@ -1,10 +1,13 @@
1
  """Span attributes demo: entities carry per-span attribute groups (sentiment,
2
- role, assertion, impact), single- or multi-label, scoped with applies_to.
3
 
4
- MOCK until GLiNER 2.5 ships. Response contract:
 
 
 
 
5
  {"spans": [{"start", "end", "text", "label", "score",
6
  "attributes": {"<group>": [{"label": str, "score": float}]}}]}
7
- Multi-label groups return several entries in the group list.
8
  """
9
 
10
  import json
@@ -12,7 +15,6 @@ import os
12
 
13
  MOCK = os.environ.get("GLINER_MOCK", "1") == "1"
14
  MODEL_ID = os.environ.get("MODEL_ID", "fastino/gliner2.5-multi-v1")
15
- MODEL_URL = os.environ.get("MODEL_URL", f"https://huggingface.co/{MODEL_ID}")
16
 
17
  DEMO = {
18
  "title": "Sentiment and role, <em>per span</em>",
@@ -25,56 +27,98 @@ DEMO = {
25
  'extractor = AutoExtractor.from_pretrained("fastino/gliner2.5-multi-v1", map_location="cpu")\n'
26
  'extractor.float()\n'
27
  '\n'
 
 
28
  'schema = (extractor.create_schema()\n'
29
- ' .entities({"product": "A product mentioned in the text",\n'
30
- ' "person": "A person by name",\n'
31
- ' "condition": "A medical condition"})\n'
32
  ' .entity_attributes({\n'
33
- ' "sentiment": AttributeGroup(\n'
34
- ' labels=["positive", "neutral", "negative"],\n'
35
- ' applies_to=["product"], qualify_labels=True),\n'
36
- ' "role": AttributeGroup(\n'
37
- ' labels=["executive", "employee", "customer", "analyst"],\n'
38
- ' applies_to=["person"]),\n'
39
  ' "assertion": AttributeGroup(\n'
40
  ' labels=["present", "absent", "possible", "historical"],\n'
41
- ' applies_to=["condition"]),\n'
42
- ' "impact": AttributeGroup(\n'
43
- ' labels=["blocks_work", "data_loss", "security_risk"],\n'
44
- ' multi_label=True, threshold=0.40)}))\n'
45
  '\n'
46
- 'result = extractor.extract(text, schema, include_spans=True, include_confidence=True)'
 
47
  ),
48
  "examples": [
49
- {"chip": "Mixed review",
50
- "text": "The screen is gorgeous and the keyboard feels great, but the battery is disappointing and the fan noise is unacceptable.",
51
- "fixture": "review.json"},
52
- {"chip": "Org announcement",
53
- "text": "CEO Maya Chen announced that CFO Daniel Okafor will lead the acquisition, while analyst Priya Nair briefed reporters.",
54
- "fixture": "org.json"},
55
- {"chip": "Clinical note",
56
- "text": "Patient denies chest pain. No evidence of pneumonia. Possible mild anemia; history of hypertension noted.",
57
- "fixture": "clinical.json"},
58
- {"chip": "Support ticket",
59
- "text": "The export button silently deletes rows, which blocked our quarterly report and risks losing audited data.",
60
- "fixture": "support.json"},
61
  ],
62
  }
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  _HERE = os.path.dirname(os.path.abspath(__file__))
65
 
 
66
  def config():
67
  """DEMO for /api/config, with result previews baked from fixtures."""
68
  out = {k: v for k, v in DEMO.items() if k != "code"}
69
  out["mock"] = MOCK
70
  out["examples"] = []
71
  for ex in DEMO["examples"]:
72
- ex2 = {**ex, "code": DEMO["code"]}
73
  ex2["result"] = _result_preview(ex["fixture"])
74
  out["examples"].append(ex2)
75
  return out
76
 
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  def _result_preview(fixture_name, per_type=2):
79
  """Notebook-style Out[1] preview: the API response shape, truncated per label."""
80
  with open(os.path.join(_HERE, "fixtures", fixture_name)) as f:
@@ -83,14 +127,7 @@ def _result_preview(fixture_name, per_type=2):
83
  for sp in out["spans"]:
84
  entities.setdefault(sp["label"], [])
85
  if len(entities[sp["label"]]) < per_type:
86
- entities[sp["label"]].append({
87
- "text": sp["text"],
88
- "confidence": sp["score"],
89
- "start": sp["start"],
90
- "end": sp["end"],
91
- **({"attributes": {g: [{ "label": a["label"], "score": a["score"]} for a in es]
92
- for g, es in sp["attributes"].items()}} if sp.get("attributes") else {}),
93
- })
94
  counts = {}
95
  for sp in out["spans"]:
96
  counts[sp["label"]] = counts.get(sp["label"], 0) + 1
@@ -98,11 +135,12 @@ def _result_preview(fixture_name, per_type=2):
98
  for label, entries in entities.items():
99
  entry_strs = []
100
  for e in entries:
101
- base = (f'{{"text": {json.dumps(e["text"])}, "confidence": {e["confidence"]}, '
 
 
102
  f'"start": {e["start"]}, "end": {e["end"]}')
103
- if e.get("attributes"):
104
- attrs = json.dumps(e["attributes"])
105
- base += f', "attributes": {attrs}'
106
  base += "}"
107
  entry_strs.append(base)
108
  more = counts[label] - len(entries)
@@ -113,24 +151,6 @@ def _result_preview(fixture_name, per_type=2):
113
  return f'{{\n "entities": {{\n' + ",\n".join(body) + "\n }\n}"
114
 
115
 
116
-
117
- def warmup():
118
- if MOCK:
119
- for ex in DEMO["examples"]:
120
- _fixture(ex["fixture"], ex["text"])
121
- else:
122
- _load_real()
123
-
124
-
125
- def infer(data):
126
- text = (data.get("text") or "").replace("\r", "")
127
- if not text.strip():
128
- return {"spans": []}
129
- if MOCK:
130
- return _infer_mock(text)
131
- return _infer_real(text)
132
-
133
-
134
  def _fixture(name, expect_text=None):
135
  with open(os.path.join(_HERE, "fixtures", name)) as f:
136
  out = json.load(f)
@@ -143,7 +163,7 @@ def _fixture(name, expect_text=None):
143
 
144
  def _infer_mock(text):
145
  for ex in DEMO["examples"]:
146
- if ex["text"] == text:
147
  return _fixture(ex["fixture"])
148
  return {"spans": []}
149
 
@@ -161,42 +181,27 @@ def _load_real():
161
  def _infer_real(text):
162
  from gliner2 import AttributeGroup
163
 
164
- schema = (_model.create_schema()
165
- .entities({
166
- "product": "A product mentioned in the text",
167
- "person": "A person by name",
168
- "condition": "A medical condition",
169
- })
170
- .entity_attributes({
171
- "sentiment": AttributeGroup(
172
- labels=["positive", "neutral", "negative"],
173
- applies_to=["product"], qualify_labels=True),
174
- "role": AttributeGroup(
175
- labels=["executive", "employee", "customer", "analyst"],
176
- applies_to=["person"]),
177
- "assertion": AttributeGroup(
178
- labels=["present", "absent", "possible", "historical"],
179
- applies_to=["condition"]),
180
- "impact": AttributeGroup(
181
- labels=["blocks_work", "data_loss", "security_risk"],
182
- multi_label=True, threshold=0.40),
183
- }))
184
 
185
  result = _model.extract(text, schema, include_spans=True, include_confidence=True)
186
 
187
- # Convert API response to demo contract: flat list of spans with attributes
188
  spans = []
189
  for label, entries in result.get("entities", {}).items():
190
  for e in entries:
191
  span = {
192
- "start": e["start"],
193
- "end": e["end"],
194
- "text": e["text"],
195
- "label": label,
196
- "score": e.get("confidence", 1.0),
197
  "attributes": {},
198
  }
199
- # Extract attribute groups (any key that's not text/start/end/confidence)
200
  for k, v in e.items():
201
  if k not in ("text", "start", "end", "confidence"):
202
  if isinstance(v, dict) and "label" in v:
@@ -204,5 +209,5 @@ def _infer_real(text):
204
  elif isinstance(v, list):
205
  span["attributes"][k] = [{"label": x["label"], "score": x.get("confidence", 1.0)} for x in v]
206
  spans.append(span)
207
-
208
  return {"spans": spans}
 
1
  """Span attributes demo: entities carry per-span attribute groups (sentiment,
2
+ assertion, role), single- or multi-label, scoped with applies_to.
3
 
4
+ Two examples: a clinical note (conditions with assertion status, medications,
5
+ procedures) and a mixed product review (products with sentiment, people with
6
+ role). Free-typed text falls back to the review schema.
7
+
8
+ Response contract:
9
  {"spans": [{"start", "end", "text", "label", "score",
10
  "attributes": {"<group>": [{"label": str, "score": float}]}}]}
 
11
  """
12
 
13
  import json
 
15
 
16
  MOCK = os.environ.get("GLINER_MOCK", "1") == "1"
17
  MODEL_ID = os.environ.get("MODEL_ID", "fastino/gliner2.5-multi-v1")
 
18
 
19
  DEMO = {
20
  "title": "Sentiment and role, <em>per span</em>",
 
27
  'extractor = AutoExtractor.from_pretrained("fastino/gliner2.5-multi-v1", map_location="cpu")\n'
28
  'extractor.float()\n'
29
  '\n'
30
+ 'note = open("chart_note.txt").read()\n'
31
+ '\n'
32
  'schema = (extractor.create_schema()\n'
33
+ ' .entities({"medication": "A medication or drug mentioned in the note",\n'
34
+ ' "condition": "A medical condition, diagnosis, or symptom",\n'
35
+ ' "procedure": "A medical procedure or test"})\n'
36
  ' .entity_attributes({\n'
 
 
 
 
 
 
37
  ' "assertion": AttributeGroup(\n'
38
  ' labels=["present", "absent", "possible", "historical"],\n'
39
+ ' applies_to=["condition"])}))\n'
 
 
 
40
  '\n'
41
+ 'result = extractor.extract(note, schema, include_spans=True, include_confidence=True)\n'
42
+ '# -> {"entities": {"condition": [{"text": "chest pain", "assertion": {"label": "present"}, ...}], ...}}'
43
  ),
44
  "examples": [
45
+ {"chip": "Clinical note", "fixture": "clinical.json"},
46
+ {"chip": "Mixed review", "fixture": "review.json"},
 
 
 
 
 
 
 
 
 
 
47
  ],
48
  }
49
 
50
+ # Per-example schemas: entity types + attribute groups differ by domain.
51
+ # Keys match the fixture .txt basenames.
52
+ EXAMPLE_SCHEMAS = {
53
+ "clinical": {
54
+ "entities": {
55
+ "medication": "A medication or drug mentioned in the note",
56
+ "condition": "A medical condition, diagnosis, or symptom",
57
+ "procedure": "A medical procedure or test",
58
+ },
59
+ "attributes": {
60
+ "assertion": (["present", "absent", "possible", "historical"], ["condition"]),
61
+ },
62
+ },
63
+ "review": {
64
+ "entities": {
65
+ "product": "A product or product component mentioned in the review",
66
+ "person": "A person by name or role",
67
+ },
68
+ "attributes": {
69
+ "sentiment": (["positive", "neutral", "negative"], ["product"]),
70
+ "role": (["executive", "employee", "customer", "analyst"], ["person"]),
71
+ },
72
+ },
73
+ }
74
+
75
  _HERE = os.path.dirname(os.path.abspath(__file__))
76
 
77
+
78
  def config():
79
  """DEMO for /api/config, with result previews baked from fixtures."""
80
  out = {k: v for k, v in DEMO.items() if k != "code"}
81
  out["mock"] = MOCK
82
  out["examples"] = []
83
  for ex in DEMO["examples"]:
84
+ ex2 = {**ex, "code": DEMO["code"], "text": _text(ex)}
85
  ex2["result"] = _result_preview(ex["fixture"])
86
  out["examples"].append(ex2)
87
  return out
88
 
89
 
90
+ def warmup():
91
+ if MOCK:
92
+ for ex in DEMO["examples"]:
93
+ _fixture(ex["fixture"], _text(ex))
94
+ else:
95
+ _load_real()
96
+
97
+
98
+ def infer(data):
99
+ text = (data.get("text") or "").replace("\r", "")
100
+ if not text.strip():
101
+ return {"spans": []}
102
+ if MOCK:
103
+ return _infer_mock(text)
104
+ return _infer_real(text)
105
+
106
+
107
+ def _text(ex):
108
+ txt = ex.get("text")
109
+ if txt is None:
110
+ with open(os.path.join(_HERE, "fixtures", ex["fixture"].replace(".json", ".txt"))) as f:
111
+ txt = f.read().rstrip("\n")
112
+ return txt
113
+
114
+
115
+ def _schema_key(text):
116
+ for ex in DEMO["examples"]:
117
+ if _text(ex) == text:
118
+ return ex["fixture"].replace(".json", "")
119
+ return None
120
+
121
+
122
  def _result_preview(fixture_name, per_type=2):
123
  """Notebook-style Out[1] preview: the API response shape, truncated per label."""
124
  with open(os.path.join(_HERE, "fixtures", fixture_name)) as f:
 
127
  for sp in out["spans"]:
128
  entities.setdefault(sp["label"], [])
129
  if len(entities[sp["label"]]) < per_type:
130
+ entities[sp["label"]].append(sp)
 
 
 
 
 
 
 
131
  counts = {}
132
  for sp in out["spans"]:
133
  counts[sp["label"]] = counts.get(sp["label"], 0) + 1
 
135
  for label, entries in entities.items():
136
  entry_strs = []
137
  for e in entries:
138
+ attrs = {g: [{"label": a["label"], "score": a["score"]} for a in es]
139
+ for g, es in e.get("attributes", {}).items() if es}
140
+ base = (f'{{"text": {json.dumps(e["text"])}, "confidence": {e["score"]}, '
141
  f'"start": {e["start"]}, "end": {e["end"]}')
142
+ if attrs:
143
+ base += f', "attributes": {json.dumps(attrs)}'
 
144
  base += "}"
145
  entry_strs.append(base)
146
  more = counts[label] - len(entries)
 
151
  return f'{{\n "entities": {{\n' + ",\n".join(body) + "\n }\n}"
152
 
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  def _fixture(name, expect_text=None):
155
  with open(os.path.join(_HERE, "fixtures", name)) as f:
156
  out = json.load(f)
 
163
 
164
  def _infer_mock(text):
165
  for ex in DEMO["examples"]:
166
+ if _text(ex) == text:
167
  return _fixture(ex["fixture"])
168
  return {"spans": []}
169
 
 
181
  def _infer_real(text):
182
  from gliner2 import AttributeGroup
183
 
184
+ key = _schema_key(text)
185
+ cfg = EXAMPLE_SCHEMAS.get(key, EXAMPLE_SCHEMAS["review"])
186
+
187
+ schema = _model.create_schema().entities(cfg["entities"])
188
+ attr_groups = {}
189
+ for group, (labels, applies_to) in cfg["attributes"].items():
190
+ kwargs = {"applies_to": applies_to} if applies_to else {}
191
+ attr_groups[group] = AttributeGroup(labels=labels, **kwargs)
192
+ if attr_groups:
193
+ schema = schema.entity_attributes(attr_groups)
 
 
 
 
 
 
 
 
 
 
194
 
195
  result = _model.extract(text, schema, include_spans=True, include_confidence=True)
196
 
 
197
  spans = []
198
  for label, entries in result.get("entities", {}).items():
199
  for e in entries:
200
  span = {
201
+ "start": e["start"], "end": e["end"], "text": e["text"],
202
+ "label": label, "score": e.get("confidence", 1.0),
 
 
 
203
  "attributes": {},
204
  }
 
205
  for k, v in e.items():
206
  if k not in ("text", "start", "end", "confidence"):
207
  if isinstance(v, dict) and "label" in v:
 
209
  elif isinstance(v, list):
210
  span["attributes"][k] = [{"label": x["label"], "score": x.get("confidence", 1.0)} for x in v]
211
  spans.append(span)
212
+ spans.sort(key=lambda s: s["start"])
213
  return {"spans": spans}