GlinerNer
Zero-shot named-entity recognition over a Document. The labels are the prompt, and every entity carries the boxes it came from.
- Import
import { GlinerNer } from '@stabrise/scaledp/ner'- Group
- Understand
- Reads
- document
- Writes
- ner
- Needs
@huggingface/transformers
import { Pipeline } from '@stabrise/scaledp'
import { PdfToImage } from '@stabrise/scaledp/pdf'
import { PaddleTextRecognizer } from '@stabrise/scaledp/ocr'
import { GlinerNer } from '@stabrise/scaledp/ner'
const rows = await new Pipeline([
new PdfToImage({ resolution: 300 }),
new PaddleTextRecognizer({ keepFormatting: true }),
new GlinerNer({
inputCol: 'text',
labels: ['person', 'organization', 'email', 'phone', 'address'],
threshold: 0.5,
}),
]).transform(file)
for (const entity of rows[0].ner.entities) {
console.log(entity.entity_group, entity.word, entity.score, entity.boxes)
}333 MB on first run
That is the default gliner-multi-pii model. It downloads once into IndexedDB.
See Models and caching for the alternatives
and their sizes.
Labels are the prompt
GLiNER scores a label by its text, so the wording is part of the query.
'phone' and 'phone_number' are different questions, and a model fine-tuned on
one will score the other lower. Ask for 'medical_condition' and it looks for
one, with no retraining.
import { DEFAULT_PII_LABELS, GLINER2_PII_LABELS } from '@stabrise/scaledp/ner'whiteList filters the results by group after the fact, which is different from
narrowing labels — narrowing the labels changes the inference.
Entities carry boxes
Entity.start/end are character offsets into Document.text, and boxes are
the page boxes those characters fall in. The mapping is built by locating each
box's text in the document text.
A deliberate divergence from Python
Python derives the map from len(box.text) + 1, assuming exactly one separator
per box. That drifts as soon as keepFormatting inserts several spaces or a
newline, shifting every entity's boxes after the first wide gap.
That is what makes redaction work: the boxes are where the entity actually is on the page.
Chunking
Long documents are windowed at chunkLength 500 characters with a chunkStride
of 480, so a 20-character overlap catches entities that straddle a seam.
Duplicates from that overlap are de-duplicated here; Python's sliding window
reports them twice.
normaliseCasing
On by default, and it matters for scans. GLiNER1 models are cased, and a document set entirely in capitals looks unlike anything in training. The rewrite title-cases all-caps runs before inference and is length-preserving, so character offsets stay valid and reported words keep their original casing.
Model registry
import { NER_MODELS, getNerModel, modelSizeBytes } from '@stabrise/scaledp/ner'An id not in the registry is rejected by the constructor. Two of the four models
are private StabRise repos and need configure({ auth }) — see
Private models and auth.
Parameters
| Parameter | Type | Default | Meaning |
|---|---|---|---|
model | 'gliner-multi-pii' | 'gliner-small' | 'stabrise-pii-multi' | 'stabrise-pii-multi-g2' | 'gliner-multi-pii' | Registry id. Private repos need configure({ auth }). |
labels | string[] | ['person', 'organization', 'location', 'email', 'phone_number', 'url', 'id', 'account_number', 'zip_code', 'address', 'ip_address', 'date', 'ssn', 'driver_license', 'passport', 'age', 'credit_card', 'medical_condition'] | GLiNER scores a label by its prompt text, so other wording asks a different question. |
threshold | number 0–1 | 0.5 | Minimum score an entity must reach. |
whiteList | string[] | [] | Keep only these entity groups; empty keeps everything. |
normaliseCasing | boolean | true | Title-case runs of capitals first. GLiNER1 models are cased; scans are often all caps. |