SqueakyCleanText 0.7.0__tar.gz → 0.7.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.7.0 → squeakycleantext-0.7.1}/PKG-INFO +11 -2
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/README.md +10 -1
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/SqueakyCleanText.egg-info/PKG-INFO +11 -2
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/SqueakyCleanText.egg-info/SOURCES.txt +4 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/__init__.py +1 -1
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/adapters.py +16 -7
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/browser.py +53 -2
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/config.py +45 -1
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/model_benchmark.py +2 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/privacy.py +5 -1
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/sct.py +68 -25
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/anonymization_map.py +4 -2
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/constants.py +63 -6
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/contact.py +35 -5
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/ner.py +74 -46
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/onnx_pipeline.py +5 -1
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/stopwords.py +2 -2
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/tests/test_adapters.py +45 -0
- squeakycleantext-0.7.1/tests/test_benchmark_release.py +67 -0
- squeakycleantext-0.7.1/tests/test_chunking.py +170 -0
- squeakycleantext-0.7.1/tests/test_config_state.py +121 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/tests/test_mcp_server.py +50 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/tests/test_privacy.py +67 -0
- squeakycleantext-0.7.1/tests/test_regex_boundaries.py +334 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/tests/test_sct.py +3 -2
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/LICENSE +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/SqueakyCleanText.egg-info/dependency_links.txt +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/SqueakyCleanText.egg-info/entry_points.txt +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/SqueakyCleanText.egg-info/requires.txt +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/SqueakyCleanText.egg-info/top_level.txt +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/pyproject.toml +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/cli.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/evaluation.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/mcp_server.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/model_candidates.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/ocr.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/risk.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/streaming.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/__init__.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/datetime.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/gliclass_adapter.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/gliner_adapter.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/normtext.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/process_result.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/resources.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/special.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/synthetic.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/sct/utils/torch_pipeline.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/setup.cfg +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/setup.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/tests/test_browser.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/tests/test_cli.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/tests/test_contracts.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/tests/test_evaluation.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/tests/test_functional.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/tests/test_model_benchmarks.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/tests/test_ocr.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/tests/test_risk.py +0 -0
- {squeakycleantext-0.7.0 → squeakycleantext-0.7.1}/tests/test_streaming.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SqueakyCleanText
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.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-Expression: MIT
|
|
@@ -308,7 +308,7 @@ lm_text, stat_text, lang = cleaner.process(
|
|
|
308
308
|
# Entities are anonymized: names, SSNs, emails, dates of birth, and 50+ more PII types
|
|
309
309
|
```
|
|
310
310
|
|
|
311
|
-
PII mode auto-configures: `ner_backend='gliner'`, uses [`knowledgator/gliner-pii-base-v1.0`](https://huggingface.co/knowledgator/gliner-pii-base-v1.0), sets
|
|
311
|
+
PII mode auto-configures: `ner_backend='gliner'`, uses [`knowledgator/gliner-pii-base-v1.0`](https://huggingface.co/knowledgator/gliner-pii-base-v1.0), sets `gliner_threshold` and the final `ner_confidence_threshold` to 0.3 (recall-focused), and expands positional tags. User-provided values always take priority.
|
|
312
312
|
|
|
313
313
|
**Alternative PII models** (pass as `gliner_model`):
|
|
314
314
|
|
|
@@ -592,11 +592,14 @@ new_cfg = dataclasses.replace(cfg, check_ner_process=False)
|
|
|
592
592
|
| `check_replace_html` | `True` | Strip/replace HTML tags |
|
|
593
593
|
| `check_replace_urls` | `True` | Replace URLs with token |
|
|
594
594
|
| `check_replace_emails` | `True` | Replace emails with token |
|
|
595
|
+
| `check_replace_ips` | `True` | Replace IPv4/IPv6 addresses (runs before dates, phones, years) |
|
|
596
|
+
| `replace_ips_public_only` | `False` | Keep private, loopback and reserved IPs; replace only public ones |
|
|
595
597
|
| `check_replace_years` | `True` | Replace years (1900-2099) |
|
|
596
598
|
| `check_replace_dates` | `False` | Replace full dates (ISO 8601, month names) |
|
|
597
599
|
| `check_fuzzy_replace_dates` | `False` | Fuzzy match misspelled months (requires `[fuzzy]`) |
|
|
598
600
|
| `check_replace_phone_numbers` | `True` | Replace phone numbers |
|
|
599
601
|
| `check_replace_numbers` | `True` | Replace standalone numbers |
|
|
602
|
+
| `replace_numbers_in_words` | `False` | Also replace digits inside Latin/Greek/Cyrillic words (`GPT4` to `GPT<NUMBER>`) |
|
|
600
603
|
| `check_replace_currency_symbols` | `True` | Replace currency symbols |
|
|
601
604
|
| `check_ner_process` | `True` | Run NER entity recognition |
|
|
602
605
|
| `check_remove_isolated_letters` | `True` | Remove single letters |
|
|
@@ -619,6 +622,7 @@ new_cfg = dataclasses.replace(cfg, check_ner_process=False)
|
|
|
619
622
|
| `replace_with_url` | `"<URL>"` |
|
|
620
623
|
| `replace_with_html` | `"<HTML>"` |
|
|
621
624
|
| `replace_with_email` | `"<EMAIL>"` |
|
|
625
|
+
| `replace_with_ips` | `"<IP>"` |
|
|
622
626
|
| `replace_with_years` | `"<YEAR>"` |
|
|
623
627
|
| `replace_with_dates` | `"<DATE>"` |
|
|
624
628
|
| `replace_with_phone_numbers` | `"<PHONE>"` |
|
|
@@ -686,6 +690,11 @@ print(result.metadata["findings"])
|
|
|
686
690
|
print(result.metadata["detector"])
|
|
687
691
|
```
|
|
688
692
|
|
|
693
|
+
Finding offsets (`start`, `end`) and NER entries in the reversible map are
|
|
694
|
+
document-relative positions in the post-regex text: the text after the regex
|
|
695
|
+
stages replaced URLs, emails, phones and the like. They are not offsets into the
|
|
696
|
+
raw input.
|
|
697
|
+
|
|
689
698
|
Reversible output is pseudonymized personal data. Anyone with the token map can
|
|
690
699
|
restore the source values. Keep token maps outside logs and cleaned datasets.
|
|
691
700
|
|
|
@@ -229,7 +229,7 @@ lm_text, stat_text, lang = cleaner.process(
|
|
|
229
229
|
# Entities are anonymized: names, SSNs, emails, dates of birth, and 50+ more PII types
|
|
230
230
|
```
|
|
231
231
|
|
|
232
|
-
PII mode auto-configures: `ner_backend='gliner'`, uses [`knowledgator/gliner-pii-base-v1.0`](https://huggingface.co/knowledgator/gliner-pii-base-v1.0), sets
|
|
232
|
+
PII mode auto-configures: `ner_backend='gliner'`, uses [`knowledgator/gliner-pii-base-v1.0`](https://huggingface.co/knowledgator/gliner-pii-base-v1.0), sets `gliner_threshold` and the final `ner_confidence_threshold` to 0.3 (recall-focused), and expands positional tags. User-provided values always take priority.
|
|
233
233
|
|
|
234
234
|
**Alternative PII models** (pass as `gliner_model`):
|
|
235
235
|
|
|
@@ -513,11 +513,14 @@ new_cfg = dataclasses.replace(cfg, check_ner_process=False)
|
|
|
513
513
|
| `check_replace_html` | `True` | Strip/replace HTML tags |
|
|
514
514
|
| `check_replace_urls` | `True` | Replace URLs with token |
|
|
515
515
|
| `check_replace_emails` | `True` | Replace emails with token |
|
|
516
|
+
| `check_replace_ips` | `True` | Replace IPv4/IPv6 addresses (runs before dates, phones, years) |
|
|
517
|
+
| `replace_ips_public_only` | `False` | Keep private, loopback and reserved IPs; replace only public ones |
|
|
516
518
|
| `check_replace_years` | `True` | Replace years (1900-2099) |
|
|
517
519
|
| `check_replace_dates` | `False` | Replace full dates (ISO 8601, month names) |
|
|
518
520
|
| `check_fuzzy_replace_dates` | `False` | Fuzzy match misspelled months (requires `[fuzzy]`) |
|
|
519
521
|
| `check_replace_phone_numbers` | `True` | Replace phone numbers |
|
|
520
522
|
| `check_replace_numbers` | `True` | Replace standalone numbers |
|
|
523
|
+
| `replace_numbers_in_words` | `False` | Also replace digits inside Latin/Greek/Cyrillic words (`GPT4` to `GPT<NUMBER>`) |
|
|
521
524
|
| `check_replace_currency_symbols` | `True` | Replace currency symbols |
|
|
522
525
|
| `check_ner_process` | `True` | Run NER entity recognition |
|
|
523
526
|
| `check_remove_isolated_letters` | `True` | Remove single letters |
|
|
@@ -540,6 +543,7 @@ new_cfg = dataclasses.replace(cfg, check_ner_process=False)
|
|
|
540
543
|
| `replace_with_url` | `"<URL>"` |
|
|
541
544
|
| `replace_with_html` | `"<HTML>"` |
|
|
542
545
|
| `replace_with_email` | `"<EMAIL>"` |
|
|
546
|
+
| `replace_with_ips` | `"<IP>"` |
|
|
543
547
|
| `replace_with_years` | `"<YEAR>"` |
|
|
544
548
|
| `replace_with_dates` | `"<DATE>"` |
|
|
545
549
|
| `replace_with_phone_numbers` | `"<PHONE>"` |
|
|
@@ -607,6 +611,11 @@ print(result.metadata["findings"])
|
|
|
607
611
|
print(result.metadata["detector"])
|
|
608
612
|
```
|
|
609
613
|
|
|
614
|
+
Finding offsets (`start`, `end`) and NER entries in the reversible map are
|
|
615
|
+
document-relative positions in the post-regex text: the text after the regex
|
|
616
|
+
stages replaced URLs, emails, phones and the like. They are not offsets into the
|
|
617
|
+
raw input.
|
|
618
|
+
|
|
610
619
|
Reversible output is pseudonymized personal data. Anyone with the token map can
|
|
611
620
|
restore the source values. Keep token maps outside logs and cleaned datasets.
|
|
612
621
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SqueakyCleanText
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.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-Expression: MIT
|
|
@@ -308,7 +308,7 @@ lm_text, stat_text, lang = cleaner.process(
|
|
|
308
308
|
# Entities are anonymized: names, SSNs, emails, dates of birth, and 50+ more PII types
|
|
309
309
|
```
|
|
310
310
|
|
|
311
|
-
PII mode auto-configures: `ner_backend='gliner'`, uses [`knowledgator/gliner-pii-base-v1.0`](https://huggingface.co/knowledgator/gliner-pii-base-v1.0), sets
|
|
311
|
+
PII mode auto-configures: `ner_backend='gliner'`, uses [`knowledgator/gliner-pii-base-v1.0`](https://huggingface.co/knowledgator/gliner-pii-base-v1.0), sets `gliner_threshold` and the final `ner_confidence_threshold` to 0.3 (recall-focused), and expands positional tags. User-provided values always take priority.
|
|
312
312
|
|
|
313
313
|
**Alternative PII models** (pass as `gliner_model`):
|
|
314
314
|
|
|
@@ -592,11 +592,14 @@ new_cfg = dataclasses.replace(cfg, check_ner_process=False)
|
|
|
592
592
|
| `check_replace_html` | `True` | Strip/replace HTML tags |
|
|
593
593
|
| `check_replace_urls` | `True` | Replace URLs with token |
|
|
594
594
|
| `check_replace_emails` | `True` | Replace emails with token |
|
|
595
|
+
| `check_replace_ips` | `True` | Replace IPv4/IPv6 addresses (runs before dates, phones, years) |
|
|
596
|
+
| `replace_ips_public_only` | `False` | Keep private, loopback and reserved IPs; replace only public ones |
|
|
595
597
|
| `check_replace_years` | `True` | Replace years (1900-2099) |
|
|
596
598
|
| `check_replace_dates` | `False` | Replace full dates (ISO 8601, month names) |
|
|
597
599
|
| `check_fuzzy_replace_dates` | `False` | Fuzzy match misspelled months (requires `[fuzzy]`) |
|
|
598
600
|
| `check_replace_phone_numbers` | `True` | Replace phone numbers |
|
|
599
601
|
| `check_replace_numbers` | `True` | Replace standalone numbers |
|
|
602
|
+
| `replace_numbers_in_words` | `False` | Also replace digits inside Latin/Greek/Cyrillic words (`GPT4` to `GPT<NUMBER>`) |
|
|
600
603
|
| `check_replace_currency_symbols` | `True` | Replace currency symbols |
|
|
601
604
|
| `check_ner_process` | `True` | Run NER entity recognition |
|
|
602
605
|
| `check_remove_isolated_letters` | `True` | Remove single letters |
|
|
@@ -619,6 +622,7 @@ new_cfg = dataclasses.replace(cfg, check_ner_process=False)
|
|
|
619
622
|
| `replace_with_url` | `"<URL>"` |
|
|
620
623
|
| `replace_with_html` | `"<HTML>"` |
|
|
621
624
|
| `replace_with_email` | `"<EMAIL>"` |
|
|
625
|
+
| `replace_with_ips` | `"<IP>"` |
|
|
622
626
|
| `replace_with_years` | `"<YEAR>"` |
|
|
623
627
|
| `replace_with_dates` | `"<DATE>"` |
|
|
624
628
|
| `replace_with_phone_numbers` | `"<PHONE>"` |
|
|
@@ -686,6 +690,11 @@ print(result.metadata["findings"])
|
|
|
686
690
|
print(result.metadata["detector"])
|
|
687
691
|
```
|
|
688
692
|
|
|
693
|
+
Finding offsets (`start`, `end`) and NER entries in the reversible map are
|
|
694
|
+
document-relative positions in the post-regex text: the text after the regex
|
|
695
|
+
stages replaced URLs, emails, phones and the like. They are not offsets into the
|
|
696
|
+
raw input.
|
|
697
|
+
|
|
689
698
|
Reversible output is pseudonymized personal data. Anyone with the token map can
|
|
690
699
|
restore the source values. Keep token maps outside logs and cleaned datasets.
|
|
691
700
|
|
|
@@ -39,8 +39,11 @@ sct/utils/stopwords.py
|
|
|
39
39
|
sct/utils/synthetic.py
|
|
40
40
|
sct/utils/torch_pipeline.py
|
|
41
41
|
tests/test_adapters.py
|
|
42
|
+
tests/test_benchmark_release.py
|
|
42
43
|
tests/test_browser.py
|
|
44
|
+
tests/test_chunking.py
|
|
43
45
|
tests/test_cli.py
|
|
46
|
+
tests/test_config_state.py
|
|
44
47
|
tests/test_contracts.py
|
|
45
48
|
tests/test_evaluation.py
|
|
46
49
|
tests/test_functional.py
|
|
@@ -48,6 +51,7 @@ tests/test_mcp_server.py
|
|
|
48
51
|
tests/test_model_benchmarks.py
|
|
49
52
|
tests/test_ocr.py
|
|
50
53
|
tests/test_privacy.py
|
|
54
|
+
tests/test_regex_boundaries.py
|
|
51
55
|
tests/test_risk.py
|
|
52
56
|
tests/test_sct.py
|
|
53
57
|
tests/test_streaming.py
|
|
@@ -49,7 +49,7 @@ from sct.streaming import (
|
|
|
49
49
|
from sct.utils.anonymization_map import AnonymizationMap, MapEntry
|
|
50
50
|
from sct.utils.process_result import ProcessResult
|
|
51
51
|
|
|
52
|
-
__version__ = "0.7.
|
|
52
|
+
__version__ = "0.7.1"
|
|
53
53
|
__all__ = [
|
|
54
54
|
"TextCleaner", "TextCleanerConfig",
|
|
55
55
|
"PII_LABELS", "PII_LABEL_MAP",
|
|
@@ -6,6 +6,7 @@ from typing import Any, Iterable, Mapping, Optional
|
|
|
6
6
|
|
|
7
7
|
from sct.privacy import EntityFinding
|
|
8
8
|
from sct.sct import TextCleaner
|
|
9
|
+
from sct.utils import constants
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@dataclass(frozen=True)
|
|
@@ -75,10 +76,16 @@ def is_sensitive_result(
|
|
|
75
76
|
and result.lm_text != source
|
|
76
77
|
):
|
|
77
78
|
return True
|
|
79
|
+
# The HTML step rewrites marker-like tags already in the source, so they
|
|
80
|
+
# cannot offset markers emitted later (``<EMAIL> Alice`` -> ``<PERSON>``).
|
|
81
|
+
baseline = source
|
|
82
|
+
if cleaner.cfg.check_replace_html:
|
|
83
|
+
baseline = constants.HTML_REGEX.sub("", source)
|
|
78
84
|
configured_markers = (
|
|
79
85
|
cleaner.cfg.replace_with_url,
|
|
80
86
|
cleaner.cfg.replace_with_html,
|
|
81
87
|
cleaner.cfg.replace_with_email,
|
|
88
|
+
cleaner.cfg.replace_with_ips,
|
|
82
89
|
cleaner.cfg.replace_with_years,
|
|
83
90
|
cleaner.cfg.replace_with_dates,
|
|
84
91
|
cleaner.cfg.replace_with_phone_numbers,
|
|
@@ -88,14 +95,15 @@ def is_sensitive_result(
|
|
|
88
95
|
if any(
|
|
89
96
|
isinstance(marker, str)
|
|
90
97
|
and marker
|
|
91
|
-
and marker
|
|
92
|
-
and marker not in source
|
|
98
|
+
and result.lm_text.count(marker) > baseline.count(marker)
|
|
93
99
|
for marker in configured_markers
|
|
94
100
|
):
|
|
95
101
|
return True
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
# Compare counts, not presence: a source that already holds a marker
|
|
103
|
+
# still gains one per replacement.
|
|
104
|
+
marker_pattern = r"<[A-Z][A-Z0-9_]*>"
|
|
105
|
+
return len(re.findall(marker_pattern, result.lm_text)) > len(
|
|
106
|
+
re.findall(marker_pattern, baseline)
|
|
99
107
|
)
|
|
100
108
|
|
|
101
109
|
|
|
@@ -122,9 +130,10 @@ def process_json(cleaner: TextCleaner, value: Any) -> StructuredResult:
|
|
|
122
130
|
return result.lm_text
|
|
123
131
|
if isinstance(item, dict):
|
|
124
132
|
processed = {}
|
|
125
|
-
for key, child in item.items():
|
|
133
|
+
for index, (key, child) in enumerate(item.items()):
|
|
134
|
+
# The raw key may itself be PII, so its path uses the index.
|
|
126
135
|
processed_key = (
|
|
127
|
-
visit(key, path + ("@key",
|
|
136
|
+
visit(key, path + ("@key", index))
|
|
128
137
|
if isinstance(key, str) else key
|
|
129
138
|
)
|
|
130
139
|
if processed_key in processed:
|
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
from sct.config import TextCleanerConfig
|
|
9
9
|
from sct.utils import constants
|
|
10
|
+
from sct.utils.contact import numbers_regex
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def _rule(pattern, replacement, enabled: bool) -> dict[str, Any]:
|
|
@@ -30,6 +31,51 @@ def build_browser_manifest(config: TextCleanerConfig) -> dict[str, Any]:
|
|
|
30
31
|
"Browser export currently supports placeholder replacement only"
|
|
31
32
|
)
|
|
32
33
|
|
|
34
|
+
if config.check_replace_ips and config.replace_ips_public_only:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
"Browser export does not support replace_ips_public_only=True"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# The browser runs rules in order with nothing protecting emitted tokens,
|
|
40
|
+
# so a digit in one ("<EMAIL1>") would be rewritten by the number rules.
|
|
41
|
+
tokens = (
|
|
42
|
+
config.replace_with_html, config.replace_with_url, config.replace_with_email,
|
|
43
|
+
config.replace_with_ips, config.replace_with_dates,
|
|
44
|
+
config.replace_with_phone_numbers, config.replace_with_years,
|
|
45
|
+
)
|
|
46
|
+
if any(ch.isdigit() for token in tokens for ch in token):
|
|
47
|
+
raise ValueError("Browser export requires replacement tokens without digits")
|
|
48
|
+
if config.check_replace_currency_symbols and any(
|
|
49
|
+
constants.CURRENCY_REGEX.search(token) for token in tokens
|
|
50
|
+
):
|
|
51
|
+
raise ValueError(
|
|
52
|
+
"Browser export requires replacement tokens without currency symbols"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Same reason for any token a later enabled rule would match
|
|
56
|
+
# (an HTML token holding an email or URL, a URL token holding an email).
|
|
57
|
+
ordered = [
|
|
58
|
+
("html", constants.HTML_REGEX, config.replace_with_html, config.check_replace_html),
|
|
59
|
+
("url", constants.URL_REGEX, config.replace_with_url, config.check_replace_urls),
|
|
60
|
+
("email", constants.EMAIL_REGEX, config.replace_with_email, config.check_replace_emails),
|
|
61
|
+
("ip", constants.IP_REGEX, config.replace_with_ips, config.check_replace_ips),
|
|
62
|
+
("date", constants.DATE_REGEX, config.replace_with_dates, config.check_replace_dates),
|
|
63
|
+
("phone", constants.PHONE_REGEX, config.replace_with_phone_numbers,
|
|
64
|
+
config.check_replace_phone_numbers),
|
|
65
|
+
("year", constants.YEAR_REGEX, config.replace_with_years, config.check_replace_years),
|
|
66
|
+
("number", numbers_regex(config.replace_numbers_in_words), config.replace_with_numbers,
|
|
67
|
+
config.check_replace_numbers),
|
|
68
|
+
("currency", constants.CURRENCY_REGEX, config.replace_with_currency_symbols,
|
|
69
|
+
config.check_replace_currency_symbols),
|
|
70
|
+
]
|
|
71
|
+
for i, (name, _, token, enabled) in enumerate(ordered):
|
|
72
|
+
for later, pattern, _, later_enabled in ordered[i + 1:]:
|
|
73
|
+
if enabled and later_enabled and pattern.search(token):
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"Browser export: the {name} replacement token would be "
|
|
76
|
+
f"rewritten by the later {later} rule"
|
|
77
|
+
)
|
|
78
|
+
|
|
33
79
|
model_revisions = config.ner_model_revisions or {}
|
|
34
80
|
unpinned_models = sorted(
|
|
35
81
|
key
|
|
@@ -54,7 +100,7 @@ def build_browser_manifest(config: TextCleanerConfig) -> dict[str, Any]:
|
|
|
54
100
|
"runtime": "onnxruntime-web",
|
|
55
101
|
"models": models,
|
|
56
102
|
"rule_order": [
|
|
57
|
-
"html", "url", "email", "
|
|
103
|
+
"html", "url", "email", "ip", "date", "phone", "year", "number",
|
|
58
104
|
"currency",
|
|
59
105
|
],
|
|
60
106
|
"rules": {
|
|
@@ -73,6 +119,11 @@ def build_browser_manifest(config: TextCleanerConfig) -> dict[str, Any]:
|
|
|
73
119
|
config.replace_with_email,
|
|
74
120
|
config.check_replace_emails,
|
|
75
121
|
),
|
|
122
|
+
"ip": _rule(
|
|
123
|
+
constants.IP_REGEX,
|
|
124
|
+
config.replace_with_ips,
|
|
125
|
+
config.check_replace_ips,
|
|
126
|
+
),
|
|
76
127
|
"date": _rule(
|
|
77
128
|
constants.DATE_REGEX,
|
|
78
129
|
config.replace_with_dates,
|
|
@@ -89,7 +140,7 @@ def build_browser_manifest(config: TextCleanerConfig) -> dict[str, Any]:
|
|
|
89
140
|
config.check_replace_phone_numbers,
|
|
90
141
|
),
|
|
91
142
|
"number": _rule(
|
|
92
|
-
|
|
143
|
+
numbers_regex(config.replace_numbers_in_words),
|
|
93
144
|
config.replace_with_numbers,
|
|
94
145
|
config.check_replace_numbers,
|
|
95
146
|
),
|
|
@@ -287,6 +287,12 @@ class TextCleanerConfig:
|
|
|
287
287
|
# Computed at __post_init__ — union of all language sources
|
|
288
288
|
supported_languages: frozenset = frozenset()
|
|
289
289
|
|
|
290
|
+
# Added in 0.7.1; kept last so existing positional arguments keep their meaning.
|
|
291
|
+
check_replace_ips: bool = True
|
|
292
|
+
replace_ips_public_only: bool = False # True keeps private/loopback/reserved IPs
|
|
293
|
+
replace_with_ips: str = "<IP>"
|
|
294
|
+
replace_numbers_in_words: bool = False # True also replaces digits glued to Latin/Greek/Cyrillic letters (GPT4)
|
|
295
|
+
|
|
290
296
|
def __post_init__(self):
|
|
291
297
|
if self.gliclass_onnx:
|
|
292
298
|
raise ValueError(
|
|
@@ -357,6 +363,11 @@ class TextCleanerConfig:
|
|
|
357
363
|
object.__setattr__(self, 'gliner_labels', PII_LABELS)
|
|
358
364
|
if self.gliner_threshold == 0.4:
|
|
359
365
|
object.__setattr__(self, 'gliner_threshold', PII_DEFAULT_THRESHOLD)
|
|
366
|
+
# The final ensemble filter must not drop what GLiNER kept.
|
|
367
|
+
if self.ner_confidence_threshold == 0.85:
|
|
368
|
+
object.__setattr__(
|
|
369
|
+
self, 'ner_confidence_threshold', self.gliner_threshold,
|
|
370
|
+
)
|
|
360
371
|
if self.gliner_label_map is None:
|
|
361
372
|
object.__setattr__(self, 'gliner_label_map', PII_LABEL_MAP)
|
|
362
373
|
# Expand positional_tags to include all PII label tags
|
|
@@ -439,7 +450,17 @@ class TextCleanerConfig:
|
|
|
439
450
|
f"ner_models must include {sorted(REQUIRED_NER_KEYS)}, "
|
|
440
451
|
f"missing: {sorted(missing)}"
|
|
441
452
|
)
|
|
442
|
-
|
|
453
|
+
# A map of exactly LANG_KEYS matching ner_models_list is a normalized
|
|
454
|
+
# legacy/default config (replace(), copy): keep it exact. Decided by
|
|
455
|
+
# content only, so a dict and a mapping proxy give the same config.
|
|
456
|
+
derived = (
|
|
457
|
+
set(self.ner_models) == set(LANG_KEYS)
|
|
458
|
+
and tuple(self.ner_models[k] for k in LANG_KEYS) == self.ner_models_list
|
|
459
|
+
)
|
|
460
|
+
merged = (
|
|
461
|
+
dict(self.ner_models) if derived
|
|
462
|
+
else {**DEFAULT_NER_MODELS, **self.ner_models}
|
|
463
|
+
)
|
|
443
464
|
object.__setattr__(self, 'ner_models', MappingProxyType(merged))
|
|
444
465
|
object.__setattr__(
|
|
445
466
|
self, 'ner_models_list',
|
|
@@ -532,6 +553,21 @@ class TextCleanerConfig:
|
|
|
532
553
|
f"{sorted(self.supported_languages)}"
|
|
533
554
|
)
|
|
534
555
|
|
|
556
|
+
def __getstate__(self):
|
|
557
|
+
# Restore the exact normalized state; never re-run __post_init__.
|
|
558
|
+
state = dict(self.__dict__)
|
|
559
|
+
frozen = [k for k, v in state.items() if isinstance(v, MappingProxyType)]
|
|
560
|
+
for key in frozen:
|
|
561
|
+
state[key] = dict(state[key])
|
|
562
|
+
return state, frozen
|
|
563
|
+
|
|
564
|
+
def __setstate__(self, packed):
|
|
565
|
+
state, frozen = packed
|
|
566
|
+
for key, value in state.items():
|
|
567
|
+
if key in frozen:
|
|
568
|
+
value = MappingProxyType(value)
|
|
569
|
+
object.__setattr__(self, key, value)
|
|
570
|
+
|
|
535
571
|
|
|
536
572
|
def _config_from_module_globals() -> TextCleanerConfig:
|
|
537
573
|
"""Build a TextCleanerConfig from the current module-level variable state.
|
|
@@ -546,12 +582,15 @@ def _config_from_module_globals() -> TextCleanerConfig:
|
|
|
546
582
|
check_replace_html=m.CHECK_REPLACE_HTML,
|
|
547
583
|
check_replace_urls=m.CHECK_REPLACE_URLS,
|
|
548
584
|
check_replace_emails=m.CHECK_REPLACE_EMAILS,
|
|
585
|
+
check_replace_ips=getattr(m, 'CHECK_REPLACE_IPS', True),
|
|
586
|
+
replace_ips_public_only=getattr(m, 'REPLACE_IPS_PUBLIC_ONLY', False),
|
|
549
587
|
check_replace_years=m.CHECK_REPLACE_YEARS,
|
|
550
588
|
check_replace_dates=getattr(m, 'CHECK_REPLACE_DATES', False),
|
|
551
589
|
check_fuzzy_replace_dates=getattr(m, 'CHECK_FUZZY_REPLACE_DATES', False),
|
|
552
590
|
fuzzy_date_score_cutoff=getattr(m, 'FUZZY_DATE_SCORE_CUTOFF', 85),
|
|
553
591
|
check_replace_phone_numbers=m.CHECK_REPLACE_PHONE_NUMBERS,
|
|
554
592
|
check_replace_numbers=m.CHECK_REPLACE_NUMBERS,
|
|
593
|
+
replace_numbers_in_words=getattr(m, 'REPLACE_NUMBERS_IN_WORDS', False),
|
|
555
594
|
check_replace_currency_symbols=m.CHECK_REPLACE_CURRENCY_SYMBOLS,
|
|
556
595
|
check_ner_process=m.CHECK_NER_PROCESS,
|
|
557
596
|
check_remove_isolated_letters=m.CHECK_REMOVE_ISOLATED_LETTERS,
|
|
@@ -569,6 +608,7 @@ def _config_from_module_globals() -> TextCleanerConfig:
|
|
|
569
608
|
replace_with_url=m.REPLACE_WITH_URL,
|
|
570
609
|
replace_with_html=m.REPLACE_WITH_HTML,
|
|
571
610
|
replace_with_email=m.REPLACE_WITH_EMAIL,
|
|
611
|
+
replace_with_ips=getattr(m, 'REPLACE_WITH_IPS', '<IP>'),
|
|
572
612
|
replace_with_years=m.REPLACE_WITH_YEARS,
|
|
573
613
|
replace_with_dates=getattr(m, 'REPLACE_WITH_DATES', '<DATE>'),
|
|
574
614
|
replace_with_phone_numbers=m.REPLACE_WITH_PHONE_NUMBERS,
|
|
@@ -597,12 +637,15 @@ CHECK_TO_ASCII_UNICODE = True
|
|
|
597
637
|
CHECK_REPLACE_HTML = True
|
|
598
638
|
CHECK_REPLACE_URLS = True
|
|
599
639
|
CHECK_REPLACE_EMAILS = True
|
|
640
|
+
CHECK_REPLACE_IPS = True
|
|
641
|
+
REPLACE_IPS_PUBLIC_ONLY = False
|
|
600
642
|
CHECK_REPLACE_YEARS = True
|
|
601
643
|
CHECK_REPLACE_DATES = False
|
|
602
644
|
CHECK_FUZZY_REPLACE_DATES = False
|
|
603
645
|
FUZZY_DATE_SCORE_CUTOFF = 85
|
|
604
646
|
CHECK_REPLACE_PHONE_NUMBERS = True
|
|
605
647
|
CHECK_REPLACE_NUMBERS = True
|
|
648
|
+
REPLACE_NUMBERS_IN_WORDS = False
|
|
606
649
|
CHECK_REPLACE_CURRENCY_SYMBOLS = True
|
|
607
650
|
CHECK_NER_PROCESS = True
|
|
608
651
|
CHECK_REMOVE_ISOLATED_LETTERS = True
|
|
@@ -621,6 +664,7 @@ CHECK_REMOVE_EMOJI = False
|
|
|
621
664
|
REPLACE_WITH_URL = "<URL>"
|
|
622
665
|
REPLACE_WITH_HTML = "<HTML>"
|
|
623
666
|
REPLACE_WITH_EMAIL = "<EMAIL>"
|
|
667
|
+
REPLACE_WITH_IPS = "<IP>"
|
|
624
668
|
REPLACE_WITH_YEARS = "<YEAR>"
|
|
625
669
|
REPLACE_WITH_DATES = "<DATE>"
|
|
626
670
|
REPLACE_WITH_PHONE_NUMBERS = "<PHONE>"
|
|
@@ -56,6 +56,7 @@ def candidate_config(candidate: ModelCandidate) -> TextCleanerConfig:
|
|
|
56
56
|
"check_replace_html": False,
|
|
57
57
|
"check_replace_urls": False,
|
|
58
58
|
"check_replace_emails": False,
|
|
59
|
+
"check_replace_ips": False,
|
|
59
60
|
"check_replace_years": False,
|
|
60
61
|
"check_replace_dates": False,
|
|
61
62
|
"check_replace_phone_numbers": False,
|
|
@@ -82,6 +83,7 @@ def candidate_config(candidate: ModelCandidate) -> TextCleanerConfig:
|
|
|
82
83
|
gliner_labels=PII_LABELS,
|
|
83
84
|
gliner_label_map=PII_LABEL_MAP,
|
|
84
85
|
gliner_threshold=0.3,
|
|
86
|
+
ner_confidence_threshold=0.3,
|
|
85
87
|
gliner_onnx=candidate.supports_onnx,
|
|
86
88
|
)
|
|
87
89
|
if candidate.backend == "torch":
|
|
@@ -42,7 +42,11 @@ class InMemoryTokenStore:
|
|
|
42
42
|
|
|
43
43
|
@dataclass(frozen=True)
|
|
44
44
|
class DetectedEntity:
|
|
45
|
-
"""One entity produced by a detector.
|
|
45
|
+
"""One entity produced by a detector.
|
|
46
|
+
|
|
47
|
+
``start``/``end`` index the post-regex text (after the regex replacement
|
|
48
|
+
stages ran), not the raw input passed to ``process()``.
|
|
49
|
+
"""
|
|
46
50
|
|
|
47
51
|
entity_type: str
|
|
48
52
|
score: float
|