Initial version of application
This commit is contained in:
40
tests/test_chunking.py
Normal file
40
tests/test_chunking.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import pytest
|
||||
|
||||
from audita.chunking import chunk_transcript
|
||||
from audita.errors import AuditaValidationError
|
||||
from audita.schemas import parse_transcript_json
|
||||
|
||||
|
||||
class CountEstimator:
|
||||
def estimate_json(self, value):
|
||||
return len(value) * 10
|
||||
|
||||
|
||||
def _segments(count):
|
||||
payload = [
|
||||
{"speaker": "Eric", "start": float(i), "end": float(i + 1), "text": f"Segment {i}"}
|
||||
for i in range(count)
|
||||
]
|
||||
import json
|
||||
|
||||
return parse_transcript_json(json.dumps(payload))
|
||||
|
||||
|
||||
def test_chunk_transcript_splits_on_segment_boundaries():
|
||||
sections = chunk_transcript(_segments(5), max_section_tokens=20, estimator=CountEstimator())
|
||||
|
||||
assert [len(section.segments) for section in sections] == [2, 2, 1]
|
||||
assert [section.start_index for section in sections] == [0, 2, 4]
|
||||
|
||||
|
||||
def test_chunk_transcript_allows_exact_limit():
|
||||
sections = chunk_transcript(_segments(2), max_section_tokens=20, estimator=CountEstimator())
|
||||
|
||||
assert len(sections) == 1
|
||||
assert len(sections[0].segments) == 2
|
||||
|
||||
|
||||
def test_chunk_transcript_rejects_oversized_single_segment():
|
||||
with pytest.raises(AuditaValidationError):
|
||||
chunk_transcript(_segments(1), max_section_tokens=9, estimator=CountEstimator())
|
||||
|
||||
65
tests/test_config.py
Normal file
65
tests/test_config.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from audita.config import AuditaConfig, ConfigOverrides
|
||||
from audita.config import DEFAULT_MAX_RETRIES, DEFAULT_MAX_SECTION_TOKENS, DEFAULT_WORK_DIR
|
||||
from audita.errors import AuditaConfigError
|
||||
|
||||
|
||||
def test_config_uses_defaults_with_api_key():
|
||||
config = AuditaConfig.from_sources(env={"OPENROUTER_API_KEY": "key"})
|
||||
|
||||
assert config.max_section_tokens == DEFAULT_MAX_SECTION_TOKENS
|
||||
assert config.max_retries == DEFAULT_MAX_RETRIES
|
||||
assert config.work_dir == Path(DEFAULT_WORK_DIR)
|
||||
|
||||
|
||||
def test_config_env_overrides_defaults():
|
||||
config = AuditaConfig.from_sources(
|
||||
env={
|
||||
"OPENROUTER_API_KEY": "key",
|
||||
"AUDITA_MAX_SECTION_TOKENS": "42",
|
||||
"AUDITA_CONFIDENCE_THRESHOLD": "0.9",
|
||||
"AUDITA_MAX_RETRIES": "5",
|
||||
"AUDITA_WORK_DIR": "/tmp/custom-audita",
|
||||
}
|
||||
)
|
||||
|
||||
assert config.max_section_tokens == 42
|
||||
assert config.confidence_threshold == 0.9
|
||||
assert config.max_retries == 5
|
||||
assert config.work_dir == Path("/tmp/custom-audita")
|
||||
|
||||
|
||||
def test_config_cli_overrides_env():
|
||||
config = AuditaConfig.from_sources(
|
||||
env={
|
||||
"OPENROUTER_API_KEY": "key",
|
||||
"AUDITA_MAX_SECTION_TOKENS": "42",
|
||||
"AUDITA_MAX_RETRIES": "5",
|
||||
"AUDITA_WORK_DIR": "/tmp/env-audita",
|
||||
},
|
||||
overrides=ConfigOverrides(
|
||||
max_section_tokens=100,
|
||||
max_retries=3,
|
||||
work_dir=Path("/tmp/cli-audita"),
|
||||
),
|
||||
)
|
||||
|
||||
assert config.max_section_tokens == 100
|
||||
assert config.max_retries == 3
|
||||
assert config.work_dir == Path("/tmp/cli-audita")
|
||||
|
||||
|
||||
def test_config_requires_api_key():
|
||||
with pytest.raises(AuditaConfigError):
|
||||
AuditaConfig.from_sources(env={})
|
||||
|
||||
|
||||
def test_config_rejects_bad_env_int():
|
||||
with pytest.raises(AuditaConfigError):
|
||||
AuditaConfig.from_sources(
|
||||
env={"OPENROUTER_API_KEY": "key", "AUDITA_MAX_SECTION_TOKENS": "many"}
|
||||
)
|
||||
|
||||
88
tests/test_corrections.py
Normal file
88
tests/test_corrections.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import pytest
|
||||
|
||||
from audita.corrections import apply_corrections
|
||||
from audita.errors import AuditaValidationError
|
||||
from audita.schemas import CorrectionCandidate, parse_transcript_json
|
||||
|
||||
|
||||
def _transcript():
|
||||
return parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"speaker": "Eric", "start": 10.0, "end": 11.0, "text": "I ask Chontia."},
|
||||
{"speaker": "Mike", "start": 0.0, "end": 1.0, "text": "Then Lyra."}
|
||||
]
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_apply_corrections_uses_threshold_and_sorts_chronologically():
|
||||
transcript = _transcript()
|
||||
corrections = [
|
||||
CorrectionCandidate(
|
||||
segment_index=0,
|
||||
speaker="Eric",
|
||||
start=10.0,
|
||||
end=11.0,
|
||||
original_text="I ask Chontia.",
|
||||
corrected_text="I ask Chauntea.",
|
||||
confidence=0.8,
|
||||
)
|
||||
]
|
||||
|
||||
revised = apply_corrections(transcript, corrections, confidence_threshold=0.8)
|
||||
|
||||
assert [segment.speaker for segment in revised] == ["Mike", "Eric"]
|
||||
assert revised[1].text == "I ask Chauntea."
|
||||
|
||||
|
||||
def test_apply_corrections_ignores_below_threshold():
|
||||
transcript = _transcript()
|
||||
corrections = [
|
||||
CorrectionCandidate(
|
||||
segment_index=0,
|
||||
speaker="Eric",
|
||||
start=10.0,
|
||||
end=11.0,
|
||||
original_text="I ask Chontia.",
|
||||
corrected_text="I ask Chauntea.",
|
||||
confidence=0.79,
|
||||
)
|
||||
]
|
||||
|
||||
revised = apply_corrections(transcript, corrections, confidence_threshold=0.8)
|
||||
|
||||
assert revised[1].text == "I ask Chontia."
|
||||
|
||||
|
||||
def test_apply_corrections_rejects_duplicate_targets():
|
||||
transcript = _transcript()
|
||||
correction = CorrectionCandidate(
|
||||
segment_index=0,
|
||||
speaker="Eric",
|
||||
start=10.0,
|
||||
end=11.0,
|
||||
original_text="I ask Chontia.",
|
||||
corrected_text="I ask Chauntea.",
|
||||
confidence=0.8,
|
||||
)
|
||||
|
||||
with pytest.raises(AuditaValidationError):
|
||||
apply_corrections(transcript, [correction, correction], confidence_threshold=0.8)
|
||||
|
||||
|
||||
def test_apply_corrections_rejects_mismatched_original_text():
|
||||
transcript = _transcript()
|
||||
correction = CorrectionCandidate(
|
||||
segment_index=0,
|
||||
speaker="Eric",
|
||||
start=10.0,
|
||||
end=11.0,
|
||||
original_text="Different text.",
|
||||
corrected_text="I ask Chauntea.",
|
||||
confidence=0.8,
|
||||
)
|
||||
|
||||
with pytest.raises(AuditaValidationError):
|
||||
apply_corrections(transcript, [correction], confidence_threshold=0.8)
|
||||
|
||||
99
tests/test_pipeline.py
Normal file
99
tests/test_pipeline.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from audita.config import AuditaConfig
|
||||
from audita.errors import AuditaValidationError
|
||||
from audita.pipeline import process_transcript
|
||||
from audita.schemas import CorrectionCandidate, CorrectionSet, parse_glossary_yaml, parse_transcript_json
|
||||
|
||||
|
||||
class FakeLLMClient:
|
||||
def __init__(self, responses):
|
||||
self.responses = list(responses)
|
||||
self.calls = 0
|
||||
|
||||
def create_corrections(self, messages, config):
|
||||
self.calls += 1
|
||||
return self.responses.pop(0)
|
||||
|
||||
|
||||
def _config(tmp_path):
|
||||
return AuditaConfig(
|
||||
api_key="key",
|
||||
max_section_tokens=16000,
|
||||
confidence_threshold=0.8,
|
||||
max_retries=3,
|
||||
work_dir=tmp_path / "work",
|
||||
)
|
||||
|
||||
|
||||
def _glossary():
|
||||
return parse_glossary_yaml(
|
||||
"""
|
||||
glossary:
|
||||
- name: "Chauntea"
|
||||
category: deity
|
||||
summary: "Chauntea is a deity."
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _transcript():
|
||||
return parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"speaker": "Eric", "start": 0.0, "end": 1.0, "text": "I ask Chontia."}
|
||||
]
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_pipeline_processes_with_fake_llm_and_cleans_work_dir(tmp_path):
|
||||
correction = CorrectionCandidate(
|
||||
segment_index=0,
|
||||
speaker="Eric",
|
||||
start=0.0,
|
||||
end=1.0,
|
||||
original_text="I ask Chontia.",
|
||||
corrected_text="I ask Chauntea.",
|
||||
confidence=0.95,
|
||||
)
|
||||
fake_client = FakeLLMClient([CorrectionSet(corrections=[correction])])
|
||||
|
||||
revised = process_transcript(
|
||||
_transcript(),
|
||||
_glossary(),
|
||||
_config(tmp_path),
|
||||
llm_client=fake_client,
|
||||
)
|
||||
|
||||
assert revised[0].text == "I ask Chauntea."
|
||||
assert fake_client.calls == 1
|
||||
assert list((tmp_path / "work").iterdir()) == []
|
||||
|
||||
|
||||
def test_pipeline_preserves_work_dir_on_failure(tmp_path):
|
||||
correction = CorrectionCandidate(
|
||||
segment_index=0,
|
||||
speaker="Eric",
|
||||
start=0.0,
|
||||
end=1.0,
|
||||
original_text="Different text.",
|
||||
corrected_text="I ask Chauntea.",
|
||||
confidence=0.95,
|
||||
)
|
||||
fake_client = FakeLLMClient([CorrectionSet(corrections=[correction])])
|
||||
|
||||
with pytest.raises(AuditaValidationError):
|
||||
process_transcript(
|
||||
_transcript(),
|
||||
_glossary(),
|
||||
_config(tmp_path),
|
||||
llm_client=fake_client,
|
||||
)
|
||||
|
||||
preserved = list((tmp_path / "work").iterdir())
|
||||
assert len(preserved) == 1
|
||||
assert (Path(preserved[0]) / "section-0000.json").exists()
|
||||
|
||||
76
tests/test_validation.py
Normal file
76
tests/test_validation.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import pytest
|
||||
|
||||
from audita.errors import AuditaValidationError
|
||||
from audita.schemas import parse_glossary_yaml, parse_transcript_json
|
||||
|
||||
|
||||
def test_valid_transcript_parses():
|
||||
segments = parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"speaker": "Eric", "start": 0.0, "end": 1.25, "text": "Then Lyra."}
|
||||
]
|
||||
"""
|
||||
)
|
||||
|
||||
assert len(segments) == 1
|
||||
assert segments[0].speaker == "Eric"
|
||||
|
||||
|
||||
def test_transcript_rejects_extra_fields():
|
||||
with pytest.raises(AuditaValidationError):
|
||||
parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"speaker": "Eric", "start": 0.0, "end": 1.25, "text": "Hi", "extra": true}
|
||||
]
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_transcript_rejects_bad_timestamps():
|
||||
with pytest.raises(AuditaValidationError):
|
||||
parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"speaker": "Eric", "start": 2.0, "end": 1.0, "text": "Hi"}
|
||||
]
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_transcript_rejects_empty_input():
|
||||
with pytest.raises(AuditaValidationError):
|
||||
parse_transcript_json("[]")
|
||||
|
||||
|
||||
def test_valid_glossary_parses():
|
||||
glossary = parse_glossary_yaml(
|
||||
"""
|
||||
glossary:
|
||||
- name: "Lyra"
|
||||
category: npc
|
||||
summary: "Lyra is a hostile NPC."
|
||||
"""
|
||||
)
|
||||
|
||||
assert glossary.glossary[0].name == "Lyra"
|
||||
|
||||
|
||||
def test_glossary_rejects_empty_entries():
|
||||
with pytest.raises(AuditaValidationError):
|
||||
parse_glossary_yaml("glossary: []")
|
||||
|
||||
|
||||
def test_glossary_rejects_extra_fields():
|
||||
with pytest.raises(AuditaValidationError):
|
||||
parse_glossary_yaml(
|
||||
"""
|
||||
glossary:
|
||||
- name: "Lyra"
|
||||
category: npc
|
||||
summary: "Lyra is a hostile NPC."
|
||||
extra: "nope"
|
||||
"""
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user