mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
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
This commit is contained in:
parent
da5948f56d
commit
47ee01a3b5
@ -0,0 +1,55 @@
|
||||
{
|
||||
"intent": "Split the 302-line transcribe_fw.py into a progress-reporting module and the transcription entry point, both under the 250-line cap, moving every definition verbatim.",
|
||||
"scope": [
|
||||
"transcribe_fw.py 302 -> 236 (argument parsing, model loading, diarized output, main)",
|
||||
"New: _transcribe_progress.py 91 (_run_progress_loop, _format_progress_line)",
|
||||
"Re-pointed the module under test for the two progress functions",
|
||||
"Non-goal: any behaviour change"
|
||||
],
|
||||
"changes": [
|
||||
"Extracted _run_progress_loop and _format_progress_line into _transcribe_progress.py, together with the two constants only they use (_PROGRESS_THROTTLE_SEC, _SECONDS_PER_DAY) and the hhmmss import they need.",
|
||||
"transcribe_fw.py imports _run_progress_loop by name and no longer imports time, which only the progress code used.",
|
||||
"The progress tests now patch _transcribe_progress - including its sys.stderr and time references, which is where the loop resolves them. _resolve_device_and_compute stayed in transcribe_fw, so its test was left pointing there.",
|
||||
"_transcribe_progress.py was given the executable bit to match the repo convention for a file carrying a shebang."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "python3 verify_split.py HEAD .../transcribe_fw.py <both modules>",
|
||||
"result": "pass",
|
||||
"evidence": "IDENTICAL: all 8 top-level definitions moved verbatim. Re-run after ruff reordered the imports, and still identical."
|
||||
},
|
||||
{
|
||||
"command": "AST scan for names used but not bound in each module",
|
||||
"result": "pass",
|
||||
"evidence": "Both modules report no unbound names, confirming the two constants and the hhmmss import travelled with the code that uses them."
|
||||
},
|
||||
{
|
||||
"command": "pytest linux_configuration/tests/test_transcribe_fw_*.py --cov=transcribe_fw --cov=_transcribe_progress --cov-branch",
|
||||
"result": "pass",
|
||||
"evidence": "32 passed - the same count as before the split. _transcribe_progress.py: 43 statements, 16 branches, 100%. transcribe_fw.py: 95 statements, 16 branches, 100%."
|
||||
},
|
||||
{
|
||||
"command": "python3 transcribe_fw.py --help and python3 transcribe_fw.py /nonexistent.wav",
|
||||
"result": "pass",
|
||||
"evidence": "Help prints the full usage; the missing-dependency path logs 'faster-whisper is not installed in this environment.' and returns exit 2, the code transcribe.sh branches on. Only a real run proves the sibling imports still resolve after a split."
|
||||
},
|
||||
{
|
||||
"command": "pytest linux_configuration/tests/ -q",
|
||||
"result": "pass",
|
||||
"evidence": "275 passed, unchanged from before the split."
|
||||
},
|
||||
{
|
||||
"command": "bash ~/utils/scripts/check_file_length.sh --all",
|
||||
"result": "pass",
|
||||
"evidence": "33 files over cap, down from 34. The two modules are 236 and 91 lines."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"transcribe_fw.py at 236 lines has only 14 lines of headroom under the cap. A further split is available if it grows: _parse_args and _load_whisper_model are an obvious next seam.",
|
||||
"The progress module is imported by transcribe_fw by bare name, which resolves because they are siblings on sys.path[0]. That is the existing convention for this directory, shared with _transcribe_diarize, _transcribe_model and _transcribe_output, and the real run confirms it."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; transcribe_fw.py returns to its 302-line form and _transcribe_progress.py disappears.",
|
||||
"After rollback, confirm python3 transcribe_fw.py /nonexistent.wav still returns 2 and pytest linux_configuration/tests/ reports 275 passed."
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live progress reporting for transcribe_fw.
|
||||
|
||||
``_format_progress_line`` is pure - seconds processed plus media duration in,
|
||||
a display string out - which is what makes the percentage clamp and the ETA
|
||||
testable without loading a model.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from _transcribe_output import hhmmss
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import argparse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_PROGRESS_THROTTLE_SEC = 0.2
|
||||
_SECONDS_PER_DAY = 60 * 60 * 24
|
||||
|
||||
|
||||
def _run_progress_loop(
|
||||
args: argparse.Namespace,
|
||||
model: object,
|
||||
inp: str,
|
||||
total_duration: float | None,
|
||||
) -> tuple[list[Any], object]:
|
||||
"""Transcribe with live progress output."""
|
||||
start_ts = time.time()
|
||||
iter_segments, info = model.transcribe(inp, language=args.language)
|
||||
collected: list[Any] = []
|
||||
processed = 0.0
|
||||
last_prt = 0.0
|
||||
tty = sys.stderr.isatty()
|
||||
|
||||
for seg in iter_segments:
|
||||
collected.append(seg)
|
||||
if getattr(seg, "end", None) is not None:
|
||||
processed = max(processed, float(seg.end))
|
||||
now = time.time()
|
||||
if not args.no_progress and (tty or (now - last_prt) >= _PROGRESS_THROTTLE_SEC):
|
||||
last_prt = now
|
||||
line = _format_progress_line(
|
||||
processed,
|
||||
total_duration,
|
||||
now,
|
||||
start_ts,
|
||||
)
|
||||
if tty:
|
||||
logger.info("\r%s", line)
|
||||
else:
|
||||
logger.info("%s", line)
|
||||
|
||||
if not args.no_progress and tty:
|
||||
logger.info("")
|
||||
|
||||
return collected, info
|
||||
|
||||
|
||||
def _format_progress_line(
|
||||
processed: float,
|
||||
total_duration: float | None,
|
||||
now: float,
|
||||
start_ts: float,
|
||||
) -> str:
|
||||
"""Format a progress line string."""
|
||||
if total_duration and total_duration > 0:
|
||||
pct = max(
|
||||
0.0,
|
||||
min(
|
||||
100.0,
|
||||
(processed / total_duration) * 100.0,
|
||||
),
|
||||
)
|
||||
elapsed = now - start_ts
|
||||
line = (
|
||||
f"[PROGRESS] {hhmmss(processed)} / {hhmmss(total_duration)} ({pct:5.1f}%)"
|
||||
)
|
||||
if processed > 0:
|
||||
rate = processed / max(1e-6, elapsed)
|
||||
remaining = max(0.0, total_duration - processed)
|
||||
eta = remaining / max(1e-6, rate)
|
||||
if eta < _SECONDS_PER_DAY:
|
||||
line += f" ETA ~{hhmmss(eta)}"
|
||||
return line
|
||||
return f"[PROGRESS] processed {hhmmss(processed)}"
|
||||
@ -9,7 +9,6 @@ import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from _transcribe_diarize import diarize_segments, get_media_duration
|
||||
@ -22,16 +21,13 @@ from _transcribe_output import (
|
||||
write_txt,
|
||||
write_txt_with_speakers,
|
||||
)
|
||||
from _transcribe_progress import _run_progress_loop
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import types
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Constants
|
||||
_PROGRESS_THROTTLE_SEC = 0.2
|
||||
_SECONDS_PER_DAY = 60 * 60 * 24
|
||||
|
||||
|
||||
def _try_import(name: str) -> types.ModuleType | None:
|
||||
"""Attempt to import a module, returning None on failure."""
|
||||
@ -106,73 +102,6 @@ def _resolve_device_and_compute(
|
||||
return device, compute_type
|
||||
|
||||
|
||||
def _run_progress_loop(
|
||||
args: argparse.Namespace,
|
||||
model: object,
|
||||
inp: str,
|
||||
total_duration: float | None,
|
||||
) -> tuple[list[Any], object]:
|
||||
"""Transcribe with live progress output."""
|
||||
start_ts = time.time()
|
||||
iter_segments, info = model.transcribe(inp, language=args.language)
|
||||
collected: list[Any] = []
|
||||
processed = 0.0
|
||||
last_prt = 0.0
|
||||
tty = sys.stderr.isatty()
|
||||
|
||||
for seg in iter_segments:
|
||||
collected.append(seg)
|
||||
if getattr(seg, "end", None) is not None:
|
||||
processed = max(processed, float(seg.end))
|
||||
now = time.time()
|
||||
if not args.no_progress and (tty or (now - last_prt) >= _PROGRESS_THROTTLE_SEC):
|
||||
last_prt = now
|
||||
line = _format_progress_line(
|
||||
processed,
|
||||
total_duration,
|
||||
now,
|
||||
start_ts,
|
||||
)
|
||||
if tty:
|
||||
logger.info("\r%s", line)
|
||||
else:
|
||||
logger.info("%s", line)
|
||||
|
||||
if not args.no_progress and tty:
|
||||
logger.info("")
|
||||
|
||||
return collected, info
|
||||
|
||||
|
||||
def _format_progress_line(
|
||||
processed: float,
|
||||
total_duration: float | None,
|
||||
now: float,
|
||||
start_ts: float,
|
||||
) -> str:
|
||||
"""Format a progress line string."""
|
||||
if total_duration and total_duration > 0:
|
||||
pct = max(
|
||||
0.0,
|
||||
min(
|
||||
100.0,
|
||||
(processed / total_duration) * 100.0,
|
||||
),
|
||||
)
|
||||
elapsed = now - start_ts
|
||||
line = (
|
||||
f"[PROGRESS] {hhmmss(processed)} / {hhmmss(total_duration)} ({pct:5.1f}%)"
|
||||
)
|
||||
if processed > 0:
|
||||
rate = processed / max(1e-6, elapsed)
|
||||
remaining = max(0.0, total_duration - processed)
|
||||
eta = remaining / max(1e-6, rate)
|
||||
if eta < _SECONDS_PER_DAY:
|
||||
line += f" ETA ~{hhmmss(eta)}"
|
||||
return line
|
||||
return f"[PROGRESS] processed {hhmmss(processed)}"
|
||||
|
||||
|
||||
def _write_diarized_outputs(
|
||||
args: argparse.Namespace,
|
||||
inp: str,
|
||||
|
||||
@ -12,6 +12,7 @@ import argparse
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import _transcribe_progress as progress
|
||||
import pytest
|
||||
import transcribe_fw as fw
|
||||
|
||||
@ -82,7 +83,7 @@ def test_resolve_device_and_compute(
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_format_progress_line_reports_percent_and_eta() -> None:
|
||||
"""Halfway through at 1x speed, the ETA equals the time already spent."""
|
||||
line = fw._format_progress_line(
|
||||
line = progress._format_progress_line(
|
||||
processed=50.0, total_duration=100.0, now=50.0, start_ts=0.0
|
||||
)
|
||||
|
||||
@ -93,7 +94,7 @@ def test_format_progress_line_reports_percent_and_eta() -> None:
|
||||
|
||||
def test_format_progress_line_clamps_percent_to_100() -> None:
|
||||
"""A segment ending past the reported duration cannot show 110%."""
|
||||
line = fw._format_progress_line(
|
||||
line = progress._format_progress_line(
|
||||
processed=110.0, total_duration=100.0, now=10.0, start_ts=0.0
|
||||
)
|
||||
|
||||
@ -102,7 +103,7 @@ def test_format_progress_line_clamps_percent_to_100() -> None:
|
||||
|
||||
def test_format_progress_line_omits_an_absurd_eta() -> None:
|
||||
"""An ETA of more than a day is suppressed rather than shown."""
|
||||
line = fw._format_progress_line(
|
||||
line = progress._format_progress_line(
|
||||
processed=0.001, total_duration=1_000_000.0, now=10.0, start_ts=0.0
|
||||
)
|
||||
|
||||
@ -111,7 +112,7 @@ def test_format_progress_line_omits_an_absurd_eta() -> None:
|
||||
|
||||
def test_format_progress_line_omits_eta_before_any_progress() -> None:
|
||||
"""With nothing processed there is no rate to extrapolate from."""
|
||||
line = fw._format_progress_line(
|
||||
line = progress._format_progress_line(
|
||||
processed=0.0, total_duration=100.0, now=1.0, start_ts=0.0
|
||||
)
|
||||
|
||||
@ -124,7 +125,7 @@ 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 = fw._format_progress_line(
|
||||
line = progress._format_progress_line(
|
||||
processed=42.0, total_duration=duration, now=10.0, start_ts=0.0
|
||||
)
|
||||
|
||||
@ -140,9 +141,9 @@ def test_run_progress_loop_collects_every_segment(
|
||||
) -> 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(fw.sys.stderr, "isatty", lambda: False)
|
||||
monkeypatch.setattr(progress.sys.stderr, "isatty", lambda: False)
|
||||
|
||||
collected, info = fw._run_progress_loop(_args(), model, "in.wav", 3.0)
|
||||
collected, info = progress._run_progress_loop(_args(), model, "in.wav", 3.0)
|
||||
|
||||
assert len(collected) == 3
|
||||
assert info.language == "en"
|
||||
@ -154,9 +155,9 @@ def test_run_progress_loop_tolerates_a_segment_without_an_end(
|
||||
) -> None:
|
||||
"""A segment missing its end time does not break progress tracking."""
|
||||
model = _Model([_Segment(None), _Segment(5.0)])
|
||||
monkeypatch.setattr(fw.sys.stderr, "isatty", lambda: False)
|
||||
monkeypatch.setattr(progress.sys.stderr, "isatty", lambda: False)
|
||||
|
||||
collected, _ = fw._run_progress_loop(_args(), model, "in.wav", 10.0)
|
||||
collected, _ = progress._run_progress_loop(_args(), model, "in.wav", 10.0)
|
||||
|
||||
assert len(collected) == 2
|
||||
|
||||
@ -166,10 +167,10 @@ def test_run_progress_loop_stays_silent_with_no_progress(
|
||||
) -> None:
|
||||
"""--no-progress suppresses the progress lines entirely."""
|
||||
model = _Model([_Segment(1.0), _Segment(2.0)])
|
||||
monkeypatch.setattr(fw.sys.stderr, "isatty", lambda: False)
|
||||
monkeypatch.setattr(progress.sys.stderr, "isatty", lambda: False)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
fw._run_progress_loop(_args(no_progress=True), model, "in.wav", 2.0)
|
||||
progress._run_progress_loop(_args(no_progress=True), model, "in.wav", 2.0)
|
||||
|
||||
assert "[PROGRESS]" not in caplog.text
|
||||
|
||||
@ -179,10 +180,10 @@ def test_run_progress_loop_emits_progress_on_a_tty(
|
||||
) -> None:
|
||||
"""On a terminal every segment redraws the line."""
|
||||
model = _Model([_Segment(1.0), _Segment(2.0)])
|
||||
monkeypatch.setattr(fw.sys.stderr, "isatty", lambda: True)
|
||||
monkeypatch.setattr(progress.sys.stderr, "isatty", lambda: True)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
fw._run_progress_loop(_args(), model, "in.wav", 2.0)
|
||||
progress._run_progress_loop(_args(), model, "in.wav", 2.0)
|
||||
|
||||
assert "[PROGRESS]" in caplog.text
|
||||
|
||||
@ -192,12 +193,12 @@ def test_run_progress_loop_throttles_when_not_a_tty(
|
||||
) -> 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(fw.sys.stderr, "isatty", lambda: False)
|
||||
monkeypatch.setattr(progress.sys.stderr, "isatty", lambda: False)
|
||||
clock = iter([100.0, *[100.0] * 40])
|
||||
monkeypatch.setattr(fw.time, "time", lambda: next(clock))
|
||||
monkeypatch.setattr(progress.time, "time", lambda: next(clock))
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
fw._run_progress_loop(_args(), model, "in.wav", 10.0)
|
||||
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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user