From b862b69bfe64bb0fa0a9155afec16f7c933a7742 Mon Sep 17 00:00:00 2001
From: Maziyar Panahi
Date: Sun, 19 Jul 2026 17:23:53 +0200
Subject: [PATCH] feat: add Central and Eastern European PII packs
---
README.md | 8 +-
clients/go/client.go | 3 +
clients/typescript/src/index.ts | 5 +-
docs/anonymization.md | 4 +-
docs/api/openapi.json | 30 ++-
docs/faq.md | 6 +-
docs/feature-map.md | 2 +-
docs/index.md | 4 +-
docs/languages.md | 30 +++
docs/rest-recipes.md | 7 +-
docs/troubleshooting.md | 8 +-
docs/website/assets/script.js | 2 +-
docs/website/index.html | 20 +-
models.jsonl | 10 +-
openmed/core/anonymizer/engine.py | 2 +
openmed/core/anonymizer/locales.py | 2 +
openmed/core/anonymizer/providers/__init__.py | 4 +
.../core/anonymizer/providers/clinical_ids.py | 27 +++
.../core/anonymizer/providers/registry_ids.py | 9 +
openmed/core/anonymizer/registry.py | 3 +
openmed/core/model_card.py | 2 +-
openmed/core/pii_i18n.py | 196 +++++++++++++++++-
openmed/core/script_detect.py | 6 +-
openmed/eval/golden/fixtures/i18n/uk.jsonl | 1 +
openmed/service/client.py | 3 +
openmed/service/schemas.py | 3 +
openmed/training/synthetic/locale_phi.py | 39 ++++
tests/unit/core/test_id_provider_registry.py | 2 +
tests/unit/core/test_script_detect.py | 6 +
tests/unit/test_pii_i18n.py | 145 ++++++++++++-
.../training/test_synthetic_locale_phi.py | 10 +-
31 files changed, 548 insertions(+), 51 deletions(-)
create mode 100644 openmed/eval/golden/fixtures/i18n/uk.jsonl
diff --git a/README.md b/README.md
index b57818b5d..18f7ddc12 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ No cloud. No vendor lock-in. No patient data leaving your network.
- 2,000+ models · 17 model-backed PII languages · 600+ PII checkpoints · 100% on-device · Apache-2.0
+ 2,000+ models · 20 model-backed PII languages · 600+ PII checkpoints · 100% on-device · Apache-2.0
@@ -503,10 +503,10 @@ On non-Apple-Silicon hosts, MLX model names are automatically substituted with t
---
-## Multilingual PII (17 model-backed languages)
+## Multilingual PII (20 model-backed languages)
-Extraction and de-identification support **17 supported PII language codes**:
-`ar`, `de`, `en`, `es`, `fr`, `he`, `hi`, `id`, `it`, `ja`, `ko`, `nl`, `pt`, `ro`, `te`, `th`, and `tr`, with **600+ PII checkpoints** in total.
+Extraction and de-identification support **20 supported PII language codes**:
+`ar`, `cs`, `de`, `el`, `en`, `es`, `fr`, `he`, `hi`, `id`, `it`, `ja`, `ko`, `nl`, `pt`, `ro`, `te`, `th`, `tr`, and `uk`, with **600+ PII checkpoints** in total.
These are the model-backed PII language allow-list.
OpenMed also includes validator-backed national-ID coverage for additional
ID-only locales such as Polish, Latvian, Slovak, Malay, Filipino, and Danish.
diff --git a/clients/go/client.go b/clients/go/client.go
index b8681679f..6af2ff339 100644
--- a/clients/go/client.go
+++ b/clients/go/client.go
@@ -87,6 +87,9 @@ const (
LangTH PIILanguage = "th"
LangKO PIILanguage = "ko"
LangRO PIILanguage = "ro"
+ LangUK PIILanguage = "uk"
+ LangCS PIILanguage = "cs"
+ LangEL PIILanguage = "el"
)
// DeidentificationMethod selects how detected PII spans are transformed by the
diff --git a/clients/typescript/src/index.ts b/clients/typescript/src/index.ts
index 6f8fb3816..815f9b010 100644
--- a/clients/typescript/src/index.ts
+++ b/clients/typescript/src/index.ts
@@ -26,7 +26,10 @@ export type PIILanguage =
| "id"
| "th"
| "ko"
- | "ro";
+ | "ro"
+ | "uk"
+ | "cs"
+ | "el";
export type DeidentificationMethod =
| "mask"
diff --git a/docs/anonymization.md b/docs/anonymization.md
index 6e2824b93..b1ed8d14c 100644
--- a/docs/anonymization.md
+++ b/docs/anonymization.md
@@ -341,8 +341,8 @@ local attention, sink tokens, RoPE+YaRN, tiktoken `o200k_base`), differing
only in their training data:
The per-language PII API uses `openmed.core.pii_i18n.SUPPORTED_LANGUAGES`
-as its source of truth and supports **17 supported PII language codes**:
-`ar`, `de`, `en`, `es`, `fr`, `he`, `hi`, `id`, `it`, `ja`, `ko`, `nl`, `pt`, `ro`, `te`, `th`, and `tr`.
+as its source of truth and supports **20 supported PII language codes**:
+`ar`, `cs`, `de`, `el`, `en`, `es`, `fr`, `he`, `hi`, `id`, `it`, `ja`, `ko`, `nl`, `pt`, `ro`, `te`, `th`, `tr`, and `uk`.
These are the model-backed PII language allow-list.
Additional validator-backed national-ID providers cover ID-only locales such as
Polish, Latvian, Slovak, Malay, Filipino, and Danish without adding
diff --git a/docs/api/openapi.json b/docs/api/openapi.json
index 91528c45d..f5d018802 100644
--- a/docs/api/openapi.json
+++ b/docs/api/openapi.json
@@ -196,7 +196,10 @@
"id",
"th",
"ko",
- "ro"
+ "ro",
+ "uk",
+ "cs",
+ "el"
],
"title": "Lang",
"type": "string"
@@ -417,7 +420,10 @@
"id",
"th",
"ko",
- "ro"
+ "ro",
+ "uk",
+ "cs",
+ "el"
],
"title": "Lang",
"type": "string"
@@ -540,7 +546,10 @@
"id",
"th",
"ko",
- "ro"
+ "ro",
+ "uk",
+ "cs",
+ "el"
],
"title": "Lang",
"type": "string"
@@ -636,7 +645,10 @@
"id",
"th",
"ko",
- "ro"
+ "ro",
+ "uk",
+ "cs",
+ "el"
],
"title": "Lang",
"type": "string"
@@ -752,7 +764,10 @@
"id",
"th",
"ko",
- "ro"
+ "ro",
+ "uk",
+ "cs",
+ "el"
],
"title": "Lang",
"type": "string"
@@ -876,7 +891,10 @@
"id",
"th",
"ko",
- "ro"
+ "ro",
+ "uk",
+ "cs",
+ "el"
],
"title": "Lang",
"type": "string"
diff --git a/docs/faq.md b/docs/faq.md
index 027f61835..8c5fdae84 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -87,13 +87,13 @@ model argument. Override `model_name` only when you need a specific checkpoint,
### Which languages are supported?
-PII extraction and de-identification support **17 supported PII language codes**:
-`ar`, `de`, `en`, `es`, `fr`, `he`, `hi`, `id`, `it`, `ja`, `ko`, `nl`, `pt`, `ro`, `te`, `th`, and `tr`.
+PII extraction and de-identification support **20 supported PII language codes**:
+`ar`, `cs`, `de`, `el`, `en`, `es`, `fr`, `he`, `hi`, `id`, `it`, `ja`, `ko`, `nl`, `pt`, `ro`, `te`, `th`, `tr`, and `uk`.
These are the model-backed PII language allow-list.
Validator-backed national-ID coverage is broader for specific ID-only locales,
including Polish, Latvian, Slovak, Malay, Filipino, and Danish.
The README keeps a short multilingual example set in
-[Multilingual PII](https://github.com/maziyarpanahi/openmed#multilingual-pii-17-model-backed-languages).
+[Multilingual PII](https://github.com/maziyarpanahi/openmed#multilingual-pii-20-model-backed-languages).
Clinical NER coverage depends on the selected registry model. Check each model's `languages`, `entity_types`, and
specialization in the [Model Registry](model-registry.md) before putting it behind an API or batch job.
diff --git a/docs/feature-map.md b/docs/feature-map.md
index 9d5ee5ee0..11314638a 100644
--- a/docs/feature-map.md
+++ b/docs/feature-map.md
@@ -15,7 +15,7 @@ coverage, see
| Canonical span contracts | Versioned `OpenMedSpan`, schema fingerprints, redaction action schemas, provenance, and compatibility gates. | `openmed/core/schemas/`, [De-identification API](./api/deidentification.md), `examples/v16_policy_audit_release_gates.py` |
| Audit and review evidence | Signed audit reports, reproducibility hashes, review bundles, FHIR `Provenance`/`AuditEvent`, audit diffs, and PHI-safe previews. | `openmed/core/audit.py`, `openmed/core/redaction_preview.py`, `openmed/risk/audit_diff.py`, `openmed/clinical/exporters/fhir/provenance.py`, `examples/v16_policy_audit_release_gates.py` |
| Runtime de-identification features | `DeidentificationResult.to_dataframe`, surrogate vaults, patient-keyed date shifting, format-preserving redaction, minimum-necessary action selection, streaming redaction, explain traces, section stamping, and risk budgets. | `openmed/core/pii.py`, `openmed/core/surrogate_vault.py`, `openmed/core/date_shift.py`, `openmed/core/anonymizer/format_preserve.py`, `openmed/core/redaction_strength.py`, `openmed/core/streaming.py`, `openmed/core/explain.py`, `openmed/risk/budget.py` |
-| Multilingual PII | 17 supported PII language codes: ar, de, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, and tr in the model-backed allow-list; locale validators, script detection, date/number normalization, deterministic locale PHI generation, and ID-only national-ID providers for additional locales. | `openmed/core/pii_i18n.py`, `openmed/core/script_detect.py`, `openmed/core/locale_formats.py`, `openmed/core/anonymizer/locales.py`, `openmed/training/synthetic/locale_phi.py`, `examples/pii_multilingual_new_languages.py` |
+| Multilingual PII | 20 supported PII language codes: ar, cs, de, el, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, tr, and uk in the model-backed allow-list; locale validators, script detection, date/number normalization, deterministic locale PHI generation, and ID-only national-ID providers for additional locales. | `openmed/core/pii_i18n.py`, `openmed/core/script_detect.py`, `openmed/core/locale_formats.py`, `openmed/core/anonymizer/locales.py`, `openmed/training/synthetic/locale_phi.py`, `examples/pii_multilingual_new_languages.py` |
## Multimodal And Structured Inputs
diff --git a/docs/index.md b/docs/index.md
index 8162199f8..c2286f166 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -20,8 +20,8 @@ coverage, and stronger release evidence:
- **Python, Swift, Kotlin/Android, REST, gRPC, React Native, TypeScript, and
browser paths** including OpenMedKit, typed REST clients, ONNX/WebGPU, and
Transformers.js export bundles.
-- **17 supported PII language codes: ar, de, en, es, fr, he, hi, id, it, ja,
- ko, nl, pt, ro, te, th, and tr** in the model-backed allow-list, with
+- **20 supported PII language codes: ar, cs, de, el, en, es, fr, he, hi, id,
+ it, ja, ko, nl, pt, ro, te, th, tr, and uk** in the model-backed allow-list, with
locale-aware validation and surrogate generation, plus additional
validator-backed national-ID coverage for ID-only locales.
- **Release evidence** for leakage heatmaps, model scorecards, threshold
diff --git a/docs/languages.md b/docs/languages.md
index aa20aa099..f452c9250 100644
--- a/docs/languages.md
+++ b/docs/languages.md
@@ -36,7 +36,9 @@ locale resolution, determinism, and cross-document surrogate vaults, see
| Code | Language | Default PII model | Faker locale | Notes |
| ------ | ---------- | ---------------------------------------------------------- | ------------ | ----------------------------------------------------------- |
| `ar` | Arabic | `OpenMed/OpenMed-PII-Arabic-SnowflakeMed-Large-568M-v1` | `ar_EG` | Egypt is the most-populous Arabic locale; override per call. |
+| `cs` | Czech | `OpenMed/privacy-filter-multilingual` | `cs_CZ` | Rodné číslo checksum-aware surrogates. |
| `de` | German | `OpenMed/OpenMed-PII-German-SuperClinical-Small-44M-v1` | `de_DE` | Steuer-ID surrogates via `GermanSteuerIdProvider`. |
+| `el` | Greek | `OpenMed/privacy-filter-multilingual` | `el_GR` | AMKA Luhn-aware surrogates. |
| `en` | English | `OpenMed/OpenMed-PII-SuperClinical-Small-44M-v1` | `en_US` | Default model splits names into `first_name`/`last_name`. |
| `es` | Spanish | `OpenMed/OpenMed-PII-Spanish-SuperClinical-Small-44M-v1` | `es_ES` | DNI/NIE checksum-aware surrogates. |
| `fr` | French | `OpenMed/OpenMed-PII-French-SuperClinical-Small-44M-v1` | `fr_FR` | NIR / INSEE surrogates via `fr_FR.ssn`. |
@@ -52,6 +54,7 @@ locale resolution, determinism, and cross-document surrogate vaults, see
| `te` | Telugu | `OpenMed/OpenMed-PII-Telugu-SuperClinical-Large-434M-v1` | `en_IN` | No Faker Telugu locale — `en_IN` approximation (warns once). |
| `th` | Thai | `OpenMed/privacy-filter-multilingual` | `th_TH` | Served by the multilingual privacy filter; Thai NID-aware. |
| `tr` | Turkish | `OpenMed/OpenMed-PII-Turkish-SuperClinical-Small-44M-v1` | `tr_TR` | TCKN surrogates. |
+| `uk` | Ukrainian | `OpenMed/privacy-filter-multilingual` | `uk_UA` | RNOKPP checksum-aware surrogates. |
Codes outside this list (for example `zh`, `pl`, `lv`, `sk`, `ms`, `tl`, `da`)
are **not** model-backed PII languages. Several of them still have
@@ -76,6 +79,15 @@ Before: المريضة ليلى حسن، الهاتف +20 10 1234 5678
After: المريضة [NAME]، الهاتف [PHONE]
```
+### Czech — `cs`
+
+- Model: `OpenMed/privacy-filter-multilingual` · locale `cs_CZ`
+
+```text
+Before: Pacient Jan Novák, rodné číslo 751116/0008
+After: Pacient [NAME], rodné číslo [ID]
+```
+
### German — `de`
- Model: `OpenMed/OpenMed-PII-German-SuperClinical-Small-44M-v1` · locale `de_DE`
@@ -85,6 +97,15 @@ Before: Patientin Anna Müller, Steuer-ID 86095742719
After: Patientin [NAME], Steuer-ID [ID]
```
+### Greek — `el`
+
+- Model: `OpenMed/privacy-filter-multilingual` · locale `el_GR`
+
+```text
+Before: Ασθενής Γιώργος Παπαδόπουλος, ΑΜΚΑ 16117508024
+After: Ασθενής [NAME], ΑΜΚΑ [ID]
+```
+
### English — `en`
- Model: `OpenMed/OpenMed-PII-SuperClinical-Small-44M-v1` · locale `en_US`
@@ -221,3 +242,12 @@ After: ผู้ป่วย [NAME] โทร [PHONE]
Before: Hasta Ayşe Yılmaz, TCKN 10000000146
After: Hasta [NAME], TCKN [ID]
```
+
+### Ukrainian — `uk`
+
+- Model: `OpenMed/privacy-filter-multilingual` · locale `uk_UA`
+
+```text
+Before: Пацієнтка Олена Коваль, РНОКПП 2974281300
+After: Пацієнтка [NAME], РНОКПП [ID]
+```
diff --git a/docs/rest-recipes.md b/docs/rest-recipes.md
index ca455ebac..f2f9884fc 100644
--- a/docs/rest-recipes.md
+++ b/docs/rest-recipes.md
@@ -174,9 +174,10 @@ hardware):
## Extract PII — `POST /pii/extract`
Detect personally identifiable information. Unless `model_name` is set, OpenMed
-selects the recommended PII model for `lang`. The 17 supported PII language
-codes: `ar`, `de`, `en`, `es`, `fr`, `he`, `hi`, `id`, `it`, `ja`, `ko`, `nl`,
-`pt`, `ro`, `te`, `th`, and `tr`. `confidence_threshold` defaults to `0.5`.
+selects the recommended PII model for `lang`. The 20 supported PII language
+codes: `ar`, `cs`, `de`, `el`, `en`, `es`, `fr`, `he`, `hi`, `id`, `it`, `ja`,
+`ko`, `nl`, `pt`, `ro`, `te`, `th`, `tr`, and `uk`. `confidence_threshold`
+defaults to `0.5`.
```bash
curl -sS --max-time 310 -X POST "$OPENMED_URL/pii/extract" \
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 24fffa168..85a094154 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -383,12 +383,12 @@ text = validate_input(user_supplied_text, max_length=2000, allow_empty=False)
**Symptom.** A PII call with an unrecognized `lang` raises:
```text
-ValueError: Unsupported language 'xx'. Supported: ['ar', 'de', 'en', 'es', 'fr', 'he', 'hi', 'id', 'it', 'ja', 'ko', 'nl', 'pt', 'ro', 'te', 'th', 'tr']
+ValueError: Unsupported language 'xx'. Supported: ['ar', 'cs', 'de', 'el', 'en', 'es', 'fr', 'he', 'hi', 'id', 'it', 'ja', 'ko', 'nl', 'pt', 'ro', 'te', 'th', 'tr', 'uk']
```
-**Cause.** PII extraction and de-identification support **17 supported PII language codes: ar, de, en, es,
-fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, and tr**. Passing anything outside that set (or a mistyped
-code) raises this error.
+**Cause.** PII extraction and de-identification support **20 supported PII language codes: ar, cs, de, el,
+en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, tr, and uk**. Passing anything outside that set (or a
+mistyped code) raises this error.
**Fix.** Use one of the supported codes with `extract_pii(..., lang="")`. Clinical NER coverage depends
on the selected registry model — check each model's `languages` in the
diff --git a/docs/website/assets/script.js b/docs/website/assets/script.js
index 01a9fc78b..6b7b1c66c 100644
--- a/docs/website/assets/script.js
+++ b/docs/website/assets/script.js
@@ -117,7 +117,7 @@ function moonSVG() {
return ` `;
}
-/* ---------- PHI Demo (cycles through 7 of the 17 languages OpenMed supports) ------ */
+/* ---------- PHI Demo (cycles through 7 of the 20 languages OpenMed supports) ------ */
const PHI_SAMPLES = [
{
lang: "en",
diff --git a/docs/website/index.html b/docs/website/index.html
index 87215707f..05018c26e 100644
--- a/docs/website/index.html
+++ b/docs/website/index.html
@@ -75,13 +75,13 @@
"@type": "FAQPage",
"mainEntity": [
{ "@type": "Question", "name": "What is OpenMed, exactly?",
- "acceptedAnswer": { "@type": "Answer", "text": "An open-source medical LLM toolkit. Specialized transformer models fine-tuned for biomedical named-entity recognition: diseases, drugs, genes, anatomy, chemicals, oncology, plus PII extraction and de-identification across 17 supported PII language codes: ar, de, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, and tr. These are the model-backed PII language allow-list. Ships as a Python package, Dockerized FastAPI and gRPC services, Swift and Kotlin OpenMedKit packages, React Native bridge, and browser runtime. Apache-2.0, no vendor lock-in, runs on your infrastructure." } },
+ "acceptedAnswer": { "@type": "Answer", "text": "An open-source medical LLM toolkit. Specialized transformer models fine-tuned for biomedical named-entity recognition: diseases, drugs, genes, anatomy, chemicals, oncology, plus PII extraction and de-identification across 20 supported PII language codes: ar, cs, de, el, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, tr, and uk. These are the model-backed PII language allow-list. Ships as a Python package, Dockerized FastAPI and gRPC services, Swift and Kotlin OpenMedKit packages, React Native bridge, and browser runtime. Apache-2.0, no vendor lock-in, runs on your infrastructure." } },
{ "@type": "Question", "name": "Are these generative LLMs or something else?",
"acceptedAnswer": { "@type": "Answer", "text": "Encoder transformers (BERT, ELECTRA, DeBERTa families), not generative chat models. They do extraction and classification, pulling structured entities out of unstructured clinical text, and stay small enough to run on a laptop or a phone. The reference paper reports new state-of-the-art on 10 of 12 biomedical NER benchmarks. Complementary to the larger generative medical LLM category, not a replacement." } },
{ "@type": "Question", "name": "Where does my data go when I use OpenMed?",
"acceptedAnswer": { "@type": "Answer", "text": "Nowhere you don't send it. You download the models once (Hugging Face or a private mirror) and inference runs wherever you run the Python process, the Docker container, or the Swift app: your laptop, your VPC, an on-prem server, or air-gapped hardware. No telemetry, no license check-in, no outbound calls at runtime. PHI stays on your side of the fence by default." } },
{ "@type": "Question", "name": "Does OpenMed support HIPAA-aligned workflows?",
- "acceptedAnswer": { "@type": "Answer", "text": "The PII catalog covers all 18 HIPAA Safe Harbor identifiers across 17 supported PII language codes: ar, de, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, and tr, the model-backed PII language allow-list, with locale-aware validators for SSN, NIR, Steuer-ID, Codice Fiscale, DNI, BSN, CPF/CNPJ, RRN, CNP, Luhn checks, and additional ID-only national-ID validators. Models are trained on de-identified, ethically sourced corpora. OpenMed provides the technical controls; the legal compliance boundary lives in your deployment." } },
+ "acceptedAnswer": { "@type": "Answer", "text": "The PII catalog covers all 18 HIPAA Safe Harbor identifiers across 20 supported PII language codes: ar, cs, de, el, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, tr, and uk, the model-backed PII language allow-list, with locale-aware validators for SSN, NIR, Steuer-ID, Codice Fiscale, DNI, BSN, CPF/CNPJ, RRN, CNP, Luhn checks, and additional ID-only national-ID validators. Models are trained on de-identified, ethically sourced corpora. OpenMed provides the technical controls; the legal compliance boundary lives in your deployment." } },
{ "@type": "Question", "name": "Can I fine-tune OpenMed models for my own vocabulary?",
"acceptedAnswer": { "@type": "Answer", "text": "Yes. Models are published on Hugging Face under permissive licensing with full training recipes. The reference approach combines lightweight domain-adaptive pre-training on a 350k-passage biomedical corpus with parameter-efficient LoRA fine-tuning, updating less than 1.5% of model parameters and completing in under 12 hours on a single GPU (<1.2 kg CO2e). Tokenizer assets and starter notebooks are in the openmed-starter repo." } },
{ "@type": "Question", "name": "Do I need OpenMed Agent to use OpenMed?",
@@ -240,7 +240,7 @@
- Live PHI detection · 17 model-backed languages
+ Live PHI detection · 20 model-backed languages
▶ watch
@@ -278,7 +278,7 @@
17
-
Model-backed PII languages: ar, de, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, tr
+
Model-backed PII languages: ar, cs, de, el, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, tr, uk
<1.2kg
@@ -326,7 +326,7 @@
Four lines to production
extract_pii / deidentify
-
Language-aware across 17 model-backed languages
+
Language-aware across 20 model-backed languages
@@ -455,7 +455,7 @@
Clinical text de-identification, built for HIPAA & GDPR .
- Language-aware PII models across 17 supported PII language codes: ar, de, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, and tr, plus validator-backed national-ID coverage for additional ID-only locales. These are the model-backed PII language allow-list. Process data locally, so your PHI never leaves your environment.
+ Language-aware PII models across 20 supported PII language codes: ar, cs, de, el, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, tr, and uk, plus validator-backed national-ID coverage for additional ID-only locales. These are the model-backed PII language allow-list. Process data locally, so your PHI never leaves your environment.
@@ -545,11 +545,11 @@
Flexible redaction methods
HIPAA Safe Harbor ready
- Detects all 18 HIPAA Safe Harbor identifiers, part of a wider 55+ PII entity catalog, across 17 model-backed PII language codes, with additional validator-backed national-ID coverage for ID-only locales.
+ Detects all 18 HIPAA Safe Harbor identifiers, part of a wider 55+ PII entity catalog, across 20 model-backed PII language codes, with additional validator-backed national-ID coverage for ID-only locales.
18 Safe Harbor
55+ entity types
- 17 model-backed languages
+ 20 model-backed languages
@@ -866,7 +866,7 @@ Questions from the clinical, ML, and compliance teams.+
-
An open-source medical NLP toolkit. Specialized transformer models fine-tuned for biomedical named-entity recognition: diseases, drugs, genes, anatomy, chemicals, oncology, plus PII extraction and de-identification across 17 supported PII language codes: ar, de, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, and tr. These are the model-backed PII language allow-list. Ships as a Python package, Dockerized FastAPI and gRPC services, Swift and Kotlin OpenMedKit packages, React Native bridge, and browser runtime. Apache-2.0, no vendor lock-in, runs on your infrastructure.
+
An open-source medical NLP toolkit. Specialized transformer models fine-tuned for biomedical named-entity recognition: diseases, drugs, genes, anatomy, chemicals, oncology, plus PII extraction and de-identification across 20 supported PII language codes: ar, cs, de, el, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, tr, and uk. These are the model-backed PII language allow-list. Ships as a Python package, Dockerized FastAPI and gRPC services, Swift and Kotlin OpenMedKit packages, React Native bridge, and browser runtime. Apache-2.0, no vendor lock-in, runs on your infrastructure.
@@ -893,7 +893,7 @@
Questions from the clinical, ML, and compliance teams.+
-
The PII catalog covers all 18 HIPAA Safe Harbor identifiers across 17 supported PII language codes: ar, de, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, and tr, the model-backed PII language allow-list, with locale-aware validators for SSN, NIR, Steuer-ID, Codice Fiscale, DNI, BSN, CPF/CNPJ, RRN, CNP, Luhn checks, and additional ID-only national-ID validators. Models are trained on de-identified, ethically sourced corpora. OpenMed provides the technical controls (on-device processing, configurable thresholds, multiple redaction methods); the legal compliance boundary lives in your deployment.
+
The PII catalog covers all 18 HIPAA Safe Harbor identifiers across 20 supported PII language codes: ar, cs, de, el, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, te, th, tr, and uk, the model-backed PII language allow-list, with locale-aware validators for SSN, NIR, Steuer-ID, Codice Fiscale, DNI, BSN, CPF/CNPJ, RRN, CNP, Luhn checks, and additional ID-only national-ID validators. Models are trained on de-identified, ethically sourced corpora. OpenMed provides the technical controls (on-device processing, configurable thresholds, multiple redaction methods); the legal compliance boundary lives in your deployment.
diff --git a/models.jsonl b/models.jsonl
index 916b675b5..1a91c4bf2 100644
--- a/models.jsonl
+++ b/models.jsonl
@@ -1501,16 +1501,16 @@
{"repo_id":"OpenMed/privacy-filter-ai4privacy-multilingual","family":"PII","task":"token-classification","languages":["de","en","es","fr","it","nl"],"tier":null,"param_count":null,"architecture":"privacy-filter","base_model":"privacy-filter","formats":["pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"ai4privacy/pii-masking-400k","micro_f1":null,"recall":null},"arxiv":null,"license":null,"reproducibility_hash":"sha256:d19f0d8d99f6110fb628c165f2abd74d5ad1548cade42e1b23ff08fccbc92fcc","released":"2026-04-27"}
{"repo_id":"OpenMed/privacy-filter-mlx","family":"PII","task":"token-classification","languages":["en"],"tier":null,"param_count":null,"architecture":"privacy-filter","base_model":"privacy-filter","formats":["mlx-fp","pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":null,"micro_f1":null,"recall":null},"arxiv":null,"license":"apache-2.0","reproducibility_hash":"sha256:c5cc91ec36a04a237164f1b540736f81cb45c4ce47b987c399df5d2a2e152458","released":"2026-04-23"}
{"repo_id":"OpenMed/privacy-filter-mlx-8bit","family":"PII","task":"token-classification","languages":["en"],"tier":null,"param_count":8000000000,"architecture":"privacy-filter","base_model":"privacy-filter","formats":["mlx-8bit","pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":null,"micro_f1":null,"recall":null},"arxiv":null,"license":"apache-2.0","reproducibility_hash":"sha256:fdea6d0c7d53b939197e35f2bacab8aeb29c7977efe0e91a11957ec1f2c7125d","released":"2026-04-24"}
-{"repo_id":"OpenMed/privacy-filter-multilingual","family":"PII","task":"token-classification","languages":["ar","de","en","es","fr","he","hi","id","it","ja","nl","pt","ro","te","th","tr"],"tier":null,"param_count":null,"architecture":"privacy-filter","base_model":"privacy-filter","formats":["pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"ai4privacy/pii-masking-200k","micro_f1":null,"recall":null},"arxiv":null,"license":"apache-2.0","reproducibility_hash":"sha256:0361a60a757838d1705d7f4eb964d06ce62a3444ce857a50f04475a634ad148c","released":"2026-05-03"}
-{"repo_id":"OpenMed/privacy-filter-multilingual-mlx","family":"PII","task":"token-classification","languages":["ar","de","en","es","fr","he","hi","id","it","ja","nl","pt","ro","te","th","tr"],"tier":null,"param_count":null,"architecture":"privacy-filter-multilingual","base_model":"OpenMed/privacy-filter-multilingual","formats":["mlx-fp","pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"ai4privacy/pii-masking-200k","micro_f1":null,"recall":null},"arxiv":null,"license":"apache-2.0","reproducibility_hash":"sha256:ee10aab29e25b15585c19b90e6f282d5b91264856e8b43655969ecc2a20adefa","released":"2026-05-04"}
-{"repo_id":"OpenMed/privacy-filter-multilingual-mlx-8bit","family":"PII","task":"token-classification","languages":["ar","de","en","es","fr","he","hi","id","it","ja","nl","pt","ro","te","th","tr"],"tier":null,"param_count":8000000000,"architecture":"privacy-filter-multilingual","base_model":"OpenMed/privacy-filter-multilingual","formats":["mlx-8bit","pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"ai4privacy/pii-masking-200k","micro_f1":null,"recall":null},"arxiv":null,"license":"apache-2.0","reproducibility_hash":"sha256:9b8ada55fc4ecdee7b11020fc571de344db3d184a70f852859a056e9268d490a","released":"2026-05-04"}
-{"repo_id":"OpenMed/privacy-filter-multilingual-v2","family":"PII","task":"token-classification","languages":["ar","de","en","es","fr","he","hi","id","it","ja","nl","pt","ro","te","th","tr"],"tier":null,"param_count":null,"architecture":"privacy-filter","base_model":"privacy-filter","formats":["pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"ai4privacy/pii-masking-200k","micro_f1":null,"recall":null},"arxiv":null,"license":"apache-2.0","reproducibility_hash":"sha256:e599d20337f10da7dc09602da6befb361bea6dcc9ddbab80729235e6d73a02ca","released":"2026-05-06"}
+{"repo_id":"OpenMed/privacy-filter-multilingual","family":"PII","task":"token-classification","languages":["ar","cs","de","el","en","es","fr","he","hi","id","it","ja","nl","pt","ro","te","th","tr","uk"],"tier":null,"param_count":null,"architecture":"privacy-filter","base_model":"privacy-filter","formats":["pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"ai4privacy/pii-masking-200k","micro_f1":null,"recall":null},"arxiv":null,"license":"apache-2.0","reproducibility_hash":"sha256:0361a60a757838d1705d7f4eb964d06ce62a3444ce857a50f04475a634ad148c","released":"2026-05-03"}
+{"repo_id":"OpenMed/privacy-filter-multilingual-mlx","family":"PII","task":"token-classification","languages":["ar","cs","de","el","en","es","fr","he","hi","id","it","ja","nl","pt","ro","te","th","tr","uk"],"tier":null,"param_count":null,"architecture":"privacy-filter-multilingual","base_model":"OpenMed/privacy-filter-multilingual","formats":["mlx-fp","pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"ai4privacy/pii-masking-200k","micro_f1":null,"recall":null},"arxiv":null,"license":"apache-2.0","reproducibility_hash":"sha256:ee10aab29e25b15585c19b90e6f282d5b91264856e8b43655969ecc2a20adefa","released":"2026-05-04"}
+{"repo_id":"OpenMed/privacy-filter-multilingual-mlx-8bit","family":"PII","task":"token-classification","languages":["ar","cs","de","el","en","es","fr","he","hi","id","it","ja","nl","pt","ro","te","th","tr","uk"],"tier":null,"param_count":8000000000,"architecture":"privacy-filter-multilingual","base_model":"OpenMed/privacy-filter-multilingual","formats":["mlx-8bit","pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"ai4privacy/pii-masking-200k","micro_f1":null,"recall":null},"arxiv":null,"license":"apache-2.0","reproducibility_hash":"sha256:9b8ada55fc4ecdee7b11020fc571de344db3d184a70f852859a056e9268d490a","released":"2026-05-04"}
+{"repo_id":"OpenMed/privacy-filter-multilingual-v2","family":"PII","task":"token-classification","languages":["ar","cs","de","el","en","es","fr","he","hi","id","it","ja","nl","pt","ro","te","th","tr","uk"],"tier":null,"param_count":null,"architecture":"privacy-filter","base_model":"privacy-filter","formats":["pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"ai4privacy/pii-masking-200k","micro_f1":null,"recall":null},"arxiv":null,"license":"apache-2.0","reproducibility_hash":"sha256:e599d20337f10da7dc09602da6befb361bea6dcc9ddbab80729235e6d73a02ca","released":"2026-05-06"}
{"repo_id":"OpenMed/privacy-filter-nemotron","family":"PII","task":"token-classification","languages":["en"],"tier":null,"param_count":null,"architecture":"privacy-filter","base_model":"privacy-filter","formats":["pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"nvidia/Nemotron-PII","micro_f1":null,"recall":null},"arxiv":null,"license":"apache-2.0","reproducibility_hash":"sha256:4e48757f0c46473fd197349b0e0bd7e1e91fed94874779fa0fee42ce616ef539","released":"2026-04-28"}
{"repo_id":"OpenMed/privacy-filter-nemotron-mlx","family":"PII","task":"token-classification","languages":["en"],"tier":null,"param_count":null,"architecture":"privacy-filter-nemotron","base_model":"OpenMed/privacy-filter-nemotron","formats":["mlx-fp","pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"nvidia/Nemotron-PII","micro_f1":null,"recall":null},"arxiv":null,"license":"apache-2.0","reproducibility_hash":"sha256:8d066cc5318174155880d36163dae87945c294b246509b5fd0ae53488cdff432","released":"2026-04-28"}
{"repo_id":"OpenMed/privacy-filter-nemotron-mlx-8bit","family":"PII","task":"token-classification","languages":["en"],"tier":null,"param_count":8000000000,"architecture":"privacy-filter-nemotron","base_model":"OpenMed/privacy-filter-nemotron","formats":["mlx-8bit","pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"nvidia/Nemotron-PII","micro_f1":null,"recall":null},"arxiv":null,"license":"apache-2.0","reproducibility_hash":"sha256:03ff66c2778b8e9753b5cd9107d156a7d911b7f51c47c576cf63b7d36f860d95","released":"2026-04-28"}
{"repo_id":"OpenMed/privacy-filter-nemotron-piimb-v2","family":"PII","task":"token-classification","languages":["de","en","es","fr","it","nl","pt"],"tier":null,"param_count":null,"architecture":"privacy-filter","base_model":"privacy-filter","formats":["pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"nvidia/Nemotron-PII","micro_f1":null,"recall":null},"arxiv":null,"license":"other","reproducibility_hash":"sha256:c75c1f0b1c892524106014a77734387a3b059d1d9dbad8a122c8774a63c97641","released":"2026-05-03"}
{"repo_id":"OpenMed/privacy-filter-nemotron-v2","family":"PII","task":"token-classification","languages":["de","en","es","fr","it","nl","pt"],"tier":null,"param_count":null,"architecture":"privacy-filter","base_model":"privacy-filter","formats":["pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"nvidia/Nemotron-PII","micro_f1":null,"recall":null},"arxiv":null,"license":"other","reproducibility_hash":"sha256:307b7a1b443ecb95b0430c6ece59ff9c65c53dbd453bb6b2fe23d0e109630fbe","released":"2026-05-03"}
-{"repo_id":"OpenMed/privacy-filter-openmed-pii54-multilingual","family":"PII","task":"token-classification","languages":["ar","de","en","es","fr","he","hi","id","it","ja","nl","pt","ro","te","th","tr"],"tier":null,"param_count":null,"architecture":null,"base_model":null,"formats":["pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"ai4privacy/pii-masking-200k","micro_f1":null,"recall":null},"arxiv":null,"license":"other","reproducibility_hash":"sha256:9a7aed8b87f32bca8d00c19e2972e29b46e73b1497e24da1242e035fcd9a4b70","released":"2026-04-28"}
+{"repo_id":"OpenMed/privacy-filter-openmed-pii54-multilingual","family":"PII","task":"token-classification","languages":["ar","cs","de","el","en","es","fr","he","hi","id","it","ja","nl","pt","ro","te","th","tr","uk"],"tier":null,"param_count":null,"architecture":null,"base_model":null,"formats":["pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"ai4privacy/pii-masking-200k","micro_f1":null,"recall":null},"arxiv":null,"license":"other","reproducibility_hash":"sha256:9a7aed8b87f32bca8d00c19e2972e29b46e73b1497e24da1242e035fcd9a4b70","released":"2026-04-28"}
{"repo_id":"OpenMed/privacy-filter-piimb-fine-grained","family":"PII","task":"token-classification","languages":["de","en","es","fr","it","nl","pt"],"tier":null,"param_count":null,"architecture":"privacy-filter","base_model":"privacy-filter","formats":["pytorch"],"canonical_labels":["PERSON","FIRST_NAME","LAST_NAME","MIDDLE_NAME","PREFIX","USERNAME","EMAIL","PHONE","URL","LOCATION","STREET_ADDRESS","BUILDING_NUMBER","ZIPCODE","GPS_COORDINATES","ORDINAL_DIRECTION","DATE","DATE_OF_BIRTH","TIME","AGE","ID_NUM","SSN","ACCOUNT_NUMBER","PASSWORD","PIN","API_KEY","CREDIT_CARD","CREDIT_CARD_ISSUER","CVV","IBAN","BIC","AMOUNT","CURRENCY","BITCOIN_ADDRESS","ETHEREUM_ADDRESS","LITECOIN_ADDRESS","MASKED_NUMBER","GENDER","EYE_COLOR","HEIGHT","ORGANIZATION","JOB_TITLE","JOB_DEPARTMENT","OCCUPATION","IP_ADDRESS","MAC_ADDRESS","USER_AGENT","VIN","VEHICLE_REGISTRATION","IMEI","OTHER"],"benchmark":{"dataset":"nvidia/Nemotron-PII","micro_f1":null,"recall":null},"arxiv":null,"license":"other","reproducibility_hash":"sha256:a45a1c2f956686407ac8846aec2815796670e8b6104f02ad2a2a9860b2c043c3","released":"2026-04-29"}
{"repo_id":"OpenMed/Qwen2.5-3B-MedVL","family":"Vision","task":"visual-question-answering","languages":[],"tier":null,"param_count":3000000000,"architecture":"qwen","base_model":"Qwen/Qwen2.5-VL-3B-Instruct","formats":["pytorch"],"canonical_labels":[],"benchmark":{"dataset":null,"micro_f1":null,"recall":null},"arxiv":null,"license":"apache-2.0","reproducibility_hash":"sha256:34185498a5dcacc59a1fd9b0262017fe32c061e96b1608c1ed80c7d76bc1d74d","released":"2026-03-23"}
{"repo_id":"OpenMed/Qwen2.5-VL-3B-Medical-v1","family":"General","task":"unknown","languages":[],"tier":null,"param_count":3000000000,"architecture":"qwen","base_model":null,"formats":["pytorch"],"canonical_labels":[],"benchmark":{"dataset":null,"micro_f1":null,"recall":null},"arxiv":null,"license":null,"reproducibility_hash":"sha256:839b80b14bae3a584808c25c87455b90221467637a9b9ebc93012ad3ed2b1d48","released":"2026-03-03"}
diff --git a/openmed/core/anonymizer/engine.py b/openmed/core/anonymizer/engine.py
index ea333e525..e532a00fc 100644
--- a/openmed/core/anonymizer/engine.py
+++ b/openmed/core/anonymizer/engine.py
@@ -321,7 +321,9 @@ def format_preserving_surrogate(
"en_IN",
"pt_PT",
"pt_BR",
+ "uk_UA",
"cs_CZ",
+ "el_GR",
}
)
diff --git a/openmed/core/anonymizer/locales.py b/openmed/core/anonymizer/locales.py
index 3f2066df4..0966cb7c7 100644
--- a/openmed/core/anonymizer/locales.py
+++ b/openmed/core/anonymizer/locales.py
@@ -54,6 +54,7 @@
"pl": "pl_PL",
"lv": "lv_LV",
"ko": "ko_KR",
+ "uk": "uk_UA",
"cs": "cs_CZ",
"sk": "sk_SK",
"ms": "ms_MY",
@@ -115,6 +116,7 @@
"pl": ("pl_PL", "pesel"), # PESEL
"lv": ("lv_LV", "personas_kods"),
"ko": ("ko_KR", "korean_rrn"), # RRN
+ "uk": ("uk_UA", "rnokpp"), # Ukrainian taxpayer registration number
"cs": ("cs_CZ", "rodne_cislo"), # Czech rodne cislo (shared provider)
"sk": ("sk_SK", "rodne_cislo"), # Slovak rodne cislo
"ms": ("ms_MY", "mykad"), # Malaysian MyKad / NRIC
diff --git a/openmed/core/anonymizer/providers/__init__.py b/openmed/core/anonymizer/providers/__init__.py
index 99945d0e7..0764d197f 100644
--- a/openmed/core/anonymizer/providers/__init__.py
+++ b/openmed/core/anonymizer/providers/__init__.py
@@ -26,6 +26,7 @@
RomanianCNPProvider,
SerbianJmbgProvider,
SpanishDNIProvider,
+ UkrainianRnokppProvider,
VietnameseIdProvider,
generate_bic,
generate_bulgarian_egn,
@@ -43,6 +44,7 @@
generate_portuguese_nif,
generate_romanian_cnp,
generate_teudat_zehut,
+ generate_ukrainian_rnokpp,
generate_vietnamese_cccd,
generate_vietnamese_cmnd,
register_clinical_providers,
@@ -76,6 +78,7 @@
"RomanianCNPProvider",
"SerbianJmbgProvider",
"SpanishDNIProvider",
+ "UkrainianRnokppProvider",
"VietnameseIdProvider",
"generate_bic",
"generate_bulgarian_egn",
@@ -86,6 +89,7 @@
"generate_indonesian_nik",
"generate_jmbg",
"generate_teudat_zehut",
+ "generate_ukrainian_rnokpp",
"generate_vietnamese_cccd",
"generate_vietnamese_cmnd",
"generate_korean_rrn",
diff --git a/openmed/core/anonymizer/providers/clinical_ids.py b/openmed/core/anonymizer/providers/clinical_ids.py
index e46b75dc4..ed06ecaee 100644
--- a/openmed/core/anonymizer/providers/clinical_ids.py
+++ b/openmed/core/anonymizer/providers/clinical_ids.py
@@ -27,6 +27,7 @@
- Philippine PhilSys PSN and PhilHealth PIN structural formats
- Danish CPR / personnummer with a decodable embedded birth date
- Thai national ID (13 digits with a weighted mod-11 checksum)
+ - Ukrainian RNOKPP (10 digits with a weighted mod-11 checksum)
- Polish PESEL, Latvian personas kods, South Korean RRN, and Slovak rodne
cislo
- UK NHS Number, a patient health identifier validated with the NHS
@@ -1447,6 +1448,30 @@ def rodne_cislo(self) -> str:
return generate_rodne_cislo(rng=self.generator.random)
+# ---------------------------------------------------------------------------
+# Ukrainian RNOKPP (10 digits, weighted modulo-11 checksum)
+# ---------------------------------------------------------------------------
+
+
+def generate_ukrainian_rnokpp(*, rng: random.Random | None = None) -> str:
+ """Generate a checksum-valid Ukrainian RNOKPP surrogate."""
+ source = rng or random.Random()
+ day_count = source.randint(1, 50000)
+ serial = source.randint(0, 999)
+ gender_digit = source.randint(0, 9)
+ body = f"{day_count:05d}{serial:03d}{gender_digit}"
+ weights = (-1, 5, 7, 9, 4, 6, 10, 5, 7)
+ check = sum(int(digit) * weight for digit, weight in zip(body, weights)) % 11 % 10
+ return body + str(check)
+
+
+class UkrainianRnokppProvider(BaseProvider):
+ """Generates valid Ukrainian RNOKPP taxpayer registration numbers."""
+
+ def rnokpp(self) -> str:
+ return generate_ukrainian_rnokpp(rng=self.generator.random)
+
+
# ---------------------------------------------------------------------------
# Thai national ID (13 digits, weighted mod-11 checksum)
# ---------------------------------------------------------------------------
@@ -1948,6 +1973,7 @@ def register_clinical_providers(faker) -> None:
"SpanishNIEProvider",
"UKNHSNumberProvider",
"UKNINOProvider",
+ "UkrainianRnokppProvider",
"generate_australian_medicare",
"generate_australian_tfn",
"generate_bc_phn",
@@ -1979,6 +2005,7 @@ def register_clinical_providers(faker) -> None:
"generate_vietnamese_cccd",
"generate_vietnamese_cmnd",
"generate_uk_nhs_number",
+ "generate_ukrainian_rnokpp",
"generate_unified_social_credit_code",
"id_subtype_for_entity_type",
"register_clinical_providers",
diff --git a/openmed/core/anonymizer/providers/registry_ids.py b/openmed/core/anonymizer/providers/registry_ids.py
index 99a9537ec..57e6ec5fe 100644
--- a/openmed/core/anonymizer/providers/registry_ids.py
+++ b/openmed/core/anonymizer/providers/registry_ids.py
@@ -61,6 +61,7 @@
validate_turkish_tckn,
validate_uk_nhs_number,
validate_uk_nino,
+ validate_ukrainian_rnokpp,
validate_vietnamese_cccd,
validate_vietnamese_cmnd,
)
@@ -94,6 +95,7 @@
ThaiNationalIdProvider,
UKNHSNumberProvider,
UKNINOProvider,
+ UkrainianRnokppProvider,
VietnameseIdProvider,
validate_australian_medicare,
validate_australian_tfn,
@@ -381,6 +383,13 @@ def _register_builtin_specs() -> None:
faker_method="isikukood",
faker_provider=EstonianIsikukoodProvider,
)
+ _register_aliases(
+ ("uk", "uk_UA"),
+ id_type="rnokpp",
+ validate=validate_ukrainian_rnokpp,
+ faker_method="rnokpp",
+ faker_provider=UkrainianRnokppProvider,
+ )
_register_aliases(
("el", "el_GR"),
id_type="amka",
diff --git a/openmed/core/anonymizer/registry.py b/openmed/core/anonymizer/registry.py
index 251cd1ab6..b13d35489 100644
--- a/openmed/core/anonymizer/registry.py
+++ b/openmed/core/anonymizer/registry.py
@@ -129,8 +129,10 @@ def _gen_gps(faker, original, *, locale):
"fil_PH",
"da_DK",
"th_TH",
+ "uk_UA",
"cs_CZ",
"sk_SK",
+ "el_GR",
}
)
@@ -182,6 +184,7 @@ def _gen_age(faker, original, *, locale):
"lv_LV": "personas_kods",
"ko_KR": "korean_rrn",
"th_TH": "thai_national_id",
+ "uk_UA": "rnokpp",
"sk_SK": "rodne_cislo",
"cs_CZ": "rodne_cislo",
"ro_RO": "romanian_cnp",
diff --git a/openmed/core/model_card.py b/openmed/core/model_card.py
index 2a08baea8..1794a43e1 100644
--- a/openmed/core/model_card.py
+++ b/openmed/core/model_card.py
@@ -406,7 +406,7 @@ def _render_android_onnx_model_card(
"clinical AI stack:",
"",
"- **2,000+ medical models** for clinical NER, biomedical extraction, and privacy.",
- "- **PII detection and de-identification** across 55+ identifier types and 17 languages.",
+ "- **PII detection and de-identification** across 55+ identifier types and 20 languages.",
"- **Python, MLX, Swift, Android, React Native, Web, REST, and gRPC** runtimes.",
"- **Structured and multimodal intake** for OCR, documents, DICOM, FHIR, and HL7.",
"- **Offline and air-gapped deployment** with no telemetry by default.",
diff --git a/openmed/core/pii_i18n.py b/openmed/core/pii_i18n.py
index 5f6e29925..68f62fbd1 100644
--- a/openmed/core/pii_i18n.py
+++ b/openmed/core/pii_i18n.py
@@ -53,6 +53,9 @@
"th",
"ko",
"ro",
+ "uk",
+ "cs",
+ "el",
}
# Languages with validator-backed national-ID coverage but no bundled default
@@ -70,8 +73,6 @@
"hr",
"bg",
"fi",
- "cs",
- "el",
"vi",
}
@@ -93,6 +94,9 @@
"th": "Thai",
"ko": "Korean",
"ro": "Romanian",
+ "uk": "Ukrainian",
+ "cs": "Czech",
+ "el": "Greek",
}
LANGUAGE_MODEL_PREFIX: Dict[str, str] = {
@@ -113,6 +117,9 @@
"th": "Thai-",
"ko": "Korean-",
"ro": "Romanian-",
+ "uk": "Ukrainian-",
+ "cs": "Czech-",
+ "el": "Greek-",
}
DEFAULT_PII_MODELS: Dict[str, str] = {
@@ -133,6 +140,9 @@
"th": "OpenMed/privacy-filter-multilingual",
"ko": "OpenMed/OpenMed-PII-Korean-NomicMed-Large-395M-v1",
"ro": "OpenMed/privacy-filter-multilingual",
+ "uk": "OpenMed/privacy-filter-multilingual",
+ "cs": "OpenMed/privacy-filter-multilingual",
+ "el": "OpenMed/privacy-filter-multilingual",
}
@@ -1032,6 +1042,34 @@ def _latvian_personas_kods_check_digit(digits: list[int]) -> int:
)
+def validate_ukrainian_rnokpp(text: str) -> bool:
+ """Validate a Ukrainian RNOKPP taxpayer registration number.
+
+ RNOKPP values contain ten digits. The last digit is the modulo-11,
+ modulo-10 checksum of the first nine digits weighted by
+ ``(-1, 5, 7, 9, 4, 6, 10, 5, 7)``.
+
+ Args:
+ text: Candidate ten-digit RNOKPP value.
+
+ Returns:
+ ``True`` when the value has the required shape and checksum.
+ """
+ if not isinstance(text, str):
+ return False
+
+ digits = text.strip()
+ if re.fullmatch(r"[0-9]{10}", digits) is None:
+ return False
+
+ weights = (-1, 5, 7, 9, 4, 6, 10, 5, 7)
+ numbers = [int(digit) for digit in digits]
+ check_digit = (
+ sum(weight * digit for weight, digit in zip(weights, numbers[:9])) % 11 % 10
+ )
+ return numbers[9] == check_digit
+
+
def validate_greek_amka(text: str) -> bool:
"""Validate a Greek AMKA social-security number.
@@ -1814,6 +1852,48 @@ def validate_romanian_cnp(text: str) -> bool:
"noiembrie",
"decembrie",
],
+ "uk": [
+ "січень",
+ "лютий",
+ "березень",
+ "квітень",
+ "травень",
+ "червень",
+ "липень",
+ "серпень",
+ "вересень",
+ "жовтень",
+ "листопад",
+ "грудень",
+ ],
+ "cs": [
+ "leden",
+ "únor",
+ "březen",
+ "duben",
+ "květen",
+ "červen",
+ "červenec",
+ "srpen",
+ "září",
+ "říjen",
+ "listopad",
+ "prosinec",
+ ],
+ "el": [
+ "Ιανουάριος",
+ "Φεβρουάριος",
+ "Μάρτιος",
+ "Απρίλιος",
+ "Μάιος",
+ "Ιούνιος",
+ "Ιούλιος",
+ "Αύγουστος",
+ "Σεπτέμβριος",
+ "Οκτώβριος",
+ "Νοέμβριος",
+ "Δεκέμβριος",
+ ],
}
@@ -4884,6 +4964,102 @@ def validate_unified_social_credit_code(text: str) -> bool:
),
]
+# ---------------------------------------------------------------------------
+# Ukrainian PII patterns (Cyrillic script)
+# ---------------------------------------------------------------------------
+
+_UKRAINIAN_MONTH_PATTERN = (
+ r"січня|січень|лютого|лютий|березня|березень|квітня|квітень|"
+ r"травня|травень|червня|червень|липня|липень|серпня|серпень|"
+ r"вересня|вересень|жовтня|жовтень|листопада|листопад|грудня|грудень"
+)
+
+_UKRAINIAN_STREET_MARKER = (
+ r"вулиця|вул\.|проспект|просп\.|провулок|пров\.|бульвар|бул\.|площа|шосе"
+)
+
+_UKRAINIAN_PII_PATTERNS: List[PIIPattern] = [
+ PIIPattern(
+ r"\b\d{1,2}[./-]\d{1,2}[./-]\d{2,4}\b",
+ "date",
+ priority=9,
+ base_score=0.6,
+ context_words=[
+ "дата",
+ "народження",
+ "народився",
+ "народилася",
+ "прийом",
+ "виписка",
+ ],
+ context_boost=0.3,
+ ),
+ PIIPattern(
+ rf"\b\d{{1,2}}\s+(?:{_UKRAINIAN_MONTH_PATTERN})\s+\d{{4}}\b",
+ "date",
+ priority=8,
+ base_score=0.7,
+ context_words=[
+ "дата",
+ "народження",
+ "народився",
+ "народилася",
+ "прийом",
+ "виписка",
+ ],
+ context_boost=0.25,
+ flags=re.IGNORECASE,
+ ),
+ PIIPattern(
+ r"(? bool:
"sr": _SERBIAN_PII_PATTERNS,
"hu": _HUNGARIAN_PII_PATTERNS,
"et": _ESTONIAN_PII_PATTERNS,
+ "uk": _UKRAINIAN_PII_PATTERNS,
"el": _GREEK_PII_PATTERNS,
"cs": _CZECH_PII_PATTERNS,
"vi": _VIETNAMESE_PII_PATTERNS,
@@ -5746,6 +5923,21 @@ def validate_unified_social_credit_code(text: str) -> bool:
"LOCATION": ["Tallinn", "Tartu", "Parnu"],
"ZIPCODE": ["10115", "50090", "80010"],
},
+ "uk": {
+ "NAME": ["Олена Коваль", "Андрій Шевченко", "Ірина Бондар", "Тарас Мельник"],
+ "FIRST_NAME": ["Олена", "Андрій", "Ірина", "Тарас"],
+ "LAST_NAME": ["Коваль", "Шевченко", "Бондар", "Мельник"],
+ "EMAIL": ["patsiient@example.ua", "kontakt@example.org"],
+ "PHONE": ["+380 67 123 45 67", "044 123 45 67"],
+ "ID_NUM": ["2974281300", "3695007088"],
+ "STREET_ADDRESS": ["вулиця Хрещатик 22", "проспект Свободи 15"],
+ "URL_PERSONAL": ["https://example.ua"],
+ "USERNAME": ["patsiient123", "korystuvach456"],
+ "DATE": ["16.11.1975", "16 листопада 1975"],
+ "AGE": ["45", "62", "38"],
+ "LOCATION": ["Київ", "Львів", "Одеса"],
+ "ZIPCODE": ["01001", "79000", "65000"],
+ },
"cs": {
"NAME": ["Jana Nováková", "Petr Novák", "Marie Svobodová", "Tomáš Král"],
"FIRST_NAME": ["Jana", "Petr", "Marie", "Tomáš"],
diff --git a/openmed/core/script_detect.py b/openmed/core/script_detect.py
index 0ca90d682..12d40bcea 100644
--- a/openmed/core/script_detect.py
+++ b/openmed/core/script_detect.py
@@ -29,15 +29,15 @@
)
SCRIPT_LANGUAGE_HINTS: dict[str, tuple[str, ...]] = {
- "Latin": ("en", "fr", "de", "it", "es", "nl", "pt", "tr"),
+ "Latin": ("en", "fr", "de", "it", "es", "nl", "pt", "tr", "cs"),
"Arabic": ("ar",),
"Han": ("ja",),
"Hiragana/Katakana": ("ja",),
"Hangul": ("ko",),
- "Cyrillic": ("en",),
+ "Cyrillic": ("uk",),
"Devanagari": ("hi",),
"Telugu": ("te",),
- "Greek": ("en",),
+ "Greek": ("el",),
"Hebrew": ("en",),
"Thai": ("en",),
UNKNOWN_SCRIPT: ("en",),
diff --git a/openmed/eval/golden/fixtures/i18n/uk.jsonl b/openmed/eval/golden/fixtures/i18n/uk.jsonl
new file mode 100644
index 000000000..144a4205c
--- /dev/null
+++ b/openmed/eval/golden/fixtures/i18n/uk.jsonl
@@ -0,0 +1 @@
+{"id": "golden-i18n-uk-clinical-pii", "language": "uk", "text": "Пацієнт: Олена Коваль. Дата народження 16.11.1975, телефон +380 67 123 45 67, РНОКПП 2974281300, адреса вулиця Хрещатик 22, поштовий індекс 01001.", "gold_spans": [{"start": 39, "end": 49, "label": "DATE", "text": "16.11.1975"}, {"start": 59, "end": 76, "label": "PHONE", "text": "+380 67 123 45 67"}, {"start": 85, "end": 95, "label": "ID_NUM", "text": "2974281300", "metadata": {"checksum_status": "valid", "identifier_type": "rnokpp"}}, {"start": 104, "end": 122, "label": "STREET_ADDRESS", "text": "вулиця Хрещатик 22"}, {"start": 140, "end": 145, "label": "ZIPCODE", "text": "01001"}], "metadata": {"category": "multilingual", "expected_output": {"method": "mask", "text": "Пацієнт: Олена Коваль. Дата народження [DATE], телефон [PHONE], РНОКПП [ID_NUM], адреса [STREET_ADDRESS], поштовий індекс [ZIPCODE]."}, "identifier_type": "rnokpp", "locale": "uk_UA", "synthetic": true}}
diff --git a/openmed/service/client.py b/openmed/service/client.py
index eb3cd9720..8dae1d0d5 100644
--- a/openmed/service/client.py
+++ b/openmed/service/client.py
@@ -30,6 +30,9 @@
"th",
"ko",
"ro",
+ "uk",
+ "cs",
+ "el",
]
_DEFAULT_PII_MODEL = "OpenMed/OpenMed-PII-SuperClinical-Small-44M-v1"
diff --git a/openmed/service/schemas.py b/openmed/service/schemas.py
index 9b19817ac..a0f46885d 100644
--- a/openmed/service/schemas.py
+++ b/openmed/service/schemas.py
@@ -58,6 +58,9 @@
"th",
"ko",
"ro",
+ "uk",
+ "cs",
+ "el",
]
diff --git a/openmed/training/synthetic/locale_phi.py b/openmed/training/synthetic/locale_phi.py
index a655addc8..68450ba3a 100644
--- a/openmed/training/synthetic/locale_phi.py
+++ b/openmed/training/synthetic/locale_phi.py
@@ -18,9 +18,11 @@
from openmed.core.pii_i18n import (
SUPPORTED_LANGUAGES,
validate_aadhaar,
+ validate_czech_rodne_cislo,
validate_dutch_bsn,
validate_french_nir,
validate_german_steuer_id,
+ validate_greek_amka,
validate_indonesian_nik,
validate_israeli_teudat_zehut,
validate_italian_codice_fiscale,
@@ -30,6 +32,7 @@
validate_spanish_nie,
validate_thai_national_id,
validate_turkish_tckn,
+ validate_ukrainian_rnokpp,
)
SUPPORTED_LOCALE_PHI_LANGUAGES: Final[tuple[str, ...]] = (
@@ -50,6 +53,9 @@
"th",
"ko",
"ro",
+ "uk",
+ "cs",
+ "el",
)
LOCALE_PHI_LABELS: Final[tuple[str, ...]] = (
@@ -236,6 +242,33 @@
". Control la ",
".",
),
+ "uk": (
+ "Клінічна нотатка: пацієнт ",
+ " народився ",
+ ". РНОКПП пацієнта ",
+ ". Телефон ",
+ ". Адреса ",
+ ". Дата наступного огляду ",
+ ".",
+ ),
+ "cs": (
+ "Klinická poznámka: pacient ",
+ " se narodil ",
+ ". Rodné číslo pacienta ",
+ ". Telefon ",
+ ". Adresa ",
+ ". Datum kontroly ",
+ ".",
+ ),
+ "el": (
+ "Κλινική σημείωση: ασθενής ",
+ " γεννήθηκε στις ",
+ ". ΑΜΚΑ ασθενούς ",
+ ". Τηλέφωνο ",
+ ". Διεύθυνση ",
+ ". Ημερομηνία επανελέγχου ",
+ ".",
+ ),
}
_NATIONAL_ID_VALIDATORS: Final[Mapping[str, Callable[[str], bool]]] = {
@@ -254,6 +287,9 @@
"th": validate_thai_national_id,
"ko": validate_korean_rrn,
"ro": validate_romanian_cnp,
+ "uk": validate_ukrainian_rnokpp,
+ "cs": validate_czech_rodne_cislo,
+ "el": validate_greek_amka,
}
_NATIONAL_ID_VALIDATOR_NAMES: Final[Mapping[str, str]] = {
@@ -272,6 +308,9 @@
"th": "pii_i18n.validate_thai_national_id",
"ko": "pii_i18n.validate_korean_rrn",
"ro": "pii_i18n.validate_romanian_cnp",
+ "uk": "pii_i18n.validate_ukrainian_rnokpp",
+ "cs": "pii_i18n.validate_czech_rodne_cislo",
+ "el": "pii_i18n.validate_greek_amka",
}
diff --git a/tests/unit/core/test_id_provider_registry.py b/tests/unit/core/test_id_provider_registry.py
index f48813a9e..a60bed9b2 100644
--- a/tests/unit/core/test_id_provider_registry.py
+++ b/tests/unit/core/test_id_provider_registry.py
@@ -39,6 +39,7 @@ def _always_valid(_text: str) -> bool:
("lv", "personas_kods"),
("ko", "rrn"),
("hu", "taj"),
+ ("uk", "rnokpp"),
("cs", "rodne_cislo"),
("sk", "rodne_cislo"),
("pt", "cpf"),
@@ -69,6 +70,7 @@ def _always_valid(_text: str) -> bool:
("lv", "personas_kods", "lv_LV"),
("ko", "rrn", "ko_KR"),
("hu", "taj", "hu_HU"),
+ ("uk", "rnokpp", "uk_UA"),
("cs", "rodne_cislo", "cs_CZ"),
("sk", "rodne_cislo", "sk_SK"),
("pt", "cpf", "pt_BR"),
diff --git a/tests/unit/core/test_script_detect.py b/tests/unit/core/test_script_detect.py
index b5a31ec76..760ef4c17 100644
--- a/tests/unit/core/test_script_detect.py
+++ b/tests/unit/core/test_script_detect.py
@@ -76,6 +76,12 @@ def test_script_language_hints_cover_detectable_scripts():
assert set(hints) <= SUPPORTED_LANGUAGES | NATIONAL_ID_ONLY_LANGUAGES
+def test_cee_script_language_hints_route_to_native_packs():
+ assert "cs" in candidate_languages_for_script("Latin")
+ assert candidate_languages_for_script("Cyrillic") == ("uk",)
+ assert candidate_languages_for_script("Greek") == ("el",)
+
+
def test_normalize_for_pii_detection_folds_obfuscation_with_offset_map():
text = "Patient J\u200bo\u0301hn D\u03bfe"
normalized = normalize_for_pii_detection(text)
diff --git a/tests/unit/test_pii_i18n.py b/tests/unit/test_pii_i18n.py
index 6e9a6fe64..98082187c 100644
--- a/tests/unit/test_pii_i18n.py
+++ b/tests/unit/test_pii_i18n.py
@@ -68,6 +68,7 @@
validate_spanish_nie,
validate_thai_national_id,
validate_turkish_tckn,
+ validate_ukrainian_rnokpp,
validate_vietnamese_cccd,
validate_vietnamese_cmnd,
)
@@ -99,6 +100,9 @@ def test_supported_languages(self):
"th",
"ko",
"ro",
+ "uk",
+ "cs",
+ "el",
}
def test_national_id_only_languages(self):
@@ -115,8 +119,6 @@ def test_national_id_only_languages(self):
"hr",
"bg",
"fi",
- "cs",
- "el",
"vi",
}
@@ -141,6 +143,9 @@ def test_language_model_prefix(self):
assert LANGUAGE_MODEL_PREFIX["th"] == "Thai-"
assert LANGUAGE_MODEL_PREFIX["ko"] == "Korean-"
assert LANGUAGE_MODEL_PREFIX["ro"] == "Romanian-"
+ assert LANGUAGE_MODEL_PREFIX["uk"] == "Ukrainian-"
+ assert LANGUAGE_MODEL_PREFIX["cs"] == "Czech-"
+ assert LANGUAGE_MODEL_PREFIX["el"] == "Greek-"
def test_default_pii_models_all_languages(self):
assert set(DEFAULT_PII_MODELS.keys()) == SUPPORTED_LANGUAGES
@@ -165,6 +170,9 @@ def test_default_pii_models_naming(self):
== "OpenMed/OpenMed-PII-Korean-NomicMed-Large-395M-v1"
)
assert DEFAULT_PII_MODELS["ro"] == "OpenMed/privacy-filter-multilingual"
+ assert DEFAULT_PII_MODELS["uk"] == "OpenMed/privacy-filter-multilingual"
+ assert DEFAULT_PII_MODELS["cs"] == "OpenMed/privacy-filter-multilingual"
+ assert DEFAULT_PII_MODELS["el"] == "OpenMed/privacy-filter-multilingual"
# English has no language prefix
assert "French" not in DEFAULT_PII_MODELS["en"]
assert "German" not in DEFAULT_PII_MODELS["en"]
@@ -3650,6 +3658,139 @@ def test_finnish_i18n_golden_fixture_deidentifies_with_no_leakage_offline():
assert span["text"] not in result.deidentified_text
+def test_validate_ukrainian_rnokpp():
+ assert validate_ukrainian_rnokpp("2974281300")
+ assert validate_ukrainian_rnokpp("3695007088")
+
+ assert validate_ukrainian_rnokpp("2974281301") is False
+ assert validate_ukrainian_rnokpp("297428130") is False
+ assert validate_ukrainian_rnokpp("29742 81300") is False
+ assert validate_ukrainian_rnokpp("abcdefghij") is False
+
+
+def test_generated_ukrainian_surrogate_passes_validator():
+ assert LANG_TO_LOCALE["uk"] == "uk_UA"
+
+ anonymizer = Anonymizer(lang="uk", consistent=True, seed=42)
+ surrogate = anonymizer.surrogate("2974281300", "national_id")
+
+ assert validate_ukrainian_rnokpp(surrogate) is True
+ assert surrogate != "2974281300"
+
+
+def test_ukrainian_clinical_sample_expected_spans():
+ text = (
+ "Пацієнт: Олена Коваль. Дата народження 16.11.1975, "
+ "телефон +380 67 123 45 67, РНОКПП 2974281300, "
+ "адреса вулиця Хрещатик 22, поштовий індекс 01001."
+ )
+ expected = {
+ ("date", 39, 49, "16.11.1975"),
+ ("phone_number", 59, 76, "+380 67 123 45 67"),
+ ("national_id", 85, 95, "2974281300"),
+ ("street_address", 104, 122, "вулиця Хрещатик 22"),
+ ("postcode", 140, 145, "01001"),
+ }
+ observed = set()
+ for pattern in get_patterns_for_language("uk"):
+ for match in re.finditer(pattern.pattern, text, pattern.flags):
+ value = match.group(0)
+ if pattern.validator is not None and not pattern.validator(value):
+ continue
+ observed.add((pattern.entity_type, match.start(), match.end(), value))
+
+ assert expected <= observed
+
+
+def test_ukrainian_textual_date_and_street_patterns():
+ text = "Дата народження 16 листопада 1975, адреса проспект Свободи 15."
+ observed = {
+ (pattern.entity_type, match.group(0))
+ for pattern in get_patterns_for_language("uk")
+ for match in re.finditer(pattern.pattern, text, pattern.flags)
+ }
+
+ assert ("date", "16 листопада 1975") in observed
+ assert ("street_address", "проспект Свободи 15") in observed
+
+
+def test_ukrainian_i18n_golden_fixture_offsets():
+ fixture_path = Path("openmed/eval/golden/fixtures/i18n/uk.jsonl")
+ rows = [
+ json.loads(line)
+ for line in fixture_path.read_text(encoding="utf-8").splitlines()
+ if line.strip()
+ ]
+
+ assert len(rows) == 1
+ row = rows[0]
+ assert row["language"] == "uk"
+ assert row["metadata"]["synthetic"] is True
+ assert row["metadata"]["category"] == "multilingual"
+
+ text = row["text"]
+ expected = {
+ ("DATE", 39, 49, "16.11.1975"),
+ ("PHONE", 59, 76, "+380 67 123 45 67"),
+ ("ID_NUM", 85, 95, "2974281300"),
+ ("STREET_ADDRESS", 104, 122, "вулиця Хрещатик 22"),
+ ("ZIPCODE", 140, 145, "01001"),
+ }
+ actual = {
+ (span["label"], span["start"], span["end"], span["text"])
+ for span in row["gold_spans"]
+ }
+ assert actual == expected
+ for label, start, end, value in actual:
+ assert text[start:end] == value, label
+
+ identifier = next(
+ span["text"] for span in row["gold_spans"] if span["label"] == "ID_NUM"
+ )
+ assert validate_ukrainian_rnokpp(identifier)
+
+
+def test_ukrainian_i18n_golden_fixture_deidentifies_with_no_leakage_offline():
+ from openmed.core.pii import (
+ _apply_safety_sweep_to_result,
+ _build_deidentification_result,
+ )
+ from openmed.processing.outputs import PredictionResult
+
+ fixture_path = Path("openmed/eval/golden/fixtures/i18n/uk.jsonl")
+ row = json.loads(fixture_path.read_text(encoding="utf-8").strip())
+ empty_result = PredictionResult(
+ text=row["text"],
+ entities=[],
+ model_name="offline-safety-sweep",
+ timestamp="2026-07-19T00:00:00Z",
+ metadata={},
+ )
+
+ swept_result, added_count = _apply_safety_sweep_to_result(
+ row["text"],
+ empty_result,
+ lang="uk",
+ )
+ result = _build_deidentification_result(
+ row["text"],
+ swept_result,
+ effective_method="mask",
+ keep_year=False,
+ date_shift_days=None,
+ keep_mapping=False,
+ lang="uk",
+ consistent=False,
+ seed=None,
+ locale=None,
+ use_safety_sweep=True,
+ )
+
+ assert added_count == len(row["gold_spans"])
+ for span in row["gold_spans"]:
+ assert span["text"] not in result.deidentified_text
+
+
def test_validate_czech_rodne_cislo():
# Modern ten-digit form (delegated to the Czechoslovak validator).
assert validate_czech_rodne_cislo("751116/0008")
diff --git a/tests/unit/training/test_synthetic_locale_phi.py b/tests/unit/training/test_synthetic_locale_phi.py
index d2c1c8939..db299a7a6 100644
--- a/tests/unit/training/test_synthetic_locale_phi.py
+++ b/tests/unit/training/test_synthetic_locale_phi.py
@@ -8,9 +8,11 @@
from openmed.core.pii_i18n import (
SUPPORTED_LANGUAGES,
validate_aadhaar,
+ validate_czech_rodne_cislo,
validate_dutch_bsn,
validate_french_nir,
validate_german_steuer_id,
+ validate_greek_amka,
validate_indonesian_nik,
validate_israeli_teudat_zehut,
validate_italian_codice_fiscale,
@@ -20,6 +22,7 @@
validate_spanish_nie,
validate_thai_national_id,
validate_turkish_tckn,
+ validate_ukrainian_rnokpp,
)
from openmed.training.synthetic import (
LOCALE_PHI_LABELS,
@@ -44,6 +47,9 @@
"th": validate_thai_national_id,
"ko": validate_korean_rrn,
"ro": validate_romanian_cnp,
+ "uk": validate_ukrainian_rnokpp,
+ "cs": validate_czech_rodne_cislo,
+ "el": validate_greek_amka,
}
_SCRIPT_RANGES = {
@@ -54,6 +60,8 @@
"ko": ("\uac00", "\ud7a3"),
"te": ("\u0c00", "\u0c7f"),
"th": ("\u0e00", "\u0e7f"),
+ "uk": ("\u0400", "\u04ff"),
+ "el": ("\u0370", "\u03ff"),
}
@@ -103,7 +111,7 @@ def test_locale_phi_generation_is_deterministic_per_seed():
assert first == second
-@pytest.mark.parametrize("language", ("ar", "he", "hi", "ja", "te", "th"))
+@pytest.mark.parametrize("language", ("ar", "he", "hi", "ja", "te", "th", "uk", "el"))
def test_non_latin_locale_templates_render_target_script(language):
example = LocalePhiGenerator(seed=29).generate(language)
low, high = _SCRIPT_RANGES[language]