Files
audita/python/tests/test_protected_validator.py

321 lines
11 KiB
Python

from audita.core.schemas import parse_glossary_yaml, parse_transcript_json
from audita.framework.models import CorrectionProposal, ModuleRunSpec
from audita.validators import (
GlossaryStageProtectedGlossaryTermsValidator,
ProtectedGlossaryTermsValidator,
ProtectedVocabulary,
)
from audita.validators.base import ValidationContext
def _glossary():
return parse_glossary_yaml(
"""
glossary:
- name: "Hrank"
aliases:
- "Greenfield"
category: pc
summary: "Hrank Greenfield is a player character."
- name: "Popov"
category: npc
summary: "Popov is an allied NPC."
- name: "Jesters"
aliases:
- "Jester"
category: faction
summary: "The Jesters are a faction."
- name: "Svend"
category: pc
summary: "Svend is a player character."
- name: "Godfrey"
category: npc
summary: "Godfrey is an NPC."
- name: "Lyra"
category: npc
summary: "Lyra is an NPC."
- name: "Loviator"
category: deity
summary: "Loviator is a deity."
"""
)
def test_protected_vocabulary_blocks_replacing_protected_term():
vocabulary = ProtectedVocabulary.from_glossary(_glossary())
assert (
vocabulary.violation_reason("Hrank moves.", "Frank moves.")
== "correction changes protected glossary term usage"
)
def test_protected_vocabulary_glossary_stage_allows_glossary_to_glossary_changes():
vocabulary = ProtectedVocabulary.from_glossary(_glossary())
assert vocabulary.violation_reason("Hrank moves.", "Popov moves.") == "correction changes protected glossary term usage"
assert vocabulary.glossary_stage_violation_reason("Hrank moves.", "Popov moves.") is None
def test_protected_vocabulary_glossary_stage_still_blocks_glossary_to_nonglossary_changes():
vocabulary = ProtectedVocabulary.from_glossary(_glossary())
assert (
vocabulary.glossary_stage_violation_reason("Hrank moves.", "Frank moves.")
== "correction changes protected glossary term usage"
)
def test_protected_vocabulary_blocks_noncanonical_capitalization():
vocabulary = ProtectedVocabulary.from_glossary(_glossary())
assert (
vocabulary.violation_reason("Popov moves.", "POPOV moves.")
== "correction changes protected glossary term capitalization"
)
assert (
vocabulary.violation_reason("Jesters", "jesters")
== "correction changes protected glossary term capitalization"
)
assert (
vocabulary.glossary_stage_violation_reason("Popov moves.", "POPOV moves.")
== "correction changes protected glossary term capitalization"
)
def test_protected_vocabulary_allows_corrections_toward_protected_terms():
vocabulary = ProtectedVocabulary.from_glossary(_glossary())
assert vocabulary.violation_reason("Pawpaw moves.", "Popov moves.") is None
assert vocabulary.violation_reason("gestures", "Jesters") is None
assert vocabulary.violation_reason("gestures", "jesters") is None
assert vocabulary.violation_reason("rank", "Hrank") is None
assert vocabulary.violation_reason("rank", "hrank") is None
assert vocabulary.violation_reason("spend", "Svend") is None
def test_protected_vocabulary_allows_unchanged_noncanonical_terms_and_quote_wrapping():
vocabulary = ProtectedVocabulary.from_glossary(_glossary())
assert vocabulary.violation_reason("jesters advance.", "jesters advance.") is None
before = (
"When you say that, Popov will say, when I was in that room with the jesters, "
"I just knew that Godfrey and Lyra came directly from Loviator herself."
)
after = (
'When you say that, Popov will say, "When I was in that room with the jesters, '
'I just knew that Godfrey and Lyra came directly from Loviator herself."'
)
assert vocabulary.violation_reason(before, after) is None
def test_protected_vocabulary_allows_inferred_and_explicit_plurals():
vocabulary = ProtectedVocabulary.from_glossary(_glossary())
explicit = ProtectedVocabulary.from_glossary(
parse_glossary_yaml(
"""
glossary:
- name: "Mox"
plural: "Moxen"
category: faction
summary: "The Mox are a faction."
"""
)
)
assert vocabulary.violation_reason("Godfrey's", "Godfreys") is None
assert vocabulary.violation_reason("gesture", "Jesters") is None
assert explicit.violation_reason("Mox's", "Moxen") is None
def test_protected_vocabulary_does_not_match_embedded_substrings():
vocabulary = ProtectedVocabulary.from_glossary(_glossary())
assert vocabulary.violation_reason("The shrank spell worked.", "The shrank spell works.") is None
def test_protected_glossary_terms_validator_returns_proposal_indexed_decisions():
validator = ProtectedGlossaryTermsValidator("protected_glossary_guard")
transcript = parse_transcript_json(
"""
[
{"id": 1, "speaker": "Eric", "start": 0.0, "end": 1.0, "text": "Frank moves."},
{"id": 2, "speaker": "Eric", "start": 1.0, "end": 2.0, "text": "Pawpaw waits."}
]
"""
)
proposals = [
CorrectionProposal(
proposal_index=0,
module_instance="glossary_primary",
module_key="glossary",
id=1,
original_text="Hrank",
corrected_text="Frank",
confidence=0.9,
),
CorrectionProposal(
proposal_index=1,
module_instance="glossary_primary",
module_key="glossary",
id=2,
original_text="Pawpaw",
corrected_text="Popov",
confidence=0.9,
),
]
result = validator.validate(
ValidationContext(
proposals=proposals,
transcript=transcript,
glossary=_glossary(),
config=None, # type: ignore[arg-type]
run_spec=ModuleRunSpec(instance_name="glossary_primary", module_key="glossary", module=None), # type: ignore[arg-type]
run_dir=transcript[0].__class__.__module__ and __import__("pathlib").Path("."),
)
)
assert [decision.proposal_index for decision in result.decisions] == [0, 1]
assert result.decisions[0].approved is False
assert result.decisions[0].reason == "correction changes protected glossary term usage"
assert result.decisions[1].approved is True
def test_protected_glossary_terms_validator_allows_nonglossary_to_lowercase_glossary_replacement():
validator = ProtectedGlossaryTermsValidator("protected_glossary_guard")
transcript = parse_transcript_json(
"""
[
{"id": 1, "speaker": "Eric", "start": 0.0, "end": 1.0, "text": "gestures advance."},
{"id": 2, "speaker": "Eric", "start": 1.0, "end": 2.0, "text": "rank moves."}
]
"""
)
proposals = [
CorrectionProposal(
proposal_index=0,
module_instance="homophones",
module_key="homophones",
id=1,
original_text="gestures",
corrected_text="jesters",
confidence=0.9,
),
CorrectionProposal(
proposal_index=1,
module_instance="homophones",
module_key="homophones",
id=2,
original_text="rank",
corrected_text="hrank",
confidence=0.9,
),
]
result = validator.validate(
ValidationContext(
proposals=proposals,
transcript=transcript,
glossary=_glossary(),
config=None, # type: ignore[arg-type]
run_spec=ModuleRunSpec(instance_name="homophones", module_key="homophones", module=None), # type: ignore[arg-type]
run_dir=__import__("pathlib").Path("."),
)
)
assert [decision.approved for decision in result.decisions] == [True, True]
def test_glossary_stage_protected_glossary_terms_validator_allows_glossary_to_glossary_replacement():
validator = GlossaryStageProtectedGlossaryTermsValidator("glossary_stage_protected_glossary_guard")
transcript = parse_transcript_json(
"""
[
{"id": 1, "speaker": "Eric", "start": 0.0, "end": 1.0, "text": "Hrank moves."},
{"id": 2, "speaker": "Eric", "start": 1.0, "end": 2.0, "text": "Hrank moves."}
]
"""
)
proposals = [
CorrectionProposal(
proposal_index=0,
module_instance="glossary_1",
module_key="glossary",
id=1,
original_text="Hrank",
corrected_text="Popov",
confidence=0.9,
),
CorrectionProposal(
proposal_index=1,
module_instance="glossary_1",
module_key="glossary",
id=2,
original_text="Hrank",
corrected_text="POPOV",
confidence=0.9,
),
]
result = validator.validate(
ValidationContext(
proposals=proposals,
transcript=transcript,
glossary=_glossary(),
config=None, # type: ignore[arg-type]
run_spec=ModuleRunSpec(instance_name="glossary_1", module_key="glossary", module=None), # type: ignore[arg-type]
run_dir=__import__("pathlib").Path("."),
)
)
assert [decision.proposal_index for decision in result.decisions] == [0, 1]
assert result.decisions[0].approved is True
assert result.decisions[1].approved is True
assert result.decisions[1].reason is None
def test_protected_glossary_terms_validator_uses_proposal_span_only():
validator = ProtectedGlossaryTermsValidator("protected_glossary_guard")
glossary = parse_glossary_yaml(
"""
glossary:
- name: "Svend"
category: pc
summary: "Svend is a player character."
- name: "Jesters"
category: faction
summary: "The Jesters are a faction."
"""
)
transcript = parse_transcript_json(
"""
[
{"id": 1, "speaker": "Eric", "start": 0.0, "end": 1.0, "text": "You have to keep it bind. Svend sees the jesters."}
]
"""
)
proposals = [
CorrectionProposal(
proposal_index=0,
module_instance="spoken_word",
module_key="spoken_word",
id=1,
original_text="keep it bind",
corrected_text="keep in mind",
confidence=0.9,
)
]
result = validator.validate(
ValidationContext(
proposals=proposals,
transcript=transcript,
glossary=glossary,
config=None, # type: ignore[arg-type]
run_spec=ModuleRunSpec(instance_name="spoken_word", module_key="spoken_word", module=None), # type: ignore[arg-type]
run_dir=__import__("pathlib").Path("."),
)
)
assert result.decisions[0].approved is True