mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
transcribe_helpers: split 289 -> 177 + 142
Split along the natural seam now that the file is fully covered:
_transcribe_probes.py 177 _try_import, the four dependency probes,
generate_sine_wav, prepare_model, test_cuda
transcribe_helpers.py 142 the eight _handle_* functions and main()
All 18 top-level definitions verified AST-identical to HEAD, and an AST
scan confirms neither module references a name it does not bind.
transcribe.sh is untouched - it invokes tools/transcribe_helpers.py by
path, and that path still exists with the same name and executable bit.
The CLI tests' patch targets did not move either, because main() dispatches
to names imported into its own namespace.
Verified by running every subcommand and reading the exit status directly,
since those codes are the contract transcribe.sh branches on:
python-version 0, check-faster-whisper 7, check-ctranslate2 1,
check-diarization 0, deps-installed 0; generate-wav writes the same
96044-byte WAV as before the split. 41 tests pass at 100% line and branch
on both modules; 243 pass in the directory. Over-cap files 35 -> 34.
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
7587cd9604
commit
bda24b6305
@ -0,0 +1,18 @@
|
||||
{
|
||||
"title": "Split transcribe_helpers.py 289 -> 177 + 142",
|
||||
"objective": "transcribe_helpers.py was 289 lines against the 250-line cap. Now that it sits at 100% line and branch coverage, split it along its natural seam: the dependency probes and audio/model operations move to _transcribe_probes.py, while transcribe_helpers.py keeps the CLI dispatch that transcribe.sh invokes by path. Code moves verbatim; the tests predate the seam and are re-pointed, not rewritten.",
|
||||
"acceptance_criteria": [
|
||||
"Both resulting modules are under 250 lines; the over-cap total falls 35 -> 34",
|
||||
"All 18 top-level definitions are AST-identical to the pre-split file",
|
||||
"The same 41 tests pass, with both modules at 100% line and branch coverage",
|
||||
"Every CLI subcommand returns the same exit code as before the split, since transcribe.sh branches on them",
|
||||
"transcribe.sh needs no change: tools/transcribe_helpers.py still exists at the same path with its executable bit",
|
||||
"No suppression of any kind"
|
||||
],
|
||||
"out_of_scope": [
|
||||
"transcribe_fw.py (302) and _transcribe_diarize.py (356), which are separate targets",
|
||||
"Removing the unreachable dispatch guard, a behaviour change",
|
||||
"Moving anything under python_pkg/"
|
||||
],
|
||||
"verifier": "A throwaway ast.dump identity check against HEAD for the verbatim claim; pytest with --cov=transcribe_helpers --cov=_transcribe_probes --cov-branch; running each CLI subcommand and reading $? directly; bash -n transcribe.sh; bash ~/utils/scripts/check_file_length.sh --all."
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
{
|
||||
"intent": "Split the 289-line transcribe_helpers.py into a probes/operations module and a CLI dispatch module, both under the 250-line cap, moving every definition verbatim.",
|
||||
"scope": [
|
||||
"transcribe_helpers.py 289 -> 142 (CLI dispatch and main)",
|
||||
"New: _transcribe_probes.py 177 (dependency probes, sine WAV, model prep, CUDA probe)",
|
||||
"Re-pointed the module under test in three test files",
|
||||
"Non-goal: any behaviour change, including the unreachable dispatch guard"
|
||||
],
|
||||
"changes": [
|
||||
"Extracted _try_import, the four dependency probes, generate_sine_wav, prepare_model and test_cuda into _transcribe_probes.py; transcribe_helpers.py keeps the eight _handle_* functions and main().",
|
||||
"transcribe_helpers.py imports the probe functions by name, so the eight names main() dispatches to still resolve in its own namespace - which is why the CLI tests' patch targets did not have to move.",
|
||||
"The three probe-level test modules now import _transcribe_probes, the module whose namespace resolves those functions after the split. No re-export barrel was added.",
|
||||
"transcribe.sh is untouched: it invokes tools/transcribe_helpers.py by path, and that path still exists with the same name and executable bit.",
|
||||
"_transcribe_probes.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_helpers.py <both modules>",
|
||||
"result": "pass",
|
||||
"evidence": "IDENTICAL: all 18 top-level definitions moved verbatim, normalised through ast.dump(ast.parse(ast.unparse(node))). Re-run after ruff's autofix, and still identical."
|
||||
},
|
||||
{
|
||||
"command": "AST scan for names used but not bound in each module",
|
||||
"result": "pass",
|
||||
"evidence": "Both modules report no unbound names, so no symbol was left behind by the extraction."
|
||||
},
|
||||
{
|
||||
"command": "pytest linux_configuration/tests/test_transcribe_*.py --cov=transcribe_helpers --cov=_transcribe_probes --cov-branch",
|
||||
"result": "pass",
|
||||
"evidence": "41 passed - the same count as before the split. _transcribe_probes.py: 77 statements, 10 branches, 100%. transcribe_helpers.py: 46 statements, 16 branches, 100%."
|
||||
},
|
||||
{
|
||||
"command": "python3 transcribe_helpers.py <each of the five no-argument commands>, checking $? directly",
|
||||
"result": "pass",
|
||||
"evidence": "python-version exit 0 printing 3.14; check-faster-whisper exit 7; check-ctranslate2 exit 1; check-diarization exit 0 with the warning; deps-installed exit 0. These codes are the contract transcribe.sh branches on, and they are unchanged by the split."
|
||||
},
|
||||
{
|
||||
"command": "python3 transcribe_helpers.py generate-wav --file <scratch>/tone2.wav",
|
||||
"result": "pass",
|
||||
"evidence": "Wrote 96044 bytes, byte-for-byte the same size as the pre-split run."
|
||||
},
|
||||
{
|
||||
"command": "bash -n transcribe.sh && ls -l tools/transcribe_helpers.py",
|
||||
"result": "pass",
|
||||
"evidence": "Syntax OK, and the path transcribe.sh references still exists with the executable bit. The caller needed no change."
|
||||
},
|
||||
{
|
||||
"command": "pytest linux_configuration/tests/ -q",
|
||||
"result": "pass",
|
||||
"evidence": "243 passed, unchanged from before the split."
|
||||
},
|
||||
{
|
||||
"command": "bash ~/utils/scripts/check_file_length.sh --all",
|
||||
"result": "pass",
|
||||
"evidence": "34 files over cap, down from 35. The two modules are 177 and 142 lines."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"transcribe.sh runs the module inside a dedicated venv this session cannot exercise, since the ML wheels are not installed here. Mitigated: the bare-name sibling import that a split could break is proved by running every CLI subcommand directly, and the venv only changes which interpreter resolves the same two files in the same directory.",
|
||||
"A future command added to argparse's choices but not to the dispatch dict would silently do nothing. Pre-existing, now covered by a test that documents it."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; transcribe_helpers.py returns to its 289-line form and _transcribe_probes.py disappears.",
|
||||
"After rollback, confirm python3 transcribe_helpers.py check-faster-whisper still exits 7 and pytest linux_configuration/tests/ reports 243 passed."
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dependency probes and audio/model operations for transcribe.sh.
|
||||
|
||||
Every optional dependency is reached through ``_try_import`` so this module
|
||||
imports cleanly on a machine with none of them installed - which is what lets
|
||||
it be unit-tested in CI.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import array
|
||||
import importlib
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
import wave
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import types
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _try_import(name: str) -> types.ModuleType | None:
|
||||
"""Attempt to import a module, returning None on failure."""
|
||||
try:
|
||||
return importlib.import_module(name)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def get_python_version() -> str:
|
||||
"""Return Python major.minor version string."""
|
||||
return f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
|
||||
|
||||
def check_faster_whisper() -> bool:
|
||||
"""Check if faster_whisper is importable. Exit 7 if not."""
|
||||
return _try_import("faster_whisper") is not None
|
||||
|
||||
|
||||
def check_diarization_deps() -> bool:
|
||||
"""Check if diarization dependencies are available.
|
||||
|
||||
Returns False with warning if missing.
|
||||
"""
|
||||
_sf = _try_import("soundfile")
|
||||
_sb = _try_import("speechbrain")
|
||||
_torch = _try_import("torch")
|
||||
if _sf is None or _sb is None or _torch is None:
|
||||
logger.warning(
|
||||
"Diarization deps missing offline; speaker labels will be skipped.",
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_ctranslate2() -> bool:
|
||||
"""Check if ctranslate2 is importable."""
|
||||
return _try_import("ctranslate2") is not None
|
||||
|
||||
|
||||
def print_deps_installed() -> None:
|
||||
"""Print confirmation that Python dependencies are installed."""
|
||||
logger.info("Python %s dependencies installed.", sys.version.split()[0])
|
||||
|
||||
|
||||
def generate_sine_wav(
|
||||
outfile: str,
|
||||
frequency: float = 1000.0,
|
||||
duration: int = 3,
|
||||
sample_rate: int = 16000,
|
||||
amplitude: float = 0.3,
|
||||
) -> bool:
|
||||
"""Generate a sine wave WAV file using only Python stdlib.
|
||||
|
||||
Args:
|
||||
outfile: Output WAV file path
|
||||
frequency: Tone frequency in Hz (default: 1000)
|
||||
duration: Duration in seconds (default: 3)
|
||||
sample_rate: Sample rate in Hz (default: 16000)
|
||||
amplitude: Amplitude 0.0-1.0 (default: 0.3)
|
||||
|
||||
Returns:
|
||||
True on success, False on failure
|
||||
"""
|
||||
try:
|
||||
n_samples = sample_rate * duration
|
||||
data = array.array(
|
||||
"h",
|
||||
[
|
||||
int(
|
||||
max(
|
||||
-1.0,
|
||||
min(
|
||||
1.0,
|
||||
amplitude
|
||||
* math.sin(2 * math.pi * frequency * (i / sample_rate)),
|
||||
),
|
||||
)
|
||||
* 32767
|
||||
)
|
||||
for i in range(n_samples)
|
||||
],
|
||||
)
|
||||
with wave.open(outfile, "w") as wf:
|
||||
wf.setnchannels(1)
|
||||
wf.setsampwidth(2)
|
||||
wf.setframerate(sample_rate)
|
||||
wf.writeframes(data.tobytes())
|
||||
except OSError:
|
||||
logger.exception("Failed to generate WAV")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def prepare_model(model_name: str, model_dir: str) -> bool:
|
||||
"""Download a whisper model for offline use.
|
||||
|
||||
Args:
|
||||
model_name: Model name (tiny, base, small, medium, large-v3, etc.)
|
||||
model_dir: Directory to store the model
|
||||
|
||||
Returns:
|
||||
True on success, False on failure
|
||||
"""
|
||||
fw = _try_import("faster_whisper")
|
||||
if fw is None:
|
||||
logger.error("faster_whisper is not installed")
|
||||
return False
|
||||
|
||||
try:
|
||||
hf_logging = _try_import("huggingface_hub.logging")
|
||||
if hf_logging is not None:
|
||||
hf_logging.set_verbosity_info()
|
||||
hh = _try_import("huggingface_hub")
|
||||
if hh is not None:
|
||||
hh.constants.HF_HUB_DISABLE_PROGRESS_BARS = False
|
||||
os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "0"
|
||||
|
||||
logger.info("Preparing model '%s' into %s", model_name, model_dir)
|
||||
logger.info(
|
||||
"Downloading model files (progress bar should appear below)...",
|
||||
)
|
||||
fw.WhisperModel(
|
||||
model_name,
|
||||
device="cpu",
|
||||
compute_type="int8",
|
||||
download_root=model_dir,
|
||||
)
|
||||
logger.info("Model prepared.")
|
||||
except (OSError, RuntimeError):
|
||||
logger.exception("Failed to prepare model")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def test_cuda() -> bool:
|
||||
"""Test CUDA initialization with faster-whisper.
|
||||
|
||||
Returns:
|
||||
True if CUDA works, False otherwise
|
||||
"""
|
||||
fw = _try_import("faster_whisper")
|
||||
if fw is None:
|
||||
logger.error("faster_whisper is not installed")
|
||||
return False
|
||||
|
||||
try:
|
||||
fw.WhisperModel("tiny", device="cuda", compute_type="float16")
|
||||
logger.info("CUDA test init succeeded.")
|
||||
except (OSError, RuntimeError):
|
||||
logger.exception("CUDA test failed")
|
||||
return False
|
||||
return True
|
||||
@ -1,178 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Helper utilities for transcribe.sh - replaces inline Python snippets."""
|
||||
"""Command-line dispatch for transcribe.sh's Python helpers.
|
||||
|
||||
transcribe.sh branches on the exit status, so the codes are the contract:
|
||||
7 means faster_whisper is missing, 2 means a required argument is missing,
|
||||
and 1 means the requested operation failed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import array
|
||||
import importlib
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
import wave
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import types
|
||||
from _transcribe_probes import (
|
||||
check_ctranslate2,
|
||||
check_diarization_deps,
|
||||
check_faster_whisper,
|
||||
generate_sine_wav,
|
||||
get_python_version,
|
||||
prepare_model,
|
||||
print_deps_installed,
|
||||
test_cuda,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _try_import(name: str) -> types.ModuleType | None:
|
||||
"""Attempt to import a module, returning None on failure."""
|
||||
try:
|
||||
return importlib.import_module(name)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def get_python_version() -> str:
|
||||
"""Return Python major.minor version string."""
|
||||
return f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
|
||||
|
||||
def check_faster_whisper() -> bool:
|
||||
"""Check if faster_whisper is importable. Exit 7 if not."""
|
||||
return _try_import("faster_whisper") is not None
|
||||
|
||||
|
||||
def check_diarization_deps() -> bool:
|
||||
"""Check if diarization dependencies are available.
|
||||
|
||||
Returns False with warning if missing.
|
||||
"""
|
||||
_sf = _try_import("soundfile")
|
||||
_sb = _try_import("speechbrain")
|
||||
_torch = _try_import("torch")
|
||||
if _sf is None or _sb is None or _torch is None:
|
||||
logger.warning(
|
||||
"Diarization deps missing offline; speaker labels will be skipped.",
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_ctranslate2() -> bool:
|
||||
"""Check if ctranslate2 is importable."""
|
||||
return _try_import("ctranslate2") is not None
|
||||
|
||||
|
||||
def print_deps_installed() -> None:
|
||||
"""Print confirmation that Python dependencies are installed."""
|
||||
logger.info("Python %s dependencies installed.", sys.version.split()[0])
|
||||
|
||||
|
||||
def generate_sine_wav(
|
||||
outfile: str,
|
||||
frequency: float = 1000.0,
|
||||
duration: int = 3,
|
||||
sample_rate: int = 16000,
|
||||
amplitude: float = 0.3,
|
||||
) -> bool:
|
||||
"""Generate a sine wave WAV file using only Python stdlib.
|
||||
|
||||
Args:
|
||||
outfile: Output WAV file path
|
||||
frequency: Tone frequency in Hz (default: 1000)
|
||||
duration: Duration in seconds (default: 3)
|
||||
sample_rate: Sample rate in Hz (default: 16000)
|
||||
amplitude: Amplitude 0.0-1.0 (default: 0.3)
|
||||
|
||||
Returns:
|
||||
True on success, False on failure
|
||||
"""
|
||||
try:
|
||||
n_samples = sample_rate * duration
|
||||
data = array.array(
|
||||
"h",
|
||||
[
|
||||
int(
|
||||
max(
|
||||
-1.0,
|
||||
min(
|
||||
1.0,
|
||||
amplitude
|
||||
* math.sin(2 * math.pi * frequency * (i / sample_rate)),
|
||||
),
|
||||
)
|
||||
* 32767
|
||||
)
|
||||
for i in range(n_samples)
|
||||
],
|
||||
)
|
||||
with wave.open(outfile, "w") as wf:
|
||||
wf.setnchannels(1)
|
||||
wf.setsampwidth(2)
|
||||
wf.setframerate(sample_rate)
|
||||
wf.writeframes(data.tobytes())
|
||||
except OSError:
|
||||
logger.exception("Failed to generate WAV")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def prepare_model(model_name: str, model_dir: str) -> bool:
|
||||
"""Download a whisper model for offline use.
|
||||
|
||||
Args:
|
||||
model_name: Model name (tiny, base, small, medium, large-v3, etc.)
|
||||
model_dir: Directory to store the model
|
||||
|
||||
Returns:
|
||||
True on success, False on failure
|
||||
"""
|
||||
fw = _try_import("faster_whisper")
|
||||
if fw is None:
|
||||
logger.error("faster_whisper is not installed")
|
||||
return False
|
||||
|
||||
try:
|
||||
hf_logging = _try_import("huggingface_hub.logging")
|
||||
if hf_logging is not None:
|
||||
hf_logging.set_verbosity_info()
|
||||
hh = _try_import("huggingface_hub")
|
||||
if hh is not None:
|
||||
hh.constants.HF_HUB_DISABLE_PROGRESS_BARS = False
|
||||
os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "0"
|
||||
|
||||
logger.info("Preparing model '%s' into %s", model_name, model_dir)
|
||||
logger.info(
|
||||
"Downloading model files (progress bar should appear below)...",
|
||||
)
|
||||
fw.WhisperModel(
|
||||
model_name,
|
||||
device="cpu",
|
||||
compute_type="int8",
|
||||
download_root=model_dir,
|
||||
)
|
||||
logger.info("Model prepared.")
|
||||
except (OSError, RuntimeError):
|
||||
logger.exception("Failed to prepare model")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def test_cuda() -> bool:
|
||||
"""Test CUDA initialization with faster-whisper.
|
||||
|
||||
Returns:
|
||||
True if CUDA works, False otherwise
|
||||
"""
|
||||
fw = _try_import("faster_whisper")
|
||||
if fw is None:
|
||||
logger.error("faster_whisper is not installed")
|
||||
return False
|
||||
|
||||
try:
|
||||
fw.WhisperModel("tiny", device="cuda", compute_type="float16")
|
||||
logger.info("CUDA test init succeeded.")
|
||||
except (OSError, RuntimeError):
|
||||
logger.exception("CUDA test failed")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _handle_python_version() -> None:
|
||||
"""Handle python-version command."""
|
||||
logger.info("%s", get_python_version())
|
||||
|
||||
@ -9,7 +9,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import transcribe_helpers as helpers
|
||||
import _transcribe_probes as helpers
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
@ -11,8 +11,8 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _transcribe_probes as helpers
|
||||
import pytest
|
||||
import transcribe_helpers as helpers
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
@ -10,7 +10,7 @@ import logging
|
||||
from typing import TYPE_CHECKING
|
||||
import wave
|
||||
|
||||
import transcribe_helpers as helpers
|
||||
import _transcribe_probes as helpers
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
Loading…
Reference in New Issue
Block a user