Back to rankings

maziyarpanahi/openmed

Pythonopenmed.life

Local-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 1,000+ medical models, 12 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0

healthcarellmon-deviceon-premisesovereign-aimlxswiftiosswiftuinerpiipii-detection
Star Growth
Stars
4.7k
Forks
569
Weekly Growth
Issues
469
2k4k
Oct 25Jan 26Apr 26Jul 26
ArtifactsPyPIpip install openmed
README
OpenMed · on-device clinical AI · 2,000+ models

Your Data. Your Model. Your Hardware.

maziyarpanahi%2Fopenmed | Trendshift

Turn clinical text into structured, de-identified insight, with nothing uploaded.
OpenMed extracts biomedical entities and removes 55+ PHI types entirely on the hardware you control, so your data never leaves the device. The same 2,000+ open models run from a phone to a GPU server, fully offline: iOS, iPadOS, and Android via OpenMedKit, React Native, plain CPUs, Apple Silicon, NVIDIA GPUs, the browser, and REST/gRPC services. No cloud. No vendor lock-in. No patient data leaving your network.

PyPI Python Models arXiv License Stars

Swift: OpenMedKit Apple Silicon: MLX Android: ONNX Runtime Mobile Browser: Transformers.js Platforms Docs

2,000+ models  ·  21 model-backed PII languages  ·  600+ PII checkpoints  ·  100% on-device  ·  Apache-2.0

English · 简体中文 · Español · Français · Deutsch · Italiano · Português · Nederlands · العربية · हिन्दी · తెలుగు · 日本語 · Türkçe · فارسی


See it in action

OpenMed runs entirely on the device; clinical text never leaves it. Here it is on iPhone, fully offline:

OpenMed Scan on iPhone · on-device PII de-identification and clinical extraction via OpenMedKit
On iPhone via OpenMedKit: scan a clinical note, de-identify it, and extract clinical signals, all locally with Apple MLX. Nothing is uploaded.

OpenMed redacting PII from a clinical discharge document in real time
Real-time PII de-identification: the Nemotron Privacy Filter redacting names, addresses, IDs, and billing data from a clinical discharge packet, entirely on-device. (All values shown are synthetic.)

30-second example

from openmed import analyze_text

result = analyze_text(
    "Patient started on imatinib for chronic myeloid leukemia.",
    model_name="disease_detection_superclinical",
)

for entity in result.entities:
    print(f"{entity.label:<12} {entity.text:<28} {entity.confidence:.2f}")
# DISEASE      chronic myeloid leukemia     0.98
# DRUG         imatinib                     0.95

A state-of-the-art clinical NER model running locally: no API key, no network call.


Why OpenMed?

OpenMed Cloud medical APIs
Runs on your device / servers
Patient data leaves your network Never Sent to the vendor
Cost Free & open-source Per-call pricing
Specialized medical models 2,000+ Limited
Model-backed PII languages 21 Varies
Offline / air-gapped
Apple Silicon (MLX) acceleration n/a
Native iOS / macOS apps ✅ via OpenMedKit
Browser/WebGPU token classification ✅ via Transformers.js Varies
Vendor lock-in None (Apache-2.0) Yes
  • Specialized models: 2,000+ curated biomedical & clinical models, many outperforming proprietary stacks.
  • HIPAA-aware de-identification: all 18 Safe Harbor identifiers, smart entity merging, format-preserving fakes.
  • Runs everywhere: CPU, CUDA, Apple Silicon (MLX), iOS/macOS via OpenMedKit, Android/Kotlin, React Native, REST/gRPC services, and browser/WebGPU bundles via Transformers.js.
  • One-line deployment: Python API, Dockerized REST service, or batch pipelines.
  • Zero lock-in: Apache-2.0, your infrastructure, your data.

On-device on Apple: Swift, MLX & iOS

OpenMed is built to run where your data already lives. On Apple hardware it accelerates with MLX, and it ships straight into iPhone, iPad, and Mac apps through OpenMedKit: so PII detection and clinical extraction happen fully offline, on the device.

