Updated configuration to utilize segment ids provided in the input transcript
This commit is contained in:
@@ -42,7 +42,7 @@ Useful configuration can be supplied by CLI flag or environment variable:
|
||||
- `AUDITA_MODEL`, default `openrouter/mistralai/mistral-small-3.2-24b-instruct`
|
||||
- `AUDITA_BASE_URL`, default `https://openrouter.ai/api/v1`
|
||||
- `AUDITA_MAX_SECTION_TOKENS`, default `16000`
|
||||
- `AUDITA_CONFIDENCE_THRESHOLD`, default `0.80`
|
||||
- `AUDITA_CONFIDENCE_THRESHOLD`, default `0.60`
|
||||
- `AUDITA_MAX_RETRIES`, default `3`
|
||||
- `AUDITA_GLOSSARY_MAX_LLM_PASSES`, default `3`, for total glossary correction passes
|
||||
- `AUDITA_WORK_DIR`, default `/tmp/audita`
|
||||
|
||||
@@ -41,7 +41,7 @@ class IndexedSegment:
|
||||
return self.segment.model_dump(mode="json")
|
||||
|
||||
def prompt_payload(self) -> dict:
|
||||
return {"segment_id": self.index, "original_text": self.segment.text}
|
||||
return {"id": self.segment.id, "original_text": self.segment.text}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -116,7 +116,7 @@ def chunk_indexed_segments(
|
||||
sections.append(_make_section(len(sections), current, current_tokens))
|
||||
|
||||
for section in sections:
|
||||
parse_transcript_json(section.transcript_json())
|
||||
parse_transcript_json(section.transcript_json(), require_sequential_ids=False)
|
||||
|
||||
return sections
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from .errors import AuditaConfigError
|
||||
DEFAULT_MODEL = "openrouter/mistralai/mistral-small-3.2-24b-instruct"
|
||||
DEFAULT_BASE_URL = "https://openrouter.ai/api/v1"
|
||||
DEFAULT_MAX_SECTION_TOKENS = 16000
|
||||
DEFAULT_CONFIDENCE_THRESHOLD = 0.80
|
||||
DEFAULT_CONFIDENCE_THRESHOLD = 0.60
|
||||
DEFAULT_MAX_RETRIES = 3
|
||||
DEFAULT_WORK_DIR = "/tmp/audita"
|
||||
DEFAULT_GLOSSARY_MAX_LLM_PASSES = 3
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Iterable, List, Optional, Tuple
|
||||
from typing import Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
from .errors import AuditaValidationError
|
||||
from .schemas import CorrectionCandidate, TranscriptSegment
|
||||
@@ -7,7 +7,7 @@ from .schemas import CorrectionCandidate, TranscriptSegment
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SkippedCorrection:
|
||||
segment_id: int
|
||||
id: int
|
||||
reason: str
|
||||
original_text: str
|
||||
corrected_text: str
|
||||
@@ -22,8 +22,8 @@ class SkippedCorrection:
|
||||
class CorrectionApplicationResult:
|
||||
transcript: List[TranscriptSegment]
|
||||
skipped: List[SkippedCorrection]
|
||||
applied_segment_ids: List[int]
|
||||
ignored_segment_ids: List[int]
|
||||
applied_ids: List[int]
|
||||
ignored_ids: List[int]
|
||||
|
||||
|
||||
def apply_corrections(
|
||||
@@ -35,40 +35,43 @@ def apply_corrections(
|
||||
raise AuditaValidationError("Confidence threshold must be between 0.0 and 1.0.")
|
||||
|
||||
revised = list(transcript)
|
||||
id_to_position = _id_to_position(revised)
|
||||
skipped: List[SkippedCorrection] = []
|
||||
applied_segment_ids: List[int] = []
|
||||
ignored_segment_ids: List[int] = []
|
||||
applied_ids: List[int] = []
|
||||
ignored_ids: List[int] = []
|
||||
for correction in corrections:
|
||||
if correction.confidence < confidence_threshold:
|
||||
ignored_segment_ids.append(correction.segment_id)
|
||||
ignored_ids.append(correction.id)
|
||||
continue
|
||||
|
||||
reason, actual_text = _target_error(revised, correction)
|
||||
reason, actual_text = _target_error(revised, id_to_position, correction)
|
||||
if reason is not None:
|
||||
skipped.append(_skip(correction, reason, actual_text=actual_text))
|
||||
continue
|
||||
|
||||
segment = revised[correction.segment_id]
|
||||
revised_text = segment.text.replace(correction.original_text, correction.corrected_text, 1)
|
||||
revised[correction.segment_id] = segment.model_copy(update={"text": revised_text})
|
||||
applied_segment_ids.append(correction.segment_id)
|
||||
position = id_to_position[correction.id]
|
||||
segment = revised[position]
|
||||
revised_text = segment.text.replace(correction.original_text, correction.corrected_text)
|
||||
revised[position] = segment.model_copy(update={"text": revised_text})
|
||||
applied_ids.append(correction.id)
|
||||
|
||||
return CorrectionApplicationResult(
|
||||
transcript=revised,
|
||||
skipped=skipped,
|
||||
applied_segment_ids=applied_segment_ids,
|
||||
ignored_segment_ids=ignored_segment_ids,
|
||||
applied_ids=applied_ids,
|
||||
ignored_ids=ignored_ids,
|
||||
)
|
||||
|
||||
|
||||
def _target_error(
|
||||
transcript: List[TranscriptSegment],
|
||||
id_to_position: Dict[int, int],
|
||||
correction: CorrectionCandidate,
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
if correction.segment_id >= len(transcript):
|
||||
return "segment_id does not exist in transcript", None
|
||||
if correction.id not in id_to_position:
|
||||
return "id does not exist in transcript", None
|
||||
|
||||
segment = transcript[correction.segment_id]
|
||||
segment = transcript[id_to_position[correction.id]]
|
||||
if correction.original_text == "":
|
||||
return "original_text is empty", segment.text
|
||||
if correction.original_text == correction.corrected_text:
|
||||
@@ -77,19 +80,21 @@ def _target_error(
|
||||
match_count = segment.text.count(correction.original_text)
|
||||
if match_count == 0:
|
||||
return "original_text does not match any substring in segment text", segment.text
|
||||
if match_count > 1:
|
||||
return "original_text appears multiple times in segment text", segment.text
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def _id_to_position(transcript: List[TranscriptSegment]) -> Dict[int, int]:
|
||||
return {segment.id: position for position, segment in enumerate(transcript)}
|
||||
|
||||
|
||||
def _skip(
|
||||
correction: CorrectionCandidate,
|
||||
reason: str,
|
||||
actual_text: Optional[str] = None,
|
||||
) -> SkippedCorrection:
|
||||
return SkippedCorrection(
|
||||
segment_id=correction.segment_id,
|
||||
id=correction.id,
|
||||
reason=reason,
|
||||
original_text=correction.original_text,
|
||||
corrected_text=correction.corrected_text,
|
||||
|
||||
@@ -41,12 +41,12 @@ def process_transcript(
|
||||
|
||||
for pass_number in range(1, config.glossary_max_llm_passes + 1):
|
||||
if pass_number == 1:
|
||||
indexed_segments = _indexed_segments_for_ids(working, list(range(len(working))))
|
||||
indexed_segments = _indexed_segments_for_ids(working, [segment.id for segment in working])
|
||||
else:
|
||||
retry_segment_ids = sorted(unresolved_retry_skips)
|
||||
if not retry_segment_ids:
|
||||
retry_ids = sorted(unresolved_retry_skips)
|
||||
if not retry_ids:
|
||||
break
|
||||
indexed_segments = _indexed_segments_for_ids(working, retry_segment_ids)
|
||||
indexed_segments = _indexed_segments_for_ids(working, retry_ids)
|
||||
if not indexed_segments:
|
||||
break
|
||||
|
||||
@@ -80,13 +80,13 @@ def process_transcript(
|
||||
)
|
||||
working = application_result.transcript
|
||||
|
||||
for segment_id in application_result.applied_segment_ids:
|
||||
unresolved_retry_skips.pop(segment_id, None)
|
||||
for segment_id in application_result.ignored_segment_ids:
|
||||
unresolved_retry_skips.pop(segment_id, None)
|
||||
for correction_id in application_result.applied_ids:
|
||||
unresolved_retry_skips.pop(correction_id, None)
|
||||
for correction_id in application_result.ignored_ids:
|
||||
unresolved_retry_skips.pop(correction_id, None)
|
||||
for skipped in application_result.skipped:
|
||||
if _is_retryable_skip(skipped, len(working)):
|
||||
unresolved_retry_skips[skipped.segment_id] = skipped
|
||||
if _is_retryable_skip(skipped, working):
|
||||
unresolved_retry_skips[skipped.id] = skipped
|
||||
else:
|
||||
final_nonretry_skips.append(skipped)
|
||||
|
||||
@@ -97,8 +97,8 @@ def process_transcript(
|
||||
"section_count": len(sections),
|
||||
"segment_count": len(indexed_segments),
|
||||
"corrections_returned": len(corrections),
|
||||
"applied_count": len(application_result.applied_segment_ids),
|
||||
"ignored_below_threshold_count": len(application_result.ignored_segment_ids),
|
||||
"applied_count": len(application_result.applied_ids),
|
||||
"ignored_below_threshold_count": len(application_result.ignored_ids),
|
||||
"skipped_count": len(application_result.skipped),
|
||||
"retry_segment_count": len(unresolved_retry_skips),
|
||||
}
|
||||
@@ -109,13 +109,13 @@ def process_transcript(
|
||||
break
|
||||
|
||||
final_skipped = final_nonretry_skips + [
|
||||
unresolved_retry_skips[segment_id] for segment_id in sorted(unresolved_retry_skips)
|
||||
unresolved_retry_skips[correction_id] for correction_id in sorted(unresolved_retry_skips)
|
||||
]
|
||||
_write_skipped_corrections(run_dir, final_skipped)
|
||||
for skipped in final_skipped:
|
||||
_log(
|
||||
progress,
|
||||
f"Skipping correction for segment {skipped.segment_id}: {skipped.reason}",
|
||||
f"Skipping correction for id {skipped.id}: {skipped.reason}",
|
||||
)
|
||||
revised = _sort_transcript_chronologically(working)
|
||||
except Exception as exc:
|
||||
@@ -164,7 +164,7 @@ def _write_and_validate_section(run_dir: Path, section: TranscriptSection) -> No
|
||||
section_path = run_dir / f"section-{section.section_index:04d}.json"
|
||||
section_json = section.transcript_json()
|
||||
section_path.write_text(section_json, encoding="utf-8")
|
||||
parse_transcript_json(section_json)
|
||||
parse_transcript_json(section_json, require_sequential_ids=False)
|
||||
|
||||
|
||||
def _write_skipped_corrections(run_dir: Path, skipped: List[SkippedCorrection]) -> None:
|
||||
@@ -182,17 +182,18 @@ def _write_skipped_corrections(run_dir: Path, skipped: List[SkippedCorrection])
|
||||
|
||||
def _indexed_segments_for_ids(
|
||||
transcript: List[TranscriptSegment],
|
||||
segment_ids: List[int],
|
||||
ids: List[int],
|
||||
) -> List[IndexedSegment]:
|
||||
id_to_position = {segment.id: position for position, segment in enumerate(transcript)}
|
||||
return [
|
||||
IndexedSegment(index=segment_id, segment=transcript[segment_id])
|
||||
for segment_id in segment_ids
|
||||
if 0 <= segment_id < len(transcript)
|
||||
IndexedSegment(index=id_to_position[segment_id], segment=transcript[id_to_position[segment_id]])
|
||||
for segment_id in ids
|
||||
if segment_id in id_to_position
|
||||
]
|
||||
|
||||
|
||||
def _is_retryable_skip(skipped: SkippedCorrection, transcript_length: int) -> bool:
|
||||
return 0 <= skipped.segment_id < transcript_length
|
||||
def _is_retryable_skip(skipped: SkippedCorrection, transcript: List[TranscriptSegment]) -> bool:
|
||||
return any(segment.id == skipped.id for segment in transcript)
|
||||
|
||||
|
||||
def _sort_transcript_chronologically(
|
||||
|
||||
@@ -44,10 +44,10 @@ def build_glossary_correction_messages(
|
||||
"- Inappropriate example: correcting \"Lyra\" to \"Jesters\" should be omitted because those words are not similar in spoken English, even if \"Jesters\" appears in the glossary.\n"
|
||||
"- Do not replace one clear glossary term, character name, location, or ordinary word with a different glossary term unless it is a plausible mishearing.\n"
|
||||
"- Assign high confidence only when the correction is supported by glossary evidence, local context, and spoken-word similarity; otherwise omit the correction.\n"
|
||||
"- Use the exact segment_id from the input segment.\n"
|
||||
"- Use the exact id from the input segment.\n"
|
||||
"- For returned corrections, original_text must be only the exact text span that needs replacement, not the full segment text unless the whole segment is the replacement span.\n"
|
||||
"- corrected_text must be only the replacement text for that span, not the full corrected segment text unless the whole segment is the replacement span.\n"
|
||||
"- Each returned correction must contain only segment_id, original_text, corrected_text, and confidence.\n"
|
||||
"- Each returned correction must contain only id, original_text, corrected_text, and confidence.\n"
|
||||
"- Do not return corrections where original_text and corrected_text are identical.\n"
|
||||
"- Do not return speaker, start, or end fields.\n"
|
||||
"- Return only changed segments; do not return entries for unchanged segments.\n"
|
||||
|
||||
@@ -11,11 +11,19 @@ from .errors import AuditaValidationError
|
||||
class TranscriptSegment(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
id: int = Field(ge=1)
|
||||
speaker: StrictStr
|
||||
start: float
|
||||
end: float
|
||||
text: StrictStr
|
||||
|
||||
@field_validator("id", mode="before")
|
||||
@classmethod
|
||||
def require_integer_id(cls, value: Any) -> int:
|
||||
if isinstance(value, bool) or not isinstance(value, int):
|
||||
raise ValueError("must be an integer")
|
||||
return value
|
||||
|
||||
@field_validator("speaker", "text")
|
||||
@classmethod
|
||||
def require_non_empty_text(cls, value: str) -> str:
|
||||
@@ -82,12 +90,12 @@ class Glossary(BaseModel):
|
||||
class CorrectionCandidate(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
segment_id: int = Field(ge=0)
|
||||
id: int = Field(ge=1)
|
||||
original_text: StrictStr
|
||||
corrected_text: StrictStr
|
||||
confidence: float = Field(ge=0.0, le=1.0)
|
||||
|
||||
@field_validator("segment_id", mode="before")
|
||||
@field_validator("id", mode="before")
|
||||
@classmethod
|
||||
def require_integer_id(cls, value: Any) -> int:
|
||||
if isinstance(value, bool) or not isinstance(value, int):
|
||||
@@ -114,23 +122,39 @@ class CorrectionSet(BaseModel):
|
||||
_TRANSCRIPT_ADAPTER = TypeAdapter(List[TranscriptSegment])
|
||||
|
||||
|
||||
def validate_transcript_data(data: Any) -> List[TranscriptSegment]:
|
||||
def validate_transcript_data(
|
||||
data: Any,
|
||||
require_sequential_ids: bool = True,
|
||||
) -> List[TranscriptSegment]:
|
||||
if not isinstance(data, list):
|
||||
raise AuditaValidationError("Transcript must be a JSON array.")
|
||||
if not data:
|
||||
raise AuditaValidationError("Transcript must contain at least one segment.")
|
||||
try:
|
||||
return _TRANSCRIPT_ADAPTER.validate_python(data)
|
||||
transcript = _TRANSCRIPT_ADAPTER.validate_python(data)
|
||||
except ValidationError as exc:
|
||||
raise AuditaValidationError(f"Transcript schema validation failed: {exc}") from exc
|
||||
if require_sequential_ids:
|
||||
_validate_sequential_ids(transcript)
|
||||
return transcript
|
||||
|
||||
|
||||
def parse_transcript_json(raw: str) -> List[TranscriptSegment]:
|
||||
def _validate_sequential_ids(transcript: List[TranscriptSegment]) -> None:
|
||||
ids = [segment.id for segment in transcript]
|
||||
expected = list(range(1, len(transcript) + 1))
|
||||
if ids != expected:
|
||||
raise AuditaValidationError("Transcript segment ids must be sequential starting at 1.")
|
||||
|
||||
|
||||
def parse_transcript_json(
|
||||
raw: str,
|
||||
require_sequential_ids: bool = True,
|
||||
) -> List[TranscriptSegment]:
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise AuditaValidationError(f"Transcript is not valid JSON: {exc}") from exc
|
||||
return validate_transcript_data(data)
|
||||
return validate_transcript_data(data, require_sequential_ids=require_sequential_ids)
|
||||
|
||||
|
||||
def parse_glossary_yaml(raw: str) -> Glossary:
|
||||
|
||||
@@ -12,7 +12,7 @@ class CountEstimator:
|
||||
|
||||
def _segments(count):
|
||||
payload = [
|
||||
{"speaker": "Eric", "start": float(i), "end": float(i + 1), "text": f"Segment {i}"}
|
||||
{"id": i + 1, "speaker": "Eric", "start": float(i), "end": float(i + 1), "text": f"Segment {i}"}
|
||||
for i in range(count)
|
||||
]
|
||||
import json
|
||||
@@ -37,4 +37,3 @@ def test_chunk_transcript_allows_exact_limit():
|
||||
def test_chunk_transcript_rejects_oversized_single_segment():
|
||||
with pytest.raises(AuditaValidationError):
|
||||
chunk_transcript(_segments(1), max_section_tokens=9, estimator=CountEstimator())
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from audita.config import AuditaConfig, ConfigOverrides
|
||||
from audita.config import DEFAULT_GLOSSARY_MAX_LLM_PASSES, DEFAULT_MAX_RETRIES, DEFAULT_MAX_SECTION_TOKENS, DEFAULT_WORK_DIR
|
||||
from audita.config import DEFAULT_CONFIDENCE_THRESHOLD, DEFAULT_GLOSSARY_MAX_LLM_PASSES, 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.confidence_threshold == DEFAULT_CONFIDENCE_THRESHOLD
|
||||
assert config.confidence_threshold == 0.6
|
||||
assert config.max_section_tokens == DEFAULT_MAX_SECTION_TOKENS
|
||||
assert config.max_retries == DEFAULT_MAX_RETRIES
|
||||
assert config.glossary_max_llm_passes == DEFAULT_GLOSSARY_MAX_LLM_PASSES
|
||||
@@ -46,6 +48,7 @@ def test_config_cli_overrides_env():
|
||||
},
|
||||
overrides=ConfigOverrides(
|
||||
max_section_tokens=100,
|
||||
confidence_threshold=0.7,
|
||||
max_retries=3,
|
||||
glossary_max_llm_passes=2,
|
||||
work_dir=Path("/tmp/cli-audita"),
|
||||
@@ -53,6 +56,7 @@ def test_config_cli_overrides_env():
|
||||
)
|
||||
|
||||
assert config.max_section_tokens == 100
|
||||
assert config.confidence_threshold == 0.7
|
||||
assert config.max_retries == 3
|
||||
assert config.glossary_max_llm_passes == 2
|
||||
assert config.work_dir == Path("/tmp/cli-audita")
|
||||
|
||||
@@ -9,18 +9,18 @@ def _transcript():
|
||||
return parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"speaker": "Eric", "start": 10.0, "end": 11.0, "text": "I ask Chontia for help."},
|
||||
{"speaker": "Mike", "start": 0.0, "end": 1.0, "text": "Then Lyra."}
|
||||
{"id": 1, "speaker": "Eric", "start": 10.0, "end": 11.0, "text": "I ask Chontia for help."},
|
||||
{"id": 2, "speaker": "Mike", "start": 0.0, "end": 1.0, "text": "Then Lyra."}
|
||||
]
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_apply_corrections_uses_threshold_and_preserves_segment_id_order():
|
||||
def test_apply_corrections_uses_threshold_and_preserves_id_order():
|
||||
transcript = _transcript()
|
||||
corrections = [
|
||||
CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="Chontia",
|
||||
corrected_text="Chauntea",
|
||||
confidence=0.8,
|
||||
@@ -38,7 +38,7 @@ def test_apply_corrections_ignores_below_threshold():
|
||||
transcript = _transcript()
|
||||
corrections = [
|
||||
CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="Chontia",
|
||||
corrected_text="Chauntea",
|
||||
confidence=0.79,
|
||||
@@ -54,13 +54,13 @@ def test_apply_corrections_ignores_below_threshold():
|
||||
def test_apply_corrections_allows_multiple_distinct_spans_in_one_segment():
|
||||
transcript = _transcript()
|
||||
first = CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="Chontia",
|
||||
corrected_text="Chauntea",
|
||||
confidence=0.8,
|
||||
)
|
||||
second = CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="help",
|
||||
corrected_text="guidance",
|
||||
confidence=0.9,
|
||||
@@ -75,7 +75,7 @@ def test_apply_corrections_allows_multiple_distinct_spans_in_one_segment():
|
||||
def test_apply_corrections_skips_missing_substring():
|
||||
transcript = _transcript()
|
||||
correction = CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="Different text.",
|
||||
corrected_text="Chauntea",
|
||||
confidence=0.8,
|
||||
@@ -85,15 +85,15 @@ def test_apply_corrections_skips_missing_substring():
|
||||
|
||||
assert result.transcript[0].text == "I ask Chontia for help."
|
||||
assert len(result.skipped) == 1
|
||||
assert result.skipped[0].segment_id == 0
|
||||
assert result.skipped[0].id == 1
|
||||
assert result.skipped[0].actual_text == "I ask Chontia for help."
|
||||
assert "does not match any substring" in result.skipped[0].reason
|
||||
|
||||
|
||||
def test_apply_corrections_skips_missing_segment_id():
|
||||
def test_apply_corrections_skips_missing_id():
|
||||
transcript = _transcript()
|
||||
correction = CorrectionCandidate(
|
||||
segment_id=99,
|
||||
id=99,
|
||||
original_text="Missing.",
|
||||
corrected_text="Still missing.",
|
||||
confidence=0.8,
|
||||
@@ -103,14 +103,14 @@ def test_apply_corrections_skips_missing_segment_id():
|
||||
|
||||
assert [segment.text for segment in result.transcript] == ["I ask Chontia for help.", "Then Lyra."]
|
||||
assert len(result.skipped) == 1
|
||||
assert result.skipped[0].segment_id == 99
|
||||
assert result.skipped[0].id == 99
|
||||
assert "does not exist" in result.skipped[0].reason
|
||||
|
||||
|
||||
def test_apply_corrections_skips_no_op():
|
||||
transcript = _transcript()
|
||||
correction = CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="Chontia",
|
||||
corrected_text="Chontia",
|
||||
confidence=0.8,
|
||||
@@ -123,16 +123,16 @@ def test_apply_corrections_skips_no_op():
|
||||
assert "identical" in result.skipped[0].reason
|
||||
|
||||
|
||||
def test_apply_corrections_skips_ambiguous_repeated_substring():
|
||||
def test_apply_corrections_replaces_all_repeated_substrings():
|
||||
transcript = parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"speaker": "Eric", "start": 0.0, "end": 1.0, "text": "Bane met Bane."}
|
||||
{"id": 1, "speaker": "Eric", "start": 0.0, "end": 1.0, "text": "Bane met Bane."}
|
||||
]
|
||||
"""
|
||||
)
|
||||
correction = CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="Bane",
|
||||
corrected_text="Bain",
|
||||
confidence=0.8,
|
||||
@@ -140,15 +140,14 @@ def test_apply_corrections_skips_ambiguous_repeated_substring():
|
||||
|
||||
result = apply_corrections(transcript, [correction], confidence_threshold=0.8)
|
||||
|
||||
assert result.transcript[0].text == "Bane met Bane."
|
||||
assert len(result.skipped) == 1
|
||||
assert "multiple times" in result.skipped[0].reason
|
||||
assert result.transcript[0].text == "Bain met Bain."
|
||||
assert result.skipped == []
|
||||
|
||||
|
||||
def test_apply_corrections_skips_empty_original_text():
|
||||
transcript = _transcript()
|
||||
correction = CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="",
|
||||
corrected_text="Chauntea",
|
||||
confidence=0.8,
|
||||
|
||||
@@ -43,8 +43,8 @@ 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."}
|
||||
{"id": 1, "speaker": "Eric", "start": 10.0, "end": 11.0, "text": "I ask Chontia."},
|
||||
{"id": 2, "speaker": "Mike", "start": 0.0, "end": 1.0, "text": "Then Lyra."}
|
||||
]
|
||||
"""
|
||||
)
|
||||
@@ -52,7 +52,7 @@ def _transcript():
|
||||
|
||||
def test_pipeline_processes_with_fake_llm_and_cleans_work_dir(tmp_path):
|
||||
correction = CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="Chontia",
|
||||
corrected_text="Chauntea",
|
||||
confidence=0.95,
|
||||
@@ -74,7 +74,7 @@ def test_pipeline_processes_with_fake_llm_and_cleans_work_dir(tmp_path):
|
||||
|
||||
def test_pipeline_skips_bad_correction_and_preserves_diagnostics(tmp_path):
|
||||
correction = CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="Different text.",
|
||||
corrected_text="Chauntea",
|
||||
confidence=0.95,
|
||||
@@ -91,25 +91,25 @@ def test_pipeline_skips_bad_correction_and_preserves_diagnostics(tmp_path):
|
||||
)
|
||||
|
||||
assert revised[1].text == "I ask Chontia."
|
||||
assert any("Skipping correction for segment 0" in message for message in progress)
|
||||
assert any("Skipping correction for id 1" in message for message in progress)
|
||||
preserved = list((tmp_path / "work").iterdir())
|
||||
assert len(preserved) == 1
|
||||
skipped_path = preserved[0] / "skipped-corrections.json"
|
||||
assert skipped_path.exists()
|
||||
diagnostics = json.loads(skipped_path.read_text(encoding="utf-8"))
|
||||
assert diagnostics["skipped_corrections"][0]["segment_id"] == 0
|
||||
assert diagnostics["skipped_corrections"][0]["id"] == 1
|
||||
assert "does not match any substring" in diagnostics["skipped_corrections"][0]["reason"]
|
||||
|
||||
|
||||
def test_pipeline_retries_skipped_segment_and_cleans_work_dir_when_fixed(tmp_path):
|
||||
first_pass = CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="Contia",
|
||||
corrected_text="Chauntea",
|
||||
confidence=0.95,
|
||||
)
|
||||
second_pass = CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="Chontia",
|
||||
corrected_text="Chauntea",
|
||||
confidence=0.95,
|
||||
@@ -134,21 +134,21 @@ def test_pipeline_retries_skipped_segment_and_cleans_work_dir_when_fixed(tmp_pat
|
||||
assert list((tmp_path / "work").iterdir()) == []
|
||||
|
||||
|
||||
def test_pipeline_retry_prompt_contains_only_valid_deduped_segment_ids(tmp_path):
|
||||
def test_pipeline_retry_prompt_contains_only_valid_deduped_ids(tmp_path):
|
||||
first_bad = CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="Contia",
|
||||
corrected_text="Chauntea",
|
||||
confidence=0.95,
|
||||
)
|
||||
second_bad_same_segment = CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="Still wrong",
|
||||
corrected_text="Chauntea",
|
||||
confidence=0.95,
|
||||
)
|
||||
invalid_segment = CorrectionCandidate(
|
||||
segment_id=99,
|
||||
id=99,
|
||||
original_text="Missing",
|
||||
corrected_text="Chauntea",
|
||||
confidence=0.95,
|
||||
@@ -170,13 +170,13 @@ def test_pipeline_retry_prompt_contains_only_valid_deduped_segment_ids(tmp_path)
|
||||
assert fake_client.calls == 2
|
||||
retry_prompt = fake_client.messages[1][1]["content"]
|
||||
retry_payload = json.loads(retry_prompt.split("Transcript section:\n", maxsplit=1)[1])
|
||||
assert retry_payload == [{"segment_id": 0, "original_text": "I ask Chontia."}]
|
||||
assert retry_payload == [{"id": 1, "original_text": "I ask Chontia."}]
|
||||
assert "Retry guidance" in retry_prompt
|
||||
|
||||
|
||||
def test_pipeline_writes_pass_metadata_for_unresolved_retries(tmp_path):
|
||||
first_pass = CorrectionCandidate(
|
||||
segment_id=0,
|
||||
id=1,
|
||||
original_text="Contia",
|
||||
corrected_text="Chauntea",
|
||||
confidence=0.95,
|
||||
|
||||
@@ -9,7 +9,7 @@ def test_prompt_requires_acoustically_plausible_transcription_errors():
|
||||
transcript = parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"speaker": "Eric", "start": 0.0, "end": 1.0, "text": "The gestures are nearby."}
|
||||
{"id": 1, "speaker": "Eric", "start": 0.0, "end": 1.0, "text": "The gestures are nearby."}
|
||||
]
|
||||
"""
|
||||
)
|
||||
@@ -43,7 +43,7 @@ def test_prompt_uses_simplified_segment_payload():
|
||||
transcript = parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"speaker": "Eric", "start": 0.0, "end": 1.0, "text": "The gestures are nearby."}
|
||||
{"id": 1, "speaker": "Eric", "start": 0.0, "end": 1.0, "text": "The gestures are nearby."}
|
||||
]
|
||||
"""
|
||||
)
|
||||
@@ -61,7 +61,7 @@ def test_prompt_uses_simplified_segment_payload():
|
||||
transcript_json = messages[1]["content"].split("Transcript section:\n", maxsplit=1)[1]
|
||||
prompt_segments = json.loads(transcript_json)
|
||||
|
||||
assert prompt_segments == [{"segment_id": 0, "original_text": "The gestures are nearby."}]
|
||||
assert prompt_segments == [{"id": 1, "original_text": "The gestures are nearby."}]
|
||||
assert "speaker" not in prompt_segments[0]
|
||||
assert "start" not in prompt_segments[0]
|
||||
assert "end" not in prompt_segments[0]
|
||||
|
||||
@@ -8,12 +8,13 @@ def test_valid_transcript_parses():
|
||||
segments = parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"speaker": "Eric", "start": 0.0, "end": 1.25, "text": "Then Lyra."}
|
||||
{"id": 1, "speaker": "Eric", "start": 0.0, "end": 1.25, "text": "Then Lyra."}
|
||||
]
|
||||
"""
|
||||
)
|
||||
|
||||
assert len(segments) == 1
|
||||
assert segments[0].id == 1
|
||||
assert segments[0].speaker == "Eric"
|
||||
|
||||
|
||||
@@ -22,7 +23,7 @@ def test_transcript_rejects_extra_fields():
|
||||
parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"speaker": "Eric", "start": 0.0, "end": 1.25, "text": "Hi", "extra": true}
|
||||
{"id": 1, "speaker": "Eric", "start": 0.0, "end": 1.25, "text": "Hi", "extra": true}
|
||||
]
|
||||
"""
|
||||
)
|
||||
@@ -33,7 +34,64 @@ def test_transcript_rejects_bad_timestamps():
|
||||
parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"speaker": "Eric", "start": 2.0, "end": 1.0, "text": "Hi"}
|
||||
{"id": 1, "speaker": "Eric", "start": 2.0, "end": 1.0, "text": "Hi"}
|
||||
]
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_transcript_rejects_missing_id():
|
||||
with pytest.raises(AuditaValidationError):
|
||||
parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"speaker": "Eric", "start": 0.0, "end": 1.0, "text": "Hi"}
|
||||
]
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_transcript_rejects_duplicate_ids():
|
||||
with pytest.raises(AuditaValidationError):
|
||||
parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"id": 1, "speaker": "Eric", "start": 0.0, "end": 1.0, "text": "Hi"},
|
||||
{"id": 1, "speaker": "Mike", "start": 1.0, "end": 2.0, "text": "There"}
|
||||
]
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_transcript_rejects_nonsequential_ids():
|
||||
with pytest.raises(AuditaValidationError):
|
||||
parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"id": 1, "speaker": "Eric", "start": 0.0, "end": 1.0, "text": "Hi"},
|
||||
{"id": 3, "speaker": "Mike", "start": 1.0, "end": 2.0, "text": "There"}
|
||||
]
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_transcript_rejects_zero_or_negative_id():
|
||||
with pytest.raises(AuditaValidationError):
|
||||
parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"id": 0, "speaker": "Eric", "start": 0.0, "end": 1.0, "text": "Hi"}
|
||||
]
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_transcript_rejects_noninteger_id():
|
||||
with pytest.raises(AuditaValidationError):
|
||||
parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"id": 1.5, "speaker": "Eric", "start": 0.0, "end": 1.0, "text": "Hi"}
|
||||
]
|
||||
"""
|
||||
)
|
||||
@@ -73,4 +131,3 @@ def test_glossary_rejects_extra_fields():
|
||||
extra: "nope"
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user