testsAndMisc/linux_configuration/tests/test_transcribe_fw_pipeline.py
Krzysztof kuhy Rudnicki da5948f56d transcribe_fw: cover 0% -> 100% before splitting
transcribe_fw.py is 302 lines against the cap and was untested. Tests land
first, in their own commit, so the split that follows is verified against
tests that predate the seam.

32 tests: argument parsing and its FW_* environment defaults, device and
compute resolution, the progress line, the transcription loop, model
loading, diarized output writing, and main()'s three exit paths.

faster_whisper is reached only through _try_import, so patching that seam
covers every path without the package installed. _format_progress_line is
tested directly rather than through the loop - it is pure, so the percentage
clamp, the ETA, and the suppression of an ETA longer than a day are each
assertable without a model.

The last coverage gap was _try_import itself, which every other test
patches; it is now exercised directly in both directions.

Split across three test files because a single pipeline module reached 321
lines - over the cap this work exists to clear. 275 tests pass in the
directory, up from 243 by exactly the 32 added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 13:40:47 +02:00

229 lines
7.8 KiB
Python

"""Tests for transcribe_fw's argument parsing, model loading and pipeline.
``main`` is the entry point transcribe.sh calls, and its return codes are the
contract: 2 for a missing dependency or a missing input file, 0 on success.
faster_whisper is reached only through ``_try_import``, so no ML package is
needed here; the output writers are patched at the module boundary.
"""
from __future__ import annotations
import logging
import sys
from typing import TYPE_CHECKING
import pytest
import transcribe_fw as fw
if TYPE_CHECKING:
from pathlib import Path
_EXIT_FAILURE = 2
class _Segment:
"""A faster_whisper segment, which the code only reads ``end`` from."""
def __init__(self, end: float) -> None:
self.end = end
self.text = "hello"
self.start = 0.0
class _Model:
"""A WhisperModel double yielding one fixed segment."""
def transcribe(self, _inp: str, **_kw: object) -> tuple[object, object]:
"""Return one segment and a language-detection info object."""
info = type("I", (), {"language": "en", "language_probability": 0.99})()
return iter([_Segment(1.0)]), info
class _Whisper:
"""A faster_whisper module double recording model construction."""
def __init__(self) -> None:
self.calls: list[dict[str, object]] = []
def _construct(self, path: str, **kwargs: object) -> _Model:
self.calls.append({"path": path, **kwargs})
return _Model()
def __getattr__(self, attr: str) -> object:
if attr == "WhisperModel":
return self._construct
raise AttributeError(attr)
@pytest.fixture
def recorded_writes(monkeypatch: pytest.MonkeyPatch) -> list[tuple[str, str]]:
"""Record output writes instead of performing them."""
written: list[tuple[str, str]] = []
for name in ("write_txt", "write_srt"):
monkeypatch.setattr(
fw,
name,
lambda _segs, path, _n=name: written.append((_n, path)),
)
return written
# --------------------------------------------------------------------------- #
# The optional-dependency import seam
# --------------------------------------------------------------------------- #
def test_try_import_returns_the_module_when_present() -> None:
"""The seam every other test patches works against a real module."""
assert fw._try_import("json") is not None
def test_try_import_returns_none_for_a_missing_module() -> None:
"""A missing faster_whisper is None, which main() turns into exit 2."""
assert fw._try_import("no_such_module_anywhere_12345") is None
# --------------------------------------------------------------------------- #
# _parse_args
# --------------------------------------------------------------------------- #
def test_parse_args_defaults_come_from_the_environment(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""transcribe.sh configures the run through FW_* variables."""
monkeypatch.setenv("FW_MODEL", "small")
monkeypatch.setenv("FW_DEVICE", "cuda")
monkeypatch.setenv("FW_COMPUTE", "int8")
monkeypatch.setenv("FW_NUM_SPEAKERS", "4")
monkeypatch.setattr(sys, "argv", ["transcribe_fw.py", "audio.wav"])
args = fw._parse_args()
assert args.model == "small"
assert args.device == "cuda"
assert args.compute_type == "int8"
assert args.num_speakers == 4
def test_parse_args_command_line_beats_the_environment(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""An explicit flag overrides the environment default."""
monkeypatch.setenv("FW_MODEL", "small")
monkeypatch.setattr(
sys, "argv", ["transcribe_fw.py", "audio.wav", "--model", "large-v3"]
)
assert fw._parse_args().model == "large-v3"
def test_parse_args_rejects_an_unknown_device(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The device choices guard against a typo silently running on CPU."""
monkeypatch.setattr(
sys, "argv", ["transcribe_fw.py", "audio.wav", "--device", "tpu"]
)
with pytest.raises(SystemExit):
fw._parse_args()
# --------------------------------------------------------------------------- #
# _load_whisper_model
# --------------------------------------------------------------------------- #
def test_load_whisper_model_uses_a_local_directory_as_is(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""A model given as an existing directory is not downloaded again."""
whisper = _Whisper()
downloads: list[str] = []
monkeypatch.setattr(fw, "download_model_with_progress", downloads.append)
args = type("A", (), {"model": str(tmp_path)})()
fw._load_whisper_model(whisper, args, "cpu", "float32")
assert downloads == []
assert whisper.calls == [
{"path": str(tmp_path), "device": "cpu", "compute_type": "float32"}
]
def test_load_whisper_model_downloads_a_named_model(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A model name that is not a directory is fetched first."""
whisper = _Whisper()
monkeypatch.setattr(
fw, "download_model_with_progress", lambda _name: "/cache/large-v3"
)
args = type("A", (), {"model": "large-v3"})()
fw._load_whisper_model(whisper, args, "cuda", "float16")
assert whisper.calls[0]["path"] == "/cache/large-v3"
# --------------------------------------------------------------------------- #
# _write_diarized_outputs
# --------------------------------------------------------------------------- #
def test_write_diarized_outputs_is_skipped_unless_requested(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Without --diarize the diarization pipeline is never entered."""
called: list[str] = []
monkeypatch.setattr(fw, "diarize_segments", lambda *_a, **_k: called.append("x"))
args = type("A", (), {"diarize": False, "num_speakers": 2})()
fw._write_diarized_outputs(args, "in.wav", tmp_path, "base", [])
assert called == []
def test_write_diarized_outputs_writes_three_files(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""A successful diarization writes .diar.txt, .diar.srt and .rttm."""
segments = [_Segment(1.0), _Segment(2.0)]
monkeypatch.setattr(fw, "diarize_segments", lambda *_a, **_k: ["A", "B"])
written: list[str] = []
for name in ("write_srt_with_speakers", "write_txt_with_speakers"):
monkeypatch.setattr(fw, name, lambda _s, _l, path: written.append(path))
monkeypatch.setattr(
fw, "write_rttm", lambda _s, _l, path, **_k: written.append(path)
)
args = type("A", (), {"diarize": True, "num_speakers": 2})()
fw._write_diarized_outputs(args, "in.wav", tmp_path, "base", segments)
assert sorted(str(p).rpartition(".")[2] for p in written) == [
"rttm",
"srt",
"txt",
]
def test_write_diarized_outputs_warns_on_mismatched_labels(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""Fewer labels than segments falls back to plain output with a warning."""
monkeypatch.setattr(fw, "diarize_segments", lambda *_a, **_k: ["only-one"])
args = type("A", (), {"diarize": True, "num_speakers": 2})()
with caplog.at_level(logging.WARNING):
fw._write_diarized_outputs(
args, "in.wav", tmp_path, "base", [_Segment(1.0), _Segment(2.0)]
)
assert "writing plain" in caplog.text
def test_write_diarized_outputs_warns_when_diarization_returns_nothing(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A failed diarization is a warning, not a crash."""
monkeypatch.setattr(fw, "diarize_segments", lambda *_a, **_k: None)
args = type("A", (), {"diarize": True, "num_speakers": 2})()
with caplog.at_level(logging.WARNING):
fw._write_diarized_outputs(args, "in.wav", tmp_path, "base", [_Segment(1.0)])
assert "writing plain" in caplog.text