Clinical document parsing and NER.OpenMed + optional GLiNER, run locally.
Deterministic parsing turns coded measurements into FHIR R4. OpenMed identifies disease and anatomy spans; optional GLiNER finds entities using labels you supply. Named entity recognition (NER) results are returned for review, with their source text.
01 / Input
Lab PDF
With a text layer
C-CDA XML
Clinical document export
FHIR JSON
Existing structured data
02 / Extract
Flexpa Extract
03 / Output
FHIR R4 Bundle
Coded observations and units
Review report
Source locations and NER candidates
NER spans are returned for review. Models do not assign codes or clinical status. No outbound network calls.
Alpha access, available to a small number of partners under agreement. These interfaces can change without notice and there is no uptime commitment. This page is unlisted; please do not share the link outside your organization.
docker run --rm -p 8000:8000 \ flexpa/extract:alpha
curl -X POST http://localhost:8000/extract \ -F file=@lab-report.pdf
Everything the service infers comes from three open-weight checkpoints, all Apache-2.0, all carried in the image. Nothing is sent to a third-party model provider, and there is no large language model anywhere in the pipeline.
The OpenMed checkpoints come from the OpenMed suite (paper): BERT-family encoders domain-adapted on PubMed and clinical corpora, published as 2,266 repositories spanning 33M to 568M parameters.
This is the part worth being skeptical about, so it is stated plainly.
Condition
micro_f1
ELECTRA encoder, token classification. Disease lane over narrative text.
ELECTRA encoder, token classification. Anatomy lane over narrative text.
GLiNER over a DeBERTa-v3-small backbone. Zero-shot lane, off by default.
curl -s http://localhost:8000/health
Either fixed lane can be pointed at a larger checkpoint from the same family. A checkpoint swapped this way has to be present in the image, so change it at build time.
Span formation is configurable too: EXTRACT_AGGREGATION decides how subword tokens become spans, and the window and overlap settings decide how a long document is split before inference.
EXTRACT_AGGREGATION
Disease checkpoint. Must be present in the image.
Anatomy checkpoint. Must be present in the image.
Lanes to run. Empty runs the deterministic path alone.
Minimum span confidence.
How subword tokens become spans: average, first, max or simple.
average
first
max
simple
Token window a long document is split into.
Token overlap between windows.
docker run --rm -p 8000:8000 \ -e EXTRACT_MODEL_DISEASE=OpenMed/OpenMed-NER-DiseaseDetect-SuperClinical-434M \ flexpa/extract:alpha
The fixed lanes answer the schema they were trained on and nothing else. The zero-shot lane answers labels you give it per request, which is how a caller reaches a field no fixed checkpoint emits.
It is off by default, because it adds roughly 580 MB and a second backbone to the image.
The default labels are the structural fields a FHIR export wants and no fixed checkpoint emits: specimen, collection method, performing organization, authorizing provider, result status, imaging modality and anatomical region. EXTRACT_ZEROSHOT_LABELS changes the default, EXTRACT_ZEROSHOT_THRESHOLD sets the minimum score, and GET /health reports whether the lane is enabled and whether its checkpoint is present.
EXTRACT_ZEROSHOT_LABELS
EXTRACT_ZEROSHOT_THRESHOLD
GET /health
docker build \ --build-arg EXTRACT_EXTRAS=cpu,gliner \ --build-arg EXTRACT_ZEROSHOT=1 \ -t flexpa/extract:alpha-zeroshot .
curl -X POST http://localhost:8000/extract \ -F file=@lab-report.pdf \ -F labels="specimen,result status"
A fully built image. There is no installer, no model download on first run, and no callback to Flexpa.
python:3.12-slim-trixie
uvicorn
SIGTERM
HF_HUB_OFFLINE=1
The published image is CPU-only by design: Torch is installed from the CPU wheel index, so the image does not carry the CUDA runtime, which is most of Torch's size. CPU is the supported path and the one the performance figures below describe.
For GPU, build against a CUDA wheel index and select the device at runtime. Nothing in the pipeline imports a device-specific module directly, so one image definition serves both. GPU builds have not been benchmarked by us.
Built and verified on linux/arm64. An linux/amd64 build has not been produced: an emulated build was abandoned, and the deployment target should be built on a native amd64 runner. Nothing in the Dockerfile is architecture-specific, but that is an expectation, not a result.
linux/arm64
linux/amd64
cuda needs a CUDA-built image and a GPU on the host.
cuda
mlx is Apple-only and is not in the container.
mlx
Execution providers, highest priority first. Empty lets the backend choose from the device.
docker build \ --build-arg TORCH_INDEX=https://download.pytorch.org/whl/cu124 \ -t flexpa/extract:alpha-cuda .
Alpha access is arranged directly. Contact your Flexpa representative, or email support@flexpa.com, with the AWS account ID that will pull the image, the documents you intend to process and the environment you intend to run the container in.
Flexpa Extract is distributed from a private Amazon ECR repository. There is no public image and no shared registry password. Access is granted per AWS account: you tell us the account that will pull, and we name it in the repository policy. Nothing to rotate, and you can revoke by asking us to drop the statement.
What we need from you
What we add on our side, a pull-only statement in the repository policy.
What you attach on your side. A repository policy grants your account; the principal doing the pull still needs its own IAM permissions. ecr:GetAuthorizationToken is account-level and takes no resource.
ecr:GetAuthorizationToken
Running on ECS or EKS: the task execution role, not the task role, is the principal that pulls the image, so the IAM policy belongs there. If your environment cannot pull from ECR at all, tell us and we will arrange another channel.
{ "Sid": "AllowPartnerPull", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::<your-account-id>:root" }, "Action": [ "ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer", "ecr:BatchCheckLayerAvailability" ] }
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "ecr:GetAuthorizationToken", "Resource": "*" }, { "Effect": "Allow", "Action": [ "ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer", "ecr:BatchCheckLayerAvailability" ], "Resource": "arn:aws:ecr:<region>:<flexpa-account-id>:repository/flexpa-extract" } ] }
aws ecr get-login-password --region <region> \ | docker login --username AWS \ --password-stdin <flexpa-account-id>.dkr.ecr.<region>.amazonaws.com docker pull <flexpa-account-id>.dkr.ecr.<region>.amazonaws.com/flexpa-extract:alpha
Measured on linux/arm64, CPU, with both fixed lanes loaded:
An INT8 ONNX CPU fast path exists upstream in OpenMed, with AVX-512, AVX2 and NEON dispatch. We will incorporate support for it soon.
One endpoint does the work. Post a document as multipart form data and read the Bundle out of the response.
Observation
The format is detected from the content of the file, not from its name or the declared content type.
The document, as multipart form data. Up to 15 MB, 100 pages, and 120,000 characters once normalized.
Query parameter. Returns the Bundle alone, without the review report.
Comma-separated labels for the zero-shot lane, when that lane is enabled. Falls back to the configured defaults.
A FHIR R4 collection Bundle. Entry identifiers are urn:uuid values derived from the document digest, so repeated processing keeps those identifiers stable. Bundle contents also depend on the resolved configuration and package versions.
urn:uuid
What was read: filename, kind, bytes, page count, SHA-256, character count and table count.
Every value the deterministic path parsed, each with its code, unit, source unit, method, locator, page and bounding box, and whether it was eligible for export.
locator
Values that parsed but were not exported, each with the reason: no terminology mapping, or a unit that is missing or not convertible to UCUM.
One row per resource in the Bundle, naming the fact and the source location behind it.
Spans the fixed model lanes recognized in narrative text. Review candidates: they are never exported as Resources.
Spans the zero-shot lane returned for your labels. Empty when the lane is off.
Each table found in a PDF, and whether it was parsed or skipped, with the reason.
Detected narrative sections, and the negation cues resolved over them.
The limits that applied to this run, in plain language.
Per-stage timings: ingest, structure, model, context and fhir.
The package versions and the resolved configuration that produced this output.
A digest over the document, the configuration and the versions. Two runs that share a fingerprint should produce the same Bundle.
Resolved runtime, device, zero-shot state and package versions.
The process is up.
A backend resolved and every configured lane loads. Use this one as the readiness probe.
{ "bundle": { "resourceType": "Bundle", "type": "collection", "id": "extract-69da7cfde2749f53", "entry": [ { "fullUrl": "urn:uuid:34252bca-b614-515c-99a1-d04fe9d71a2f", "resource": { "resourceType": "Patient", "id": "source-patient" } }, { "fullUrl": "urn:uuid:0974ca87-8aba-5158-9c34-7bbf62394684", "resource": { "resourceType": "Observation", "id": "obs-1", "status": "unknown", "code": { "coding": [ { "code": "6690-2", "display": "Leukocytes [#/volume] in Blood by Automated count", "system": "http://loinc.org" } ], "text": "WBC" }, "subject": { "reference": "urn:uuid:34252bca-b614-515c-99a1-d04fe9d71a2f" }, "valueQuantity": { "value": 6.8, "unit": "10*3/uL", "code": "10*3/uL", "system": "http://unitsofmeasure.org" }, "referenceRange": [ { "low": { "value": 4.0, "unit": "10*3/uL" }, "high": { "value": 11.0, "unit": "10*3/uL" } } ], "interpretation": [ { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", "code": "N" } ] } ] } } ] }, "document": { "filename": "lab_report_synthetic.pdf", "kind": "PDF", "bytes": 3843, "pages": 2, "sha256": "69da7cfde2749f53972eb418da99abb5a2affba7ad08c72e3323692dc1ee0a4f", "characters": 1145, "tables": 2 }, "facts": [ { "label": "WBC", "code": { "code": "6690-2", "display": "Leukocytes [#/volume] in Blood by Automated count", "system": "http://loinc.org" }, "value": 6.8, "unit": "10*3/uL", "source_unit": "10^3/uL", "value_kind": "quantity", "method": "table + curated map", "locator": "PDF page 2 / table 0 / WBC", "effective": null, "reference_range": { "low": 4.0, "high": 11.0, "low_inclusive": true, "high_inclusive": true }, "flag": "normal", "exportable": true, "bbox": [ 129.6, 152.79999999999995, 482.4, 332.8 ], "page": 2, "id": "fact-1" } ], "unmapped": [], "provenance": [ { "resource": "Patient/source-patient", "method": "Local subject placeholder; identity not extracted" }, { "resource": "Observation/obs-1", "fact_index": 0, "locator": "PDF page 2 / table 0 / WBC", "method": "table + curated map", "source_time": null, "note": "No model-generated code or clinical status" } ], "table_decisions": [ { "page": 2, "table": 0, "decision": "parse", "rows": 9, "reason": "Lab header matched" } ], "stages": [ { "id": "ingest", "description": "PDF geometry, XML tree or FHIR resources", "ms": 321.635 }, { "id": "structure", "description": "Source codes and deterministic measurements", "ms": 388.643 } ], "versions": { "openmed": "2.5.0", "pdfplumber": "0.11.10" }, "config": { "backend": "auto", "device": "auto", "lanes": [], "threshold": 0.5, "aggregation": "average", "zeroshot_labels": [ "specimen", "collection method", "performing organization", "authorizing provider", "result status", "imaging modality", "anatomical region" ] }, "elapsed_ms": 719.2, "run_fingerprint": "7d5a3664d79260df2e75120cf88edb78d14824eefd3d156f1e8c880844b68888" }
A real run against the two-page synthetic fixture. The Bundle's remaining 17 Observation entries, and the longer arrays, are trimmed here.
Every refusal is a FHIR OperationOutcome carrying a stable code, so a caller can branch without reading prose.
OperationOutcome
no-text-layer
unsupported-input
too-large
too-many-pages
malformed-input
empty-input
model-unavailable
A scanned document is refused rather than answered with an empty Bundle. There is no OCR lane, so images are not supported.
Flexpa Extract is intended for direct individual use for personal health: a person, or a service acting for that person, turning health documents they already hold into structured data they can read, search and keep. It is not part of Flexpa API.
Flexpa Extract is not a medical device. It is not intended to diagnose, treat, cure or prevent any disease, and its output is not medical advice. It restructures what a document already states. No output should be relied on for a clinical decision without review by a qualified professional, and no output should be treated as a substitute for the source document.
We would rather you read this than discover it.
Flexpa participates in the HL7 AI Transparency on FHIR project and implemented the data producer role at HL7 FHIR Connectathon 43 in September 2026. That work labels every resource an extraction authors with the AIAST security label, and emits a conforming Provenance naming the Device, the model checkpoints that ran and the input document.
AIAST
Provenance
Labeling is implemented in the reference implementation and is planned for this service. The container described on this page does not yet emit AIAST labels or AI Provenance.