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.
Fork di Rizzo-AI-Academy/rizzo-pii
(commit 96e863c), software originale di Simone Rizzo — Rizzo AI Academy, licenza MIT.
Il fork ne conserva il LICENSE e aggiunge il proprio copyright sulle sole modifiche.
Obiettivo del fork: portare il progetto da proof-of-concept a componente integrabile in un'offerta commerciale, con rischio residuo misurato invece che dichiarato. Il piano di lavoro è in docs/PIANO_LAVORO.md, l'audit del codice ereditato in docs/AUDIT_FASE0.md.
Precisazione che vale su tutto il repository: questo sistema produce pseudonimizzazione, non anonimizzazione. L'output resta un dato personale ai sensi del GDPR. Dove il README ereditato dice o lascia intendere altro, vale questa nota. Vedi docs/COMPLIANCE.md.
Il flusso ereditato è a tre tappe manuali — anonimizza, copia, incolla in un LLM esterno, riporta indietro la risposta. Il fork lo chiude dentro l'app: il documento viene pseudonimizzato, mandato alla Messages API di Anthropic con il prompt scelto, e la risposta torna con i valori reali rimessi al loro posto — con il tool use, se serve.
Le due cose da sapere, perché sono i punti in cui un sistema del genere sbaglia in silenzio:
- la conversazione inviata resta pseudonimizzata anche nei turni successivi. Il ripristino è di sola presentazione e non rientra mai nella cronologia; se lo facesse, al turno dopo il valore reale tornerebbe dentro la richiesta;
- la numerazione dei segnaposto è monotona per sessione.
[FULLNAME_3]è la stessa persona in tutti i documenti di quella sessione. Senza, due documenti riceverebbero lo stesso ID per persone diverse e il ripristino restituirebbe quella sbagliata.
Il gate di uscita è server-side — la CSP della pagina ha connect-src 'self', quindi
la chiamata al provider non può che partire dal backend, ed è il motivo per cui il gate non
si aggira dalla pagina. Misura del percorso di uscita: 0 perdite su 200 documenti, IC95
[0,0000–0,0188] (metrics/egress.json); il numero da leggere è il bordo superiore.
Cosa questo non protegge: il provider vede comunque struttura, lunghezze, conteggi per tag e i segnaposto stessi, che sono pseudonimi stabili e collegabili fra turni e documenti. Dettagli in docs/ADR-003-gateway-llm.md e docs/THREAT_MODEL.md §5.2.
pip install anthropic # opzionale, solo per la chat
PII_NO_MODEL=1 PII_PROVIDER_STUB=1 python src/app/app.py # provala senza chiave né retePeople 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 app adds a 23rd tag, URL, handled by the regex net alone — the model is not trained on it.
It matches http(s)://…, www.… and bare domains on a closed TLD list; the closed list is what
keeps Italian legalese (p.iva, n.ro, S.r.l.) from being read as a domain, at the price of
letting an exotic TLD through.
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.
Nota del fork (WP9.4). Il contesto nativo di 8192 token è una capacità dell'architettura, non una proprietà esercitata da questo sistema: il training gira a
MAX_LEN = 768e in inferenza il documento viene spezzato in chunk. Oltre la lunghezza vista in addestramento l'estrapolazione posizionale non è affidabile, quindi il numero non va usato come argomento di vendita.
📄 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. Input: pasted text, a PDF, or a
.md / .txt file.
The same process is a plain HTTP service, so you can use it as a sidecar in a fail-closed pipeline:
curl localhost:5005/health # readiness, no inference: 200 = model loaded
curl -X POST localhost:5005/analyze -H 'Content-Type: application/json' \
-d '{"text": "Mario Rossi, IBAN IT60X0542811101000000123456"}'
curl -F "file=@atto.pdf" localhost:5005/analyze # .pdf, .md, .txt
curl -F "file=@atto.pdf" localhost:5005/pdf -o atto_anonimizzato.pdf # anonymized PDF back
curl localhost:5005/settings # tag legend, excluded tags, dictionary statePOST /pdf takes the same input as /analyze and returns a PDF. Upload a PDF and you get that
same document back truly redacted: the PII is removed from the content stream (not covered with a
black box) and replaced in place by its placeholder, so the layout survives — handy for feeding a
whole stack of documents to an LLM at once. Paste text or upload .md/.txt and you get a
freshly typeset PDF of the anonymized text.
The dictionary never crosses the wire in either direction: the server rebuilds it internally to locate the PII and drops it when the request ends. That is why the button works unchanged with the reversible dictionary switched off.
Two things can leave a value in clear, and both raise a warning in the UI (response headers
X-PII-Residual and X-PII-Skipped): a value still readable in the output at the final check, and
values too short to search safely (under 2 alphanumeric characters, or 2 bare digits like 45).
Metadata, XMP, annotations, form-field values and bookmark titles are scrubbed too, and embedded
attachments are dropped. Text baked into a raster image cannot be redacted at all — if no
occurrence is found anywhere in the PDF the endpoint fails with 422 rather than handing you a file
that only looks anonymized.
Drop a PDF and the left column renders it, page by page, right there: two views, Anteprima PDF
and Testo. Anonymize and the right column gains a third view next to the highlighted preview and
the raw text — PDF censurato, the same document redacted, so you can compare the two side by
side before downloading anything.
Pages are rendered server-side by PyMuPDF and served as PNGs (GET /doc/<id>/page/<n>.png):
no PDF viewer is assumed in the webview and no pdf.js is fetched from a CDN — the app stays offline.
Documents live in a small in-memory LRU and never touch disk. POST /pdf/preview does the same
work as POST /pdf but keeps the binary server-side and answers with {doc_id, n_pages, residual, skipped}, so previewing then downloading costs one anonymization, not two.
By default every PII gets a reversible ID and a local dictionary, so you can restore the real
values from the LLM's answer. Some users want the opposite: anonymize and have no restore key
exist at all. The switch in the input card does that — and it is not cosmetic. With the dictionary
off the server builds no placeholder → value map, returns none, stores none in the browser, offers
no download, and strips the original text out of the response entirely (the per-entity t field
disappears, so the map cannot be rebuilt from the payload either). The numbering survives —
[FULLNAME_1] twice still means "same person" — but a number alone leads back to nothing.
python src/app/app.py --no-mapping # CLI
PII_MAPPING=0 python src/app/serve.py # env
curl -X POST localhost:5005/settings -H 'Content-Type: application/json' \
-d '{"mapping_enabled": false}' # persisted to prefs.json
curl -X POST localhost:5005/analyze -H 'Content-Type: application/json' \
-d '{"text": "…", "include_mapping": false}' # per-request overrideNot every tag has to be masked. Comparing the amounts in a contract, or keeping age and sex in a clinical record, needs those values in clear text. Untick them in the 🏷️ panel (they are still detected, just not replaced), or set it outside the UI:
python src/app/app.py --exclude-tags AMOUNT,AGE # CLI
PII_EXCLUDE_TAGS=AMOUNT,AGE python src/app/serve.py # env
curl -X POST localhost:5005/settings -H 'Content-Type: application/json' \
-d '{"excluded_tags": ["AMOUNT"]}' # persisted to prefs.json
curl -X POST localhost:5005/analyze -H 'Content-Type: application/json' \
-d '{"text": "…", "exclude_tags": ["AMOUNT"]}' # per-request overriderizzo_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
│ ├─ pdf_export.py anonymized-PDF download: true redaction of the uploaded PDF
│ ├─ 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).
🖥️ No API key? Use your own local model. Set
LLM_BASE_URL(any OpenAI-compatible endpoint — llama.cpp server, Ollama, vLLM, LM Studio) andLLM_MODELinstead ofGEMINI_API_KEY, and the templates are written on your machine: no key, no prompt leaving the device. Fitting, for a privacy project.
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/TRAINING.md | Training run: union of both HF datasets, subword-label handling, hyperparameters |
| 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).