// Add OpenMedKit to your app
dependencies: [
    .package(url: "https://github.com/maziyarpanahi/openmed.git", from: "1.9.1"),
]

Expected result: Swift Package Manager resolves OpenMedKit and makes import OpenMedKit available to your app target.

  • MLX runtime for PII token classification, the Privacy Filter family, experimental GLiNER-family zero-shot tasks, and Python MLX-LM text generation with Laneformer; includes a CoreML fallback path for supported token-classification artifacts.
  • One model name, every platform: MLX model names automatically fall back to the matching PyTorch checkpoint on non-Apple hardware.
  • Python on Apple Silicon too: pip install --upgrade "openmed[mlx]".

Guides: MLX backend · OpenMedKit (Swift) · CoreML export

MLX vs CPU latency on Apple Silicon: 24 to 33 times faster
MLX on Apple Silicon: 24–33× faster than CPU PyTorch for the Privacy Filter: median latency per inference step, lower is better.

On-device on Android — Kotlin & ONNX Runtime Mobile

OpenMedKit also ships as a native Android/Kotlin library for local document intake, OCR handoff, PII redaction, and token-classification inference through ONNX Runtime Mobile. Mobile model repositories include stable tensor names, dynamic sequence axes, tokenizer files, labels, and Android-ready fp32, fp16, INT8, and optional .ort outputs.

Add the scoped JitPack repository in settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("https://jitpack.io")
            content { includeGroup("com.github.maziyarpanahi") }
        }
    }
}

Then use the immutable OpenMed v1.9.1 release:

dependencies {
    implementation("com.github.maziyarpanahi:openmed:v1.9.1")
}

See the Android installation guide for local builds and publishing details.

val model = OpenMedKit.fromDirectory(modelDir)
val entities = model.analyzeText("Patient Alice Nguyen was seen in cardiology.")
  • Android ONNX profile emits model.onnx, model_fp16.onnx, model_int8.onnx, tokenizer assets, labels, and openmed-onnx.json.
  • ORT Mobile support records the minimal-build operator configuration when ONNX Runtime conversion tooling is installed.
  • Kotlin parity tests keep tokenizer offsets, span boundaries, and decoder output aligned with the Python runtime.

Guides: Android ONNX export · Android span parity · OpenMedKit Android

The same ONNX model on Python CPU

from openmed import OnnxModel

model = OnnxModel.from_pretrained(
    "OpenMed/OpenMed-PII-ClinicalE5-Small-33M-v1-onnx-android"
)
entities = model("Patient Alice Nguyen was seen in cardiology.")

The same ONNX model in the browser

npm install openmed @huggingface/transformers
import { loadOnnxModel } from "openmed";

const model = await loadOnnxModel(
  "OpenMed/OpenMed-PII-ClinicalE5-Small-33M-v1-onnx-android",
);
const entities = await model("Patient Alice Nguyen was seen in cardiology.");

How it works

flowchart LR
    A["Clinical text"] --> B["OpenMed<br/>(100% on-device)"]
    B --> C["Medical entities"]
    B --> D["PII detected"]
    B --> E["De-identified text"]
    style B fill:#0D6E6E,stroke:#0A5656,stroke-width:2px,color:#ffffff
    style C fill:#D6EBEB,stroke:#0D6E6E,color:#0E1116
    style D fill:#F7DCD8,stroke:#C5453A,color:#0E1116
    style E fill:#F5E27A,stroke:#A9A088,color:#0E1116

Rendered result: a local clinical-text pipeline that returns medical entities, PII findings, and de-identified text without sending data to a cloud API.


Quick start

# Core + Hugging Face runtime (Linux, macOS, Windows; CPU or CUDA)
pip install --upgrade "openmed[hf]"

# Add the REST service
pip install --upgrade "openmed[hf,service]"

# Apple Silicon acceleration (MLX)
pip install --upgrade "openmed[mlx]"

Expected result:

Successfully installed openmed-...

Python API

from openmed import analyze_text

result = analyze_text(
  "Patient received 75mg "
  "clopidogrel for NSTEMI.",
  model_name=
  "pharma_detection_superclinical",
)
print([(e.label, e.text) for e in result.entities])

