greeshma-ch/PII-Redaction-Tool

A zero-shot PII redaction pipeline for documents, using GLiNER for named entity recognition (0.971 F1). Detects and redacts 9 PII types from .docx files via a FastAPI backend and browser UI, containerized with Docker.

Python

0

39 commits

updated Aug 16, 2026

See the code

README

PII Redaction Tool

A production-grade tool that reads .docx documents, detects 9 types of personally identifiable information (PII), and produces a redacted copy where every PII instance is replaced with a consistent fake alternative — the same real value always maps to the same fake value everywhere it appears.

Live Demo

Status: Render deployment is pending verification. The deployment URL (<RENDER_URL_HERE>) will be updated once the free-tier deployment is confirmed to work with GLiNER's memory requirements.

Upload any .docx file → view a color-coded before/after diff → download the redacted document.

Note: The submitted redacted .docx deliverable in output/ was generated via direct pipeline execution, not the live web service — see Limitations for why.


Quick Start (Local)

# 1. Clone and install
git clone <repo-url>
cd pii-redaction-tool
pip install -r requirements.txt

# 2. Run the web UI
uvicorn api.main:app --host 0.0.0.0 --port 8000
# Open http://localhost:8000

# 3. Or run the CLI
python run_redaction.py

Approach

Hybrid detection strategy: regex + GLiNER Zero-Shot NER

PII TypeMethodRationale
EmailRegexWell-defined syntax — regex achieves near-perfect precision/recall
PhoneRegexStructured formats (+91, (XXX), international) — regex with format-specific patterns
SSNRegexExact format (XXX-XX-XXXX) with area-number validation to reject invalid prefixes
Credit CardRegex + Luhn checksumFormat matching alone produces false positives; Luhn validation eliminates them
DOBRegex + context heuristicDate patterns are ubiquitous; context keywords ("born", "DOB") disambiguate DOBs from other dates
IP AddressRegexIPv4 (with octet validation) and IPv6 — well-defined syntax
NamesGLiNER (knowledgator/gliner-pii-edge-v1.0)Zero-shot NER — catches single-token Indian names, titled names ("Shri Kamal Sharma"), and table-cell text without sentence context
CompaniesGLiNER (knowledgator/gliner-pii-edge-v1.0)Zero-shot NER — eliminates company-as-person misclassifications, preserves exclusion rules for subject company
AddressesGLiNER (knowledgator/gliner-pii-edge-v1.0) + PIN regexZero-shot location address matching + PIN-code context heuristics

The lightweight edge variant is used in production for memory-constrained deployment (see Deployment); the original evaluation in EVALUATION.md was run against this same variant unless otherwise noted.


Architecture

┌─────────────────────────────────────────────────┐
│                   FastAPI App                   │
│                  POST /redact                   │
└──────────────────────┬──────────────────────────┘
                       │
            ┌──────────▼──────────┐
            │  RedactionPipeline  │
            │   (core/redactor)   │
            └──────────┬──────────┘
                       │
     ┌─────────────────▼─────────────────┐
     │         For each paragraph /      │
     │           table cell:             │
     │                                   │
     │   ┌────────────────────────────┐  │
     │   │   Run all 9 detectors      │  │
     │   │   → List[DetectionResult]  │  │
     │   └────────────┬───────────────┘  │
     │                │                  │
     │   ┌────────────▼───────────────┐  │
     │   │   Resolve overlapping      │  │
     │   │   spans within a type      │  │
     │   │   (wider wins)*            │  │
     │   └────────────┬───────────────┘  │
     │                │                  │
     │   ┌────────────▼───────────────┐  │
     │   │   PIIMapper: real → fake   │  │
     │   │   (deterministic, seeded)  │  │
     │   └────────────┬───────────────┘  │
     │                │                  │
     │   ┌────────────▼───────────────┐  │
     │   │   Replace within Runs      │  │
     │   │   (preserve formatting)    │  │
     │   └────────────────────────────┘  │
     └───────────────────────────────────┘

