Tutorial

In-Browser Clinical PII Redaction with Transformers.js

Difficulty: Intermediate Time: 12 min read

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.