In-Browser Clinical PII Redaction with Transformers.js
Introduction
Removing Protected Health Information (PHI) from clinical notes is vital for healthcare data privacy. Traditionally, this required setting up Python servers running SpaCy or Microsoft Presidio. In this tutorial, we will construct a 100% client-side PII scrubbing engine using Transformers.js and a lightweight Named Entity Recognition (NER) model to redact clinical notes inside the browser.
Architecture Overview
graph TD
Notes(Raw Clinical Notes) --> Regex[Regex Pre-filter: SSN, Phone, Email]
Regex --> NER_Pipeline[Transformers.js NER Pipeline]
NER_Pipeline -.-> |Local Model: distilbert-NER| Entity_Detection[Entity Detection]
Entity_Detection --> Redactor[Redaction Mapping Engine]
Redactor --> Clean_Notes(HIPAA De-identified Notes)
Prerequisites
- Any modern web browser
- Basic knowledge of JavaScript / HTML
Step 1: Set Up Transformers.js via CDN
Import the pipeline modules from the CDN distribution of Transformers.js to get started with zero bundler setup.
<script type="module">
import { pipeline } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2';
// We will use this module to load the local classifier
</script>
Step 2: Define Regex Pre-filters
While statistical models are excellent for names and locations, regular expressions are more robust and faster for highly structured identifiers like Social Security Numbers (SSN), phone numbers, or dates.
function regexPreFilter(text) {
let sanitized = text;
// Redact SSNs: xxx-xx-xxxx
sanitized = sanitized.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED_SSN]");
// Redact Phone Numbers: (xxx) xxx-xxxx or xxx-xxx-xxxx
sanitized = sanitized.replace(/\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g, "[REDACTED_PHONE]");
// Redact Email Addresses
sanitized = sanitized.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, "[REDACTED_EMAIL]");
return sanitized;
}
Step 3: Load the NER Model and Classify
We load a lightweight, optimized model specifically fine-tuned for Named Entity Recognition, such as Xenova/distilbert-base-uncased-finetuned-conll03-english, which detects PERSON, LOCATION, and ORGANIZATION entities.
async function runNERDeidentification(text) {
// 1. Run regex pre-filters
let redacted = regexPreFilter(text);
// 2. Load the pipeline (runs locally in-browser)
const classifier = await pipeline('token-classification', 'Xenova/distilbert-base-uncased-finetuned-conll03-english');
const entities = await classifier(redacted);
// 3. Process entities reverse-chronologically to avoid text index shifts
const sortedEntities = entities.sort((a, b) => b.start - a.start);
for (const ent of sortedEntities) {
if (ent.entity === 'I-PER' || ent.entity === 'B-PER') {
redacted = redacted.substring(0, ent.start) + "[REDACTED_NAME]" + redacted.substring(ent.end);
} else if (ent.entity === 'I-LOC' || ent.entity === 'B-LOC') {
redacted = redacted.substring(0, ent.start) + "[REDACTED_LOCATION]" + redacted.substring(ent.end);
}
}
return redacted;
}
Conclusion
By combining deterministic regex matching for structured data and local statistical token classification for unstructured text, you can build a robust, privacy-preserving de-identification engine directly inside the browser with zero server latency or data leakage.