Example output:

[('DRUG', 'clopidogrel'), ('CONDITION', 'NSTEMI')]

REST service

uvicorn openmed.service.app:app \
  --host 0.0.0.0 --port 8080

Example output:

INFO:     Uvicorn running on http://0.0.0.0:8080
GET /health -> 200 OK

GET /health POST /analyze POST /pii/extract POST /pii/deidentify

Batch

from openmed import BatchProcessor

p = BatchProcessor(
  model_name=
  "disease_detection_superclinical",
  group_entities=True,
)
results = p.process_texts([...])
print(len(results), sum(len(r.entities) for r in results))
print([(e.label, e.text) for e in results[0].entities[:1]])

Example output:

3 7
[('DISEASE', 'leukemia')]

Browser / WebGPU

Package ONNX token-classification exports for in-browser inference through Transformers.js:

python -m openmed.onnx.convert \
  --model dslim/bert-base-NER \
  --output dist/example-onnx \
  --include-transformersjs

Example output:

Exported Transformers.js bundle to dist/example-onnx
import { pipeline } from "@huggingface/transformers";

const detector = await pipeline(
  "token-classification",
  "/models/openmed-pii/transformersjs",
  { device: "webgpu" },
);
const entities = await detector("Patient Casey Example called 212-555-0198.");
console.log(entities.slice(0, 2));

Example output:

[
  { entity: "NAME", word: "Casey Example", score: 0.99 },
  { entity: "PHONE", word: "212-555-0198", score: 0.98 },
]

Transformers.js export guide

Offline / air-gapped? Point model_name (or model_id) at a local directory and OpenMed loads it without contacting the Hugging Face Hub:

from openmed import OpenMedConfig, analyze_text

result = analyze_text(
    "Patient presents with chronic myeloid leukemia and Type 2 diabetes.",
    model_id="./models/OpenMed-NER-DiseaseDetect-SuperClinical-434M",
    config=OpenMedConfig(device="cpu"),
)
for entity in result.entities:
    print(f"{entity.label:<12} {entity.text:<28} {entity.confidence:.2f}")

Example output:

DISEASE      chronic myeloid leukemia     0.98
DISEASE      Type 2 diabetes              0.96

Because model_id points to a local directory, this example does not contact the Hugging Face Hub or any external model provider.


Models

A curated registry of specialized medical NER models; browse the full catalog.

Model Specialization Entity types Size
disease_detection_superclinical Disease & conditions DISEASE, CONDITION, DIAGNOSIS 434M
pharma_detection_superclinical Drugs & medications DRUG, MEDICATION, TREATMENT 434M
pii_superclinical_large PII & de-identification NAME, DATE, SSN, PHONE, EMAIL, ADDRESS 434M
anatomy_detection_electramed Anatomy & body parts ANATOMY, ORGAN, BODY_PART 109M
gene_detection_genecorpus Genes & proteins GENE, PROTEIN 109M

Privacy: PII detection & de-identification

from openmed import extract_pii, deidentify

text = "Patient: John Doe, DOB: 01/15/1970, SSN: 123-45-6789"

# Extract PII with smart merging (prevents tokenization fragmentation)
result = extract_pii(text, model_name="pii_superclinical_large", use_smart_merging=True)
print([(e.label, e.text) for e in result.entities])

# De-identify with the method you need
print(deidentify(text, method="mask").deidentified_text)
print(deidentify(text, method="replace").deidentified_text)
print(deidentify(text, method="hash").deidentified_text)
print(deidentify(text, method="shift_dates", date_shift_days=180).deidentified_text)

Example output:

