ββββββββββββββ ββββββββββββββ ββββββββ βββββ βββββ βββββββββββββββ ββββββββ ββββββββ
ββββββ ββββββ ββββββ ββββββ ββββββ βββββ βββββ ββββββ ββββ ββββββ ββββββ
ββββββ βββββ ββββββ βββββ ββββββ βββββ βββββ ββββββ βββ ββββββ ββββββ
βββββββββββββ βββββββββββββ ββββββ βββββ βββββ βββββββββββ ββββββ ββββββ
ββββββ ββββββ βββββ ββββββ βββββ βββββ ββββββ β ββββββ ββββββ
ββββββ ββββββ βββββ ββββββ βββββ βββββ ββββββ ββββ ββββββ ββββββ ββββ
ββββββ ββββββ βββββ ββββββ βββββ ββββββ ββββββ ββββ ββββββ ββββββ ββββ
ββββββββ ββββββββ βββββββ ββββββββ ββββββββ βββββββββββββββ ββββββββ βββββββββββββββ
A pseudonymisation service for reducing obvious PII exposure in text workflows, with strong support for Australian financial identifiers β not a substitute for true anonymisation.
Important
Read this before integrating: Priveil replaces known PII patterns with consistent placeholders. It cannot enumerate all possible identifying information, does not account for auxiliary data an attacker might possess, and makes no mathematical guarantee about re-identification risk. See On anonymisation and its limits below.
Caution
Service hardening is your responsibility: this API ships with no built-in authentication, no rate limiting, and no TLS termination. Deploy it only behind your own trusted gateway/load balancer.
Priveil is a three-layer pipeline. Each layer is independently optional.
Incoming text
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Layer 1 β Regex recognisers (always active) β
β AU_TFN Β· AU_MEDICARE Β· AU_ABN Β· AU_ACN Β· AU_BSB β
β AU_PHONE Β· EMAIL_ADDRESS Β· PHONE_NUMBER Β· CREDIT_CARD β
β Checksum-validated where possible. Zero ML. β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ (--extra gliner)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Layer 2 β NER model (optional) β
β PERSON Β· LOCATION Β· DATE_OF_BIRTH β
β Encoder-based NER; finds name/place/DOB spans that β
β regex cannot. Runs on CPU, no API key. β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ (--extra laya, mode="judge")
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Layer 3 β Span judge (optional) β
β Type-1 non-autoregressive model (~33 ms on GPU). β
β Verifies each uncertain span: "is this genuinely PII β
β in context?" Removes false positives before returning. β
β No API key. No egress. Runs entirely locally. β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
Detected entities βββΊ /pseudonymise βββΊ Redacted text
βββΊ /assess βββΊ Risk profile
Why two ML models? The NER model and the span judge do completely different jobs:
| NER model | Span judge | |
|---|---|---|
| Task | Find entity spans in text | Verify whether a found span is genuine PII |
| Input | Raw text | {entity_type, span, context} |
| Output | Character offsets + label | Probability 0β1 |
| Speed | ~50β200 ms (CPU) | ~33 ms on GPU, ~100 ms on CPU |
| Install | --extra gliner |
--extra laya |
| API key | No | No |
Neither replaces the other. The NER model finds candidates; the judge filters false positives.
# Regex-only: AU financial identifiers + email/phone/card
uv sync
uv run python -m priveil
# Full stack: add person/place/DOB detection + false-positive filtering
uv sync --extra gliner --extra laya
uv run python -m priveilAPI at http://localhost:8000. Docs at http://localhost:8000/docs.
| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness check |
POST |
/detect |
Detect PII entities in text |
POST |
/pseudonymise |
Replace PII with placeholders |
POST |
/assess |
Risk profile (requires --extra laya) |
curl -X POST http://localhost:8000/detect \
-H "Content-Type: application/json" \
-d '{
"text": "Contact Jane Smith at jane@example.com and call 0412 345 678.",
"mode": "judge"
}'{
"meta": {
"request": { "mode": "judge" },
"response": { "mode": "judge", "input_hash": "hmac-sha256:..." }
},
"data": {
"entities": [
{ "text": "Jane Smith", "entity_type": "PERSON", "is_pii": true, "sensitivity": "high", "score": 0.85 },
{ "text": "jane@example.com", "entity_type": "EMAIL_ADDRESS", "is_pii": true, "sensitivity": "medium", "score": 1.0 },
{ "text": "0412 345 678", "entity_type": "AU_PHONE", "is_pii": true, "sensitivity": "medium", "score": 0.95 }
],
"judge_applied": true
}
}Note:
PERSON,LOCATION, andDATE_OF_BIRTHonly appear when--extra glineris installed. Without it, only regex-detected types (email, phone, AU identifiers) are returned.
mode field:
| Value | Behaviour |
|---|---|
"judge" (default) |
The span judge verifies each uncertain span to remove false positives. Falls back to "fast" if laya is not installed. |
"fast" |
Raw detector output β no judge verification pass. |
curl -X POST http://localhost:8000/pseudonymise \
-H "Content-Type: application/json" \
-d '{
"text": "Jane Smith TFN 123 456 782, BSB 062-000, jane@bank.com.au",
"mode": "judge"
}'{
"data": {
"anonymised_text": "<PERSON> TFN ***-***-***, BSB XXX-XXX, <EMAIL>",
"entity_map": {
"Jane Smith": "<PERSON>",
"123 456 782": "***-***-***",
"062-000": "XXX-XXX",
"jane@bank.com.au": "<EMAIL>"
},
"judge_applied": true
}
}Default operators:
| Entity type | Output example |
|---|---|
PERSON |
<PERSON> |
EMAIL_ADDRESS |
<EMAIL> |
AU_PHONE / PHONE_NUMBER |
<PHONE> |
AU_TFN |
***-***-*** |
AU_BSB |
XXX-XXX |
AU_ABN |
** *** *** *** |
CREDIT_CARD |
**** **** **** 1234 (last 4 kept) |
LOCATION |
<LOCATION> |
DATE_OF_BIRTH |
<DOB> |
Override per request:
{
"text": "Call Jane on 0412 345 678",
"operator_overrides": { "PERSON": "redact", "AU_PHONE": "hash" }
}Available operators: replace, mask, redact, hash.
Judge-powered sensitivity assessment. Requires uv sync --extra laya.
curl -X POST http://localhost:8000/assess \
-H "Content-Type: application/json" \
-d '{
"text": "Applicant Jane Smith TFN 123 456 782. BSB 062-000.",
"context": "Australian home loan application"
}'Returns: overall_sensitivity (low/medium/high/critical), categories, regulatory_flags, recommended_handling, entity_breakdown, risk_summary, reasoning.
The NER model (default: fastino/gliner2-base-v1) is a compact encoder-based model that finds PERSON, LOCATION, and DATE_OF_BIRTH spans in text. It uses label-guided extraction and returns character offsets.
Install:
uv sync --extra glinerWithout this extra, the service runs in regex-only mode β AU financial identifiers, email, phone, and credit card are still detected; person names, locations, and dates of birth are not.
Configuration:
| Variable | Default | Description |
|---|---|---|
PRIVEIL_NER_MODEL |
fastino/gliner2-base-v1 |
HuggingFace model ID. Downloaded on first run and cached. |
The model loads at startup from the HuggingFace hub (or local cache). If the load fails, the service continues in regex-only mode rather than refusing to start.
The span judge (laya) is a Type-1 non-autoregressive decision engine. Rather than generating text, it answers typed questions (noul = yes/no probability, choice, score) in a single encoder forward pass.
Priveil uses the judge for two things:
1. Span verification (mode="judge"): after detection, each uncertain span is verified with a noul question:
entity_type: PERSON
span: "Jane Smith"
context: "Contact Jane Smith at the branch office."
β is_genuine_pii probability: 0.87 β keep
entity_type: AU_BSB
span: "062-000"
context: "Routing codes 062-000 and 013-005 direct traffic between nodes."
β is_genuine_pii probability: 0.21 β drop (false positive)
High-confidence spans (score β₯ PRIVEIL_JUDGE_SCORE_THRESHOLD) from trust-tier regex recognisers (AU_TFN, AU_MEDICARE, etc.) bypass the judge entirely. NER model outputs are always verified β neural model confidence scores are not reliable enough to skip the judge.
2. Risk assessment (POST /assess): the judge answers typed questions about the full document:
choice: overall sensitivity tier (low / medium / high / critical)noul: is this financial PII? identity PII? medical? employment?
Rule tables derive the advisory fields (regulatory_flags, recommended_handling) from the judge's typed answers plus the detected entity types.
Why the judge instead of an LLM?
| LLM | Judge (laya) | |
|---|---|---|
| Latency | 150β500 ms (network) | ~33 ms on GPU, ~100 ms on CPU |
| API key | Required | Not required |
| Data egress | Cloud provider | None β runs locally |
| Cost | Per-token | Zero |
| Determinism | Low (sampling) | High (calibrated) |
Install:
uv sync --extra layaConfiguration:
| Variable | Default | Description |
|---|---|---|
PRIVEIL_JUDGE_BACKEND |
auto |
auto = use laya if installed; laya = require laya |
PRIVEIL_ASSESS_BACKEND |
auto |
auto = use laya if installed, else 503 |
PRIVEIL_JUDGE_PII_THRESHOLD |
0.5 |
Probability threshold for keeping a span |
PRIVEIL_JUDGE_PRELOAD |
false |
Load all checkpoints at startup (recommended in production) |
PRIVEIL_JUDGE_SCORE_THRESHOLD |
0.9 |
Trust-tier regex spans at or above this bypass the judge |
PRIVEIL_JUDGE_CONTEXT_CHARS |
60 |
Characters of context window sent to the judge per span |
Performance (concurrent spans):
| Spans | Judge (GPU) | Judge (CPU) |
|---|---|---|
| 1 | ~33 ms | ~100 ms |
| 3 | ~33 ms | ~100 ms (concurrent) |
| 5 | ~66 ms | ~200 ms |
Multiple spans verify concurrently in the thread pool. Trust-tier inputs (AU_TFN, AU_MEDICARE etc.) bypass the judge entirely β zero overhead.
Production setup:
PRIVEIL_JUDGE_PRELOAD=true # warm checkpoints at startup, not on first requestPurpose-built recognisers with checksum validation where the issuing authority publishes an algorithm.
| Entity type | Description | PII | Sensitivity | Validation |
|---|---|---|---|---|
AU_TFN |
Tax File Number | β | critical | ATO mod-11 checksum |
AU_MEDICARE |
Medicare card number | β | critical | Services Australia checksum |
AU_ABN |
Australian Business Number | β | low | ATO mod-89 checksum |
AU_ACN |
Australian Company Number | β | low | ASIC complement-of-10 checksum |
AU_BSB |
Bank State Branch code | β | high | Format XXX-XXX |
AU_PHONE |
Australian mobile/landline | β | medium | 04XX, +61 4XX, (0X) XXXX XXXX |
Generic types (always active): EMAIL_ADDRESS, PHONE_NUMBER, CREDIT_CARD.
NER types (requires --extra gliner): PERSON, LOCATION, DATE_OF_BIRTH.
TFN scope: 9-digit TFNs only. Legacy 8-digit TFNs are excluded by design.
All variables prefixed PRIVEIL_. Copy .env.example to .env.
| Variable | Default | Description |
|---|---|---|
PRIVEIL_NER_MODEL |
fastino/gliner2-base-v1 |
NER model HuggingFace ID. Requires --extra gliner. |
PRIVEIL_JUDGE_BACKEND |
auto |
Span judge backend: auto (laya if installed) or laya |
PRIVEIL_ASSESS_BACKEND |
auto |
Assess backend: auto (laya if installed, else 503) or laya |
PRIVEIL_JUDGE_SCORE_THRESHOLD |
0.9 |
Trust-tier regex spans at or above this bypass the judge |
PRIVEIL_JUDGE_CONTEXT_CHARS |
60 |
Context window (chars) around each span sent to the judge |
PRIVEIL_JUDGE_PII_THRESHOLD |
0.5 |
Judge noul probability to keep a span as genuine PII |
PRIVEIL_JUDGE_PRELOAD |
false |
Preload judge checkpoints at startup |
PRIVEIL_AUDIT_HASH_KEY |
(unset) | HMAC key for input_hash. Set for stable hashes across restarts. |
PRIVEIL_EXECUTOR_MAX_WORKERS |
4 |
Thread-pool workers for CPU-bound recogniser and judge work |
PRIVEIL_DEBUG |
false |
FastAPI debug mode |
Priveil exposes detect, anonymise, and assess over the Model Context Protocol.
pip install "priveil[mcp]"Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"priveil": {
"command": "priveil-mcp",
"env": {
"PRIVEIL_JUDGE_PRELOAD": "true"
}
}
}
}The MCP server loads the same detection stack as the HTTP API β NER model if installed, span judge if installed. No API key needed.
uv sync --extra gliner --extra laya # full stack
uv run pytest tests/ -v
uv run ruff check src/ tests/
uv run mypy src/Docker:
# Full-stack local image (gliner + laya + mcp)
docker build --target local -t priveil-local .
docker run --rm -p 8000:8000 priveil-local
# CI test matrix
docker build --target test -t priveil-test .
docker run --rm priveil-testCI runs across Python 3.11, 3.12, and 3.13.
src/priveil/
βββ judge/
β βββ judge.py # SpanJudge β noul questions per span
β β # Also exports: JudgeResult, JudgeProtocol
β βββ assessor.py # JudgeAssessor β choice/noul for /assess
β # Also exports: entity_breakdown()
βββ api/
β βββ deps.py # FastAPI dependency injection
β βββ routes/ # detect, pseudonymise, assess, health
βββ domain/ # Pydantic models (entities, detection, assessment, β¦)
βββ engine/ # AsyncAnalyser, AsyncPseudonymiser
βββ mcp/ # MCP server (pip install "priveil[mcp]")
β βββ server.py # _State dataclass, lifespan, FastMCP instance
β βββ tools.py # detect / anonymise / assess tools
βββ recognisers/
β βββ au_tfn.py # AU_TFN β mod-11 checksum
β βββ au_medicare.py # AU_MEDICARE β Services Australia checksum
β βββ au_abn.py # AU_ABN β mod-89 checksum
β βββ au_acn.py # AU_ACN β complement-of-10 checksum
β βββ au_bsb.py # AU_BSB β format validation + judge
β βββ au_phone.py # AU_PHONE β pattern matching + judge
β βββ email.py # EMAIL_ADDRESS
β βββ phone.py # PHONE_NUMBER β judge
β βββ credit_card.py # CREDIT_CARD β Luhn checksum
β βββ person.py # PERSON β NER model (optional)
β βββ location.py # LOCATION β NER model (optional)
β βββ date_of_birth.py # DATE_OF_BIRTH β NER model (optional)
β βββ base.py # Span, BaseRecogniser, RegexRecogniser, NERRecogniser
β βββ registry.py # build_recognisers(ner_model=None)
βββ settings.py # All config vars, prefixed PRIVEIL_
βββ app.py # FastAPI factory + lifespan
βββ __main__.py # python -m priveil β HTTP API
tests/
βββ unit/ # Pure function tests β no network
βββ integration/ # Full requestβresponse via httpx AsyncClient
βββ mcp/ # MCP tool tests (skipped when mcp extra not installed)
Warning
The word "anonymise" appears throughout this codebase because it is the term practitioners use. It is not accurate.
What Priveil produces is pseudonymisation: detected entity spans are replaced with labelled placeholders. The entity_map returned by /pseudonymise is sensitive data β protect it with the same controls as the original text.
No pattern-matching tool can produce truly anonymous data:
- Data is more identifying than it appears. A name, postcode, and date of birth together uniquely identify most people. There is no way to enumerate what an attacker might use.
- Auxiliary data is an unknown variable. Information that looks private today may be public for specific individuals, or become identifying after an unrelated breach.
- Attacks improve. Re-identification techniques continue to advance.
Tip
What Priveil is useful for: keeping PII out of logs and analytics pipelines, reducing accidental exposure when data crosses trust boundaries, improving compliance posture. These are real and valuable goals β just not anonymisation.
For further reading: Damien Desfontaines' What anonymization techniques can you trust? and Katharine Jarmul's Probably Private.
- uv β
curl -LsSf https://astral.sh/uv/install.sh | sh - Docker (optional, for CI matrix builds)