testsAndMisc/linux_configuration/tests/test_transcribe_fw_progress.py
Krzysztof kuhy Rudnicki 47ee01a3b5
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
transcribe_fw: split 302 -> 236 + 91
_transcribe_progress.py   91  _run_progress_loop, _format_progress_line,
                                and the two constants only they use
  transcribe_fw.py         236  argument parsing, model loading, diarized
                                output writing, main

All 8 top-level definitions verified AST-identical to HEAD, and an AST scan
confirms neither module references a name it does not bind - which is what
caught that the two progress constants and the hhmmss import had to travel
with the loop. transcribe_fw.py no longer imports time.

The progress tests now patch _transcribe_progress, including its sys.stderr
and time references, since that is where the loop resolves them.
_resolve_device_and_compute stayed behind, so its test still points at
transcribe_fw.

Verified: --help prints the full usage and a missing input returns exit 2,
the code transcribe.sh branches on; 32 tests pass at 100% line and branch on
both modules; 275 pass in the directory. Over-cap files 34 -> 33.

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

206 lines
7.0 KiB
Python

"""Tests for transcribe_fw's progress reporting and device resolution.
``_format_progress_line`` is the piece a user actually watches during a long
transcription, and it is pure: given the seconds processed and the media
duration it returns a string. That makes the percentage, the ETA and the
suppression of a nonsensical ETA all directly assertable.
"""
from __future__ import annotations
import argparse
import logging
from typing import TYPE_CHECKING, Any
import _transcribe_progress as progress
import pytest
import transcribe_fw as fw
if TYPE_CHECKING:
from collections.abc import Iterator
def _args(**overrides: object) -> argparse.Namespace:
"""A parsed-argument namespace with the defaults the CLI would supply."""
defaults: dict[str, object] = {
"device": "auto",
"compute_type": "auto",
"language": None,
"no_progress": False,
"diarize": False,
"num_speakers": 2,
}
return argparse.Namespace(**{**defaults, **overrides})
class _Segment:
"""A faster_whisper segment, which the code only reads ``end`` from."""
def __init__(self, end: float | None) -> None:
self.end = end
class _Model:
"""A WhisperModel double yielding fixed segments."""
def __init__(self, segments: list[_Segment]) -> None:
self._segments = segments
self.calls: list[dict[str, object]] = []
def transcribe(self, inp: str, **kwargs: object) -> tuple[Iterator[Any], object]:
"""Return an iterator of segments plus an info object."""
self.calls.append({"input": inp, **kwargs})
info = argparse.Namespace(language="en", language_probability=0.99)
return iter(self._segments), info
# --------------------------------------------------------------------------- #
# _resolve_device_and_compute
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize(
("device", "compute", "expected"),
[
("auto", "auto", ("cpu", "float32")),
("cuda", "auto", ("cuda", "float16")),
("cpu", "auto", ("cpu", "float32")),
("cuda", "int8", ("cuda", "int8")),
("cpu", "float16", ("cpu", "float16")),
],
)
def test_resolve_device_and_compute(
device: str, compute: str, expected: tuple[str, str]
) -> None:
"""auto resolves to CPU, and the compute type follows the device."""
resolved = fw._resolve_device_and_compute(
_args(device=device, compute_type=compute)
)
assert resolved == expected
# --------------------------------------------------------------------------- #
# _format_progress_line
# --------------------------------------------------------------------------- #
def test_format_progress_line_reports_percent_and_eta() -> None:
"""Halfway through at 1x speed, the ETA equals the time already spent."""
line = progress._format_progress_line(
processed=50.0, total_duration=100.0, now=50.0, start_ts=0.0
)
assert "50.0%" in line
assert "ETA" in line
assert "00:00:50" in line
def test_format_progress_line_clamps_percent_to_100() -> None:
"""A segment ending past the reported duration cannot show 110%."""
line = progress._format_progress_line(
processed=110.0, total_duration=100.0, now=10.0, start_ts=0.0
)
assert "100.0%" in line
def test_format_progress_line_omits_an_absurd_eta() -> None:
"""An ETA of more than a day is suppressed rather than shown."""
line = progress._format_progress_line(
processed=0.001, total_duration=1_000_000.0, now=10.0, start_ts=0.0
)
assert "ETA" not in line
def test_format_progress_line_omits_eta_before_any_progress() -> None:
"""With nothing processed there is no rate to extrapolate from."""
line = progress._format_progress_line(
processed=0.0, total_duration=100.0, now=1.0, start_ts=0.0
)
assert "ETA" not in line
assert "0.0%" in line
@pytest.mark.parametrize("duration", [None, 0.0])
def test_format_progress_line_falls_back_without_a_duration(
duration: float | None,
) -> None:
"""An unknown media duration degrades to reporting elapsed audio only."""
line = progress._format_progress_line(
processed=42.0, total_duration=duration, now=10.0, start_ts=0.0
)
assert "processed" in line
assert "%" not in line
# --------------------------------------------------------------------------- #
# _run_progress_loop
# --------------------------------------------------------------------------- #
def test_run_progress_loop_collects_every_segment(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""All segments are collected and the info object is passed through."""
model = _Model([_Segment(1.0), _Segment(2.0), _Segment(3.0)])
monkeypatch.setattr(progress.sys.stderr, "isatty", lambda: False)
collected, info = progress._run_progress_loop(_args(), model, "in.wav", 3.0)
assert len(collected) == 3
assert info.language == "en"
assert model.calls == [{"input": "in.wav", "language": None}]
def test_run_progress_loop_tolerates_a_segment_without_an_end(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A segment missing its end time does not break progress tracking."""
model = _Model([_Segment(None), _Segment(5.0)])
monkeypatch.setattr(progress.sys.stderr, "isatty", lambda: False)
collected, _ = progress._run_progress_loop(_args(), model, "in.wav", 10.0)
assert len(collected) == 2
def test_run_progress_loop_stays_silent_with_no_progress(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
"""--no-progress suppresses the progress lines entirely."""
model = _Model([_Segment(1.0), _Segment(2.0)])
monkeypatch.setattr(progress.sys.stderr, "isatty", lambda: False)
with caplog.at_level(logging.INFO):
progress._run_progress_loop(_args(no_progress=True), model, "in.wav", 2.0)
assert "[PROGRESS]" not in caplog.text
def test_run_progress_loop_emits_progress_on_a_tty(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
"""On a terminal every segment redraws the line."""
model = _Model([_Segment(1.0), _Segment(2.0)])
monkeypatch.setattr(progress.sys.stderr, "isatty", lambda: True)
with caplog.at_level(logging.INFO):
progress._run_progress_loop(_args(), model, "in.wav", 2.0)
assert "[PROGRESS]" in caplog.text
def test_run_progress_loop_throttles_when_not_a_tty(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
"""Piped output is throttled, so a long run does not flood the log."""
model = _Model([_Segment(float(i)) for i in range(10)])
monkeypatch.setattr(progress.sys.stderr, "isatty", lambda: False)
clock = iter([100.0, *[100.0] * 40])
monkeypatch.setattr(progress.time, "time", lambda: next(clock))
with caplog.at_level(logging.INFO):
progress._run_progress_loop(_args(), model, "in.wav", 10.0)
# Every timestamp is identical, so after the first line the throttle
# window never elapses and no further progress lines are emitted.
assert caplog.text.count("[PROGRESS]") == 1