plenoai/pleno-anonymize

NER for PII Compliance

Python

1

502 commits

updated Oct 20, 2026

See the code

README

pleno-anonymize

Before/after image redaction: an original photo on the left; the same photo with location-revealing text blacked out on the right. Real presidio ImageRedactorEngine OCR output via POST /api/redact.

PII analysis and redaction for Japanese and English text.

  • pleno-anonymize server — HTTP API with /api/analyze, /api/redact, and OpenAI / Anthropic / Gemini proxies that mask PII before forwarding to upstream providers.
  • pleno-anonymize Python package — SDK and CLI (uvx pleno-anonymize scan .). See packages/sdk.
  • pleno-presidio-extras package — optional Presidio extensions installed separately (e.g. JevContextFilter false-positive filtering). See packages/presidio-extras.
  • pleno_anonymize_ja / pleno_anonymize_en models — spaCy NER models for Japanese and English PII.

Endpoint: https://pleno-anonymize.fly.dev (API reference at /docs). Playground: https://plenoai.com/pleno-anonymize/playground

For scanning SaaS sources or filesystems for PII, use pleno-dlp. It talks to this server's /api/analyze endpoint over HTTP.

Use the CLI

# one-shot scan via uvx (no install) — fail CI on any finding
uvx pleno-anonymize scan . --fail-on-findings

# analyze / redact stdin in-process (downloads the NER wheel on first run)
echo "連絡先 山田太郎 090-1234-5678" | uvx pleno-anonymize analyze --language ja
echo "連絡先 山田太郎 090-1234-5678" | uvx pleno-anonymize redact  --language ja

Defaults to local execution (Presidio + pleno_anonymize_{ja,en} in-process). Add --base-url https://pleno-anonymize.fly.dev to offload to the hosted server. Full flag reference: packages/sdk.

Use the API

curl -X POST https://pleno-anonymize.fly.dev/api/analyze \
  -H "content-type: application/json" \
  -d '{"text":"連絡先 山田太郎 090-1234-5678","language":"ja"}'

POST /api/redact also redacts images: presidio's ImageRedactorEngine OCRs the image with Tesseract and blacks out detected PII text (the banner above is real output — location-revealing text on the photo is OCR-detected and covered).

# black-box OCR-detected PII text in a photo
curl -X POST https://pleno-anonymize.fly.dev/api/redact \
  -H "content-type: application/json" \
  -d "{\"image\":\"data:image/webp;base64,$(base64 -i photo.webp)\",\"language\":\"en\"}"

Route chat traffic through the LLM proxy to mask PII before the request reaches the upstream provider; original values are restored in the response.