[('NAME', 'John Doe'), ('DATE', '01/15/1970'), ('SSN', '123-45-6789')]
Patient: [NAME], DOB: [DATE], SSN: [SSN]
Patient: Emily Chen, DOB: 03/22/1985, SSN: 456-78-9012
Patient: 6b8f...c4a1, DOB: 48b1...91de, SSN: 3f13...e912
Patient: John Doe, DOB: 07/14/1970, SSN: 123-45-6789
  • Smart entity merging keeps 01/15/1970 whole instead of fragmenting it.
  • Policy-aware pipelines add HIPAA/GDPR/research profiles, calibrated thresholds, signed audit reports, redaction previews, and minimum-necessary action selection.
  • Faker-backed obfuscation with custom clinical-ID providers (CPF, CNPJ, BSN, NIR, Codice Fiscale, NIE, Aadhaar, Steuer-ID, NPI).
  • HIPAA: all 18 Safe Harbor identifiers, configurable confidence thresholds.
  • Batch and streaming PII: extract or de-identify across many documents with BatchProcessor(operation="extract_pii" | "deidentify", batch_size=16) or incremental streaming helpers.
Batch PII processing throughput: up to 3.3x on CPU and 2.2x on MLX
Batch processing: up to 3.3× higher throughput on CPU and 2.2× on MLX vs. one document at a time.

Complete PII notebook · Smart merging · Anonymization quickstart

Privacy Filter family: three model families on the OpenAI Privacy Filter architecture

Same model code (gpt-oss-style sparse-MoE transformer with local attention, sink tokens, RoPE+YaRN, tiktoken o200k_base), different training data. All route through the same extract_pii() / deidentify() API; only model_name= changes. openai/privacy-filter is a Hugging Face model identifier for local weights; using it here does not call the OpenAI API.

Variant PyTorch (CPU + CUDA) MLX (Apple Silicon) MLX 8-bit
OpenAI Privacy Filter openai/privacy-filter OpenMed/privacy-filter-mlx …-mlx-8bit
Nemotron-PII fine-tune OpenMed/privacy-filter-nemotron …-nemotron-mlx …-nemotron-mlx-8bit
OpenMed Multilingual OpenMed/privacy-filter-multilingual …-multilingual-mlx …-multilingual-mlx-8bit
from openmed import extract_pii

text = "Patient Sarah Connor (DOB: 03/15/1985) at MRN 4471882."

variants = {
    "baseline": extract_pii(text, model_name="openai/privacy-filter"),
    "nemotron": extract_pii(text, model_name="OpenMed/privacy-filter-nemotron"),
    "mlx": extract_pii(text, model_name="OpenMed/privacy-filter-mlx"),
}
print([(e.label, e.text) for e in variants["baseline"].entities])

Example output:

[('NAME', 'Sarah Connor'), ('DATE', '03/15/1985'), ('ID', '4471882')]

On non-Apple-Silicon hosts, MLX model names are automatically substituted with the matching PyTorch checkpoint (with a one-time warning): ship one model name, run anywhere. See Privacy Filter architecture & backend routing.


Multilingual PII (22 supported languages)

Extraction and de-identification support 22 supported PII language codes: am, ar, de, en, es, fr, he, hi, id, it, ja, ko, nl, pt, ro, sw, te, th, tr, xh, zh, and zu, with 600+ PII checkpoints in total. Chinese routing currently uses the documented multilingual default-model placeholder while dedicated Chinese model weights remain separate. OpenMed also includes validator-backed national-ID coverage for additional ID-only locales such as Polish, Latvian, Slovak, Malay, Filipino, and Danish.

See the per-language guide for each code's default PII model, Faker locale, and a before/after de-identification example.

python -c "from openmed import extract_pii; print([(e.label, e.text) for e in extract_pii('Dr. Pedro Almeida, CPF: 123.456.789-09, email: pedro@hospital.pt', lang='pt').entities])"

Example output:

[('NAME', 'Pedro Almeida'), ('ID', '123.456.789-09'), ('EMAIL', 'pedro@hospital.pt')]
Show per-language examples (Portuguese, Dutch, Hindi, Arabic, Japanese, Turkish)
from openmed import extract_pii

