Coverage for presidio_analyzer / predefined_recognizers / ner / gliner_recognizer.py: 90%
63 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-29 09:03 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-29 09:03 +0000
1import json
2import logging
3from typing import Dict, List, Optional
5from presidio_analyzer import (
6 AnalysisExplanation,
7 LocalRecognizer,
8 RecognizerResult,
9)
10from presidio_analyzer.chunkers import BaseTextChunker
11from presidio_analyzer.nlp_engine import (
12 NerModelConfiguration,
13 NlpArtifacts,
14 device_detector,
15)
17try:
18 from gliner import GLiNER, GLiNERConfig
19except ImportError:
20 GLiNER = None
21 GLiNERConfig = None
23logger = logging.getLogger("presidio-analyzer")
26class GLiNERRecognizer(LocalRecognizer):
27 """GLiNER model based entity recognizer."""
29 def __init__(
30 self,
31 supported_entities: Optional[List[str]] = None,
32 name: str = "GLiNERRecognizer",
33 supported_language: str = "en",
34 version: str = "0.0.1",
35 context: Optional[List[str]] = None,
36 entity_mapping: Optional[Dict[str, str]] = None,
37 model_name: str = "urchade/gliner_multi_pii-v1",
38 flat_ner: bool = True,
39 multi_label: bool = False,
40 threshold: float = 0.30,
41 map_location: Optional[str] = None,
42 text_chunker: Optional[BaseTextChunker] = None,
43 load_onnx_model: bool = False,
44 onnx_model_file: str = "model.onnx",
45 **model_kwargs,
46 ):
47 """GLiNER model based entity recognizer.
49 The model is based on the GLiNER library.
51 :param supported_entities: List of supported entities for this recognizer.
52 If None, all entities in Presidio's default configuration will be used.
53 see `NerModelConfiguration`
54 :param name: Name of the recognizer
55 :param supported_language: Language code to use for the recognizer
56 :param version: Version of the recognizer
57 :param context: N/A for this recognizer
58 :param model_name: The name of the GLiNER model to load
59 :param flat_ner: Whether to use flat NER or not (see GLiNER's documentation)
60 :param multi_label: Whether to use multi-label classification or not
61 (see GLiNER's documentation)
62 :param threshold: The threshold for the model's output
63 (see GLiNER's documentation)
64 :param map_location: The device to use for the model.
65 If None, will auto-detect GPU or use CPU.
66 :param text_chunker: Custom text chunking strategy. If None, uses
67 CharacterBasedTextChunker with default settings (chunk_size=250,
68 chunk_overlap=50)
69 :param load_onnx_model: Whether to load the model using ONNX Runtime.
70 If True, uses ONNX Runtime backend which supports CPUs without AVX2.
71 Requires onnxruntime to be installed. Default is False.
72 :param onnx_model_file: The name of the ONNX model file to load.
73 Only used when load_onnx_model is True. This is passed directly to
74 GLiNER.from_pretrained(). GLiNER looks for this file in the model
75 directory (downloaded or cached model path). Default is "model.onnx".
76 :param model_kwargs: Additional keyword arguments to pass to
77 GLiNER.from_pretrained(). This allows passing future parameters
78 to the GLiNER model without explicit support in this recognizer.
81 """
83 if entity_mapping:
84 if supported_entities:
85 raise ValueError(
86 "entity_mapping and supported_entities cannot be used together"
87 )
89 self.model_to_presidio_entity_mapping = entity_mapping
90 else:
91 if not supported_entities:
92 logger.info(
93 "No supported entities provided, "
94 "using default entities from NerModelConfiguration"
95 )
96 self.model_to_presidio_entity_mapping = (
97 NerModelConfiguration().model_to_presidio_entity_mapping
98 )
99 else:
100 self.model_to_presidio_entity_mapping = {
101 entity: entity for entity in supported_entities
102 }
104 logger.info(f"Using entity mapping {json.dumps(entity_mapping, indent=2)}")
105 supported_entities = list(set(self.model_to_presidio_entity_mapping.values()))
106 self.model_name = model_name
108 self.map_location = (
109 map_location
110 if map_location is not None
111 else device_detector.get_device()
112 )
114 self.flat_ner = flat_ner
115 self.multi_label = multi_label
116 self.threshold = threshold
117 self.load_onnx_model = load_onnx_model
118 self.onnx_model_file = onnx_model_file
119 self.model_kwargs = model_kwargs
121 # Use provided chunker or default to in-house character-based chunker
122 if text_chunker is not None:
123 self.text_chunker = text_chunker
124 else:
125 from presidio_analyzer.chunkers import CharacterBasedTextChunker
127 self.text_chunker = CharacterBasedTextChunker(
128 chunk_size=250,
129 chunk_overlap=50,
130 )
132 self.gliner = None
134 super().__init__(
135 supported_entities=supported_entities,
136 name=name,
137 supported_language=supported_language,
138 version=version,
139 context=context,
140 )
142 self.gliner_labels = list(self.model_to_presidio_entity_mapping.keys())
144 def load(self) -> None:
145 """Load the GLiNER model."""
146 if not GLiNER:
147 raise ImportError("GLiNER is not installed. Please install it.")
149 self.gliner = GLiNER.from_pretrained(
150 self.model_name,
151 map_location=self.map_location,
152 load_onnx_model=self.load_onnx_model,
153 onnx_model_file=self.onnx_model_file,
154 **self.model_kwargs,
155 )
157 def analyze(
158 self,
159 text: str,
160 entities: List[str],
161 nlp_artifacts: Optional[NlpArtifacts] = None,
162 ) -> List[RecognizerResult]:
163 """Analyze text to identify entities using a GLiNER model.
165 :param text: The text to be analyzed
166 :param entities: The list of entities this recognizer is requested to return
167 :param nlp_artifacts: N/A for this recognizer
168 """
170 # combine the input labels as this model allows for ad-hoc labels
171 labels = self.__create_input_labels(entities)
173 # Process text with automatic chunking
174 def predict_func(text: str) -> List[RecognizerResult]:
175 # Get predictions from GLiNER (returns dicts)
176 gliner_predictions = self.gliner.predict_entities(
177 text=text,
178 labels=labels,
179 flat_ner=self.flat_ner,
180 threshold=self.threshold,
181 multi_label=self.multi_label,
182 )
184 # Convert dicts to RecognizerResult objects
185 results = []
186 for pred in gliner_predictions:
187 presidio_entity = self.model_to_presidio_entity_mapping.get(
188 pred["label"], pred["label"]
189 )
191 # Filter by requested entities
192 if entities and presidio_entity not in entities:
193 continue
195 analysis_explanation = AnalysisExplanation(
196 recognizer=self.name,
197 original_score=pred["score"],
198 textual_explanation=f"Identified as {presidio_entity} by GLiNER",
199 )
201 results.append(
202 RecognizerResult(
203 entity_type=presidio_entity,
204 start=pred["start"],
205 end=pred["end"],
206 score=pred["score"],
207 analysis_explanation=analysis_explanation,
208 )
209 )
210 return results
212 predictions = self.text_chunker.predict_with_chunking(
213 text=text,
214 predict_func=predict_func,
215 )
217 return predictions
219 def __create_input_labels(self, entities):
220 """Append the entities requested by the user to the list of labels if it's not there.""" # noqa: E501
221 labels = list(self.gliner_labels)
222 for entity in entities:
223 if (
224 entity not in self.model_to_presidio_entity_mapping.values()
225 and entity not in self.gliner_labels
226 ):
227 labels.append(entity)
228 return labels