agent-0 / ocr /anonymization /recognizer.py
Leon4gr45's picture
Upload folder using huggingface_hub
fd4e724 verified
Raw History Blame Contribute Delete
6.02 kB
"""Local native GLiNER runtime. Character offsets always refer to untouched text."""
import math
import re
import threading
from ocr.schemas import SafeError
from .model import MODEL_REVISION, verify_cache
from .normalization import dehyphenate
from .replacement import validate_entities
from .schemas import LABELS, Entity
DEFAULT_PROMPTS = {
"PERSON": "person",
# Privacy category includes geopolitical places, matching the existing
# qualification taxonomy (cities, states and named geographic locations).
"CITY": "location",
"RELIGION": "religion",
"EMAIL": "email address",
"PHONE": "phone number",
"ORGANIZATION": "organization",
}
class GLiNERRecognizer:
local = True
revision = MODEL_REVISION
categories = LABELS
def __init__(self, cache, *, device="cpu", threshold=0.5, prompts=None, threads=2):
self._lock = threading.Lock()
try:
self.prompts = dict(DEFAULT_PROMPTS if prompts is None else prompts)
except Exception:
raise SafeError("unsupported_required_categories") from None
if set(self.prompts) != set(LABELS) or (
any(
not isinstance(v, str) or not v.strip() or len(v) > 80
for v in self.prompts.values()
)
or len(set(self.prompts.values())) != len(LABELS)
):
raise SafeError("unsupported_required_categories")
if (
type(threshold) not in (int, float)
or not math.isfinite(threshold)
or not 0 < threshold < 1
):
raise SafeError("invalid_recognizer_configuration")
if device not in ("cpu", "cuda") or type(threads) is not int or not 1 <= threads <= 16:
raise SafeError("invalid_recognizer_configuration")
self.threshold = threshold
try:
path = verify_cache(cache)
import torch
from gliner import GLiNER
torch.set_num_threads(threads)
self.model = GLiNER.from_pretrained(
str(path),
local_files_only=True,
map_location=device,
_attn_implementation="eager",
strict=True,
)
self.model.eval()
except Exception:
raise SafeError("anonymization_model_unavailable") from None
def recognize(self, text):
if not isinstance(text, str) or len(text) > 2_000_000:
raise SafeError("anonymization_text_limit")
normalized, positions = dehyphenate(text)
predictions = self._recognize_view(normalized)
if normalized == text:
return predictions
mapped = []
for entity in predictions:
start = positions[entity.start]
end = positions[entity.end - 1] + 1
mapped.append(
entity.model_copy(
update={
"start": start,
"end": end,
"text": text[start:end],
"normalization": "dehyphenation-v1",
}
)
)
return validate_entities(text, mapped)
def _recognize_view(self, text):
if not isinstance(text, str) or len(text) > 2_000_000:
raise SafeError("anonymization_text_limit")
# Overlapping windows cover all original characters without normalizing OCR noise.
# Hard bounds prevent silent tokenizer/model truncation on unusual long tokens.
words = list(re.finditer(r"\S+", text))
if any(len(w[0]) > 256 for w in words):
raise SafeError("anonymization_token_limit")
entities = []
reverse = {v: k for k, v in self.prompts.items()}
try:
import torch
with self._lock, torch.inference_mode():
for start in range(0, len(words), 96):
window = words[start : start + 128]
left, right = window[0].start(), window[-1].end()
chunk = text[left:right]
encoded = self.model.data_processor.transformer_tokenizer(chunk)["input_ids"]
if len(encoded) > 1536:
raise SafeError("anonymization_token_limit")
raw = self.model.predict_entities(
chunk,
list(self.prompts.values()),
threshold=self.threshold,
flat_ner=True,
)
if not isinstance(raw, list):
raise ValueError()
for item in raw:
if not isinstance(item, dict) or set(item) != {
"start",
"end",
"text",
"label",
"score",
}:
raise ValueError()
# Validate runtime output before adding the original window offset.
local = Entity(
start=item["start"],
end=item["end"],
text=item["text"],
label=reverse[item["label"]],
score=item["score"],
source="multilingual_ner",
model_revision=self.revision,
)
validate_entities(chunk, [local])
entities.append(
local.model_copy(
update={"start": local.start + left, "end": local.end + left}
)
)
return validate_entities(text, entities)
except SafeError:
raise
except Exception:
raise SafeError("anonymization_inference_failed") from None