SqueakyCleanText 0.6.0__tar.gz → 0.6.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/PKG-INFO +16 -1
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/README.md +15 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/SqueakyCleanText.egg-info/PKG-INFO +16 -1
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/SqueakyCleanText.egg-info/SOURCES.txt +1 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/__init__.py +1 -1
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/config.py +2 -3
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/sct.py +5 -14
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/gliclass_adapter.py +16 -33
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/gliner_adapter.py +13 -4
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/ner.py +35 -29
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/resources.py +2 -12
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/special.py +4 -19
- squeakycleantext-0.6.1/tests/test_functional.py +956 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/tests/test_sct.py +9 -13
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/LICENSE +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/SqueakyCleanText.egg-info/dependency_links.txt +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/SqueakyCleanText.egg-info/requires.txt +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/SqueakyCleanText.egg-info/top_level.txt +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/pyproject.toml +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/__init__.py +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/anonymization_map.py +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/constants.py +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/contact.py +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/datetime.py +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/normtext.py +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/onnx_pipeline.py +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/process_result.py +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/stopwords.py +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/synthetic.py +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/torch_pipeline.py +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/setup.cfg +0 -0
- {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SqueakyCleanText
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Text preprocessing & PII anonymization pipeline for NLP/ML: ONNX NER ensemble, language detection, stopword removal, and configurable token replacement.
|
|
5
5
|
Author: Rehan Fazal
|
|
6
6
|
License: MIT
|
|
@@ -230,6 +230,21 @@ cfg = TextCleanerConfig(
|
|
|
230
230
|
cleaner = TextCleaner(cfg=cfg)
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
+
#### Language Specification
|
|
234
|
+
|
|
235
|
+
All language parameters accept Lingua names (`'ENGLISH'`), ISO 639-1 (`'en'`), or ISO 639-3 (`'eng'`) codes:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
# Pin to one language (skip auto-detection)
|
|
239
|
+
cfg = TextCleanerConfig(language='de', check_ner_process=False)
|
|
240
|
+
|
|
241
|
+
# Restrict detection to specific languages (auto-detect among them)
|
|
242
|
+
cfg = TextCleanerConfig(language=('en', 'nl', 'de'), check_ner_process=False)
|
|
243
|
+
|
|
244
|
+
# Add extra languages for detection
|
|
245
|
+
cfg = TextCleanerConfig(extra_languages=('fr', 'pt'), check_ner_process=False)
|
|
246
|
+
```
|
|
247
|
+
|
|
233
248
|
### GLiNER: Zero-Shot Custom NER
|
|
234
249
|
|
|
235
250
|
Use [GLiNER](https://github.com/urchade/GLiNER) to recognize any entity type without retraining:
|
|
@@ -153,6 +153,21 @@ cfg = TextCleanerConfig(
|
|
|
153
153
|
cleaner = TextCleaner(cfg=cfg)
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
+
#### Language Specification
|
|
157
|
+
|
|
158
|
+
All language parameters accept Lingua names (`'ENGLISH'`), ISO 639-1 (`'en'`), or ISO 639-3 (`'eng'`) codes:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
# Pin to one language (skip auto-detection)
|
|
162
|
+
cfg = TextCleanerConfig(language='de', check_ner_process=False)
|
|
163
|
+
|
|
164
|
+
# Restrict detection to specific languages (auto-detect among them)
|
|
165
|
+
cfg = TextCleanerConfig(language=('en', 'nl', 'de'), check_ner_process=False)
|
|
166
|
+
|
|
167
|
+
# Add extra languages for detection
|
|
168
|
+
cfg = TextCleanerConfig(extra_languages=('fr', 'pt'), check_ner_process=False)
|
|
169
|
+
```
|
|
170
|
+
|
|
156
171
|
### GLiNER: Zero-Shot Custom NER
|
|
157
172
|
|
|
158
173
|
Use [GLiNER](https://github.com/urchade/GLiNER) to recognize any entity type without retraining:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SqueakyCleanText
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Text preprocessing & PII anonymization pipeline for NLP/ML: ONNX NER ensemble, language detection, stopword removal, and configurable token replacement.
|
|
5
5
|
Author: Rehan Fazal
|
|
6
6
|
License: MIT
|
|
@@ -230,6 +230,21 @@ cfg = TextCleanerConfig(
|
|
|
230
230
|
cleaner = TextCleaner(cfg=cfg)
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
+
#### Language Specification
|
|
234
|
+
|
|
235
|
+
All language parameters accept Lingua names (`'ENGLISH'`), ISO 639-1 (`'en'`), or ISO 639-3 (`'eng'`) codes:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
# Pin to one language (skip auto-detection)
|
|
239
|
+
cfg = TextCleanerConfig(language='de', check_ner_process=False)
|
|
240
|
+
|
|
241
|
+
# Restrict detection to specific languages (auto-detect among them)
|
|
242
|
+
cfg = TextCleanerConfig(language=('en', 'nl', 'de'), check_ner_process=False)
|
|
243
|
+
|
|
244
|
+
# Add extra languages for detection
|
|
245
|
+
cfg = TextCleanerConfig(extra_languages=('fr', 'pt'), check_ner_process=False)
|
|
246
|
+
```
|
|
247
|
+
|
|
233
248
|
### GLiNER: Zero-Shot Custom NER
|
|
234
249
|
|
|
235
250
|
Use [GLiNER](https://github.com/urchade/GLiNER) to recognize any entity type without retraining:
|
|
@@ -5,7 +5,7 @@ from sct.sct import TextCleaner
|
|
|
5
5
|
from sct.utils.anonymization_map import AnonymizationMap, MapEntry
|
|
6
6
|
from sct.utils.process_result import ProcessResult
|
|
7
7
|
|
|
8
|
-
__version__ = "0.6.
|
|
8
|
+
__version__ = "0.6.1"
|
|
9
9
|
__all__ = [
|
|
10
10
|
"TextCleaner", "TextCleanerConfig",
|
|
11
11
|
"PII_LABELS", "PII_LABEL_MAP",
|
|
@@ -25,6 +25,7 @@ DEFAULT_LANGUAGES = frozenset({'ENGLISH', 'DUTCH', 'GERMAN', 'SPANISH'})
|
|
|
25
25
|
VALID_NER_BACKENDS = frozenset({
|
|
26
26
|
'onnx', 'torch', 'gliner', 'ensemble_onnx', 'ensemble_torch', 'presidio_gliner',
|
|
27
27
|
})
|
|
28
|
+
GLINER_BACKENDS = frozenset({'gliner', 'ensemble_onnx', 'ensemble_torch', 'presidio_gliner'})
|
|
28
29
|
|
|
29
30
|
DEFAULT_NER_MODELS: dict[str, str] = {
|
|
30
31
|
'ENGLISH': 'rhnfzl/xlm-roberta-large-conll03-english-onnx',
|
|
@@ -320,9 +321,7 @@ class TextCleanerConfig:
|
|
|
320
321
|
)
|
|
321
322
|
|
|
322
323
|
# GLiNER fields required for gliner/ensemble backends
|
|
323
|
-
needs_gliner = self.ner_backend in
|
|
324
|
-
'gliner', 'ensemble_onnx', 'ensemble_torch', 'presidio_gliner',
|
|
325
|
-
)
|
|
324
|
+
needs_gliner = self.ner_backend in GLINER_BACKENDS
|
|
326
325
|
if needs_gliner:
|
|
327
326
|
if not self.gliner_model:
|
|
328
327
|
raise ValueError(
|
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
from concurrent.futures import ThreadPoolExecutor
|
|
8
8
|
from typing import Any, Callable, Dict, List, Optional
|
|
9
9
|
|
|
10
|
-
from sct.config import TextCleanerConfig, _config_from_module_globals
|
|
10
|
+
from sct.config import TextCleanerConfig, GLINER_BACKENDS, _config_from_module_globals
|
|
11
11
|
from sct.utils import constants, contact, datetime, ner, normtext, resources, special, stopwords
|
|
12
12
|
from sct.utils.anonymization_map import AnonymizationMap
|
|
13
13
|
from sct.utils.process_result import ProcessResult
|
|
@@ -71,9 +71,7 @@ class TextCleaner:
|
|
|
71
71
|
if self.cfg.check_ner_process:
|
|
72
72
|
# Build GLiNER config dict (if needed)
|
|
73
73
|
gliner_config = None
|
|
74
|
-
needs_gliner = self.cfg.ner_backend in
|
|
75
|
-
'gliner', 'ensemble_onnx', 'ensemble_torch', 'presidio_gliner',
|
|
76
|
-
)
|
|
74
|
+
needs_gliner = self.cfg.ner_backend in GLINER_BACKENDS
|
|
77
75
|
if needs_gliner:
|
|
78
76
|
gliner_config = {
|
|
79
77
|
'model': self.cfg.gliner_model,
|
|
@@ -109,10 +107,8 @@ class TextCleaner:
|
|
|
109
107
|
else None
|
|
110
108
|
),
|
|
111
109
|
replacement_mode=self.cfg.replacement_mode,
|
|
110
|
+
synthetic_replacer=self._synthetic_replacer,
|
|
112
111
|
)
|
|
113
|
-
else:
|
|
114
|
-
pass # self.GeneralNER already initialized to None above
|
|
115
|
-
|
|
116
112
|
# GLiClass document-level pre-classification (optional, lazy-loaded)
|
|
117
113
|
self._gliclass: Any = None
|
|
118
114
|
if self.cfg.check_classify_document:
|
|
@@ -126,7 +122,6 @@ class TextCleaner:
|
|
|
126
122
|
onnx=self.cfg.gliclass_onnx,
|
|
127
123
|
)
|
|
128
124
|
|
|
129
|
-
self.batch_size = 8
|
|
130
125
|
self._pipeline: List[Callable[[str], str]] = []
|
|
131
126
|
self._post_fuzzy_pipeline: List[Callable[[str], str]] = []
|
|
132
127
|
self._init_pipeline()
|
|
@@ -227,9 +222,6 @@ class TextCleaner:
|
|
|
227
222
|
# Detect language (pure function, thread-safe)
|
|
228
223
|
language = self._detect_language(text)
|
|
229
224
|
|
|
230
|
-
# Pass language explicitly through pipeline context dict
|
|
231
|
-
ctx = {"language": language}
|
|
232
|
-
|
|
233
225
|
current_text = text
|
|
234
226
|
|
|
235
227
|
# Pre-fuzzy pipeline steps (unicode fix → html → urls → emails → dates)
|
|
@@ -239,12 +231,11 @@ class TextCleaner:
|
|
|
239
231
|
# Fuzzy date replacement — requires language context, called explicitly
|
|
240
232
|
# to avoid thread-local; positioned between replace_dates and replace_years.
|
|
241
233
|
if self.cfg.check_fuzzy_replace_dates:
|
|
242
|
-
lang = ctx.get("language")
|
|
243
234
|
current_text = self.ProcessDateTime.fuzzy_replace_dates(
|
|
244
235
|
current_text,
|
|
245
236
|
replace_with=self.cfg.replace_with_dates,
|
|
246
237
|
score_cutoff=self.cfg.fuzzy_date_score_cutoff,
|
|
247
|
-
language=
|
|
238
|
+
language=language,
|
|
248
239
|
)
|
|
249
240
|
|
|
250
241
|
# Post-fuzzy pipeline steps (years → phones → numbers → symbols → whitespace)
|
|
@@ -255,7 +246,7 @@ class TextCleaner:
|
|
|
255
246
|
if self.cfg.check_ner_process and self.GeneralNER is not None:
|
|
256
247
|
current_text = self.GeneralNER.ner_process(
|
|
257
248
|
current_text,
|
|
258
|
-
positional_tags=
|
|
249
|
+
positional_tags=self.cfg.positional_tags,
|
|
259
250
|
ner_confidence_threshold=self.cfg.ner_confidence_threshold,
|
|
260
251
|
language=language,
|
|
261
252
|
anon_map=anon_map,
|
|
@@ -32,18 +32,21 @@ class GLiClassAdapter:
|
|
|
32
32
|
self._onnx = onnx
|
|
33
33
|
self._pipeline = None
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
self._init_onnx(model_id)
|
|
37
|
-
else:
|
|
38
|
-
self._init_pytorch(model_id)
|
|
35
|
+
self._init_model(model_id)
|
|
39
36
|
|
|
40
37
|
logger.info(
|
|
41
38
|
"Loaded GLiClass model: %s (onnx=%s, labels=%d)",
|
|
42
39
|
model_id, onnx, len(self.labels),
|
|
43
40
|
)
|
|
44
41
|
|
|
45
|
-
def
|
|
46
|
-
"""Load GLiClass model via
|
|
42
|
+
def _init_model(self, model_id: str) -> None:
|
|
43
|
+
"""Load GLiClass model via gliclass package.
|
|
44
|
+
|
|
45
|
+
Note: gliclass does not yet expose a native ONNX loader, so the
|
|
46
|
+
``onnx`` flag is recorded but both paths use the same PyTorch-backed
|
|
47
|
+
``GLiClassModel.from_pretrained``. When gliclass adds ONNX support,
|
|
48
|
+
this method should branch on ``self._onnx``.
|
|
49
|
+
"""
|
|
47
50
|
try:
|
|
48
51
|
from gliclass import GLiClassModel, ZeroShotClassificationPipeline # noqa: S404
|
|
49
52
|
from transformers import AutoTokenizer
|
|
@@ -62,26 +65,6 @@ class GLiClassAdapter:
|
|
|
62
65
|
device='cpu',
|
|
63
66
|
)
|
|
64
67
|
|
|
65
|
-
def _init_onnx(self, model_id: str) -> None:
|
|
66
|
-
"""Load GLiClass model via ONNX Runtime (torch-free)."""
|
|
67
|
-
try:
|
|
68
|
-
from gliclass import GLiClassModel, ZeroShotClassificationPipeline # noqa: S404
|
|
69
|
-
from transformers import AutoTokenizer
|
|
70
|
-
except ImportError:
|
|
71
|
-
raise ImportError(
|
|
72
|
-
"gliclass + onnxruntime are required for ONNX GLiClass backend. "
|
|
73
|
-
"Install with: pip install squeakycleantext[classify] squeakycleantext[classify-onnx]"
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
model = GLiClassModel.from_pretrained(model_id)
|
|
77
|
-
tokenizer = AutoTokenizer.from_pretrained(model_id)
|
|
78
|
-
self._pipeline = ZeroShotClassificationPipeline(
|
|
79
|
-
model=model,
|
|
80
|
-
tokenizer=tokenizer,
|
|
81
|
-
classification_type=self.classification_type,
|
|
82
|
-
device='cpu',
|
|
83
|
-
)
|
|
84
|
-
|
|
85
68
|
def classify(self, text: str) -> List[Dict[str, float]]:
|
|
86
69
|
"""Classify text against configured labels.
|
|
87
70
|
|
|
@@ -94,16 +77,16 @@ class GLiClassAdapter:
|
|
|
94
77
|
|
|
95
78
|
result = self._pipeline(
|
|
96
79
|
text,
|
|
97
|
-
|
|
80
|
+
labels=self.labels,
|
|
98
81
|
)
|
|
99
82
|
|
|
100
|
-
# Pipeline returns
|
|
83
|
+
# Pipeline returns list[list[dict]] — one list per input text,
|
|
84
|
+
# each containing {"label": str, "score": float} dicts.
|
|
101
85
|
classifications = []
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
classifications.append({'label': label, 'score': score})
|
|
86
|
+
entries = result[0] if result else []
|
|
87
|
+
for entry in entries:
|
|
88
|
+
if entry['score'] >= self.threshold:
|
|
89
|
+
classifications.append({'label': entry['label'], 'score': entry['score']})
|
|
107
90
|
|
|
108
91
|
classifications.sort(key=lambda x: x['score'], reverse=True)
|
|
109
92
|
return classifications
|
|
@@ -48,10 +48,19 @@ class GLiNERAdapter:
|
|
|
48
48
|
"Install with: pip install squeakycleantext[gliner]"
|
|
49
49
|
)
|
|
50
50
|
if onnx:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
try:
|
|
52
|
+
self.model = GLiNER.from_pretrained(
|
|
53
|
+
model_id, load_onnx_model=True, load_tokenizer=True,
|
|
54
|
+
)
|
|
55
|
+
logger.info("Loaded GLiNER model in ONNX mode: %s", model_id)
|
|
56
|
+
except FileNotFoundError:
|
|
57
|
+
logger.warning(
|
|
58
|
+
"ONNX model not found for %s (GLiNER issue #314: most models "
|
|
59
|
+
"don't ship model.onnx at repo root). Falling back to PyTorch.",
|
|
60
|
+
model_id,
|
|
61
|
+
)
|
|
62
|
+
self.model = GLiNER.from_pretrained(model_id)
|
|
63
|
+
self._onnx = False
|
|
55
64
|
else:
|
|
56
65
|
self.model = GLiNER.from_pretrained(model_id)
|
|
57
66
|
if device == 'cuda' and not onnx:
|
|
@@ -3,7 +3,7 @@ import gc
|
|
|
3
3
|
import threading
|
|
4
4
|
from collections import defaultdict
|
|
5
5
|
import logging
|
|
6
|
-
from typing import Dict, List, Optional, Union
|
|
6
|
+
from typing import Dict, List, Optional, Sequence, Union
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
import onnxruntime as ort
|
|
@@ -11,11 +11,18 @@ import onnxruntime as ort
|
|
|
11
11
|
from presidio_anonymizer import AnonymizerEngine
|
|
12
12
|
from presidio_anonymizer.entities import RecognizerResult
|
|
13
13
|
|
|
14
|
+
from typing import NamedTuple as _NamedTuple
|
|
15
|
+
|
|
14
16
|
from sct.utils import constants
|
|
15
17
|
from sct.utils.anonymization_map import AnonymizationMap
|
|
16
18
|
from sct.utils.onnx_pipeline import load_onnx_ner_model
|
|
17
19
|
from sct.config import DEFAULT_NER_MODELS, DEFAULT_NER_ENSEMBLE, NER_ENSEMBLE_DEFAULT_KEYS, LANG_KEYS
|
|
18
20
|
|
|
21
|
+
|
|
22
|
+
class AnonymizeResult(_NamedTuple):
|
|
23
|
+
"""Result of text anonymization — lightweight typed container."""
|
|
24
|
+
text: str
|
|
25
|
+
|
|
19
26
|
ort.set_default_logger_severity(3) # Silence ONNX Runtime warnings
|
|
20
27
|
|
|
21
28
|
logger = logging.getLogger(__name__)
|
|
@@ -53,7 +60,8 @@ class GeneralNER:
|
|
|
53
60
|
ner_batch_size: int = 8,
|
|
54
61
|
ensemble_models: Optional[Dict] = None,
|
|
55
62
|
ensemble_default_keys: Optional[tuple] = None,
|
|
56
|
-
replacement_mode: str = 'placeholder'
|
|
63
|
+
replacement_mode: str = 'placeholder',
|
|
64
|
+
synthetic_replacer=None):
|
|
57
65
|
"""Initialize NER processor.
|
|
58
66
|
|
|
59
67
|
Args:
|
|
@@ -67,11 +75,12 @@ class GeneralNER:
|
|
|
67
75
|
Required when ner_backend involves GLiNER.
|
|
68
76
|
torch_model_names: Language-keyed dict of PyTorch model repo IDs.
|
|
69
77
|
Required when ner_backend involves torch.
|
|
78
|
+
synthetic_replacer: Shared SyntheticReplacer instance (from TextCleaner).
|
|
70
79
|
"""
|
|
71
80
|
self._ner_backend = ner_backend
|
|
72
81
|
self._ner_batch_size = ner_batch_size
|
|
73
82
|
self._replacement_mode = replacement_mode
|
|
74
|
-
self._synthetic_replacer =
|
|
83
|
+
self._synthetic_replacer = synthetic_replacer
|
|
75
84
|
self._gliner_pipe = None
|
|
76
85
|
self._ensemble_models: Dict[str, tuple] = ensemble_models if ensemble_models is not None else DEFAULT_NER_ENSEMBLE
|
|
77
86
|
self._ensemble_default_keys: tuple = (
|
|
@@ -195,11 +204,12 @@ class GeneralNER:
|
|
|
195
204
|
try:
|
|
196
205
|
from presidio_analyzer.predefined_recognizers import GLiNERRecognizer # noqa: S404
|
|
197
206
|
gliner_recognizer = GLiNERRecognizer(
|
|
198
|
-
|
|
207
|
+
model_name=gliner_config['model'],
|
|
199
208
|
supported_entities=[
|
|
200
209
|
label.upper()
|
|
201
210
|
for label in gliner_config.get('labels', ['person', 'organization', 'location'])
|
|
202
211
|
],
|
|
212
|
+
threshold=gliner_config.get('threshold', 0.4),
|
|
203
213
|
)
|
|
204
214
|
self._analyzer.registry.add_recognizer(gliner_recognizer)
|
|
205
215
|
except ImportError:
|
|
@@ -208,13 +218,6 @@ class GeneralNER:
|
|
|
208
218
|
"Install with: pip install presidio-analyzer gliner"
|
|
209
219
|
)
|
|
210
220
|
|
|
211
|
-
def _get_synthetic_replacer(self):
|
|
212
|
-
"""Lazily initialize the SyntheticReplacer."""
|
|
213
|
-
if self._synthetic_replacer is None:
|
|
214
|
-
from sct.utils.synthetic import SyntheticReplacer
|
|
215
|
-
self._synthetic_replacer = SyntheticReplacer()
|
|
216
|
-
return self._synthetic_replacer
|
|
217
|
-
|
|
218
221
|
def _get_ensemble_keys(self, language: str) -> tuple:
|
|
219
222
|
"""Return ordered model keys to run for the given language."""
|
|
220
223
|
return self._ensemble_models.get(language, self._ensemble_default_keys)
|
|
@@ -313,9 +316,8 @@ class GeneralNER:
|
|
|
313
316
|
return self._anonymize_reversible(text, filtered_data, anon_map)
|
|
314
317
|
|
|
315
318
|
if replacement_mode == 'synthetic':
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
return type('AnonymizeResult', (), {'text': result_text})()
|
|
319
|
+
result_text = self._synthetic_replacer.generate_for_entities(text, filtered_data)
|
|
320
|
+
return AnonymizeResult(text=result_text)
|
|
319
321
|
|
|
320
322
|
has_custom = any(
|
|
321
323
|
items['entity_group'] not in ENTITY_TYPE_MAP
|
|
@@ -328,7 +330,7 @@ class GeneralNER:
|
|
|
328
330
|
for items in sorted_data:
|
|
329
331
|
tag = ENTITY_TYPE_MAP.get(items['entity_group'], items['entity_group'])
|
|
330
332
|
text = text[:items['start']] + f"<{tag}>" + text[items['end']:]
|
|
331
|
-
return
|
|
333
|
+
return AnonymizeResult(text=text)
|
|
332
334
|
else:
|
|
333
335
|
# Standard entities only: use Presidio (existing behavior)
|
|
334
336
|
analyzer_result = []
|
|
@@ -348,13 +350,14 @@ class GeneralNER:
|
|
|
348
350
|
if 0 <= entry.start < text_length and 0 < entry.end <= text_length
|
|
349
351
|
]
|
|
350
352
|
|
|
351
|
-
|
|
353
|
+
engine_result = self.engine.anonymize(text=text, analyzer_results=analyzer_result)
|
|
354
|
+
return AnonymizeResult(text=engine_result.text)
|
|
352
355
|
|
|
353
356
|
def _anonymize_reversible(self, text, filtered_data, anon_map=None):
|
|
354
357
|
"""Replace entities with indexed placeholders and populate the map.
|
|
355
358
|
|
|
356
359
|
Uses right-to-left replacement to preserve character offsets.
|
|
357
|
-
Returns an AnonymizeResult
|
|
360
|
+
Returns an ``AnonymizeResult`` with the anonymized ``.text``.
|
|
358
361
|
"""
|
|
359
362
|
if anon_map is None:
|
|
360
363
|
anon_map = AnonymizationMap()
|
|
@@ -373,7 +376,7 @@ class GeneralNER:
|
|
|
373
376
|
)
|
|
374
377
|
text = text[:item['start']] + placeholder + text[item['end']:]
|
|
375
378
|
|
|
376
|
-
return
|
|
379
|
+
return AnonymizeResult(text=text)
|
|
377
380
|
|
|
378
381
|
def ner_ensemble(self, ner_results, t):
|
|
379
382
|
"""Apply ensemble voting across multiple model results.
|
|
@@ -440,7 +443,7 @@ class GeneralNER:
|
|
|
440
443
|
def ner_process(
|
|
441
444
|
self,
|
|
442
445
|
text: str,
|
|
443
|
-
positional_tags: Optional[
|
|
446
|
+
positional_tags: Optional[Sequence[str]] = None,
|
|
444
447
|
ner_confidence_threshold: Optional[float] = None,
|
|
445
448
|
language: Optional[str] = None,
|
|
446
449
|
anon_map: Optional['AnonymizationMap'] = None,
|
|
@@ -474,6 +477,16 @@ class GeneralNER:
|
|
|
474
477
|
if not chunks:
|
|
475
478
|
return text
|
|
476
479
|
|
|
480
|
+
# Pre-compute GLiNER-only tag set (constant across chunks)
|
|
481
|
+
gliner_all_tags = None
|
|
482
|
+
if self._ner_backend == 'gliner' and self._gliner_pipe:
|
|
483
|
+
gliner_all_tags = set(positional_tags)
|
|
484
|
+
gliner_all_tags.update(self._gliner_pipe.label_map.values())
|
|
485
|
+
gliner_all_tags.update(
|
|
486
|
+
label.upper() for label in self._gliner_pipe.labels
|
|
487
|
+
if label not in self._gliner_pipe.label_map
|
|
488
|
+
)
|
|
489
|
+
|
|
477
490
|
# --- Inference + ensemble per chunk ---
|
|
478
491
|
ner_clean_text = []
|
|
479
492
|
for chunk in chunks:
|
|
@@ -486,7 +499,7 @@ class GeneralNER:
|
|
|
486
499
|
model_name = self._model_names.get(key, key)
|
|
487
500
|
model_lock = self._get_lock(model_name)
|
|
488
501
|
with model_lock:
|
|
489
|
-
batch = self.
|
|
502
|
+
batch = self._pipelines[key]([chunk])
|
|
490
503
|
ner_results.extend(self.ner_data(batch[0], positional_tags))
|
|
491
504
|
|
|
492
505
|
# GLiNER backend
|
|
@@ -495,15 +508,8 @@ class GeneralNER:
|
|
|
495
508
|
gliner_lock = self._get_lock('gliner')
|
|
496
509
|
with gliner_lock:
|
|
497
510
|
gliner_batch = self._gliner_pipe([chunk])
|
|
498
|
-
if
|
|
499
|
-
|
|
500
|
-
all_tags = set(positional_tags)
|
|
501
|
-
all_tags.update(self._gliner_pipe.label_map.values())
|
|
502
|
-
all_tags.update(
|
|
503
|
-
label.upper() for label in self._gliner_pipe.labels
|
|
504
|
-
if label not in self._gliner_pipe.label_map
|
|
505
|
-
)
|
|
506
|
-
ner_results.extend(self.ner_data(gliner_batch[0], all_tags))
|
|
511
|
+
if gliner_all_tags is not None:
|
|
512
|
+
ner_results.extend(self.ner_data(gliner_batch[0], gliner_all_tags))
|
|
507
513
|
else:
|
|
508
514
|
# Ensemble: filter to positional_tags only
|
|
509
515
|
ner_results.extend(self.ner_data(gliner_batch[0], positional_tags))
|
|
@@ -76,18 +76,6 @@ DETECTOR = _LazyDetectorProxy()
|
|
|
76
76
|
# Language extensibility utilities (used by TextCleanerConfig and TextCleaner)
|
|
77
77
|
# ---------------------------------------------------------------------------
|
|
78
78
|
|
|
79
|
-
DEFAULT_LANGUAGES = frozenset({'ENGLISH', 'DUTCH', 'GERMAN', 'SPANISH'})
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def validate_language_name(name: str) -> str:
|
|
83
|
-
"""Validate and resolve a language identifier to a Lingua name.
|
|
84
|
-
|
|
85
|
-
Accepts uppercase names, ISO 639-1, and ISO 639-3 codes.
|
|
86
|
-
Returns the canonical uppercase Lingua name.
|
|
87
|
-
Raises ValueError if unrecognized.
|
|
88
|
-
"""
|
|
89
|
-
return resolve_language(name)
|
|
90
|
-
|
|
91
79
|
|
|
92
80
|
def build_detector(supported_languages: frozenset):
|
|
93
81
|
"""Build a Lingua detector for the given language set.
|
|
@@ -95,6 +83,8 @@ def build_detector(supported_languages: frozenset):
|
|
|
95
83
|
Filters out 'MULTILINGUAL' (not a real Lingua language).
|
|
96
84
|
Returns a Lingua LanguageDetector instance.
|
|
97
85
|
"""
|
|
86
|
+
from sct.config import DEFAULT_LANGUAGES # lazy: avoids circular import with config.py
|
|
87
|
+
|
|
98
88
|
lingua_langs = []
|
|
99
89
|
for name in supported_languages:
|
|
100
90
|
if name == 'MULTILINGUAL':
|
|
@@ -1,20 +1,5 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import string
|
|
3
|
-
#---
|
|
4
1
|
from sct.utils import constants
|
|
5
2
|
|
|
6
|
-
# Pre-compile punctuation regex at module level (avoids recompilation per call)
|
|
7
|
-
_PUNCTUATION_CHARS = re.escape(string.punctuation)
|
|
8
|
-
_PUNCTUATION_REGEX = re.compile('[' + _PUNCTUATION_CHARS + ']')
|
|
9
|
-
|
|
10
|
-
# Pre-compile bracket/brace regexes
|
|
11
|
-
_BRACKET_CONTENT_REGEX = re.compile(r'\[[^\]]+\]')
|
|
12
|
-
_BRACE_CONTENT_REGEX = re.compile(r'\{[^}]+\}')
|
|
13
|
-
_ISOLATED_QUOTES_REGEX = re.compile(
|
|
14
|
-
r"(?<![a-zA-Z0-9])['\"\-*%](?![a-zA-Z0-9])",
|
|
15
|
-
flags=re.UNICODE | re.IGNORECASE,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
3
|
|
|
19
4
|
class ProcessSpecialSymbols:
|
|
20
5
|
|
|
@@ -58,13 +43,13 @@ class ProcessSpecialSymbols:
|
|
|
58
43
|
"""
|
|
59
44
|
cleaned_text = text
|
|
60
45
|
if remove_brackets:
|
|
61
|
-
cleaned_text =
|
|
46
|
+
cleaned_text = constants.BRACKET_CONTENT_REGEX.sub('', cleaned_text)
|
|
62
47
|
if remove_braces:
|
|
63
|
-
cleaned_text =
|
|
48
|
+
cleaned_text = constants.BRACE_CONTENT_REGEX.sub('', cleaned_text)
|
|
64
49
|
cleaned_text = constants.ISOLATED_SPECIAL_SYMBOLS_REGEX.sub('', cleaned_text)
|
|
65
|
-
cleaned_text =
|
|
50
|
+
cleaned_text = constants.ISOLATED_QUOTES_REGEX.sub('', cleaned_text)
|
|
66
51
|
|
|
67
52
|
return cleaned_text
|
|
68
53
|
|
|
69
54
|
def remove_punctuation(self, text):
|
|
70
|
-
return
|
|
55
|
+
return constants.PUNCTUATION_REGEX.sub('', text)
|