The distribution, console script and import package are now audio-scribe / audio_scribe (src/audio_scribe). Everything named for the old project follows: - CcnError -> AudioScribeError, and its code "ccn_error" -> "audio_scribe_error" (the code is never persisted, so existing job state still loads) - CCN_LIVE -> AUDIO_SCRIBE_LIVE for the live-GPU tests - OpenVINO kernel cache moves to <cache>/audio-scribe/ov_cache; the first run after upgrading recompiles kernels, and the old directory is left in place - README, build-binary.sh, hatch/coverage config and uv.lock updated to match Breaking: the command is now `audio-scribe`; reinstall any tool install of the old name with `uv tool uninstall ccn-transcribe && uv tool install .`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
168 lines
6.2 KiB
Python
168 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from audio_scribe import errors
|
|
from audio_scribe.jobs import state as job_state
|
|
|
|
|
|
def make() -> job_state.JobState:
|
|
return job_state.JobState.new(job_id="youtube-abc", source_url="https://y.test/watch?v=abc")
|
|
|
|
|
|
class TestNew:
|
|
def test_starts_with_everything_missing(self) -> None:
|
|
job = make()
|
|
assert job.video.status is job_state.ArtifactStatus.MISSING
|
|
assert job.audio.status is job_state.ArtifactStatus.MISSING
|
|
assert job.outputs.status is job_state.ArtifactStatus.MISSING
|
|
|
|
def test_records_identity_and_timestamps(self) -> None:
|
|
job = make()
|
|
assert job.job_id == "youtube-abc"
|
|
assert job.source_url == "https://y.test/watch?v=abc"
|
|
assert job.created_at and job.updated_at
|
|
|
|
def test_carries_the_current_schema_version(self) -> None:
|
|
assert make().schema_version == job_state.SCHEMA_VERSION
|
|
|
|
|
|
class TestRoundTrip:
|
|
def test_survives_serialization(self) -> None:
|
|
job = make()
|
|
job.video = job_state.Artifact(
|
|
job_state.ArtifactStatus.PRESENT, path="media/v.mkv", duration_s=12.5
|
|
)
|
|
job.title = "A title"
|
|
assert job_state.JobState.from_dict(job.to_dict()) == job
|
|
|
|
def test_enums_serialize_as_plain_strings(self) -> None:
|
|
doc = make().to_dict()
|
|
assert doc["artifacts"]["video"]["status"] == "missing"
|
|
|
|
def test_outputs_paths_round_trip(self) -> None:
|
|
job = make()
|
|
job.outputs = job_state.Outputs(
|
|
job_state.ArtifactStatus.PRESENT, paths={"srt": "out/transcript.srt"}
|
|
)
|
|
assert job_state.JobState.from_dict(job.to_dict()).outputs.paths == {
|
|
"srt": "out/transcript.srt"
|
|
}
|
|
|
|
def test_transcription_record_round_trips(self) -> None:
|
|
job = make()
|
|
job.transcription = job_state.TranscriptionRecord(
|
|
model_id="m",
|
|
model_revision="r",
|
|
backend="openvino",
|
|
device="GPU",
|
|
language="en",
|
|
task="transcribe",
|
|
num_beams=1,
|
|
)
|
|
restored = job_state.JobState.from_dict(job.to_dict())
|
|
assert restored.transcription is not None
|
|
assert restored.transcription.device == "GPU"
|
|
|
|
def test_unknown_keys_are_ignored_not_fatal(self) -> None:
|
|
doc = make().to_dict()
|
|
doc["something_from_the_future"] = 1
|
|
assert job_state.JobState.from_dict(doc).job_id == "youtube-abc"
|
|
|
|
|
|
class TestSchemaVersioning:
|
|
def test_a_newer_schema_is_refused(self) -> None:
|
|
doc = make().to_dict()
|
|
doc["schema_version"] = job_state.SCHEMA_VERSION + 1
|
|
# Guessing at a future layout risks destroying the user's state dir.
|
|
with pytest.raises(errors.SchemaTooNewError):
|
|
job_state.JobState.from_dict(doc)
|
|
|
|
def test_the_current_schema_is_accepted(self) -> None:
|
|
assert job_state.JobState.from_dict(make().to_dict()) is not None
|
|
|
|
|
|
class TestParamsChanged:
|
|
def _record(self, **kw: object) -> job_state.TranscriptionRecord:
|
|
base: dict[str, object] = {
|
|
"model_id": "m",
|
|
"model_revision": "r",
|
|
"backend": "openvino",
|
|
"device": "GPU",
|
|
"language": "en",
|
|
"task": "transcribe",
|
|
"num_beams": 1,
|
|
}
|
|
base.update(kw)
|
|
return job_state.TranscriptionRecord(**base) # type: ignore[arg-type]
|
|
|
|
def test_no_prior_run_counts_as_changed(self) -> None:
|
|
assert make().params_changed(self._record()) is True
|
|
|
|
def test_identical_params_are_unchanged(self) -> None:
|
|
job = make()
|
|
job.transcription = self._record()
|
|
assert job.params_changed(self._record()) is False
|
|
|
|
@pytest.mark.parametrize(
|
|
("field", "value"),
|
|
[("model_id", "other"), ("language", "de"), ("task", "translate"), ("num_beams", 5)],
|
|
)
|
|
def test_a_different_request_forces_a_rerun(self, field: str, value: object) -> None:
|
|
job = make()
|
|
job.transcription = self._record()
|
|
assert job.params_changed(self._record(**{field: value})) is True
|
|
|
|
def test_device_and_backend_do_not_force_a_rerun(self) -> None:
|
|
# Falling back from GPU to CPU must not invalidate a good transcript.
|
|
job = make()
|
|
job.transcription = self._record()
|
|
assert job.params_changed(self._record(device="CPU", backend="x")) is False
|
|
|
|
|
|
class TestDefaultsFromSparseDocuments:
|
|
def test_a_document_with_no_artifacts_key_gets_defaults(self) -> None:
|
|
doc = make().to_dict()
|
|
del doc["artifacts"]
|
|
restored = job_state.JobState.from_dict(doc)
|
|
assert restored.video.status is job_state.ArtifactStatus.MISSING
|
|
assert not restored.outputs.paths
|
|
|
|
def test_an_empty_artifact_entry_gets_defaults(self) -> None:
|
|
doc = make().to_dict()
|
|
doc["artifacts"] = {"video": None, "audio": {}, "outputs": None}
|
|
restored = job_state.JobState.from_dict(doc)
|
|
assert restored.video.status is job_state.ArtifactStatus.MISSING
|
|
assert restored.audio.status is job_state.ArtifactStatus.MISSING
|
|
assert restored.outputs.status is job_state.ArtifactStatus.MISSING
|
|
|
|
|
|
class TestModelComparison:
|
|
def _record(self, model_id: str | None) -> job_state.TranscriptionRecord:
|
|
return job_state.TranscriptionRecord(
|
|
model_id=model_id,
|
|
model_revision=None,
|
|
backend="openvino",
|
|
device="GPU",
|
|
language="en",
|
|
task="transcribe",
|
|
num_beams=1,
|
|
)
|
|
|
|
def test_an_unpinned_model_matches_whatever_ran(self) -> None:
|
|
# The request carries None; the record carries the resolved id. Comparing
|
|
# them directly would make every re-run look like a parameter change.
|
|
job = make()
|
|
job.transcription = self._record("OpenVINO/whisper-large-v3-turbo-int8-ov")
|
|
assert job.params_changed(self._record(None)) is False
|
|
|
|
def test_an_explicit_pin_that_differs_forces_a_rerun(self) -> None:
|
|
job = make()
|
|
job.transcription = self._record("model-a")
|
|
assert job.params_changed(self._record("model-b")) is True
|
|
|
|
def test_an_explicit_pin_that_matches_does_not(self) -> None:
|
|
job = make()
|
|
job.transcription = self._record("model-a")
|
|
assert job.params_changed(self._record("model-a")) is False
|