pltobing's picture
chore(repo): finalize licensing and documentation
51e84d2
Raw History Blame Contribute Delete
13.5 kB
# License: Apache-2.0 License
# Created by: Patrick Lumbantobing, VertoX-AI
# Copyright (c) 2026 VertoX-AI. All rights reserved.
#
# This work is licensed under the Apache-2.0 License.
# To view a copy of this license, visit
# https://www.apache.org/licenses/LICENSE-2.0
from __future__ import annotations
import json
import os
from collections.abc import Sequence
from pathlib import Path
import pytest
from speech_text_data_aligners.application.annotate import (
AnnotationOptions,
_write_failures,
annotate_metadata,
)
from speech_text_data_aligners.core.errors import (
BackendExecutionError,
BackendOutputError,
ConfigurationError,
SpeechTextAlignerError,
)
from speech_text_data_aligners.core.models import (
AlignmentFailure,
AlignmentOutcome,
AlignmentProvenance,
AlignmentRequest,
AlignmentResult,
WordAlignment,
)
from speech_text_data_aligners.core.normalization import lexicalize
from speech_text_data_aligners.data.metadata import read_metadata
class FakeBackend:
def __init__(
self,
*,
fail_ids: set[str] | None = None,
raise_error: bool = False,
bad_count: bool = False,
wrong_id: bool = False,
invalid_timestamps: bool = False,
offset: float = 0.0,
) -> None:
self.fail_ids = fail_ids or set()
self.raise_error = raise_error
self.bad_count = bad_count
self.wrong_id = wrong_id
self.invalid_timestamps = invalid_timestamps
self.offset = offset
self.batches: list[tuple[AlignmentRequest, ...]] = []
@property
def name(self) -> str:
return "fake"
def _result(self, request: AlignmentRequest) -> AlignmentResult:
words = lexicalize(request.transcript)
alignments = tuple(
WordAlignment(
word,
self.offset + index * 0.3,
(2.0 if self.invalid_timestamps else self.offset + index * 0.3 + 0.2),
)
for index, word in enumerate(words)
)
return AlignmentResult(
"wrong" if self.wrong_id else request.request_id,
alignments,
AlignmentProvenance("fake", backend_version="1"),
runtime_s=0.1,
)
def align(self, request: AlignmentRequest) -> AlignmentResult:
return self._result(request)
def align_batch(
self, requests: Sequence[AlignmentRequest]
) -> tuple[AlignmentOutcome, ...]:
batch = tuple(requests)
self.batches.append(batch)
if self.raise_error:
raise BackendExecutionError("provider unavailable", retryable=True)
outcomes = tuple(
AlignmentOutcome(
failure=AlignmentFailure(
request.request_id, "fake_failure", "test", "failed"
)
)
if request.request_id in self.fail_ids
else AlignmentOutcome(result=self._result(request))
for request in batch
)
return outcomes[:-1] if self.bad_count else outcomes
def test_annotation_success_preserves_rows_and_writes_optional_receipt(
metadata_fixture: tuple[Path, Path], tmp_path: Path
) -> None:
metadata, audio_dir = metadata_fixture
before = metadata.read_bytes()
backend = FakeBackend()
output = tmp_path / "annotated.csv"
receipt = tmp_path / "receipt.json"
run = annotate_metadata(
metadata_csv=metadata,
audio_dir=audio_dir,
output_csv=output,
backend=backend,
options=AnnotationOptions(receipt_path=receipt),
)
table = read_metadata(output)
assert metadata.read_bytes() == before
assert [table.row_dict(i)["en_transcript"] for i in range(2)] == [
"Hello world.",
"Can't stop",
]
assert table.row_dict(0)["first_word_text"] == "Hello"
assert table.row_dict(1)["first_word_text"] == "Can't"
assert len(backend.batches) == 1
assert [request.request_id for request in backend.batches[0]] == [
"000000",
"000001",
]
assert run.failures == ()
assert run.receipt.success_count == 2
assert run.receipt.failure_count == 0
assert json.loads(receipt.read_text(encoding="utf-8"))["operation"] == "annotate"
def test_continue_mode_blanks_failed_rows_and_writes_diagnostics(
metadata_fixture: tuple[Path, Path], tmp_path: Path
) -> None:
metadata, audio_dir = metadata_fixture
output = tmp_path / "annotated.csv"
run = annotate_metadata(
metadata_csv=metadata,
audio_dir=audio_dir,
output_csv=output,
backend=FakeBackend(fail_ids={"000001"}),
)
table = read_metadata(output)
failed_row = table.row_dict(1)
assert failed_row["first_word_text"] == ""
assert failed_row["last_word_end_s"] == ""
assert run.diagnostics_path == tmp_path / "annotated.diagnostics.json"
diagnostics = json.loads(run.diagnostics_path.read_text(encoding="utf-8"))
assert diagnostics[0]["request_id"] == "000001"
assert run.failures[0].code == "fake_failure"
def test_continue_mode_structures_preflight_and_whole_backend_failures(
metadata_fixture: tuple[Path, Path], tmp_path: Path
) -> None:
metadata, audio_dir = metadata_fixture
(audio_dir / "a2.wav").unlink()
backend = FakeBackend(raise_error=True)
run = annotate_metadata(
metadata_csv=metadata,
audio_dir=audio_dir,
output_csv=tmp_path / "out.csv",
backend=backend,
)
assert [failure.code for failure in run.failures] == [
"backend_execution_error",
"audio_error",
]
assert len(backend.batches[0]) == 1
def test_continue_mode_converts_bad_count_wrong_identity_and_invalid_timestamp(
metadata_fixture: tuple[Path, Path], tmp_path: Path
) -> None:
metadata, audio_dir = metadata_fixture
for index, backend in enumerate(
(
FakeBackend(bad_count=True),
FakeBackend(wrong_id=True),
FakeBackend(invalid_timestamps=True),
)
):
run = annotate_metadata(
metadata_csv=metadata,
audio_dir=audio_dir,
output_csv=tmp_path / f"out{index}.csv",
backend=backend,
)
assert len(run.failures) == 2
assert run.failures[0].code == "alignment_validation_error"
def test_strict_mode_aborts_without_output_on_row_or_backend_failure(
metadata_fixture: tuple[Path, Path], tmp_path: Path
) -> None:
metadata, audio_dir = metadata_fixture
output = tmp_path / "out.csv"
with pytest.raises(SpeechTextAlignerError):
annotate_metadata(
metadata_csv=metadata,
audio_dir=audio_dir,
output_csv=output,
backend=FakeBackend(fail_ids={"000000"}),
options=AnnotationOptions(strict=True),
)
assert not output.exists()
(audio_dir / "a1.wav").unlink()
with pytest.raises(SpeechTextAlignerError):
annotate_metadata(
metadata_csv=metadata,
audio_dir=audio_dir,
output_csv=output,
backend=FakeBackend(),
options=AnnotationOptions(strict=True),
)
assert not output.exists()
def test_annotation_handles_no_valid_requests(
metadata_fixture: tuple[Path, Path], tmp_path: Path
) -> None:
metadata, audio_dir = metadata_fixture
(audio_dir / "a1.wav").unlink()
(audio_dir / "a2.wav").unlink()
backend = FakeBackend()
run = annotate_metadata(
metadata_csv=metadata,
audio_dir=audio_dir,
output_csv=tmp_path / "out.csv",
backend=backend,
)
assert len(run.failures) == 2
assert backend.batches == []
def test_annotation_preflights_existing_diagnostics_before_output(
metadata_fixture: tuple[Path, Path], tmp_path: Path
) -> None:
metadata, audio_dir = metadata_fixture
diagnostics = tmp_path / "existing.json"
diagnostics.write_text("old", encoding="utf-8")
output = tmp_path / "out.csv"
backend = FakeBackend(fail_ids={"000000"})
with pytest.raises(ConfigurationError, match="exists"):
annotate_metadata(
metadata_csv=metadata,
audio_dir=audio_dir,
output_csv=output,
backend=backend,
options=AnnotationOptions(diagnostics_path=diagnostics),
)
assert not output.exists()
assert backend.batches == []
@pytest.mark.parametrize(
"collision",
[
"output-source",
"diagnostics-source",
"receipt-source",
"diagnostics-output",
"receipt-output",
"receipt-diagnostics",
],
)
def test_annotation_rejects_all_artifact_collisions_before_backend_or_mutation(
metadata_fixture: tuple[Path, Path], tmp_path: Path, collision: str
) -> None:
metadata, audio_dir = metadata_fixture
output = tmp_path / "out.csv"
diagnostics = tmp_path / "diagnostics.json"
receipt = tmp_path / "receipt.json"
if collision == "output-source":
output = metadata
elif collision == "diagnostics-source":
diagnostics = metadata
elif collision == "receipt-source":
receipt = metadata
elif collision == "diagnostics-output":
diagnostics = output
elif collision == "receipt-output":
receipt = output
else:
receipt = diagnostics
for path in {output, diagnostics, receipt} - {metadata}:
path.write_bytes(f"preserve:{path.name}".encode())
snapshots = {
path: path.read_bytes() for path in {metadata, output, diagnostics, receipt}
}
backend = FakeBackend()
with pytest.raises(ConfigurationError, match="paths alias"):
annotate_metadata(
metadata_csv=metadata,
audio_dir=audio_dir,
output_csv=output,
backend=backend,
options=AnnotationOptions(
overwrite=True,
diagnostics_path=diagnostics,
receipt_path=receipt,
),
)
assert backend.batches == []
assert {path: path.read_bytes() for path in snapshots} == snapshots
def test_annotation_rejects_normalized_relative_alias_before_backend(
metadata_fixture: tuple[Path, Path], tmp_path: Path
) -> None:
metadata, audio_dir = metadata_fixture
relative_alias = Path(
os.path.relpath(metadata.parent / "not-created" / ".." / metadata.name)
)
backend = FakeBackend()
before = metadata.read_bytes()
with pytest.raises(ConfigurationError, match="paths alias"):
annotate_metadata(
metadata_csv=metadata,
audio_dir=audio_dir,
output_csv=relative_alias,
backend=backend,
options=AnnotationOptions(overwrite=True),
)
assert backend.batches == []
assert metadata.read_bytes() == before
def test_annotation_rejects_existing_symlink_alias_before_backend(
metadata_fixture: tuple[Path, Path], tmp_path: Path
) -> None:
metadata, audio_dir = metadata_fixture
alias = tmp_path / "metadata-alias.csv"
try:
alias.symlink_to(metadata)
except OSError as error:
pytest.skip(f"symlinks are unavailable: {error}")
backend = FakeBackend()
before = metadata.read_bytes()
with pytest.raises(ConfigurationError, match="paths alias"):
annotate_metadata(
metadata_csv=metadata,
audio_dir=audio_dir,
output_csv=alias,
backend=backend,
options=AnnotationOptions(overwrite=True),
)
assert backend.batches == []
assert metadata.read_bytes() == before
def test_annotation_rejects_default_diagnostics_aliasing_source(
metadata_fixture: tuple[Path, Path], tmp_path: Path
) -> None:
metadata, audio_dir = metadata_fixture
aliased_metadata = tmp_path / "out.diagnostics.json"
metadata.replace(aliased_metadata)
backend = FakeBackend()
before = aliased_metadata.read_bytes()
with pytest.raises(ConfigurationError, match="paths alias"):
annotate_metadata(
metadata_csv=aliased_metadata,
audio_dir=audio_dir,
output_csv=tmp_path / "out.csv",
backend=backend,
options=AnnotationOptions(overwrite=True),
)
assert backend.batches == []
assert aliased_metadata.read_bytes() == before
def test_failure_writer_overwrites_and_translates_oserror(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
failure = AlignmentFailure("r", "bad", "test", "failed")
path = tmp_path / "failures.json"
path.write_text("old", encoding="utf-8")
_write_failures((failure,), path, overwrite=True)
assert json.loads(path.read_text(encoding="utf-8"))[0]["code"] == "bad"
def fail_replace(source: Path, destination: Path) -> None:
raise OSError(source, destination)
monkeypatch.setattr(os, "replace", fail_replace)
with pytest.raises(BackendOutputError, match="publish"):
_write_failures((failure,), tmp_path / "new.json", overwrite=False)
def test_duplicate_line_ids_remain_separate_ordered_rows(
metadata_fixture: tuple[Path, Path], tmp_path: Path
) -> None:
metadata, audio_dir = metadata_fixture
run = annotate_metadata(
metadata_csv=metadata,
audio_dir=audio_dir,
output_csv=tmp_path / "out.csv",
backend=FakeBackend(),
)
assert [outcome.request_id for outcome in run.outcomes] == ["000000", "000001"]