GLiNER2-multi / validators.py
quynong's picture
Upload 3 files
4f54f48 verified
Raw
History Blame Contribute Delete
4.76 kB
"""Span-level validators for GLiNER2 post-processing.
Mirrors the GLiNER2 RegexValidator(pattern, exclude=True) concept but operates
on already-extracted span dicts (list of {label, start, end, text, score}).
Usage:
from evaluation.validators import MASKING_VALIDATOR, NUMERIC_MASK_VALIDATOR
spans = MASKING_VALIDATOR.filter(spans)
spans = NUMERIC_MASK_VALIDATOR.filter(spans)
# Or combined convenience call:
spans = apply_span_validators(spans)
"""
import re
from typing import Dict, List, Optional, Set
class SpanRegexValidator:
"""Exclude (or keep) span dicts whose ``text`` field matches a regex pattern.
Mirrors ``RegexValidator(pattern, mode, exclude, flags)`` from the GLiNER2
schema API, but applied to a flat list of span dicts rather than a schema
field pipeline.
Args:
pattern: Regex pattern string or compiled ``re.Pattern``.
mode: ``"full"`` (re.fullmatch) or ``"partial"`` (re.search).
exclude: When ``True`` (default), spans that *match* are removed.
When ``False``, spans that do *not* match are removed.
flags: ``re.RegexFlag`` value used when *pattern* is a string.
labels: If given, validator only applies to spans whose ``label``
is in this set. Spans with other labels pass through unchanged.
"""
def __init__(
self,
pattern: str,
mode: str = "partial",
exclude: bool = True,
flags: int = 0,
labels: Optional[Set[str]] = None,
):
if isinstance(pattern, str):
self._re = re.compile(pattern, flags)
else:
self._re = pattern
self._mode = mode
self._exclude = exclude
self._labels: Optional[Set[str]] = (
{lbl.upper() for lbl in labels} if labels is not None else None
)
def _matches(self, text: str) -> bool:
if self._mode == "full":
return bool(self._re.fullmatch(text))
return bool(self._re.search(text))
def validate(self, span: Dict) -> bool:
"""Return ``True`` if the span should be *kept*."""
label = str(span.get("label", "")).upper()
if self._labels is not None and label not in self._labels:
return True # not in scope → keep unconditionally
text = str(span.get("text") or span.get("value") or "")
matched = self._matches(text)
return not matched if self._exclude else matched
def filter(self, spans: List[Dict]) -> List[Dict]:
"""Return only spans that pass this validator."""
return [s for s in spans if self.validate(s)]
# ---------------------------------------------------------------------------
# Numeric / identifier labels that may be masked with "XX…" placeholders
# ---------------------------------------------------------------------------
_NUMERIC_LABELS: Set[str] = {
"CARD_NUMBER",
"PHONE",
"PIN",
"CVV",
"TIN",
"BANK_ACCOUNT",
"IBAN",
"SWIFT",
"WALLET",
"IP",
"ZIP_CODE",
}
# ---------------------------------------------------------------------------
# Pre-built validators
# ---------------------------------------------------------------------------
# Rule 1: Any entity whose value contains two or more consecutive asterisks (**).
# Pattern excludes "**", "***", "****", etc.
MASKING_VALIDATOR = SpanRegexValidator(
pattern=r"\*{2,}",
mode="partial",
exclude=True,
labels=None, # applies to ALL labels
)
# Rule 2: Numeric/identifier labels whose value contains two or more consecutive
# uppercase X's (XX, XXX, XXXX …) — masked placeholders like "XXXX-XXXX".
NUMERIC_MASK_VALIDATOR = SpanRegexValidator(
pattern=r"X{2,}",
mode="partial",
exclude=True,
flags=0, # case-sensitive: only uppercase X triggers the rule
labels=_NUMERIC_LABELS,
)
# Ordered list applied by apply_span_validators()
_DEFAULT_VALIDATORS: List[SpanRegexValidator] = [
MASKING_VALIDATOR,
NUMERIC_MASK_VALIDATOR,
]
def apply_span_validators(
spans: List[Dict],
validators: Optional[List[SpanRegexValidator]] = None,
) -> List[Dict]:
"""Run all validators over *spans* in order, returning only passing spans.
Args:
spans: Flat list of span dicts ({label, start, end, text, score}).
validators: Override the default validator list. ``None`` → use the
module-level ``_DEFAULT_VALIDATORS``.
"""
active = _DEFAULT_VALIDATORS if validators is None else validators
result = spans
for v in active:
result = v.filter(result)
return result