*Overlap resolution currently applies within a single detector's own output. Cross-type deduplication (e.g. suppressing a NAME match when the same span is already claimed by COMPANY or ADDRESS with higher confidence) is not yet implemented — see EVALUATION.md's remaining-false-positives analysis, where 3 of 4 residual NAME errors are exactly this case.

Key modules

ModulePurpose
core/gliner_client.pySingleton model loader for knowledgator/gliner-pii-edge-v1.0
detectors/*.pyOne module per PII type, each returns List[DetectionResult(text, start, end, pii_type, confidence)]
core/mapper.pyHash-seeded Faker generates deterministic fake values; persists to JSON for audit
core/redactor.pyWalks docx paragraph-by-paragraph and table-cell-by-cell; runs detectors; resolves overlaps; replaces text within existing Run objects to preserve formatting
eval/evaluate.pySpan-based evaluation: IoU > 0.5 matching, per-type precision/recall/F1
api/main.pyFastAPI with upload, redaction, text diff extraction, and download
frontend/index.htmlSingle-page UI with drag-and-drop, diff view, and animated redaction bars

Explicit Design Decisions

1. Company names: selective redaction

Decision: Redact organization names detected by GLiNER, except the document's own subject company ("KSH International Limited" and its short forms). Rationale: The prospectus mentions its subject company hundreds of times in structural boilerplate. Redacting these would make the document unreadable.

2. "Order" / "Ticket" numbers: not treated as PII

Sequential identifiers with no intrinsic personal info are not redacted.

3. Phone numbers: redacted

Mobile and corporate phone numbers are redacted per assignment requirements.

4. Missing PII types in source document

SSN, credit card, DOB, and IP detectors are validated via a synthetic test set with ground truth.


Evaluation Results

Full methodology, false-positive/negative root-cause analysis, and the complete six-stage tuning history are in EVALUATION.md. Summary of the final, in-document gold-standard results:

TypePrecisionRecallF1
EMAIL1.0001.0001.000
NAME0.9180.9570.938
COMPANY1.0000.9600.980
ADDRESS1.0001.0001.000
OVERALL0.9670.9750.971

The detection engine evolved through six stages — spaCy baseline → GLiNER zero-shot swap → threshold/label refinements → empirical false-positive root-cause fixes → PIN-city address merge → email-boundary filter correction — with overall F1 improving from 0.580 to 0.971. Each stage's fix was diagnosed from actual false-positive/negative text rather than assumption; see EVALUATION.md for the full before/after table and an explicit note on where these numbers may be optimistic due to iterative tuning against a fixed evaluation sample.


Project Structure

├── api/
│   └── main.py              # FastAPI app
├── core/
│   ├── gliner_client.py     # GLiNER zero-shot model loader
│   ├── mapper.py             # Real → fake value mapper
│   └── redactor.py           # Document-level pipeline
├── detectors/
│   ├── base.py               # DetectionResult type + constants
│   ├── names.py              # GLiNER Person Name detector
│   ├── companies.py          # GLiNER Company detector with exclusion list
│   ├── addresses.py          # GLiNER Address detector + PIN regex
│   ├── emails.py             # Regex
│   ├── phones.py             # Regex (Indian + US + international)
│   ├── ssn.py                # Regex with area validation
│   ├── credit_card.py        # Regex + Luhn checksum
│   ├── dob.py                # Regex + context heuristic
│   └── ip_address.py         # Regex (IPv4 + IPv6)
├── eval/
│   ├── evaluate.py           # Evaluation harness
│   ├── gold_standard.json    # Manual annotations
│   └── synthetic_test.py     # Synthetic SSN/CC/DOB/IP tests
├── frontend/
│   └── index.html            # Web UI
├── tests/                    # Unit tests per detector
├── output/                   # Generated redacted files
├── requirements.txt
├── render.yaml                # Render deployment configuration
├── Dockerfile                 # Container build for deployment
└── README.md

Deployment

This application is containerized via the included Dockerfile.

Migration: Cloud Run → Render

The project was originally deployed on Google Cloud Run, which provided configurable per-service memory allocation needed for the GLiNER model's ~1 GB memory footprint. However, Cloud Run's pay-per-request billing model introduced unpredictable cost exposure, making a persistent free/low-cost deployment preferable for a portfolio project.

Render is now the intended deployment target using Docker deployment on the free tier. This deployment is an experiment — it has not yet been verified that the GLiNER + PyTorch stack fits within Render free-tier resource limits.

Fallback options if the free tier proves insufficient:

  • Render paid instance (Starter plan, ~$7/month)
  • Hugging Face Spaces (free tier with 2 vCPU / 16 GB RAM)

Running via Docker (any platform)

docker build -t pii-redaction-tool .
docker run -p 8080:8080 pii-redaction-tool

The model is loaded lazily on first request (not at container startup), so the first request after a cold start includes a one-time model-load delay.


Limitations

  • Processing time on constrained CPU: full-document processing (~90s on a local dev machine) takes considerably longer on resource-constrained cloud instances — budget several minutes for a large, table-heavy document like the sample prospectus on a cold instance.
  • Table cell alignment on the live service: for documents with very large, multi-page tables using repeated header rows, the live web service has been observed to occasionally misalign redacted content between cells (a python-docx table-indexing edge case with repeated headers). The submitted output/*.docx deliverable was generated via direct pipeline execution (python run_redaction.py), which does not exhibit this issue — only the FastAPI web-upload code path is affected. This is a known open issue, not fixed as of submission.
  • Tuning generalizability: see EVALUATION.md's overfitting caveat — several detector refinements were built from, and evaluated against, the same 120-block gold-standard sample.
  • Indian-specific identifiers (Aadhaar, PAN, passport numbers) are not yet supported; would be straightforward regex additions.

greeshma-ch/PII-Redaction-Tool

A zero-shot PII redaction pipeline for documents, using GLiNER for named entity recognition (0.971 F1). Detects and redacts 9 PII types from .docx files via a FastAPI backend and browser UI, containerized with Docker.

Python

0

39 commits

updated Aug 16, 2026

See the code

README

PII Redaction Tool

A production-grade tool that reads .docx documents, detects 9 types of personally identifiable information (PII), and produces a redacted copy where every PII instance is replaced with a consistent fake alternative — the same real value always maps to the same fake value everywhere it appears.

Live Demo

Status: Render deployment is pending verification. The deployment URL (<RENDER_URL_HERE>) will be updated once the free-tier deployment is confirmed to work with GLiNER's memory requirements.

Upload any .docx file → view a color-coded before/after diff → download the redacted document.

Note: The submitted redacted .docx deliverable in output/ was generated via direct pipeline execution, not the live web service — see Limitations for why.


Quick Start (Local)

# 1. Clone and install
git clone <repo-url>
cd pii-redaction-tool
pip install -r requirements.txt

# 2. Run the web UI
uvicorn api.main:app --host 0.0.0.0 --port 8000
# Open http://localhost:8000

# 3. Or run the CLI
python run_redaction.py

Approach

Hybrid detection strategy: regex + GLiNER Zero-Shot NER

PII TypeMethodRationale
EmailRegexWell-defined syntax — regex achieves near-perfect precision/recall
PhoneRegexStructured formats (+91, (XXX), international) — regex with format-specific patterns
SSNRegexExact format (XXX-XX-XXXX) with area-number validation to reject invalid prefixes
Credit CardRegex + Luhn checksumFormat matching alone produces false positives; Luhn validation eliminates them
DOBRegex + context heuristicDate patterns are ubiquitous; context keywords ("born", "DOB") disambiguate DOBs from other dates
IP AddressRegexIPv4 (with octet validation) and IPv6 — well-defined syntax
NamesGLiNER (knowledgator/gliner-pii-edge-v1.0)Zero-shot NER — catches single-token Indian names, titled names ("Shri Kamal Sharma"), and table-cell text without sentence context
CompaniesGLiNER (knowledgator/gliner-pii-edge-v1.0)Zero-shot NER — eliminates company-as-person misclassifications, preserves exclusion rules for subject company
AddressesGLiNER (knowledgator/gliner-pii-edge-v1.0) + PIN regexZero-shot location address matching + PIN-code context heuristics

The lightweight edge variant is used in production for memory-constrained deployment (see Deployment); the original evaluation in EVALUATION.md was run against this same variant unless otherwise noted.


Architecture

┌─────────────────────────────────────────────────┐
│                   FastAPI App                   │
│                  POST /redact                   │
└──────────────────────┬──────────────────────────┘
                       │
            ┌──────────▼──────────┐
            │  RedactionPipeline  │
            │   (core/redactor)   │
            └──────────┬──────────┘
                       │
     ┌─────────────────▼─────────────────┐
     │         For each paragraph /      │
     │           table cell:             │
     │                                   │
     │   ┌────────────────────────────┐  │
     │   │   Run all 9 detectors      │  │
     │   │   → List[DetectionResult]  │  │
     │   └────────────┬───────────────┘  │
     │                │                  │
     │   ┌────────────▼───────────────┐  │
     │   │   Resolve overlapping      │  │
     │   │   spans within a type      │  │
     │   │   (wider wins)*            │  │
     │   └────────────┬───────────────┘  │
     │                │                  │
     │   ┌────────────▼───────────────┐  │
     │   │   PIIMapper: real → fake   │  │
     │   │   (deterministic, seeded)  │  │
     │   └────────────┬───────────────┘  │
     │                │                  │
     │   ┌────────────▼───────────────┐  │
     │   │   Replace within Runs      │  │
     │   │   (preserve formatting)    │  │
     │   └────────────────────────────┘  │
     └───────────────────────────────────┘

*Overlap resolution currently applies within a single detector's own output. Cross-type deduplication (e.g. suppressing a NAME match when the same span is already claimed by COMPANY or ADDRESS with higher confidence) is not yet implemented — see EVALUATION.md's remaining-false-positives analysis, where 3 of 4 residual NAME errors are exactly this case.

Key modules

ModulePurpose
core/gliner_client.pySingleton model loader for knowledgator/gliner-pii-edge-v1.0
detectors/*.pyOne module per PII type, each returns List[DetectionResult(text, start, end, pii_type, confidence)]
core/mapper.pyHash-seeded Faker generates deterministic fake values; persists to JSON for audit
core/redactor.pyWalks docx paragraph-by-paragraph and table-cell-by-cell; runs detectors; resolves overlaps; replaces text within existing Run objects to preserve formatting
eval/evaluate.pySpan-based evaluation: IoU > 0.5 matching, per-type precision/recall/F1
api/main.pyFastAPI with upload, redaction, text diff extraction, and download
frontend/index.htmlSingle-page UI with drag-and-drop, diff view, and animated redaction bars

Explicit Design Decisions

1. Company names: selective redaction

Decision: Redact organization names detected by GLiNER, except the document's own subject company ("KSH International Limited" and its short forms). Rationale: The prospectus mentions its subject company hundreds of times in structural boilerplate. Redacting these would make the document unreadable.

2. "Order" / "Ticket" numbers: not treated as PII

Sequential identifiers with no intrinsic personal info are not redacted.

3. Phone numbers: redacted

Mobile and corporate phone numbers are redacted per assignment requirements.

4. Missing PII types in source document

SSN, credit card, DOB, and IP detectors are validated via a synthetic test set with ground truth.


Evaluation Results

Full methodology, false-positive/negative root-cause analysis, and the complete six-stage tuning history are in EVALUATION.md. Summary of the final, in-document gold-standard results:

TypePrecisionRecallF1
EMAIL1.0001.0001.000
NAME0.9180.9570.938
COMPANY1.0000.9600.980
ADDRESS1.0001.0001.000
OVERALL0.9670.9750.971

The detection engine evolved through six stages — spaCy baseline → GLiNER zero-shot swap → threshold/label refinements → empirical false-positive root-cause fixes → PIN-city address merge → email-boundary filter correction — with overall F1 improving from 0.580 to 0.971. Each stage's fix was diagnosed from actual false-positive/negative text rather than assumption; see EVALUATION.md for the full before/after table and an explicit note on where these numbers may be optimistic due to iterative tuning against a fixed evaluation sample.


Project Structure

├── api/
│   └── main.py              # FastAPI app
├── core/
│   ├── gliner_client.py     # GLiNER zero-shot model loader
│   ├── mapper.py             # Real → fake value mapper
│   └── redactor.py           # Document-level pipeline
├── detectors/
│   ├── base.py               # DetectionResult type + constants
│   ├── names.py              # GLiNER Person Name detector
│   ├── companies.py          # GLiNER Company detector with exclusion list
│   ├── addresses.py          # GLiNER Address detector + PIN regex
│   ├── emails.py             # Regex
│   ├── phones.py             # Regex (Indian + US + international)
│   ├── ssn.py                # Regex with area validation
│   ├── credit_card.py        # Regex + Luhn checksum
│   ├── dob.py                # Regex + context heuristic
│   └── ip_address.py         # Regex (IPv4 + IPv6)
├── eval/
│   ├── evaluate.py           # Evaluation harness
│   ├── gold_standard.json    # Manual annotations
│   └── synthetic_test.py     # Synthetic SSN/CC/DOB/IP tests
├── frontend/
│   └── index.html            # Web UI
├── tests/                    # Unit tests per detector
├── output/                   # Generated redacted files
├── requirements.txt
├── render.yaml                # Render deployment configuration
├── Dockerfile                 # Container build for deployment
└── README.md

Deployment

This application is containerized via the included Dockerfile.

Migration: Cloud Run → Render

The project was originally deployed on Google Cloud Run, which provided configurable per-service memory allocation needed for the GLiNER model's ~1 GB memory footprint. However, Cloud Run's pay-per-request billing model introduced unpredictable cost exposure, making a persistent free/low-cost deployment preferable for a portfolio project.

Render is now the intended deployment target using Docker deployment on the free tier. This deployment is an experiment — it has not yet been verified that the GLiNER + PyTorch stack fits within Render free-tier resource limits.

Fallback options if the free tier proves insufficient:

  • Render paid instance (Starter plan, ~$7/month)
  • Hugging Face Spaces (free tier with 2 vCPU / 16 GB RAM)

Running via Docker (any platform)

docker build -t pii-redaction-tool .
docker run -p 8080:8080 pii-redaction-tool

The model is loaded lazily on first request (not at container startup), so the first request after a cold start includes a one-time model-load delay.


Limitations

  • Processing time on constrained CPU: full-document processing (~90s on a local dev machine) takes considerably longer on resource-constrained cloud instances — budget several minutes for a large, table-heavy document like the sample prospectus on a cold instance.
  • Table cell alignment on the live service: for documents with very large, multi-page tables using repeated header rows, the live web service has been observed to occasionally misalign redacted content between cells (a python-docx table-indexing edge case with repeated headers). The submitted output/*.docx deliverable was generated via direct pipeline execution (python run_redaction.py), which does not exhibit this issue — only the FastAPI web-upload code path is affected. This is a known open issue, not fixed as of submission.
  • Tuning generalizability: see EVALUATION.md's overfitting caveat — several detector refinements were built from, and evaluated against, the same 120-block gold-standard sample.
  • Indian-specific identifiers (Aadhaar, PAN, passport numbers) are not yet supported; would be straightforward regex additions.