Use frontier models without giving up your data.
📄 Read the full technical report (PDF) — model, dataset, method and experiments in detail
🪟 Windows installer · 🍎 macOS (Apple Silicon) · 🐧 Linux AppImage — all available now
rizzo-pii:0.3B is a lightweight, CPU-friendly, Italian-first token-classification
model (≈0.3B parameters, mmBERT / ModernBERT
backbone) that detects 22 categories of personal data — including the Italian-legal
identifiers (codice fiscale, partita IVA, dati catastali) that no other open model
covers — and drives a fully reversible anonymization workflow:
🔒 anonymize locally → 🏷️ placeholder + reversible local dictionary → ☁️ frontier LLM → 🔓 restore locally
It is built for law firms, accountants, notaries and anyone bound by the GDPR who wants to keep using ChatGPT / Claude / Gemini on sensitive documents without ever sending the real data out.
People summarize contracts, draft replies and ask legal questions simply by pasting the document in. It is fast and useful — and it quietly moves enormous amounts of personal and confidential data off the user's device. Names, addresses, tax codes, IBANs, health details, unsigned-contract clauses: all of it crosses the network to servers the user does not control, where it may be logged, cached, retained or exposed in a breach. For a law firm or a hospital this is not hypothetical; under the GDPR it can be a direct compliance failure.
The intuitive fix is to stop sending data out and run an open model locally — but a frontier-grade open model is large and expensive to serve (€9,000–€10,000 of hardware), and the small models that fit a normal laptop are not in the same league on the hard tasks (legal reasoning, dense contracts, long official documents) — exactly where Italian professionals need the most help.
The trade-off we actually want: keep the frontier model and remove the data from the equation. Anonymize the document locally on a CPU, send only placeholders to the cloud, and restore the real values locally from the answer. The sensitive content never leaves the machine.
The workflow has three local steps and one remote step. Locally, rizzo-pii tags every span of
personal data and replaces each one with a stable, type-aware placeholder
([FULLNAME_1], [IBAN_1], [CF_1]), recording the mapping placeholder → real value in a
dictionary that stays on disk. Identical values share the same placeholder, so the frontier
model still sees a coherent text and can reason about it. The anonymized text is sent to
ChatGPT / Claude / Gemini; when the answer comes back, a local pass swaps the placeholders for the
true values. The cloud provider never receives a single real name, code or number.
Everything except the frontier query happens on the user's CPU; only placeholder text crosses the boundary, and the answer is re-identified locally.
This is not "yet another PII detector". It is an architecture for using powerful models without surrendering data, built so the privacy guarantee is structural rather than a promise:
- The data never leaves the device. Detection and re-identification run locally on a CPU. No API key, no telemetry, no upload. What the cloud receives is already stripped of identifiers.
- GDPR by design. The workflow implements data minimization (Art. 5) almost literally: the third-party processor only ever sees pseudonymized text, so the most common reason a cloud LLM call is unlawful (transferring identifiable data to a third party without a basis) is removed at the source.
- Aligned with the EU AI Act. Keeping personal data under local control and out of third-party model pipelines supports the Act's emphasis on data governance.
- Accessible to everyone. The model is ≈0.3B parameters and runs on a CPU in well under 1 GB of RAM — the privacy layer costs nothing extra in hardware. A normal laptop is enough.
- Reversible, not destructive. Classic redaction throws information away. rizzo-pii pseudonymizes: the answer from the frontier model is reconstructed with the real values, so the tool is useful in real work, not just compliance theater.
| Property | rizzo-pii:0.3B | OpenAI Privacy Filter | MS Presidio |
|---|---|---|---|
| Type | Dense encoder (mmBERT / ModernBERT) | Sparse MoE encoder | NER + rules pipeline |
| Parameters | ≈0.3B dense (all active) | 1.5B total / ≈50M active | spaCy model + rules |
| Memory to load | 0.5–1.2 GB | 1.5B params resident | Varies (spaCy) |
| Runs on | CPU, under 1 GB RAM | On-device | CPU |
| Categories | 22 (incl. IT-legal) | 8 generic | Configurable; EN defaults |
| Italian CF / PIVA / catasto | Yes | No | Not by default |
| Primary language | Italian (+7 more) | English | English |
| Checksum validation | Yes (IBAN/CF/PIVA/card) | No | Some recognizers |
| Reversible mapping | Yes (local dict) | Masking | Anonymization |
The differentiators are Italian-legal coverage, a smaller memory footprint, and a checksum-backed safety net (mod-97 for IBAN, Luhn for cards, the official CF/PIVA algorithms) that the larger generic models do not provide.
Concrete example. Take "Il Sig. Mario Rossi, C.F. RSSMRA85H12F205Z, P.IVA 12345678901, è titolare dell'immobile al Foglio 12, particella 345, sub. 6." rizzo-pii tags
FULLNAME,CF,PIVAandCATASTOand rewrites it as "Il Sig. [FULLNAME_1], C.F. [CF_1], P.IVA [PIVA_1], è titolare dell'immobile al [CATASTO_1]." A generic English-first model has no label for the fiscal code, the VAT number or the cadastral reference — the three most sensitive identifiers in the sentence — and would leave them in the clear.
rizzo-pii predicts 22 entity types in BIO format (a B-/I- label per tag, plus O). The
raw datasets are left untouched; the mapping to these 22 tags is applied at load time through a
single TAG_MAP in train_pii.py, so the taxonomy can be changed in one place without
re-annotating anything. Details in docs/TASSONOMIA_TAG.md.
| Tag | Meaning | Example | Source |
|---|---|---|---|
FULLNAME |
Person name (incl. legal roles: judge, lawyer, parties, witness) | Mario Rossi | real+synth |
AGE |
Age | 45 anni | real |
GENDER |
Sex / gender | Femmina | real |
DATE |
Calendar date | 12/06/1985 | real+synth |
TIME |
Time of day | ore 15:30 | real |
STREET |
Street / square | Via Garibaldi | real+synth |
BUILDINGNUM |
Street number | 24 | real+synth |
ZIPCODE |
Postal code (CAP) | 00185 | real+synth |
CITY |
City | Milano | real+synth |
PROVINCE |
Province abbreviation | MI | synth |
EMAIL |
E-mail (incl. PEC) | m.rossi@studio.it | real+synth |
TELEPHONENUM |
Phone number | +39 333 1234567 | real+synth |
CF |
Codice fiscale (personal tax code) | RSSMRA85H12F205Z | synth |
PIVA |
Partita IVA (VAT number) | 12345678901 | real+synth |
ID_DOC |
ID / passport / licence / social number | CA12345AB | real+synth |
IBAN |
IBAN / bank account | IT60X05428… | synth |
CREDITCARDNUMBER |
Credit-card number | 4111 1111 1111 1111 | real |
AMOUNT |
Money amount | € 12.500,00 | synth |
TARGA |
Vehicle plate | AB 123 CD | synth |
ORG |
Private company / firm / bank | Edilnord S.r.l. | synth |
DOCID |
Act identifier (RG, protocol, repertory, ruling) | 1234/2024 | synth |
CATASTO |
Cadastral data (sheet, parcel, sub.) | Foglio 12, part. 345 | synth |
Two design decisions stand out. First, legal roles collapse into FULLNAME — whether
"Mario Rossi" is the judge, the lawyer or a witness is not a property of the string; the role, if
needed, is recovered downstream as metadata. Second, raw types that mean the same thing are
merged: names + surnames → FULLNAME; PEC → EMAIL; TAXNUM → PIVA; ID card / passport /
licence / social number → ID_DOC; account number → IBAN. Honorifics (Dott., Avv.) and the
name of the court itself are dropped to O because they are not identifiers to mask.
The five Italian-legal tags (CF, PIVA, CATASTO, DOCID, PROVINCE) are the reason
rizzo-pii exists: they do not appear as labeled data in any public corpus, so they are created
through synthesis with mathematically valid checksums.
The model is fine-tuned on a multilingual pool of ≈745k labeled rows (Italian reinforced to
~45%) assembled from four sources — real (Ai4Privacy, DeepMount) and synthetic — all remapped to the
22 tags at load time. The synthetic part follows the "LLM author, code labeler" principle: an
LLM writes only Italian legal prose with placeholders ({SLOT}) and our code injects the real
values (CF/PIVA/IBAN with valid checksums), so BIO labels are exact, identifiers are valid by
construction, and no real personal data is ever produced by the LLM. The backbone is
mmBERT-base (ModernBERT architecture, native 8192-token context) and training runs in a single
epoch on one 16 GB consumer GPU.
📄 The full dataset composition, the synthesis method, the training recipe and the experiments are described in the technical report. See also docs/DATASET.md and CLAUDE.md for the operational details.
Training was a single epoch (~26.6k steps over 744,912 rows). The loss fell from ≈6.2 to below 0.1 within the first few hundred steps, then settled into a clean, monotone, low regime; the validation loss decreased monotonically and was still falling when the epoch ended. Final training loss ≈0.003 and validation loss ≈0.006 are both very low and very close, so the model is not over-fitting — a second epoch would very likely push it lower still.
Left: training loss (smoothed zoom) — fast drop, then low and stable. Right: validation loss — monotone, still decreasing when training stopped.
On the 7,000-row held-out real Italian benchmark (validation_real.jsonl):
| 0.987 | 0.990 | 0.989 | 0.998 |
|---|---|---|---|
| micro precision | micro recall | micro F1 | token accuracy |
The unweighted per-tag mean (macro-F1) across all 22 tags is 0.987, and every one of the five Italian-legal identifiers scores a perfect 1.000.
| Tag | Sup. | P | R | F1 | Tag | Sup. | P | R | F1 | |
|---|---|---|---|---|---|---|---|---|---|---|
| FULLNAME | 4390 | .989 | .990 | .990 | GENDER | 472 | 1.00 | 1.00 | 1.00 | |
| CATASTO | 1200 | 1.00 | 1.00 | 1.00 | PROVINCE | 400 | 1.00 | 1.00 | 1.00 | |
| CITY | 953 | .961 | .963 | .962 | DOCID | 400 | 1.00 | 1.00 | 1.00 | |
| DATE | 922 | 1.00 | 1.00 | 1.00 | CF | 400 | 1.00 | 1.00 | 1.00 | |
| TELEPHONENUM | 874 | 1.00 | 1.00 | 1.00 | AGE | 385 | .979 | .977 | .978 | |
| ID_DOC | 800 | 1.00 | 1.00 | 1.00 | ZIPCODE | 299 | .938 | .967 | .952 | |
| 748 | .999 | .999 | .999 | IBAN | 278 | .996 | .996 | .996 | ||
| TIME | 637 | .991 | .992 | .991 | CREDITCARD | 257 | .919 | .973 | .945 | |
| STREET | 617 | .951 | .969 | .960 | AMOUNT | 146 | 1.00 | .993 | .997 | |
| BUILDINGNUM | 594 | .969 | .958 | .964 | ORG | 145 | .967 | 1.00 | .983 | |
| PIVA | 514 | 1.00 | 1.00 | 1.00 | TARGA | 43 | 1.00 | 1.00 | 1.00 |
All five Italian-legal identifiers (CF, PIVA, CATASTO, DOCID, PROVINCE) score a perfect
1.000, as do ID_DOC, DATE, TELEPHONENUM, GENDER and TARGA. The remaining soft spots are
the open, high-variability classes (ZIPCODE, CREDITCARDNUMBER, STREET, CITY) and ORG,
which is exactly where a larger, better-balanced dataset would help.
The released checkpoint is ~1.2 GB on disk in fp32 and runs comfortably on a CPU: quantized, its memory footprint is on the order of 0.5 GB, with no GPU required. That is the whole point — the privacy layer is cheap enough to run on the laptop the user already owns.
In production the neural model is never used alone. It is paired with a deterministic regex + checksum network for the structured identifiers (EMAIL, phone, IBAN, CF, PIVA, credit card, amount, plate), where IBAN/PIVA/card must pass their checksum (mod-97 / Luhn) to be accepted, and a valid checksum overrides the model. This eliminates the classic failure mode of a neural tagger fragmenting a long code, and gives mathematically certain detection for exactly the identifiers whose leakage is most damaging. The app adds the reversible layer (stable placeholders, downloadable local dictionary, a "restore" tab tolerant to markdown/format drift), chunking with overlap for long PDFs, and a colored per-tag UI.
| To use the model (inference) | To retrain the model |
|---|---|
| Any 64-bit CPU (no GPU) | A single 16 GB GPU is enough |
| 0.5–1.2 GB RAM for the model | Reference run: RTX 5060 Ti, ~2 h |
| Windows (installer) / Linux / macOS | PyTorch cu128 for Blackwell |
| Fully offline; no API key | ~745k rows, regenerable from scripts |
The desktop app Rizzo PII (Tauri) launches the Python/Flask backend as a bundled CPU "sidecar"; a CPU-only PyTorch build keeps it fully offline on Windows (WebView2), macOS and Linux. Packaging instructions in docs/BUILD.md.
⬇️ Download. Grab the ready-to-use build from the Releases page — no Python or setup required: a Windows installer (double-click), a macOS
.dmg(Apple Silicon / arm64 — signed & notarized by Apple, just open it), and a Linux AppImage (chmod +xthen run) are all available now.
Prerequisites and critical environment constraints (Blackwell GPU, torch cu128, etc.) are in CLAUDE.md. The
dataset/raw/sources are downloaded from Hugging Face (see thehf downloadcommands in CLAUDE.md). All scripts force UTF-8 and resolve their paths from__file__, so they run from any working directory.
💡 Just want to use the app? You don't need any of this — download the ready-to-use build (Windows / macOS / Linux) from the Releases page. The steps below are for developers who want to regenerate the data and retrain the model.
git clone https://github.com/Rizzo-AI-Academy/rizzo-pii
cd rizzo-pii
python -m venv .venv; .\.venv\Scripts\Activate.ps1
pip install -r requirements.txt # NVIDIA Blackwell? install torch cu128 first — see requirements.txt
copy .env.example .env # optional: add W&B / Gemini keyspython src/data_pipeline/llm_template_bank.py --per-type 5 --append # (opt.) legal templates via Gemini
python src/data_pipeline/generate_synthetic_pii.py -n 200000 --out dataset/synthetic/synthetic_pii_it_200k.jsonl
python src/data_pipeline/augment_real_pii.py -n 40000 --out dataset/synthetic/synthetic_pii_it_realaug.jsonl
python src/data_pipeline/prepare_deepmount.py # requires HF login
python src/data_pipeline/build_validation.py # real validation (7k, it)
python src/data_pipeline/build_subset.py # 10k/5k subsets for smoke tests# fast smoke test / tuning on the subset (~3 min) -> experiments/subset_smoke/
python src/training/train_pii.py --type subset
# full run on the whole dataset -> models/rizzo-pii-0.3B-v{VERSION}/ + experiments/full_run_v{VERSION}/
python src/training/train_pii.py --type full
python src/training/train_pii.py --type full --version 1.2.0 # or an explicit versionpython src/training/test_pii.py "Mi chiamo Mario Rossi, IBAN IT60X0542811101000000123456"
python src/app/app.py # http://127.0.0.1:5005 (paste text or upload a PDF)The web app assigns every PII a reversible ID ([FULLNAME_1], [IBAN_1]…) plus a local
dictionary, pairing the model with the regex/checksum net. You copy the anonymized text into an LLM
and restore the real values from the response.
rizzo_pii/
├─ README.md this file
├─ LICENSE MIT
├─ CONTRIBUTING.md how to contribute (code, docs, data)
├─ requirements.txt Python dependencies (see the cu128 note for Blackwell GPUs)
├─ .env.example template for the optional W&B / Gemini keys
├─ CLAUDE.md operating instructions + environment constraints (GPU, CUDA…)
├─ report/ the technical report (PDF + Typst source)
├─ docs/
│ ├─ DATASET.md full composition of train/validation
│ ├─ TASSONOMIA_TAG.md the 22 final tags and the merge decisions
│ ├─ BUILD.md desktop app build (Tauri recommended + PyInstaller legacy)
│ └─ CHANGELOG.md change log, with rationale
├─ src/
│ ├─ data_pipeline/ data generation & preparation
│ │ ├─ llm_template_bank.py Gemini writes legal templates → legal_templates.json
│ │ ├─ generate_synthetic_pii.py injects checksum-valid values into the templates
│ │ ├─ augment_real_pii.py injects synthetic entities into real Ai4Privacy sentences
│ │ ├─ prepare_deepmount.py remaps DeepMount (56 types) onto our 22 tags
│ │ ├─ build_validation.py builds the single real validation set (it)
│ │ └─ build_subset.py stratified subsets for smoke tests / tuning
│ ├─ training/
│ │ ├─ train_pii.py train, evaluate, save the model + metrics
│ │ ├─ evaluate_pii.py per-tag (P/R/F1) evaluation on validation_real
│ │ └─ test_pii.py CLI inference on the saved model
│ ├─ inspect/ read-only utilities (counts, lengths, checksums)
│ └─ app/ local anonymization app
│ ├─ app.py Flask server: reversible anonymization + regex/checksum net
│ ├─ serve.py headless entry (backend of the Tauri app, no browser)
│ ├─ desktop_app.py legacy PyInstaller entry (opens the browser)
│ └─ assets/ mascot (the hedgehog) and icons
├─ tauri/ native desktop app (Tauri) + Windows installer — see docs/BUILD.md
├─ dataset/ (gitignored — regenerable from the scripts)
├─ models/ (gitignored) trained models, one folder per version
└─ experiments/ (gitignored) run artifacts (logs, plots, metrics, checkpoints)
Stated plainly:
- Validation is Italian-only. Training is multilingual, but the 7,000-row benchmark measures Italian only, by design. The other seven languages are trained but not certified.
- The IT-legal tags are validated against injected entities.
CF,PIVA,CATASTO,DOCID,PROVINCEhave no real public data, so even in validation they are generated entities placed into real sentences — a good proxy, not a fully blind test. - Class imbalance and under-represented categories. The corpus is heavily skewed (
FULLNAMEoutnumbersCREDITCARDNUMBER~97×), so the rarer tags are noisier. The clearest case is organizations (ORG): an open, highly variable class that today comes largely from synthetic templates and off-domain Faker data — exactly where a larger, balanced dataset would help most. - Off-domain synthetic values. DeepMount supplies US-style names/addresses: useful for form/context, not as Italian values.
- Evaluation is sentence-level, not document-level. The benchmark is built from short sentences, whereas the real use case is whole documents. Measuring true end-to-end behaviour (long context, PDF chunking with overlap, real act structure) needs a dedicated test set of large, real Italian documents, which still has to be assembled.
The mitigation that matters in practice: always pair the model with the regex/checksum safety
net (src/inspect/validate_checksums.py is the blueprint). The two together are stronger than
either alone.
rizzo-pii proves the thesis: you can keep using frontier models and still keep your data private, on ordinary hardware, with Italian-legal coverage no other open model offers. The single biggest lever on quality from here is data — a large, real, lawfully collected Italian corpus: both for the legal identifiers that are scarce today and, above all, to balance the classes and add genuine coverage where the model is weakest (organizations), plus a test set of large, real documents so the model can be measured end-to-end on the documents it is actually meant to anonymize.
So the project is open source, and this is an open invitation. The community dataset lives on
Hugging Face at
rizzoaiacademy/anonimizzazione-testi-italiano
— a public, collaborative corpus we are starting to fill. If you work with Italian documents —
lawyers, accountants, notaries, developers, researchers — help build it: contribute templates,
annotation, edge cases, and review. A privacy tool for Italy is something Italy should build
together, for the good of everyone's privacy.
The script src/data_pipeline/contribute_dataset.py
generates genuinely new synthetic examples — Gemini writes fresh legal prose on every run,
the code injects valid values (CF/PIVA/IBAN checksums) and exact BIO labels — then opens a
Pull Request on the dataset for a maintainer to review. You can let your coding agent
(Claude Code, Cursor, …) do everything: get a Gemini API key
and a Hugging Face token, then copy-paste the prompt
below (fill in the two keys and your handle):
Sei nel repository rizzo-pii. Voglio CONTRIBUIRE dati sintetici NUOVI al dataset Hugging Face
"rizzoaiacademy/anonimizzazione-testi-italiano" eseguendo src/data_pipeline/contribute_dataset.py.
I dati DEVONO essere generati davvero da zero: lo script usa Gemini per scrivere NUOVI template
legali ad ogni run (non riusare dati esistenti). Prima di partire LEGGI docs/FORMATO_DATI.md per
capire il formato esatto di ogni esempio (JSONL: source_text, tokens, bio_labels, entities, schema
BIO, checksum CF/PIVA/IBAN obbligatori, nessuna PII reale).
Fai tutto questo, in ordine:
1. Crea/attiva un venv e installa il minimo necessario: pip install huggingface_hub
(lo script per i dati NON richiede torch).
2. Imposta le credenziali (NON committarle):
export GEMINI_API_KEY="<LA_MIA_CHIAVE_GEMINI>"
export HF_TOKEN="<IL_MIO_TOKEN_HF>" # in alternativa: hf auth login
3. Fai una prova locale SENZA caricare, controlla la distribuzione dei tag in output:
python src/data_pipeline/contribute_dataset.py --n 300 --handle <IL_MIO_HANDLE> --no-upload
4. Genera il batch vero e APRI LA PR, rinforzando i tag più deboli (ORG e gli identificativi):
python src/data_pipeline/contribute_dataset.py --n 5000 --handle <IL_MIO_HANDLE> \
--per-type 3 --boost ORG=6 IBAN=4 CF=4 CATASTO=3 DOCID=3
5. Stampami l'URL della Pull Request creata.
Vincoli: SOLO dati sintetici (mai PII reali). Se Gemini non è disponibile, fermati e segnalamelo
(non usare --offline senza il mio ok, perché produce dati meno nuovi).
Prefer to do it yourself? Run the same commands manually — see CONTRIBUTING.md and the format spec in docs/FORMATO_DATI.md.
| Document | Contents |
|---|---|
| CLAUDE.md | Environment constraints, repo map, architectural decisions, commands |
| docs/DATASET.md | Full composition of train (~745k) and validation (7k) |
| docs/TASSONOMIA_TAG.md | The 22 final tags and the merges (TAG_MAP) |
| docs/FORMATO_DATI.md | Exact dataset row format for contributing data (JSONL/BIO/checksums) |
| docs/BUILD.md | Desktop executable build (CPU, Windows) |
| docs/CHANGELOG.md | Change log for the pipeline, with rationale |
| CONTRIBUTING.md | How to contribute code, docs and (above all) data |
Released under the MIT License © 2026 Simone Rizzo — Rizzo AI Academy.
Note on third-party data: the training corpus draws on Ai4Privacy (CC-BY-4.0) and DeepMount00; the backbone is mmBERT-base. Please respect their respective licenses when redistributing data or weights.
⭐ Star it · 🐛 open an issue · 🔀 send a pull request · 📎 or just share a hard example
Author — Simone Rizzo · Sponsor — Rizzo AI Academy
The mascot, a hedgehog, guards the document and stays inside the EU. Built and trained in Italy. 🇮🇹🇪🇺
- mmBERT: a multilingual ModernBERT encoder. JHU-CLSP,
jhu-clsp/mmBERT-base, Hugging Face. - Warner, B. et al. Smarter, Better, Faster, Longer: A Modern Bidirectional Encoder (ModernBERT), 2024.
- Ai4Privacy.
open-pii-masking-500k, Hugging Face (CC-BY-4.0). - DeepMount00.
pii-masking-ita, Hugging Face. - OpenAI. Introducing OpenAI Privacy Filter, 2026; model
openai/privacy-filter, Hugging Face (Apache-2.0). - Microsoft. Presidio: Data Protection and De-identification SDK. Open source (MIT).
- Regulation (EU) 2016/679: General Data Protection Regulation (GDPR).
- Regulation (EU) 2024/1689: Artificial Intelligence Act (EU AI Act).




