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.
Files changed (32) hide show
  1. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/PKG-INFO +16 -1
  2. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/README.md +15 -0
  3. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/SqueakyCleanText.egg-info/PKG-INFO +16 -1
  4. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/SqueakyCleanText.egg-info/SOURCES.txt +1 -0
  5. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/__init__.py +1 -1
  6. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/config.py +2 -3
  7. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/sct.py +5 -14
  8. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/gliclass_adapter.py +16 -33
  9. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/gliner_adapter.py +13 -4
  10. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/ner.py +35 -29
  11. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/resources.py +2 -12
  12. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/special.py +4 -19
  13. squeakycleantext-0.6.1/tests/test_functional.py +956 -0
  14. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/tests/test_sct.py +9 -13
  15. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/LICENSE +0 -0
  16. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/SqueakyCleanText.egg-info/dependency_links.txt +0 -0
  17. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/SqueakyCleanText.egg-info/requires.txt +0 -0
  18. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/SqueakyCleanText.egg-info/top_level.txt +0 -0
  19. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/pyproject.toml +0 -0
  20. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/__init__.py +0 -0
  21. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/anonymization_map.py +0 -0
  22. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/constants.py +0 -0
  23. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/contact.py +0 -0
  24. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/datetime.py +0 -0
  25. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/normtext.py +0 -0
  26. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/onnx_pipeline.py +0 -0
  27. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/process_result.py +0 -0
  28. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/stopwords.py +0 -0
  29. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/synthetic.py +0 -0
  30. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/sct/utils/torch_pipeline.py +0 -0
  31. {squeakycleantext-0.6.0 → squeakycleantext-0.6.1}/setup.cfg +0 -0
  32. {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.0
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.0
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:
@@ -26,4 +26,5 @@ sct/utils/special.py
26
26
  sct/utils/stopwords.py
27
27
  sct/utils/synthetic.py
28
28
  sct/utils/torch_pipeline.py
29
+ tests/test_functional.py
29
30
  tests/test_sct.py
@@ -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.0"
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=lang,
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=list(self.cfg.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
- if onnx:
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 _init_pytorch(self, model_id: str) -> None:
46
- """Load GLiClass model via PyTorch (gliclass package)."""
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
- candidate_labels=self.labels,
80
+ labels=self.labels,
98
81
  )
99
82
 
100
- # Pipeline returns {"sequence": ..., "labels": [...], "scores": [...]}
83
+ # Pipeline returns list[list[dict]] — one list per input text,
84
+ # each containing {"label": str, "score": float} dicts.
101
85
  classifications = []
102
- labels = result.get('labels', [])
103
- scores = result.get('scores', [])
104
- for label, score in zip(labels, scores):
105
- if score >= self.threshold:
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
- self.model = GLiNER.from_pretrained(
52
- model_id, load_onnx_model=True, load_tokenizer=True,
53
- )
54
- logger.info("Loaded GLiNER model in ONNX mode: %s", model_id)
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 = None # Lazy-loaded when replacement_mode='synthetic'
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
- model_path=gliner_config['model'],
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
- replacer = self._get_synthetic_replacer()
317
- result_text = replacer.generate_for_entities(text, filtered_data)
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 type('AnonymizeResult', (), {'text': text})()
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
- return self.engine.anonymize(text=text, analyzer_results=analyzer_result)
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-like object with ``.text`` attribute.
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 type('AnonymizeResult', (), {'text': text, 'anon_map': anon_map})()
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[List[str]] = None,
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._get_pipeline(key)([chunk])
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 self._ner_backend == 'gliner':
499
- # GLiNER-only: include all mapped entity types
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 = _BRACKET_CONTENT_REGEX.sub('', cleaned_text)
46
+ cleaned_text = constants.BRACKET_CONTENT_REGEX.sub('', cleaned_text)
62
47
  if remove_braces:
63
- cleaned_text = _BRACE_CONTENT_REGEX.sub('', 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 = _ISOLATED_QUOTES_REGEX.sub('', 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 _PUNCTUATION_REGEX.sub('', text)
55
+ return constants.PUNCTUATION_REGEX.sub('', text)