portuguese = extract_pii("Paciente: Pedro Almeida, CPF: 123.456.789-09, telefone: +351 912 345 678", lang="pt", use_smart_merging=True)
dutch      = extract_pii("Patiënt: Eva de Vries, BSN: 123456782, telefoon: +31 6 12345678", lang="nl", use_smart_merging=True)
hindi      = extract_pii("रोगी: अनीता शर्मा, फोन: +91 9876543210, पता: नई दिल्ली 110001", lang="hi", use_smart_merging=True)
arabic     = extract_pii("المريضة ليلى حسن، الهاتف +20 10 1234 5678، الرقم القومي 29801011234567.", lang="ar", use_smart_merging=True)
japanese   = extract_pii("患者 佐藤 花子、電話 +81 90 1234 5678、マイナンバー 1234 5678 9012.", lang="ja", use_smart_merging=True)
turkish    = extract_pii("Hasta Ayşe Yılmaz, telefon +90 532 123 45 67, TCKN 10000000146.", lang="tr", use_smart_merging=True)

for r in (portuguese, dutch, hindi, arabic, japanese, turkish):
    print([(e.label, e.text) for e in r.entities])

Example output:

[('NAME', 'Pedro Almeida'), ('ID', '123.456.789-09'), ('PHONE', '+351 912 345 678')]
[('NAME', 'Eva de Vries'), ('ID', '123456782'), ('PHONE', '+31 6 12345678')]
[('NAME', 'अनीता शर्मा'), ('PHONE', '+91 9876543210'), ('ADDRESS', 'नई दिल्ली 110001')]
[('NAME', 'ليلى حسن'), ('PHONE', '+20 10 1234 5678'), ('ID', '29801011234567')]
[('NAME', '佐藤 花子'), ('PHONE', '+81 90 1234 5678'), ('ID', '1234 5678 9012')]
[('NAME', 'Ayşe Yılmaz'), ('PHONE', '+90 532 123 45 67'), ('ID', '10000000146')]

REST API

A Docker-friendly FastAPI service with request validation, shared pipeline preload, and unified error envelopes.

pip install --upgrade "openmed[hf,service]"
uvicorn openmed.service.app:app --host 0.0.0.0 --port 8080

# or with Docker
docker build -t openmed:local .
docker run --rm -p 8080:8080 -e OPENMED_PROFILE=prod openmed:local

Example output:

INFO:     Uvicorn running on http://0.0.0.0:8080
curl -X POST http://127.0.0.1:8080/pii/extract \
  -H "Content-Type: application/json" \
  -d '{"text":"Paciente: Maria Garcia, DNI: 12345678Z","lang":"es"}'

Abbreviated example response:

{
  "text": "Paciente: Maria Garcia, DNI: 12345678Z",
  "entities": [
    {"text": "Maria Garcia", "label": "NAME", "confidence": 0.99, "start": 10, "end": 22},
    {"text": "12345678Z", "label": "ID", "confidence": 0.98, "start": 29, "end": 38}
  ],
  "model_name": "OpenMed/privacy-filter-multilingual"
}

Model lifecycle and service controls: free memory on demand with GET /models/loaded, POST /models/unload, and a keep_alive idle window; v1.8 also includes API-key/JWT auth, no-PHI request logging, tracing, gRPC, async jobs, webhooks, warm pools, dynamic batching, request coalescing, rate and concurrency limits, /livez, /readyz, and opt-in metrics:

OPENMED_SERVICE_KEEP_ALIVE=10m uvicorn openmed.service.app:app --host 0.0.0.0 --port 8080
curl -X POST http://127.0.0.1:8080/models/unload -H "Content-Type: application/json" -d '{"all":true}'

Example response:

{
  "unloaded": true,
  "released": {"models": 1, "tokenizers": 1, "pipelines": 1},
  "active_models": {}
}

See the full REST service guide.


Documentation

Full guides at openmed.life/docs.

AI agents can load the curated llms.txt index or the inlined llms-full.txt feed. Both are regenerated from the current documentation during every strict MkDocs build.

Getting Started Analyze Text Model Registry
FAQ Anonymization Batch Processing
Configuration Profiles REST Service MLX Backend
Transformers.js Export FHIR Interop HL7 v2 De-identification
OpenMed 1.9.1 Release Notes OpenMed 1.9.0 Release Notes Examples
Release Streams Generative Model Policy Contributing
Security Policy Compliance Posture Detector Plugin SDK
v1 to v2 Migration MCP Client Connections

