agent-0 / ocr /tests /test_anonymization_engine.py
Leon4gr45's picture
Upload folder using huggingface_hub
fd4e724 verified
Raw History Blame Contribute Delete
4.97 kB
"""Replacement correctness is independent of model recognition accuracy."""
import pytest
from ocr.anonymization.detectors import detect
from ocr.anonymization.model import MODEL_REVISION
from ocr.anonymization.replacement import replace
from ocr.anonymization.schemas import Entity
from ocr.schemas import SafeError
def entity(text, label, start, end):
return Entity(
start=start,
end=end,
label=label,
text=text[start:end],
source="multilingual_ner",
model_revision=MODEL_REVISION,
score=0.8,
)
def test_every_qualification_gold_span_replaces_exactly():
# Preserve the merged historical corpus byte-for-byte, and use its gold spans
# to measure replacement alone. This is NOT a recognition-accuracy test.
from ocr.tests.test_anonymization_qualification import SYNTHETIC_CASES
for case in SYNTHETIC_CASES:
predictions = [
entity(case["text"], s["label"], s["start"], s["end"]) for s in case["spans"]
]
output, tags = replace(case["text"], predictions)
# Historical Google Inc. fixture includes the terminal period in its
# gold span but also expects a period outside replacement. Preserve the
# fixture; source spans take precedence over that inconsistent string.
expected = case["expected"]
if case["text"] == "I work for Google Inc.":
expected = "I work for [ORGANIZATION]"
assert output == expected
assert len(tags) == len(predictions)
assert all("text" not in t.model_dump() for t in tags)
def test_repeated_umlaut_hyphenated_broken_words_preserve_other_spacing():
text = "Änne-Marie grüßt\nÄnne-Marie.\n\nBer-\nlin!"
spans = [
entity(text, "PERSON", 0, 10),
entity(text, "PERSON", 18, 28),
entity(text, "CITY", 31, 39),
]
output, _ = replace(text, spans)
assert output == "[PERSON] grüßt\n[PERSON].\n\n[CITY]!"
def test_containment_and_duplicate_policy():
text = "Mary Jane Doe"
spans = [
entity(text, "PERSON", 0, len(text)),
entity(text, "PERSON", 5, len(text)),
entity(text, "PERSON", 0, len(text)),
]
output, tags = replace(text, spans)
assert output == "[PERSON]" and len(tags) == 1
def test_crossing_or_conflicting_overlap_blocks_whole_transformation():
text = "Mary Jane Doe"
with pytest.raises(SafeError, match="ambiguous_entity_overlap"):
replace(text, [entity(text, "PERSON", 0, 9), entity(text, "PERSON", 5, len(text))])
with pytest.raises(SafeError, match="ambiguous_entity_overlap"):
replace(text, [entity(text, "PERSON", 0, 4), entity(text, "CITY", 0, 4)])
@pytest.mark.parametrize(
"text,expected",
[
("Mail: föö@example.de.\nCall +49 30 12345678!", "Mail: [EMAIL].\nCall [PHONE]!"),
("test@example.com and 555-0199.", "[EMAIL] and [PHONE]."),
("12345 is a number.", "12345 is a number."),
],
)
def test_contact_patterns(text, expected):
output, tags = replace(text, detect(text))
assert output == expected
assert all(t.score is None for t in tags)
def test_dehyphenation_preserves_original_character_mapping():
from ocr.anonymization.normalization import dehyphenate
text = "Än-\nne in Ber-\r\nlin, and Anne-Marie."
normalized, positions = dehyphenate(text)
assert normalized == "Änne in Berlin, and Anne-Marie."
start = normalized.index("Berlin")
end = start + len("Berlin")
assert text[positions[start] : positions[end - 1] + 1] == "Ber-\r\nlin"
assert all(normalized[i] == text[p] for i, p in enumerate(positions))
def test_recognizer_maps_normalized_predictions_without_inventing_offsets():
from ocr.anonymization.recognizer import GLiNERRecognizer
model = GLiNERRecognizer.__new__(GLiNERRecognizer)
def view(text):
if text != "Berlin":
return []
return [entity(text, "CITY", 0, 6)]
model._recognize_view = view
found = model.recognize("Ber-\nlin")
assert len(found) == 1 and found[0].text == "Ber-\nlin"
assert found[0].normalization == "dehyphenation-v1"
assert replace("Ber-\nlin", found)[0] == "[CITY]"
def test_entity_level_evaluator_separates_labels_coverage_and_partial_masking():
from ocr.anonymization.evaluate import score_cases
class Recognition:
def recognize(self, text):
return [entity(text, "ORGANIZATION", 0, 3)]
case = dict(
text="ABC DEF",
expected="[PERSON]",
spans=[dict(start=0, end=7, label="PERSON", text="ABC DEF")],
)
report = score_cases(Recognition(), [case])
assert report["combined_entity_metrics"]["PERSON"]["fn"] == 1
assert report["combined_entity_metrics"]["ORGANIZATION"]["fp"] == 1
assert report["unredacted_canaries"]["PERSON"] == 1
assert report["literal_unredacted_canaries"]["PERSON"] == 0
assert report["qualification_passed"] is False