Upload folder using huggingface_hub
Browse files- __pycache__/model.cpython-312.pyc +0 -0
- fixtures/clinical.json +138 -21
- fixtures/clinical.txt +7 -0
- fixtures/review.json +22 -22
- fixtures/review.txt +5 -0
- model.py +95 -90
__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":
|
| 5 |
-
"end":
|
| 6 |
"text": "chest pain",
|
| 7 |
"label": "condition",
|
| 8 |
-
"score": 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
"attributes": {
|
| 10 |
"assertion": [
|
| 11 |
{
|
| 12 |
"label": "absent",
|
| 13 |
-
"score": 0.
|
| 14 |
}
|
| 15 |
]
|
| 16 |
}
|
| 17 |
},
|
| 18 |
{
|
| 19 |
-
"start":
|
| 20 |
-
"end":
|
| 21 |
-
"text": "
|
| 22 |
"label": "condition",
|
| 23 |
-
"score": 0.
|
| 24 |
"attributes": {
|
| 25 |
"assertion": [
|
| 26 |
{
|
| 27 |
"label": "absent",
|
| 28 |
-
"score": 0.
|
| 29 |
}
|
| 30 |
]
|
| 31 |
}
|
| 32 |
},
|
| 33 |
{
|
| 34 |
-
"start":
|
| 35 |
-
"end":
|
| 36 |
-
"text": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
"label": "condition",
|
| 38 |
-
"score": 0.
|
| 39 |
"attributes": {
|
| 40 |
"assertion": [
|
| 41 |
{
|
| 42 |
-
"label": "
|
| 43 |
-
"score": 0.
|
| 44 |
}
|
| 45 |
]
|
| 46 |
}
|
| 47 |
},
|
| 48 |
{
|
| 49 |
-
"start":
|
| 50 |
-
"end":
|
| 51 |
-
"text": "
|
| 52 |
"label": "condition",
|
| 53 |
-
"score": 0.
|
| 54 |
"attributes": {
|
| 55 |
"assertion": [
|
| 56 |
{
|
| 57 |
-
"label": "
|
| 58 |
-
"score": 0.
|
| 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":
|
| 5 |
-
"end":
|
| 6 |
-
"text": "
|
| 7 |
"label": "product",
|
| 8 |
-
"score": 0.
|
| 9 |
"attributes": {
|
| 10 |
"sentiment": [
|
| 11 |
{
|
| 12 |
-
"label": "
|
| 13 |
-
"score": 0.
|
| 14 |
}
|
| 15 |
]
|
| 16 |
}
|
| 17 |
},
|
| 18 |
{
|
| 19 |
-
"start":
|
| 20 |
-
"end":
|
| 21 |
-
"text": "
|
| 22 |
"label": "product",
|
| 23 |
-
"score": 0.
|
| 24 |
"attributes": {
|
| 25 |
"sentiment": [
|
| 26 |
{
|
| 27 |
-
"label": "
|
| 28 |
-
"score": 0.
|
| 29 |
}
|
| 30 |
]
|
| 31 |
}
|
| 32 |
},
|
| 33 |
{
|
| 34 |
-
"start":
|
| 35 |
-
"end":
|
| 36 |
-
"text": "
|
| 37 |
"label": "product",
|
| 38 |
-
"score": 0.
|
| 39 |
"attributes": {
|
| 40 |
"sentiment": [
|
| 41 |
{
|
| 42 |
"label": "negative",
|
| 43 |
-
"score": 0.
|
| 44 |
}
|
| 45 |
]
|
| 46 |
}
|
| 47 |
},
|
| 48 |
{
|
| 49 |
-
"start":
|
| 50 |
-
"end":
|
| 51 |
-
"text": "
|
| 52 |
"label": "product",
|
| 53 |
-
"score": 0.
|
| 54 |
"attributes": {
|
| 55 |
"sentiment": [
|
| 56 |
{
|
| 57 |
"label": "negative",
|
| 58 |
-
"score": 0.
|
| 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 |
-
|
| 3 |
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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({"
|
| 30 |
-
' "
|
| 31 |
-
' "
|
| 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"])
|
| 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(
|
|
|
|
| 47 |
),
|
| 48 |
"examples": [
|
| 49 |
-
{"chip": "
|
| 50 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 102 |
f'"start": {e["start"]}, "end": {e["end"]}')
|
| 103 |
-
if
|
| 104 |
-
|
| 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
|
| 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 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 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 |
-
"
|
| 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}
|