Meet the mascot

OpenMed mascot

OpenMed's guardian is a fluffy Persian cat styled as a tiny Avicenna (Ibn Sina): the great Persian physician whose Canon of Medicine was the world's standard medical text for some 600 years. He keeps watch over the open book of medical knowledge, in a palette built around Persian turquoise (fīrūza): a local-first guardian for your most private data.



Contributing

Contributions welcome: bug reports, feature requests, and PRs alike. Please read the Contributing guide and our Code of Conduct first.


Security

Found a vulnerability? OpenMed redacts PHI, so a redaction bypass or PHI/PII leak is a security issue: please report it privately, never as a public issue. See SECURITY.md for the responsible-disclosure policy and the private reporting form. Never include real patient data in a report.


Credits

OpenMed builds on excellent open-source work: particular thanks to OpenAI (the Privacy Filter architecture), NVIDIA (the Nemotron PII dataset), Hugging Face (transformers, Transformers.js & the model ecosystem), Apple (MLX), and the Faker maintainers.

License

Released under the Apache-2.0 License. Third-party asset notices are recorded in NOTICE.

Citation

@misc{panahi2025openmedneropensourcedomainadapted,
      title={OpenMed NER: Open-Source, Domain-Adapted State-of-the-Art Transformers for Biomedical NER Across 12 Public Datasets},
      author={Maziyar Panahi},
      year={2025},
      eprint={2508.01630},
      archivePrefix={arXiv},
      primaryClass={cs.CL},
      url={https://arxiv.org/abs/2508.01630},
}

Expected result: BibTeX-compatible citation metadata for referencing OpenMed in papers, posters, and derived documentation.


Star History

If OpenMed is useful to you, a star helps others discover it.

Star History Chart


Built by the OpenMed team

Website · Docs · X / Twitter · LinkedIn

Related repositories
frappe/erpnext

Free and Open Source Enterprise Resource Planning (ERP)

PythonPyPIGNU General Public License v3.0manufacturingerpnext
frappe.io/erpnext
37.1k12.1k
pliang279/awesome-multimodal-ml

Reading list for research topics in multimodal machine learning

MIT Licensemultimodal-learningmachine-learning
6.9k900
openemr/openemr

The most popular open source electronic health records and medical practice management solution.

PHPPackagistGNU General Public License v3.0openemrpractice-management
open-emr.org
5.3k3k
OpenHealthForAll/open-health

OpenHealth, AI Health Assistant | Powered by Your Data

TypeScriptnpmGNU Affero General Public License v3.0aihealthcare
open-health.me
3.9k423
fastenhealth/fasten-onprem

Fasten is an open-source, self-hosted, personal/family electronic medical record manager

GoGo ModulesGNU General Public License v3.0electronic-health-recordelectronic-medical-record
2.8k171
medplum/medplum

Medplum is a healthcare platform that helps you quickly develop high-quality compliant applications.

TypeScriptnpmApache License 2.0healthcarefhir
medplum.com
2.5k859
hapifhir/hapi-fhir

🔥 HAPI FHIR - Java API for HL7 FHIR Clients and Servers

JavaMavenApache License 2.0javahl7
hapifhir.io
2.4k1.5k
the-momentum/open-wearables

Self-hosted platform to unify wearable health data through one AI-ready API.

PythonPyPIMIT Licensewearablesapple-health
docs.openwearables.io
2.2k409
openmrs/openmrs-core

OpenMRS API and web application code

JavaMavenOtheremrehr
openmrs.org
1.9k4.3k
sunlabuiuc/PyHealth

A Deep Learning Python Toolkit for Healthcare Applications.

PythonPyPIMIT Licensehealthcaredata-mining
pyhealth.dev
1.6k782
bowang-lab/MedRAX

MedRAX: Medical Reasoning Agent for Chest X-ray - ICML 2025

PythonPyPIApache License 2.0agentagentic-ai
bowang-lab.github.io/MedRAX/
1.2k202
nextgenhealthcare/connect

The swiss army knife of healthcare integration.

JavaMavenOthermirthmirth-connect
1.2k457