Spaces:
Running
Running
File size: 5,229 Bytes
f390b04 2e3f5aa caf4ed9 f390b04 2e3f5aa caf4ed9 f390b04 2e3f5aa f390b04 2e3f5aa f390b04 2e3f5aa f390b04 2e3f5aa f390b04 2e3f5aa f390b04 2e3f5aa f390b04 caf4ed9 f390b04 2e3f5aa f390b04 2e3f5aa | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | """
Named Entity Recognition (NER).
Uses the pre-trained model ``dslim/bert-base-NER`` (fine-tuned on CoNLL-2003,
~92.6% F1) via the Hugging Face ``token-classification`` pipeline by default.
No training is required here - this is direct inference on a pre-trained
model, in line with the methodology. The default model is loaded lazily on
first use so the application starts quickly and only pays the memory cost
when NER is actually needed. Since V3, callers may instead pick any other
token-classification model from the Hugging Face Hub at request time; that
model is loaded and cached via ``processors.model_registry``. Since V6,
``extract_entities_batch`` runs NER over many texts in one pipeline call
instead of one call per text, which is significantly faster for large
``/ingest`` batches.
"""
from __future__ import annotations
from typing import Any
from processors import model_registry
DEFAULT_MODEL_NAME = "dslim/bert-base-NER"
_BATCH_SIZE = 16
_pipeline = None
_load_error: str | None = None
def _get_default_pipeline():
"""Lazy-load the default NER pipeline on first call."""
global _pipeline, _load_error
if _pipeline is not None or _load_error is not None:
return _pipeline
try:
from transformers import pipeline
_pipeline = pipeline(
"token-classification",
model=DEFAULT_MODEL_NAME,
tokenizer=DEFAULT_MODEL_NAME,
aggregation_strategy="simple", # merge sub-word tokens into whole entities
)
print(f"[ner] Loaded model {DEFAULT_MODEL_NAME}.")
except Exception as e: # pragma: no cover
_load_error = str(e)
print(f"[ner] Failed to load model: {e}")
return _pipeline
def extract_entities(text: str, model_id: str | None = None) -> list[dict[str, Any]]:
"""
Extract named entities from ``text``.
Returns a list of dicts: {"text", "type", "start", "end", "score"}.
Entity types follow CoNLL-2003: PER (person), LOC (location),
ORG (organization), MISC (miscellaneous) for the default model; a custom
``model_id`` may use a different label set.
``model_id`` optionally selects a different Hugging Face Hub model
(loaded/cached on demand via ``model_registry``) instead of the default.
Raises ``RuntimeError`` if that model cannot be loaded, so the API layer
can turn it into a clean 400 response.
"""
if not isinstance(text, str) or not text.strip():
return []
if model_id and model_id != DEFAULT_MODEL_NAME:
nlp = model_registry.get_pipeline(
model_id, task="token-classification", aggregation_strategy="simple"
)
else:
nlp = _get_default_pipeline()
if nlp is None:
return []
try:
raw = nlp(text)
except Exception as e: # pragma: no cover
print(f"[ner] Inference failed: {e}")
return []
return _format_entities(raw)
def extract_entities_batch(
texts: list[str], model_id: str | None = None
) -> list[list[dict[str, Any]]]:
"""
Batched version of ``extract_entities``: runs NER once over the whole
list of texts instead of once per text. The pipeline batches the
underlying forward passes internally (``batch_size``), which is much
faster for a large ``/ingest`` batch than calling ``extract_entities``
in a Python loop. Empty/blank texts are skipped and get an empty list
back, at their original position.
"""
results: list[list[dict[str, Any]]] = [[] for _ in texts]
valid = [(i, t) for i, t in enumerate(texts) if isinstance(t, str) and t.strip()]
if not valid:
return results
if model_id and model_id != DEFAULT_MODEL_NAME:
nlp = model_registry.get_pipeline(
model_id, task="token-classification", aggregation_strategy="simple"
)
else:
nlp = _get_default_pipeline()
if nlp is None:
return results
indices, valid_texts = zip(*valid)
try:
raw_batch = nlp(list(valid_texts), batch_size=_BATCH_SIZE)
except Exception as e: # pragma: no cover
print(f"[ner] Batch inference failed: {e}")
return results
# A single-item input list should still come back as a list-of-one, but
# be defensive in case a given pipeline/version collapses it.
if len(valid_texts) == 1 and (not raw_batch or not isinstance(raw_batch[0], list)):
raw_batch = [raw_batch]
for idx, raw in zip(indices, raw_batch):
results[idx] = _format_entities(raw)
return results
def _format_entities(raw: list[dict[str, Any]]) -> list[dict[str, Any]]:
entities: list[dict[str, Any]] = []
for ent in raw:
entities.append(
{
"text": ent.get("word", ""),
"type": ent.get("entity_group", ent.get("entity", "")),
"start": int(ent.get("start", 0)),
"end": int(ent.get("end", 0)),
"score": round(float(ent.get("score", 0.0)), 4),
}
)
return entities
def is_ready() -> bool:
"""True if the default model is loaded or can be loaded (no fatal error)."""
return _load_error is None
def model_name() -> str:
return DEFAULT_MODEL_NAME
|