EndpointUpstream
POST /api/openai/*OpenAI Chat Completions / Responses
POST /api/anthropic/*Anthropic Messages
POST /api/gemini/*Google Gemini

Use the SDK

from pleno_anonymize import PlenoAnonymize, scan_paths

# default: local engine, auto-downloads pleno_anonymize_ja on first call
engine = PlenoAnonymize()
engine.analyze("山田太郎 090-1234-5678", language="ja")
engine.redact("Contact [email protected]", language="en")

# walk paths, aggregate findings per entity
summary = scan_paths(engine, ["src", "docs"], language="ja")
print(summary.by_entity, summary.total_findings)

# remote mode — same surface, no local model footprint
PlenoAnonymize(base_url="https://pleno-anonymize.fly.dev").analyze("...")

Full API surface: packages/sdk.

Detection backends

EngineInstallSpeedNotes
builtin (default)pip install pleno-anonymize~50 ms/doc CPURegex + checksum validators for structured IDs; slim deps
openai-privacy-filterpip install pleno-anonymize 'opf @ git+https://github.com/openai/privacy-filter@main'~1.2 s/doc CPU, ~30 ms GPUEnglish prose, secret detection, higher recall
# default — builtin Presidio + pleno_anonymize_ja
pleno-anonymize analyze 'Alice Johnson, [email protected]'

# OPF (downloads ~3GB checkpoint to ~/.opf/privacy_filter on first call)
pleno-anonymize analyze --engine openai-privacy-filter --language en \
  'Alice Johnson, [email protected]'

OPF adds a SECRET class not covered by the builtin engine.

Quality

Both models are spaCy tok2vec NER trained on ai4privacy/pii-masking-300k (EN) and 0xhikae/pii-masking-300k-ja (JA). In-distribution F1 ≈ 0.96–0.97. On real-text news (CoNLL-2003 EN F1 ≈ 0.57, stockmark JP Wikipedia F1 ≈ 0.47) the models trail spaCy's *_core_web_lg baselines because they are tuned for form- and record-style PII, not narrative prose. For higher recall on English prose, use the openai-privacy-filter engine. Full numbers, CIs, and methodology: docs/benchmark-pleno-anonymize-en.md, docs/benchmark-pleno-anonymize-ja.md.

Detected entities

ClassBackendEntities
Free textspaCy NER pleno_anonymize_ja plus PresidioPERSON ADDRESS ORGANIZATION DATE_OF_BIRTH BANK_ACCOUNT
APPI Art. 2(3) 要配慮個人情報spaCy NER (context-dependent)RACE CREED SOCIAL_STATUS MEDICAL_HISTORY HEALTH_CHECKUP DISABILITY CRIMINAL_RECORD CRIME_VICTIM
Structuredregex plus checksum (Luhn, My Number, corporate number)PHONE_NUMBER MY_NUMBER MY_NUMBER_CORPORATE CREDIT_CARD PASSPORT DRIVER_LICENSE HEALTH_INSURANCE RESIDENCE_CARD POSTAL_CODE EMAIL_ADDRESS IP_ADDRESS URL
OPF (opt-in)openai/privacy-filter 1.5B (50M active MoE)PERSON ADDRESS EMAIL_ADDRESS PHONE_NUMBER URL DATE_OF_BIRTH BANK_ACCOUNT SECRET

The APPI Art. 2(3) 要配慮個人情報 (special care-required personal information) row covers categories that Japan's Act on the Protection of Personal Information regulates more strictly than ordinary PII. These are context-dependent attributes — the same vocabulary (e.g., a disease name) is only tagged when it describes a specific individual, not in general medical or legal commentary. OpenAI Privacy Filter has structurally zero coverage for these categories.

APPI Art. 2(3) per-subtype quality (DeBERTa v2 base, 10 epochs, held-out test set):

EntityPrecisionRecallF1Support
RACE0.9060.8970.90297
CREED0.9100.9290.91998
SOCIAL_STATUS0.9180.9270.92296
MEDICAL_HISTORY0.7260.8710.79270
HEALTH_CHECKUP0.9330.9630.948320
DISABILITY0.9390.9390.93933
CRIMINAL_RECORD1.0001.0001.00013
CRIME_VICTIM0.6250.7140.66714
micro avg0.9680.9760.9724740

Self-host

docker build -t pleno-anonymize .
docker run -p 8080:8080 pleno-anonymize

fly.toml is included. flyctl deploy --local-only ships the same image to fly.io.

FAQ

Why doesn't the OPF engine support Apple MPS (Metal)?

OPF's 128-expert sparse MoE layer relies on Triton JIT kernels for GPU-accelerated inference. Triton only targets NVIDIA CUDA — it cannot compile for Apple Metal. Without Triton, OPF falls back to a pure-PyTorch codepath that dispatches many small per-expert matrix multiplications. On Apple M3, we measured this fallback on MPS at 1,848 ms/doc vs 1,151 ms/doc on CPU (n = 300, sequential runs) — MPS is 38 % slower because the kernel launch overhead for many small ops exceeds the compute benefit. Use --opf-device cpu on macOS (the default) or a CUDA GPU for production throughput. Full methodology: docs/benchmark.md §5.1a.

License

Apache-2.0 · Privacy Policy · DPIA

plenoai/pleno-anonymize

NER for PII Compliance

Python

1

502 commits

updated Oct 20, 2026

See the code

README

pleno-anonymize

Before/after image redaction: an original photo on the left; the same photo with location-revealing text blacked out on the right. Real presidio ImageRedactorEngine OCR output via POST /api/redact.

PII analysis and redaction for Japanese and English text.

  • pleno-anonymize server — HTTP API with /api/analyze, /api/redact, and OpenAI / Anthropic / Gemini proxies that mask PII before forwarding to upstream providers.
  • pleno-anonymize Python package — SDK and CLI (uvx pleno-anonymize scan .). See packages/sdk.
  • pleno-presidio-extras package — optional Presidio extensions installed separately (e.g. JevContextFilter false-positive filtering). See packages/presidio-extras.
  • pleno_anonymize_ja / pleno_anonymize_en models — spaCy NER models for Japanese and English PII.

Endpoint: https://pleno-anonymize.fly.dev (API reference at /docs). Playground: https://plenoai.com/pleno-anonymize/playground

For scanning SaaS sources or filesystems for PII, use pleno-dlp. It talks to this server's /api/analyze endpoint over HTTP.

Use the CLI

# one-shot scan via uvx (no install) — fail CI on any finding
uvx pleno-anonymize scan . --fail-on-findings

# analyze / redact stdin in-process (downloads the NER wheel on first run)
echo "連絡先 山田太郎 090-1234-5678" | uvx pleno-anonymize analyze --language ja
echo "連絡先 山田太郎 090-1234-5678" | uvx pleno-anonymize redact  --language ja

Defaults to local execution (Presidio + pleno_anonymize_{ja,en} in-process). Add --base-url https://pleno-anonymize.fly.dev to offload to the hosted server. Full flag reference: packages/sdk.

Use the API

curl -X POST https://pleno-anonymize.fly.dev/api/analyze \
  -H "content-type: application/json" \
  -d '{"text":"連絡先 山田太郎 090-1234-5678","language":"ja"}'

POST /api/redact also redacts images: presidio's ImageRedactorEngine OCRs the image with Tesseract and blacks out detected PII text (the banner above is real output — location-revealing text on the photo is OCR-detected and covered).

# black-box OCR-detected PII text in a photo
curl -X POST https://pleno-anonymize.fly.dev/api/redact \
  -H "content-type: application/json" \
  -d "{\"image\":\"data:image/webp;base64,$(base64 -i photo.webp)\",\"language\":\"en\"}"

Route chat traffic through the LLM proxy to mask PII before the request reaches the upstream provider; original values are restored in the response.

EndpointUpstream
POST /api/openai/*OpenAI Chat Completions / Responses
POST /api/anthropic/*Anthropic Messages
POST /api/gemini/*Google Gemini

Use the SDK

from pleno_anonymize import PlenoAnonymize, scan_paths

# default: local engine, auto-downloads pleno_anonymize_ja on first call
engine = PlenoAnonymize()
engine.analyze("山田太郎 090-1234-5678", language="ja")
engine.redact("Contact [email protected]", language="en")

# walk paths, aggregate findings per entity
summary = scan_paths(engine, ["src", "docs"], language="ja")
print(summary.by_entity, summary.total_findings)

# remote mode — same surface, no local model footprint
PlenoAnonymize(base_url="https://pleno-anonymize.fly.dev").analyze("...")

Full API surface: packages/sdk.

Detection backends

EngineInstallSpeedNotes
builtin (default)pip install pleno-anonymize~50 ms/doc CPURegex + checksum validators for structured IDs; slim deps
openai-privacy-filterpip install pleno-anonymize 'opf @ git+https://github.com/openai/privacy-filter@main'~1.2 s/doc CPU, ~30 ms GPUEnglish prose, secret detection, higher recall
# default — builtin Presidio + pleno_anonymize_ja
pleno-anonymize analyze 'Alice Johnson, [email protected]'

# OPF (downloads ~3GB checkpoint to ~/.opf/privacy_filter on first call)
pleno-anonymize analyze --engine openai-privacy-filter --language en \
  'Alice Johnson, [email protected]'

OPF adds a SECRET class not covered by the builtin engine.

Quality

Both models are spaCy tok2vec NER trained on ai4privacy/pii-masking-300k (EN) and 0xhikae/pii-masking-300k-ja (JA). In-distribution F1 ≈ 0.96–0.97. On real-text news (CoNLL-2003 EN F1 ≈ 0.57, stockmark JP Wikipedia F1 ≈ 0.47) the models trail spaCy's *_core_web_lg baselines because they are tuned for form- and record-style PII, not narrative prose. For higher recall on English prose, use the openai-privacy-filter engine. Full numbers, CIs, and methodology: docs/benchmark-pleno-anonymize-en.md, docs/benchmark-pleno-anonymize-ja.md.

Detected entities

ClassBackendEntities
Free textspaCy NER pleno_anonymize_ja plus PresidioPERSON ADDRESS ORGANIZATION DATE_OF_BIRTH BANK_ACCOUNT
APPI Art. 2(3) 要配慮個人情報spaCy NER (context-dependent)RACE CREED SOCIAL_STATUS MEDICAL_HISTORY HEALTH_CHECKUP DISABILITY CRIMINAL_RECORD CRIME_VICTIM
Structuredregex plus checksum (Luhn, My Number, corporate number)PHONE_NUMBER MY_NUMBER MY_NUMBER_CORPORATE CREDIT_CARD PASSPORT DRIVER_LICENSE HEALTH_INSURANCE RESIDENCE_CARD POSTAL_CODE EMAIL_ADDRESS IP_ADDRESS URL
OPF (opt-in)openai/privacy-filter 1.5B (50M active MoE)PERSON ADDRESS EMAIL_ADDRESS PHONE_NUMBER URL DATE_OF_BIRTH BANK_ACCOUNT SECRET

The APPI Art. 2(3) 要配慮個人情報 (special care-required personal information) row covers categories that Japan's Act on the Protection of Personal Information regulates more strictly than ordinary PII. These are context-dependent attributes — the same vocabulary (e.g., a disease name) is only tagged when it describes a specific individual, not in general medical or legal commentary. OpenAI Privacy Filter has structurally zero coverage for these categories.

APPI Art. 2(3) per-subtype quality (DeBERTa v2 base, 10 epochs, held-out test set):

EntityPrecisionRecallF1Support
RACE0.9060.8970.90297
CREED0.9100.9290.91998
SOCIAL_STATUS0.9180.9270.92296
MEDICAL_HISTORY0.7260.8710.79270
HEALTH_CHECKUP0.9330.9630.948320
DISABILITY0.9390.9390.93933
CRIMINAL_RECORD1.0001.0001.00013
CRIME_VICTIM0.6250.7140.66714
micro avg0.9680.9760.9724740

Self-host

docker build -t pleno-anonymize .
docker run -p 8080:8080 pleno-anonymize

fly.toml is included. flyctl deploy --local-only ships the same image to fly.io.

FAQ

Why doesn't the OPF engine support Apple MPS (Metal)?

OPF's 128-expert sparse MoE layer relies on Triton JIT kernels for GPU-accelerated inference. Triton only targets NVIDIA CUDA — it cannot compile for Apple Metal. Without Triton, OPF falls back to a pure-PyTorch codepath that dispatches many small per-expert matrix multiplications. On Apple M3, we measured this fallback on MPS at 1,848 ms/doc vs 1,151 ms/doc on CPU (n = 300, sequential runs) — MPS is 38 % slower because the kernel launch overhead for many small ops exceeds the compute benefit. Use --opf-device cpu on macOS (the default) or a CUDA GPU for production throughput. Full methodology: docs/benchmark.md §5.1a.

License

Apache-2.0 · Privacy Policy · DPIA

Project health

Latest release

0.3.0 · Sep 19, 2026

Releases this year

6

Significant stargazers

JaeSoon (jason)

95 followers · starred May 2026