You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

100 lines
4.8 KiB

import json
from pathlib import Path
import pytest
from voice_transcriptor.models import MediaInfo
from voice_transcriptor.services.preprocessing import CancellationToken, PreprocessingCancelled, PreprocessingOptions, PreprocessingService
from voice_transcriptor.services.job_manifest import JobManifestRepository
class FakeProbe:
def __init__(self, media: MediaInfo): self.media = media
def probe(self, path: Path) -> MediaInfo: return self.media
class FakeProcess:
def __init__(self, command):
self.stdout = iter(["out_time_us=450000000\n", "progress=continue\n", "progress=end\n"])
self.returncode = 0
self.terminated = False
def wait(self, timeout=None): return self.returncode
def poll(self): return self.returncode
def terminate(self): self.terminated = True
def kill(self): self.terminated = True
def make_media(path: Path, duration="1800") -> MediaInfo:
path.write_bytes(b"source")
return MediaInfo(path, 6, float(duration), "aac", duration, True, path.suffix in {".mp4", ".mov", ".mkv", ".webm"})
def test_preprocess_builds_streaming_commands_and_exact_manifest(tmp_path: Path) -> None:
source = tmp_path / "recording.mp4"
commands = []
def factory(command, **kwargs):
commands.append(command); Path(command[-1]).write_bytes(b"chunk"); return FakeProcess(command)
progress = []
service = PreprocessingService(Path("ffmpeg"), FakeProbe(make_media(source)), tmp_path / "jobs", factory)
result = service.preprocess(source, PreprocessingOptions(), progress=progress.append)
manifest = json.loads(result.manifest_path.read_text(encoding="utf-8"))
assert len(commands) == 3
assert [commands[0][commands[0].index(f) + 1] for f in ("-ss", "-t", "-ac", "-ar", "-c:a", "-b:a")] == ["0", "900", "1", "24000", "aac", "64k"]
assert all("0:a:0" in command and "-progress" in command for command in commands)
assert manifest["state"] == "completed"
assert manifest["chunks"][1]["source_start_seconds"] == "885"
assert manifest["chunks"][-1]["source_end_seconds"] == "1800"
assert manifest["completed_chunks"] == 3
assert progress[-1].percent == 100
assert [p.percent for p in progress] == sorted(p.percent for p in progress)
def test_cleanup_and_retention_are_job_scoped(tmp_path: Path) -> None:
source = tmp_path / "recording.wav"
def factory(command, **kwargs): Path(command[-1]).write_bytes(b"x"); return FakeProcess(command)
service = PreprocessingService(Path("ffmpeg"), FakeProbe(make_media(source, "10")), tmp_path / "jobs", factory)
result = service.preprocess(source, PreprocessingOptions()); job = result.job_directory; result.cleanup(); assert not job.exists()
retained = service.preprocess(source, PreprocessingOptions(retain_temporary_files=True)); retained.cleanup(); assert retained.job_directory.exists()
def test_durable_preprocessing_creates_schema_2_pending_transcription_job(tmp_path: Path) -> None:
source = tmp_path / "recording.wav"
output = tmp_path / "output"
def factory(command, **kwargs):
Path(command[-1]).write_bytes(b"encoded")
return FakeProcess(command)
service = PreprocessingService(
Path("ffmpeg"),
FakeProbe(make_media(source, "10")),
process_factory=factory,
manifest_repository=JobManifestRepository(),
)
result = service.preprocess(source, PreprocessingOptions(), durable_root=output)
manifest = json.loads(result.manifest_path.read_text(encoding="utf-8"))
assert result.job_directory.parent == (output / "voice-transcriptor-jobs").resolve()
assert result.retained is True
assert manifest["schema_version"] == 2
assert manifest["state"] == "transcribing"
assert manifest["completed_chunks"] == 0
assert manifest["chunks"][0]["status"] == "pending"
assert manifest["chunks"][0]["encoded"] is True
result.cleanup()
assert result.job_directory.exists()
def test_cancelled_job_stops_before_process(tmp_path: Path) -> None:
source = tmp_path / "recording.m4a"; token = CancellationToken(); token.cancel()
service = PreprocessingService(Path("ffmpeg"), FakeProbe(make_media(source, "10")), tmp_path / "jobs", lambda *a, **k: pytest.fail("started"))
with pytest.raises(PreprocessingCancelled): service.preprocess(source, PreprocessingOptions(), token)
@pytest.mark.parametrize("suffix", [".m4a", ".mp3", ".wav", ".mp4", ".mov", ".webm", ".mkv"])
def test_supported_extensions(tmp_path: Path, suffix: str) -> None:
source = tmp_path / f"recording{suffix}"
def factory(command, **kwargs): Path(command[-1]).write_bytes(b"x"); return FakeProcess(command)
result = PreprocessingService(Path("ffmpeg"), FakeProbe(make_media(source, "1")), tmp_path / "jobs", factory).preprocess(source, PreprocessingOptions())
assert result.manifest_path.exists()