OpenMed for Web 实战指南:在浏览器与 Node.js 中运行本地医疗 NER 与 PII 去标识化
【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed
OpenMed for Web(npm 包openmed)将 OpenMed 的医疗命名实体识别(NER)与 PII 去标识化能力带进浏览器和 Node.js,推理全部在本地完成,临床文本不会发送到任何托管 API。本指南以 js/openmedkit-web/README.md 为核心,结合 js/openmedkit-web/src 源码与 tests/web 测试,完整讲解安装、deidentify()/extractPii()用法、ONNX 模型加载与变体选择、token 偏移对齐原理、本地浏览器运行时(WebGPU/WASM)以及隐私安全边界,读完后你可以在自己的 Web 应用中直接落地一套"数据不出本地"的临床文本脱敏能力。
一、OpenMed for Web 是什么
OpenMed for Web 是 OpenMed 多语言运行时家族(Python、Swift、Android、Web)中的 Web/Node 实现,包名为openmed(当前仓库版本 2.3.0,见 js/openmedkit-web/package.json)。它与 Python、Swift、Android 端使用同一套 OpenMed 模型,因此一次训练的模型可以在全平台获得一致的识别结果。
核心特性:
- 本地推理:文本在浏览器或 Node.js 进程内处理,不依赖云 API;从 Hugging Face 加载模型仅发生一次下载,之后推理不再产生任何网络请求。
- 两条运行路径:
- 通过 Transformers.js 加载 OpenMed ONNX 模型(高层 API,一行
deidentify()即可用); - 通过 ONNX Runtime Web 直接创建 session(低层 API,可完全控制模型与运行时资源路径、执行后端)。
- 通过 Transformers.js 加载 OpenMed ONNX 模型(高层 API,一行
- 双端兼容:同一套 API 既可用于浏览器(本地路径 + 本地资源)也可用于 Node.js 服务端脱敏。
二、安装
根据你选择的加载路径安装依赖。两种方式的openmed包本体一致,只是推理后端不同:
# 通过 Transformers.js 加载 OpenMed 模型(推荐,最省事) npm install openmed @huggingface/transformers # 直接使用 ONNX Runtime Web session npm install openmed onnxruntime-web两个运行时均为可选 peer 依赖(见 js/openmedkit-web/package.json):@huggingface/transformers >= 3.0.0与onnxruntime-web >= 1.20.0,peerDependenciesMeta中两者都可选,意味着你只需安装实际使用的那个。包要求node >= 20(package.json),并在engines中声明。源码层面,resolveRuntime()会先尝试动态import("@huggingface/transformers"),失败时抛出 "Install @huggingface/transformers or pass a token-classification pipeline." 的明确错误(见 src/model-loader.ts),ONNX Runtime Web 路径同理。
三、快速开始:一行代码完成临床文本脱敏
3.1 最基本的 deidentify
import { deidentify } from "openmed"; const result = await deidentify( "Patient Alice Nguyen was seen in cardiology.", ); console.log(result.deidentifiedText); console.log(result.spans);返回的OpenMedDeidentifyResult包含三个字段(见 src/index.ts):
| 字段 | 说明 |
|---|---|
text | 原始输入文本 |
deidentifiedText | 已替换敏感实体的脱敏文本(默认将每个 span 替换为[规范标签]形式) |
spans | 每个敏感实体的完整记录(OpenMedSpan[]) |
deidentify()的底层实现是extractPii()拿到 span 后,再经spansToRedactedText()按start从大到小排序、用replacement(默认为[${span.canonical_label}])切片替换原文而成(见 src/index.ts)。
3.2 extractPii:只取实体不脱敏
如果只想获得实体列表而不修改原文,使用extractPii():
import { extractPii } from "openmed"; const spans = await extractPii( "Patient Alice Nguyen was seen in cardiology.", { docId: "note-001" }, );每条OpenMedSpan(见 src/types.ts)携带:
- 位置与内容:
start/end(JavaScript UTF-16 索引)、text_hash(HMAC-SHA256 哈希而非原文); - 标签体系:
entity_type(模型原始标签)、canonical_label(50 个规范标签之一,如PERSON、PHONE、SSN)、policy_label(DIRECT_IDENTIFIER/QUASI_IDENTIFIER/CLINICAL_CONCEPT之一); - 元数据:
score、doc_id、detector、section、regulatory_tags、replacement、reversible_id等,且所有 span 都带schema_version: 1(OPENMED_SPAN_SCHEMA_VERSION)。
规范标签的完整清单见 src/types.ts(含ACCOUNT_NUMBER、CREDIT_CARD、EMAIL、IBAN、IP_ADDRESS、GPS_COORDINATES、STREET_ADDRESS、ZIPCODE等);每种规范标签到策略标签的映射表定义在 src/decoder.ts,例如SSN → DIRECT_IDENTIFIER、AGE → QUASI_IDENTIFIER、DATE → QUASI_IDENTIFIER。normalizeLabel()还内置了一张跨语言/跨模型别名表(ALIAS_MAP,src/decoder.ts),例如aadhaar → ID_NUM、cpf → ID_NUM、medicalrecordnumber → ID_NUM、nhsnumber → ID_NUM、teudatzehut → ID_NUM,让不同来源的模型标签归一到同一套规范体系。
3.3 常用选项
ExtractPiiOptions(src/types.ts)支持以下常用参数:
| 参数 | 类型 | 作用 |
|---|---|---|
model | string | 指定 Hugging Face 模型仓库,默认DEFAULT_MODEL_ID |
pipeline | RawTokenClassificationPipeline | 传入自定义 token 分类 pipeline(优先级高于model) |
modelLoader/loaderOptions | ModelLoader/LoadModelOptions | 自定义模型加载器及其选项 |
threshold | number | 实体平均置信度阈值,低于阈值的 span 被丢弃(默认0) |
docId | string | 文档标识,写入每条 span(默认"document") |
hashSecret | string \| Uint8Array | 计算text_hash的 HMAC 密钥(默认"openmedkit-web") |
detector | string \| null | 记录检测后端标识(默认"transformersjs") |
section | string \| null | 章节信息 |
regulatoryTags | string[] | 监管标签 |
pipelineOptions | TokenClassificationCallOptions | 透传给底层 pipeline 的调用选项 |
DeidentifyOptions在ExtractPiiOptions之上增加replacement?: (span) => string,可自定义每个实体的替换策略(src/types.ts)。
四、默认模型与 ONNX 模型加载
4.1 DEFAULT_MODEL_ID:开箱即用的临床 PII 模型
不传model或pipeline时,deidentify()与extractPii()默认加载(见 src/index.ts):
export const DEFAULT_MODEL_ID = "OpenMed/OpenMed-PII-ClinicalE5-Small-33M-v1-onnx-android";即33M 参数的临床 PII 模型根目录 INT8 工件(约 70 MB),通过 Transformers.js 提供token-classification能力。extractPii()内部会判断模型名是否匹配/-onnx-android$/i(ONNX_ANDROID_REPO_PATTERN),匹配则走loadOnnxModel()(按 ONNX 工件文件名加载),否则走通用loadTokenClassificationPipeline()(见 src/index.ts)。
4.2 指定其他模型或自建 pipeline
可以传入任意公开的OpenMed/<model>-onnx-android仓库,或自行加载后通过pipeline注入:
import { deidentify, loadOnnxModel } from "openmed"; const model = await loadOnnxModel("OpenMed/<model>-onnx-android"); const result = await deidentify( "Patient Alice Nguyen was seen in cardiology.", { pipeline: model }, );4.3 变体选择:int8 / fp32 / fp16
loadOnnxModel()默认选择根目录INT8模型。需要其他已发布的精度变体时传入variant:
const fp32 = await loadOnnxModel("OpenMed/<model>-onnx-android", { variant: "fp32", }); const fp16 = await loadOnnxModel("OpenMed/<model>-onnx-android", { variant: "fp16", });OpenMedOnnxVariant = "int8" | "fp32" | "fp16"(src/types.ts),三种变体分别映射到 ONNX 仓库内文件名model_int8/model/model_fp16(ONNX_MODEL_FILENAMES,src/model-loader.ts)。加载时强制quantized: false以避免 Transformers.js 二次量化,并设置subfolder: ""指向仓库根目录(src/model-loader.ts)。
4.4 本地文件与离线模型
isLocalModelReference()(src/model-loader.ts)识别file://前缀、/、./、../、~开头以及 Windows 盘符路径(/^[A-Za-z]:[\\/]/)作为本地模型引用。识别为本地引用时自动启用local_files_only,并在加载期间临时把 Transformers.js 的allowRemoteModels设为与本地引用一致的布尔值,加载完成后在finally中恢复原状态(src/model-loader.ts),确保远程加载开关不影响其他调用方。
五、Token 偏移对齐:从 token 到原文字符的精确定位
5.1 为什么需要对齐
Transformers.js 的 token-classification 输出不携带字符偏移(只有entity、score、index、word)。OpenMed 在解码 span 前,会先把每个 token 对齐回源文本,保证result.spans总是携带指向原始字符串的start/end。
对齐实现位于 src/offsets.ts 的alignTokenOffsets():
- 大小写与重音不敏感:先做小写化 + NFD 分解 + 去除组合变音标记(
normalizeString,src/offsets.ts),因此 BERT 风格的小写化 tokenizer 仍能对齐; - 处理多种子词标记:WordPiece
##、SentencePiece▁、byte-level BPEĠ前缀(stripTokenMarkers,src/offsets.ts); - 跳过特殊 token:
[CLS]、[SEP]、[PAD]、<s>、</s>、<pad>等(SPECIAL_TOKEN_WORDS,src/offsets.ts); - 顺序游标 + 连续搜索窗口:已带有效偏移的 token 直接保留并推进游标;无偏移 token 在剩余文本中按顺序查找,contiguous 模式在 16 字符窗口内搜索(
CONTIGUOUS_SEARCH_WINDOW,src/offsets.ts),并检查词边界; - 对齐失败即报错:未知或无法对齐的 token 抛出无内容的错误("Token offset alignment failed; provide source offsets."),而不是静默返回不完整的脱敏结果(src/offsets.ts)。
5.2 偏移的坐标约定
- 偏移使用JavaScript UTF-16 索引(
for (const character of text)按码点迭代、按 UTF-16 code unit 返回偏移,见 src/offsets.ts); - 分解的重音归属其源字符的区间(NFD 分解出的组合变音标记被并入前一个字符的
toOriginalEnd); - 自定义 tokenizer 或过滤后的输出应提供精确的源偏移;遇到对齐错误时,应把该次扫描视为失败而非"无 PII"文档。
5.3 面向无偏移运行时的类型契约
既有TokenClassificationEntity与TokenClassificationPipeline输出保留必需的数值偏移;对于不带偏移的运行时,使用加性类型RawTokenClassificationEntity、RawTokenClassificationPipeline与RawTransformersRuntime(src/types.ts)。模型加载器与alignTokenOffsets()返回已对齐的实体,从而保持 v2.2 以来的 typed-consumer 契约——该契约有测试锁定:tests/web/test_npm_deidentify.spec.ts中通过alignTokenOffsets(...).map(numericOffsets)断言输出均为数值偏移,并以RawTokenClassificationPipeline/RawTransformersRuntime注入无偏移运行时验证extractPii()仍可正常工作。
5.4 从 logits 到 span 的完整解码链路
extractPii()的解码管线(src/index.ts):
- 以
aggregation_strategy: "none"、ignore_labels: []调用 pipeline,保留Otoken使偏移对齐能看到完整 token 序列; decodeBioTokenSpans()(src/decoder.ts)先对齐偏移、解析 BIO/BIES 边界标签(B-/I-/E-/S-/O),再聚合相邻同标签 token 为实体,最后mergeAdjacentSpans()合并仅以空白分隔的同类型相邻 span;refinePrivacyFilterSpan()(src/decoder.ts)对 email/URL/phone 等结构化实体用正则精修边界,并去掉 " and" / " or" 尾缀,最终按threshold过滤低置信度 span。
六、本地浏览器运行时:WebGPU → WASM 三级后端
6.1 后端选择优先级
loadOrtWebSession()与loadOrtWebTokenClassificationPipeline()是低层 ONNX Runtime Web 加载器(src/runtime/ort-web-loader.ts),模型与运行时资源全部使用本地路径,并按以下优先级选择最强的执行路径:
- WebGPU
- WebAssembly + SIMD + threads
- 单线程 WebAssembly
能力探测由 src/runtime/capability.ts 的detectOrtWebCapabilities()完成:检查navigator.gpu(WebGPU)、WebAssembly、SIMD(通过内置的 WASM SIMD 探针字节码WASM_SIMD_PROBE验证)、SharedArrayBuffer、crossOriginIsolated与hardwareConcurrency,产出一个OrtWebCapabilityProfile;probeOrtWebCapabilities()还会调用navigator.gpu.requestAdapter()验证适配器真实可用。selectOrtWebBackend()依据该画像决策,多线程时线程数上限为MAX_WASM_THREADS = 4(src/runtime/capability.ts)。
6.2 加载本地 token 分类 pipeline
import { deidentify, loadOrtWebTokenClassificationPipeline, } from "openmed"; const pipeline = await loadOrtWebTokenClassificationPipeline({ modelPath: "/models/openmed/model.onnx", assetPath: "/models/openmed/onnxruntime/", tokenize: tokenizeClinicalNote, // (text) => OrtFeeds decode: decodeTokenClassificationOutputs, // ({text, inputs, outputs, session, backend}) => entities }); const result = await deidentify(clinicalNote, { pipeline, detector: "ort-web", });tokenize与decode是必填回调(OrtWebTokenClassificationPipelineOptions,src/runtime/ort-web-loader.ts),decode收到完整上下文(OrtTokenClassificationDecodeContext:文本、输入 feeds、输出张量、session、后端与调用选项),把 logits 转成 token 分类输出后,由deidentify()完成后续对齐与脱敏。底层createOrtWebSession()会先configureOrtWebRuntime()设置wasmPaths、simd、numThreads、proxy: false,再以graphOptimizationLevel: "all"创建 session([src/runtime/ort-web-loader.ts](https://link.gitcode.com/i/420950490c0bb257e807f2e2314acfcf#L167-L171, L228-L239)。session 默认按(模型路径、资源路径、后端、session 选项)稳定序列化出的 key 缓存于DEFAULT_ORT_WEB_SESSION_CACHE,失败时自动从缓存剔除,可用clearOrtWebSessionCache()清空。
6.3 多线程 WASM 的前置条件:跨源隔离
多线程 WebAssembly 需要跨源隔离(cross-origin isolation)。服务端必须下发响应头:
Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp否则 OpenMed 自动回退到单线程 WebAssembly。这是因为SharedArrayBuffer仅在crossOriginIsolated === true时可用,而线程共享依赖它(见OrtWebCapabilityProfile.crossOriginIsolated,src/runtime/capability.ts)。
6.4 类型化的 WebGPU 会话与 fail-closed 校验
loadWebGpuTokenClassificationSession()提供类型化的run(tokens) -> logits契约(src/runtime/webgpu-session.ts),并支持 WebGPU / WASM双工件:
import { loadWebGpuTokenClassificationSession } from "openmed"; const session = await loadWebGpuTokenClassificationSession({ modelPath: { webgpu: "/models/openmed/model.webgpu.onnx", wasm: "/models/openmed/model.onnx", }, assetPath: "/models/openmed/onnxruntime/", }); const logits = await session.run({ inputIds, attentionMask, batchSize: 1, sequenceLength: inputIds.length, }); await session.dispose();该 session 还提供两套工程化保障(源码见 src/runtime/webgpu-session.ts):
- 本地基准记录:通过
benchmarkSink回调输出仅保存在本地的 warm/cold 每设备基准报告(WebGpuBenchmarkReport,基准套件名webgpu-token-classification-runtime),可用于对比不同设备的推理性能; - fail-closed 校验门:
- Python 参考一致性门:
certifyWebGpuReference()将 WebGPU 输出的 logits 与参考实现对比,默认 logit 容差DEFAULT_WEBGPU_LOGIT_TOLERANCE = 1e-3,并给出 span 级一致性结论(WebGpuReferenceCertification); - 关键标签召回门:
evaluateWebGpuRecallGate()计算候选 span 对参考 span 的召回率与recall_delta(默认最大差DEFAULT_WEBGPU_MAX_RECALL_DELTA = 0,即不允许召回率退化),并逐标签统计关键标签遗漏数(WebGpuRecallGate),任一指标不达标即passed: false,杜绝 WebGPU 实现悄悄产生错误脱敏。
- Python 参考一致性门:
七、隐私与安全设计
OpenMed for Web 从设计上把隐私与安全内建为硬约束(与仓库 docs/security 与 docs/operations/no-phi-telemetry.md 的定位一致):
- 默认无遥测:不启用任何 telemetry;
- 本地路径拒绝远程 URL:
assertOfflineAssetPath()拒绝//、\\前缀、普通 URL scheme(仅放行file://且仅限 localhost/空 hostname)等远程资源路径(src/runtime/ort-web-loader.ts); - span 只存哈希与偏移:
text_hash使用 Web CryptoHMAC-SHA256(Node 环境回退node:crypto)对实体原文计算,span 记录不含原始标识符文本(src/index.ts),配合可配置的hashSecret支持证据审计; - 风险边界:OpenMed不是医疗器械,不得自主做出临床决策;脱敏结果是辅助处理输出,需结合 docs/compliance 中的 HIPAA 安全港、21 CFR Part 11 审计追踪等规范评估使用方式。
八、与仓库其他部分的协作
- 模型一致性:Web 端默认模型与 Android/Swift 共用同一模型族,Swift 端
OpenMedKitRN.swift中也引用同名的OpenMed-PII-ClinicalE5-Small-33M-v1tokenizer(见 js/openmedkit-react-native),跨端可以共享模型工件与标签体系; - 测试覆盖:Node 侧测试位于 tests/web/test_npm_deidentify.spec.ts(对齐与类型契约)、tests/web/test_ort_web_loader.spec.ts(加载器与离线路径校验)、tests/web/test_webgpu_session.spec.ts(WebGPU 会话与能力探测),公共 API 快照见 tests/web/snapshots/openmedkit-web-public-api.json;构建命令为
npm run build(tsup 输出 ESM/CJS 双格式),测试为npm test(构建 + 类型检查 + tsx 运行 Node 测试); - 文档入口:更完整的跨端定位可参考 docs/android-integration.md、docs/export-onnx-webgpu.md 与 docs/onnxruntime-web.md 对应主题文档。
九、许可与适用边界
OpenMed for Web 采用Apache-2.0许可(js/openmedkit-web/LICENSE)。模型与 Python、Swift、Android 运行时的完整文档与发布物可在本仓库根目录 README.md 与 models.jsonl 中查阅。适用前提:浏览器端请确保模型/运行时资源为本地路径并正确配置跨源隔离响应头以启用多线程;Node.js 端需node >= 20并按需安装@huggingface/transformers或onnxruntime-web之一。
从一行deidentify()到可校验的 WebGPU 会话,OpenMed for Web 让"医疗级 PII 去标识化"真正跑在了终端用户的设备上——推理不出网、原文不进日志、span 只留哈希与偏移,是构建隐私优先医疗应用的可靠起点。
【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考