Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rizzo-pii mascot — a purple hedgehog guarding a document with a shield

rizzo-pii

Local, reversible PII anonymization for Italian legal text

Use frontier models without giving up your data.

100% local GDPR by design EU AI Act aligned

0.3B parameters 0.5 GB RAM CPU 22 PII categories 0.989 micro-F1 offline, no API key

📄 Read the full technical report (PDF) — model, dataset, method and experiments in detail

Download for Windows Download for macOS Download for Linux

🪟 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.

rizzo-pii redacting a document
≈0.3B ~0.5 GB 22 0.989
parameters (mmBERT-base) RAM footprint, CPU PII categories micro-F1 (real IT validation)

The hedgehog mascot does one job: it grabs your document, blacks out every identifier, and stays inside the EU while doing it.


⚠️ Questo repository è un fork — Ema-PII

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.

La terza scheda: parlare col modello senza mandargli le PII

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é rete

The problem: convenience is leaking your data

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.


rizzo-pii in one picture

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.

The rizzo-pii workflow: everything runs locally on CPU; only placeholder text crosses to the cloud, and the answer is re-identified locally

Everything except the frontier query happens on the user's CPU; only placeholder text crosses the boundary, and the answer is re-identified locally.


Why this is different: privacy that is actually private

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.

How it compares

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, PIVA and CATASTO and 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.


The taxonomy: 22 tags, and why

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; PECEMAIL; TAXNUMPIVA; 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.


Dataset & training

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 = 768 e 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.


Results

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.

Training loss (smoothed zoom): drops sharply then stays low and stable Validation loss: decreases monotonically across the epoch, still falling at the end

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.

Per-tag precision / recall / F1 (v1.2.0)

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
EMAIL 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.


Deployment: it runs on a normal computer

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 +x then run) are all available now.


Quickstart

Prerequisites and critical environment constraints (Blackwell GPU, torch cu128, etc.) are in CLAUDE.md. The dataset/raw/ sources are downloaded from Hugging Face (see the hf download commands 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.

0) Install

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 keys

1) Generate the data

python 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

2) Train

# 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 version

3) Use the model

python 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 local HTTP API

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 state

Get the anonymized document back as a PDF

POST /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.

See the document, not just its text

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.

Reversible or irreversible: your call

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 override

Not 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 override

Repository structure

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
│       ├─ 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)

Limitations

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, PROVINCE have 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 (FULLNAME outnumbers CREDITCARDNUMBER ~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.


Next step: a community-owned Italian PII dataset

rizzo-pii mascot wearing an EU cap, thumbs up

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.

Contribute data in one shot — paste this to your coding agent

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) and LLM_MODEL instead of GEMINI_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.


Documentation

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

License

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.


rizzo-pii mascot waving

Contribute to the project

⭐ Star it · 🐛 open an issue · 🔀 send a pull request · 📎 or just share a hard example

Author — Simone Rizzo · SponsorRizzo AI Academy

The mascot, a hedgehog, guards the document and stays inside the EU. Built and trained in Italy. 🇮🇹🇪🇺


Star History


References

  1. mmBERT: a multilingual ModernBERT encoder. JHU-CLSP, jhu-clsp/mmBERT-base, Hugging Face.
  2. Warner, B. et al. Smarter, Better, Faster, Longer: A Modern Bidirectional Encoder (ModernBERT), 2024.
  3. Ai4Privacy. open-pii-masking-500k, Hugging Face (CC-BY-4.0).
  4. DeepMount00. pii-masking-ita, Hugging Face.
  5. OpenAI. Introducing OpenAI Privacy Filter, 2026; model openai/privacy-filter, Hugging Face (Apache-2.0).
  6. Microsoft. Presidio: Data Protection and De-identification SDK. Open source (MIT).
  7. Regulation (EU) 2016/679: General Data Protection Regulation (GDPR).
  8. Regulation (EU) 2024/1689: Artificial Intelligence Act (EU AI Act).

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages