Skip to main content

edshield

Local-first student-privacy layer for AI in education. Detects and removes student PII from essays, tutoring transcripts and chat messages before the text reaches any language model. Runs on a laptop CPU, Apple Silicon, or inside a Chromebook browser. No cloud, no student text leaves the device. Apache-2.0.

Try the live demo: it runs in your browser, and the text you paste stays on your device. Models: edshield/piilo-deberta-v3-small and its browser export.

from edshield import extract_pii, deidentify

text = "Hi, this is Marcus. My email is marcus.t2012@gmail.com and my Discord is @marcus_hoops."

print([(e.label, e.text) for e in extract_pii(text).entities])
# [('NAME_STUDENT', 'Marcus'), ('EMAIL', 'marcus.t2012@gmail.com'), ('USERNAME', 'marcus_hoops')]

print(deidentify(text, policy="coppa").deidentified_text)
# Hi, this is [CHILD]. My email is [EMAIL] and my Discord is @[USERNAME].

Why

Every AI tutor, writing assistant and classroom chatbot sends student text to a model. FERPA, COPPA and a growing set of state laws (NY Ed Law 2-d, Illinois SOPPA, California SOPIPA) say identifiable student data cannot be handed to a third party without consent, and district AI policies are now saying it outright. Today each vendor and each research group rebuilds the same de-identification pipeline privately. edshield is the shared, open one.

The design follows OpenMed: small fine-tuned encoders for the domain, a rule layer for the identifiers where regex plus validation beats a neural model, policy profiles named after the regulation, and runtimes for Python, ONNX and the browser.

What it does

Layer Covers Needs a model?
Rules EMAIL, PHONE_NUM, URL_PERSONAL, USERNAME, ID_NUM, STREET_ADDRESS, SSN, DATE, and names introduced with a cue ("my name is…", a signature) No
Rules, beyond PIILO NAME_RELATED (family, friends, teachers), SCHOOL, LOCATION, AGE, IP_ADDRESS, DEVICE_ID, GEO No
Model The authority for NAME_STUDENT, ID_NUM and STREET_ADDRESS, plus a second opinion on every other label Yes (PIILO-trained encoder)
Propagation Once a name is found, every other mention of it in the document is caught No
Policies ferpa, coppa, research decide which labels to act on, the confidence floor, and the method per label (mask, surrogate, hash, date-shift) No
Verifier Refuses to return output if any acted-on value still appears verbatim No
Audit record Policy, version, detector and counts for every document, with no student data in it No

When a model is loaded, the rules for NAME_STUDENT, ID_NUM and STREET_ADDRESS are switched off: they are recall-oriented fallbacks that over-flag ordinary essay text. Without a model the rules cover every label. analyze_text(..., model_authority=()) runs both layers on everything.

Loading a model. The default model is edshield/piilo-deberta-v3-small on the Hugging Face Hub. edshield downloads nothing unless you allow it, so fetch the model once:

pip install "edshield[hf]"
EDSHIELD_ALLOW_DOWNLOAD=1 edshield extract essay.txt     # PowerShell: $env:EDSHIELD_ALLOW_DOWNLOAD = "1"

After that it loads from the Hugging Face cache with no network and the variable is not needed. Without the variable, models load from disk only: a directory you pass, the local_path in edshield/models.jsonl, or the cache. If you name a model (model_name=... or EDSHIELD_MODEL) and it cannot be loaded, edshield raises ModelUnavailableError rather than quietly doing less. If you name none and the default is not installed, the rules run alone and a RuntimeWarning says so; pass model_name="rules" to choose that on purpose.

Recall-first decoding. analyze_text(..., o_threshold=0.99) marks a token as an entity whenever P(O) < 0.99 instead of taking the most likely class. It is off by default: on held-out PIILO essays it lowered precision from 0.69 to 0.57 with recall already at 1.00.

Label schema is the seven types of the PIILO corpus (The Learning Agency Lab, CC BY 4.0), so models trained on it drop straight in.

Install

pip install edshield                 # rules, policies, CLI. No torch.
pip install "edshield[hf]"           # + PyTorch model inference
pip install "edshield[service]"      # + REST service
pip install "edshield[train,onnx]"   # + training and ONNX/browser export

From a clone, use pip install -e ".[dev]" instead.

edshield redact essay.txt --policy ferpa
edshield extract essay.txt --model rules
edshield serve --port 8080       # POST /pii/extract, POST /pii/deidentify

Demo

Live at https://hemangnagar.github.io/edshield/, or from a clone:

python -m http.server 8000 -d demo
# open http://localhost:8000

Three synthetic samples (essay, tutoring transcript, chatbot message), three policies, and a detector switch. "Rules only" runs entirely from the page's own JavaScript, with no network. "Rules + on-device model" runs an ONNX model in the browser through Transformers.js, so the same layer works on a Chromebook with no backend. The model file (205 MB) is fetched once from the Hugging Face Hub and cached by the browser; the text is never uploaded. To run the model with no network at all, put a copy in demo/models/piilo-deberta-v3-small-onnx and the page uses that instead.

Benchmarks

All numbers are span-level from eval/evaluate.py; F5 weights recall 5:1, as the PIILO competition did.

Real student essays. 680 PIILO documents held out from training of piilo-deberta-v3-small (DeBERTa-v3-small, 3 epochs), in the corpus's natural mix: 581 of them contain no PII at all.

Detector Precision Recall F5 Missed entities
Rules only 0.538 0.388 0.392 101 of 165
Rules + model 0.642 1.000 0.979 0 of 165
Rules + INT8 model (browser) 0.639 1.000 0.979 0 of 165

The browser file is 205 MB against 566 MB at full precision. It is measured with eval/evaluate_onnx.py. Quantizing every layer gives 172 MB but missed 8 of the 165 (precision 0.692, recall 0.952), so the export leaves the first two encoder layers at full precision. That setting was chosen on this same set, so the row is a little optimistic; on the synthetic K-12 hard set below, which played no part in the choice, the browser file lets 324 of 1,433 through against 319 for the full model.

Rules + model, by label Precision Recall n
NAME_STUDENT 0.656 1.000 143
URL_PERSONAL 0.381 1.000 8
ID_NUM 0.700 1.000 7
EMAIL 1.000 1.000 4
USERNAME 1.000 1.000 2
STREET_ADDRESS 0.500 1.000 1

The rare labels have a handful of examples each, so their rows say little. Most name false positives are names of people other than the essay's author (personas, lecturers, friends), which PIILO does not label but which a privacy tool should remove. Reports are in eval/results/.

Synthetic K-12 writing. PIILO is adult writing, so eval/k12_bench.py generates short essays, tutoring transcripts and chat messages in children's registers, covering every label. "Got through" counts identifiers that no flag of any label touched.

Set Detector Identifiers Got through
Cued: worded the way the rules expect Rules + model 1,445 0
Hard: the way children type Rules + model 1,433 319 (22%)
Hard Rules only 1,433 1,100 (77%)

The hard set is the honest baseline. What gets through is mostly lowercase schools and towns, ages in chat shorthand, spoken dates and streets without a house number. docs/COVERAGE.md has the breakdown and maps each identifier type in FERPA and COPPA to what edshield does.

Synthetic transcripts and essays. Rules only, 500 documents (eval/synthetic_bench.py --n 500 --seed 1). The generator's sentences use the same cues the rules look for, so read this as a regression check, not as expected accuracy on real text:

Label Precision Recall F5
EMAIL 1.000 1.000 1.000
PHONE_NUM 1.000 1.000 1.000
USERNAME 1.000 1.000 1.000
URL_PERSONAL 1.000 1.000 1.000
ID_NUM 0.926 1.000 0.997
STREET_ADDRESS 1.000 0.945 0.947
NAME_STUDENT 1.000 0.761 0.768
overall 0.994 0.903 0.906

Names are the gap, and the reason the model layer exists: rules only catch names the writer introduces, so a friend mentioned in passing is missed.

Train a model

pip install kaggle
kaggle competitions download -c pii-detection-removal-from-educational-data
unzip pii-detection-removal-from-educational-data.zip -d data/piilo

python eval/synthetic_bench.py --n 2000 --out data/synthetic.json      # augmentation for rare labels
python training/prepare_piilo.py --input data/piilo/train.json --out data/piilo_hf --extra data/synthetic.json
python training/train.py --data data/piilo_hf --base microsoft/deberta-v3-small --out models/piilo-deberta-v3-small --epochs 3
python eval/evaluate.py --input data/piilo_hf/validation.json --model models/piilo-deberta-v3-small --device cuda
python training/export_onnx.py --model models/piilo-deberta-v3-small --out demo/models/piilo-deberta-v3-small-onnx

train.py uses the recall tricks that won the competition: down-weighted O class, a P(O) threshold instead of argmax at inference, long context with stride, synthetic augmentation. edshield/models.jsonl is the manifest; add a line per published model. training/publish_hub.py uploads a model and its card from hub/ to the Hugging Face Hub.

Layout

edshield/         runtime: rules.py, ner.py, deid.py, policies/, cli.py, service.py, models.jsonl (model manifest)
training/         prepare_piilo.py, train.py, export_onnx.py, publish_hub.py
hub/              model cards for the Hugging Face Hub
eval/             evaluate.py (F5 + leak count), evaluate_onnx.py, synthetic_bench.py, results/
demo/             single-file browser demo; a model in demo/models/ is used in place of the Hub
tests/            pytest
docs/             COVERAGE.md: what is detected, how well, and what can be claimed
.github/          GitHub Actions: tests on every push, the demo to GitHub Pages, releases to PyPI

Roadmap

  • Rule layer with validators, name propagation, leak verifier
  • FERPA / COPPA / research policies
  • Browser demo, REST service, CLI
  • First PIILO-trained encoder (DeBERTa-v3-small) and its ONNX INT8 export published to the Hub
  • DeBERTa-v3-base and ModernBERT-base
  • Browser benchmark on a Chromebook
  • Transcript models: teacher/tutor discourse moves (TalkMoves, NCTE), argumentative elements (PERSUADE)
  • MLX backend and a Swift package
  • MCP server and agent skills

For research contributors

The corpus is public, the metric is defined, and the winning recipes are documented. That makes this a good place for a first applied-ML paper: fine-tune a model on PIILO, compare it with ChatGPT and Presidio on eval/evaluate.py, measure what leaks, and publish. Open an issue with the experiment you want to run; results that beat the current manifest entry get merged and credited in the model card.

What this is not

Running edshield does not by itself make a product FERPA- or COPPA-compliant. It removes direct identifiers with measured recall; the institution still owns the reasonable-determination review, the consent process, and the data-handling agreement. Never paste real student data into a cloud-hosted agent to test this; use the synthetic samples.

Credits

The Learning Agency Lab and Vanderbilt University for the PIILO corpus; OpenMed for the pattern; Hugging Face transformers and Transformers.js; Faker.

License

Apache-2.0. Model weights carry the license of their training data (PIILO is CC BY 4.0).

Metadata

Release files for edshield 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for edshield 0.2.0
File Size Uploaded
edshield-0.2.0.tar.gz 53.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for edshield 0.2.0
File Interpreter ABI Platform
edshield-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 89.0 kB

Release files / edshield-0.2.0.tar.gz

Download URL edshield-0.2.0.tar.gz
Size 53.1 kB
Tags Source
SHA-256 checksum
How to use checksums
04badeee48ec16fc80c034dbe389660d59289e6102af5a5f9b24e8da1a703ed0
BLAKE2b-256 checksum
How to use checksums
70a0af44a9342c10355be470e0ca5d7714b0c8f230958be9f2d4652ad2edb5a0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release files / edshield-0.2.0-py3-none-any.whl

Download URL edshield-0.2.0-py3-none-any.whl
Size 35.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0cb76d7f36191abd4e7aea192a54a92975705b9e1580c316e05100c911eace8e
BLAKE2b-256 checksum
How to use checksums
1110cc3ea1191b1aa489b09b9bd326e1904caa288ddc71fa7c50f97d776c6de5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page