From dbf2f29b00e435fd7ed5c625bb7188874d4c805f Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Mon, 17 Aug 2026 13:59:39 +0200 Subject: [PATCH] _transcribe_diarize: split 356 -> 164 + 143 + 105 The last Python file over the cap: _transcribe_media.py 143 _try_import, both duration probes, get_media_duration, ffmpeg transcode, cleanup _transcribe_cluster.py 105 _resample_linear, _kmeans_cosine and its iteration loop _transcribe_diarize.py 164 the pipeline: audio loading, the classifier, embedding extraction, diarize_segments The graph is a tree: _transcribe_media imports nothing local, _cluster takes only _try_import from it, _diarize imports from both. All 13 top-level definitions verified AST-identical to HEAD, and an AST scan confirms the three module constants travelled to the modules that use them. transcribe_fw.py now imports get_media_duration from _transcribe_media rather than through _transcribe_diarize. Ruff removed that re-export as unused and broke the import; running the program is what caught it, and naming the owning module is the correct fix under the no-barrels rule. Verified: transcribe_fw.py /nonexistent.wav still exits 2 and --help exits 0; 63 tests pass at 100% line and branch on all three modules; 338 pass in the directory. Over-cap files 33 -> 32, and no Python file is over the cap any more - the remaining 32 are 29 shell, 2 kotlin, 1 dart. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY --- .../transcribe-diarize-split-2026-08-17.json | 18 ++ .../transcribe-diarize-split-2026-08-17.json | 61 +++++ .../tools/_transcribe_cluster.py | 104 +++++++++ .../tools/_transcribe_diarize.py | 220 ++---------------- .../tools/_transcribe_media.py | 140 +++++++++++ .../testsAndMisc-bash/tools/transcribe_fw.py | 3 +- .../tests/test_transcribe_diarize_audio.py | 17 +- .../tests/test_transcribe_diarize_cluster.py | 2 +- .../tests/test_transcribe_diarize_probe.py | 2 +- 9 files changed, 350 insertions(+), 217 deletions(-) create mode 100644 docs/superpowers/contracts/transcribe-diarize-split-2026-08-17.json create mode 100644 docs/superpowers/evidence/transcribe-diarize-split-2026-08-17.json create mode 100755 linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/_transcribe_cluster.py mode change 100644 => 100755 linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/_transcribe_diarize.py create mode 100755 linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/_transcribe_media.py diff --git a/docs/superpowers/contracts/transcribe-diarize-split-2026-08-17.json b/docs/superpowers/contracts/transcribe-diarize-split-2026-08-17.json new file mode 100644 index 00000000..a10204db --- /dev/null +++ b/docs/superpowers/contracts/transcribe-diarize-split-2026-08-17.json @@ -0,0 +1,18 @@ +{ + "title": "Split _transcribe_diarize.py 356 -> 164 + 143 + 105", + "objective": "_transcribe_diarize.py was the last Python file over the 250-line cap. Now that it sits at 100% line and branch coverage, split it three ways along its natural seams: media probing, clustering maths, and the diarization pipeline itself. Code moves verbatim; the tests predate the seam and are re-pointed, not rewritten.", + "acceptance_criteria": [ + "All three resulting modules are under 250 lines; the over-cap total falls 33 -> 32 and no Python file remains over the cap", + "All 13 top-level definitions are AST-identical to the pre-split file", + "The same 63 tests pass, with all three modules at 100% line and branch coverage", + "python3 transcribe_fw.py /nonexistent.wav still exits 2, the code transcribe.sh branches on", + "Import sites name the owning module - no re-export barrels", + "No suppression of any kind" + ], + "out_of_scope": [ + "Any behaviour change", + "The 29 over-cap shell scripts, which are the remaining work", + "Moving anything under python_pkg/" + ], + "verifier": "A throwaway ast.dump identity check against HEAD; pytest with --cov of all three modules --cov-branch; running transcribe_fw.py and reading $? directly; pre-commit run --files; bash ~/utils/scripts/check_file_length.sh --all." +} diff --git a/docs/superpowers/evidence/transcribe-diarize-split-2026-08-17.json b/docs/superpowers/evidence/transcribe-diarize-split-2026-08-17.json new file mode 100644 index 00000000..191e3914 --- /dev/null +++ b/docs/superpowers/evidence/transcribe-diarize-split-2026-08-17.json @@ -0,0 +1,61 @@ +{ + "intent": "Split the 356-line _transcribe_diarize.py into media probing, clustering maths, and the diarization pipeline, all under the 250-line cap, moving every definition verbatim.", + "scope": [ + "_transcribe_diarize.py 356 -> 164 (audio loading, classifier, embeddings, diarize_segments)", + "New: _transcribe_media.py 143 (duration probing, ffmpeg transcode, temp cleanup)", + "New: _transcribe_cluster.py 105 (linear resampling, cosine k-means)", + "transcribe_fw.py: one import line re-pointed", + "Non-goal: any behaviour change" + ], + "changes": [ + "Extracted _try_import, both duration probes, get_media_duration, the ffmpeg transcode and _cleanup_temp into _transcribe_media.py; _resample_linear, _kmeans_cosine and _run_kmeans_iterations into _transcribe_cluster.py. What remains in _transcribe_diarize.py is the pipeline: loading audio, loading the classifier, extracting embeddings, and diarize_segments.", + "The dependency graph is a tree: _transcribe_media imports nothing local, _transcribe_cluster imports only _try_import from it, and _transcribe_diarize imports from both.", + "transcribe_fw.py now imports get_media_duration from _transcribe_media rather than through _transcribe_diarize. This was forced by ruff removing the now-unused re-export, and is the correct outcome under the no-barrels rule - import sites should name the owning module.", + "Test modules were re-pointed to the module that owns each function. The audio tests span two modules: _load_audio stayed put and still patches its own namespace, while the direct transcode and cleanup calls now name _transcribe_media." + ], + "verification": [ + { + "command": "python3 verify_split.py HEAD .../_transcribe_diarize.py ", + "result": "pass", + "evidence": "IDENTICAL: all 13 top-level definitions moved verbatim. Re-run after ruff's autofixes and the import re-point, and still identical." + }, + { + "command": "AST scan for names used but not bound in each of the three modules", + "result": "pass", + "evidence": "All three report no unbound names, confirming the three module-level constants (_NDIM_2D, _SAMPLE_RATE_16K, _MIN_SAMPLES_DIAR) travelled to the modules that use them." + }, + { + "command": "pytest linux_configuration/tests/test_transcribe_diarize_*.py --cov of all three modules --cov-branch", + "result": "pass", + "evidence": "63 passed - the same count as before the split. _transcribe_cluster.py 51 statements / 18 branches, _transcribe_diarize.py 81 / 22, _transcribe_media.py 59 / 12, all 100%." + }, + { + "command": "python3 transcribe_fw.py /nonexistent.wav; echo $?", + "result": "pass", + "evidence": "Exit 2, the code transcribe.sh branches on, and --help exits 0. This is what caught a real breakage: ruff had removed the get_media_duration re-export as unused, and transcribe_fw.py imported it from there. Only running it (and the full suite) surfaced that." + }, + { + "command": "pytest linux_configuration/tests/ -q", + "result": "pass", + "evidence": "338 passed, unchanged from before the split." + }, + { + "command": "pre-commit run --files ", + "result": "pass", + "evidence": "All hooks pass. The shebang-executable hook reads the git index rather than the filesystem, so the new modules' mode change had to be staged with git add --chmod=+x before it would pass." + }, + { + "command": "bash ~/utils/scripts/check_file_length.sh --all", + "result": "pass", + "evidence": "32 files over cap, down from 33. The three modules are 164, 143 and 105 lines. No Python file in the repo is now over the cap." + } + ], + "risks": [ + "A three-way split adds two import edges between sibling modules resolved by bare name on sys.path[0]. That is the existing convention in this directory and the real run confirms it resolves.", + "The get_media_duration re-export is gone, so any out-of-tree caller importing it from _transcribe_diarize would break. Only transcribe_fw.py imported it, and it was updated in the same commit." + ], + "rollback": [ + "git revert the commit; _transcribe_diarize.py returns to its 356-line form and the two new modules disappear.", + "After rollback, confirm python3 transcribe_fw.py /nonexistent.wav still returns 2 and pytest linux_configuration/tests/ reports 338 passed." + ] +} diff --git a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/_transcribe_cluster.py b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/_transcribe_cluster.py new file mode 100755 index 00000000..c9461634 --- /dev/null +++ b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/_transcribe_cluster.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Resampling and cosine k-means, the maths behind speaker labelling. + +numpy is fetched through ``_try_import`` rather than imported at module +scope, so this file remains importable on a machine without it - which is +what lets the transcribe tools be unit-tested in CI. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from _transcribe_media import _try_import + +if TYPE_CHECKING: + import types + + import numpy as np + import numpy.typing as npt + +_NDIM_2D = 2 + + +def _resample_linear( + x: npt.NDArray[np.float32], + src_sr: int, + tgt_sr: int, +) -> npt.NDArray[np.float32]: + """Linearly resample 1-D audio array.""" + np_mod = _try_import("numpy") + if np_mod is None: + msg = "numpy is required for resampling" + raise RuntimeError(msg) + + if src_sr == tgt_sr: + return x + ratio = float(tgt_sr) / float(src_sr) + n_out = max(1, round(x.shape[-1] * ratio)) + xp = np_mod.linspace(0.0, 1.0, num=x.shape[-1], endpoint=False) + xq = np_mod.linspace(0.0, 1.0, num=n_out, endpoint=False) + y = np_mod.interp(xq, xp, x.astype(np_mod.float32)) + return y.astype(np_mod.float32) + + +def _kmeans_cosine( + embs: list[Any], + k: int, + iters: int = 50, + seed: int = 0, +) -> npt.NDArray[np.int64]: + """Cluster embeddings with cosine-similarity k-means.""" + np_mod = _try_import("numpy") + if np_mod is None: + msg = "numpy is required for clustering" + raise RuntimeError(msg) + + rng = np_mod.random.default_rng(seed) + features = np_mod.asarray(embs, dtype=np_mod.float32) + if features.ndim != _NDIM_2D or features.shape[0] == 0: + return np_mod.zeros((0,), dtype=np_mod.int64) + features = features / (np_mod.linalg.norm(features, axis=1, keepdims=True) + 1e-8) + idxs = rng.choice( + features.shape[0], + size=min(k, features.shape[0]), + replace=False, + ) + centroids = features[idxs] + if centroids.shape[0] < k: + pad = rng.standard_normal( + size=( + k - centroids.shape[0], + features.shape[1], + ) + ).astype(np_mod.float32) + pad /= np_mod.linalg.norm(pad, axis=1, keepdims=True) + 1e-8 + centroids = np_mod.concatenate([centroids, pad], axis=0) + return _run_kmeans_iterations(np_mod, features, centroids, k, iters) + + +def _run_kmeans_iterations( + np_mod: types.ModuleType, + features: npt.NDArray[np.float32], + centroids: npt.NDArray[np.float32], + k: int, + iters: int, +) -> npt.NDArray[np.intp]: + """Run k-means iteration loop and return labels.""" + labels: Any = None + for _ in range(iters): + sims = features @ centroids.T + labels = sims.argmax(axis=1) + new_c = np_mod.zeros_like(centroids) + for j in range(k): + sel = features[labels == j] + if sel.shape[0] == 0: + new_c[j] = centroids[j] + else: + v = sel.mean(axis=0) + v /= np_mod.linalg.norm(v) + 1e-8 + new_c[j] = v + if np_mod.allclose(new_c, centroids, atol=1e-4): + break + centroids = new_c + return labels diff --git a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/_transcribe_diarize.py b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/_transcribe_diarize.py old mode 100644 new mode 100755 index c323e762..f5632355 --- a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/_transcribe_diarize.py +++ b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/_transcribe_diarize.py @@ -1,16 +1,24 @@ -"""Speaker diarization and audio processing utilities.""" +#!/usr/bin/env python3 +"""Speaker diarization: embed each segment, then cluster the embeddings. + +Every heavy dependency (torch, speechbrain, soundfile) is reached through +``_try_import``, so a machine without them degrades to no speaker labels +rather than failing the transcription. +""" from __future__ import annotations -import contextlib -import importlib import logging from pathlib import Path -import shutil -import subprocess -import tempfile from typing import TYPE_CHECKING, Any +from _transcribe_cluster import _kmeans_cosine, _resample_linear +from _transcribe_media import ( + _cleanup_temp, + _ffmpeg_transcode_to_wav16_mono, + _try_import, +) + if TYPE_CHECKING: import types @@ -24,206 +32,6 @@ _SAMPLE_RATE_16K = 16000 _MIN_SAMPLES_DIAR = 1600 -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 _probe_with_ffmpeg_python( - path: str, -) -> float | None: - """Try ffmpeg-python to get duration.""" - ffmpeg_mod = _try_import("ffmpeg") - if ffmpeg_mod is None: - return None - try: - probe = ffmpeg_mod.probe(path) - fmt = probe.get("format", {}) - if "duration" in fmt: - return float(fmt["duration"]) - except (OSError, RuntimeError): - pass - return None - - -def _probe_with_ffprobe(path: str) -> float | None: - """Try ffprobe CLI to get duration.""" - ffprobe_bin = shutil.which("ffprobe") - if ffprobe_bin is None: - return None - try: - out = subprocess.check_output( - [ - ffprobe_bin, - "-v", - "error", - "-show_entries", - "format=duration", - "-of", - "default=noprint_wrappers=1:nokey=1", - path, - ], - stderr=subprocess.DEVNULL, - ) - return float(out.decode().strip()) - except ( - OSError, - subprocess.CalledProcessError, - ValueError, - ): - return None - - -def get_media_duration(path: str) -> float | None: - """Try to get media duration in seconds. - - Returns None if unavailable. - """ - result = _probe_with_ffmpeg_python(path) - if result is not None: - return result - return _probe_with_ffprobe(path) - - -def _resample_linear( - x: npt.NDArray[np.float32], - src_sr: int, - tgt_sr: int, -) -> npt.NDArray[np.float32]: - """Linearly resample 1-D audio array.""" - np_mod = _try_import("numpy") - if np_mod is None: - msg = "numpy is required for resampling" - raise RuntimeError(msg) - - if src_sr == tgt_sr: - return x - ratio = float(tgt_sr) / float(src_sr) - n_out = max(1, round(x.shape[-1] * ratio)) - xp = np_mod.linspace(0.0, 1.0, num=x.shape[-1], endpoint=False) - xq = np_mod.linspace(0.0, 1.0, num=n_out, endpoint=False) - y = np_mod.interp(xq, xp, x.astype(np_mod.float32)) - return y.astype(np_mod.float32) - - -def _kmeans_cosine( - embs: list[Any], - k: int, - iters: int = 50, - seed: int = 0, -) -> npt.NDArray[np.int64]: - """Cluster embeddings with cosine-similarity k-means.""" - np_mod = _try_import("numpy") - if np_mod is None: - msg = "numpy is required for clustering" - raise RuntimeError(msg) - - rng = np_mod.random.default_rng(seed) - features = np_mod.asarray(embs, dtype=np_mod.float32) - if features.ndim != _NDIM_2D or features.shape[0] == 0: - return np_mod.zeros((0,), dtype=np_mod.int64) - features = features / (np_mod.linalg.norm(features, axis=1, keepdims=True) + 1e-8) - idxs = rng.choice( - features.shape[0], - size=min(k, features.shape[0]), - replace=False, - ) - centroids = features[idxs] - if centroids.shape[0] < k: - pad = rng.standard_normal( - size=( - k - centroids.shape[0], - features.shape[1], - ) - ).astype(np_mod.float32) - pad /= np_mod.linalg.norm(pad, axis=1, keepdims=True) + 1e-8 - centroids = np_mod.concatenate([centroids, pad], axis=0) - return _run_kmeans_iterations(np_mod, features, centroids, k, iters) - - -def _run_kmeans_iterations( - np_mod: types.ModuleType, - features: npt.NDArray[np.float32], - centroids: npt.NDArray[np.float32], - k: int, - iters: int, -) -> npt.NDArray[np.intp]: - """Run k-means iteration loop and return labels.""" - labels: Any = None - for _ in range(iters): - sims = features @ centroids.T - labels = sims.argmax(axis=1) - new_c = np_mod.zeros_like(centroids) - for j in range(k): - sel = features[labels == j] - if sel.shape[0] == 0: - new_c[j] = centroids[j] - else: - v = sel.mean(axis=0) - v /= np_mod.linalg.norm(v) + 1e-8 - new_c[j] = v - if np_mod.allclose(new_c, centroids, atol=1e-4): - break - centroids = new_c - return labels - - -def _ffmpeg_transcode_to_wav16_mono( - src_path: str, -) -> str | None: - """Transcode input to a temporary 16k mono WAV. - - Returns its path, or None if ffmpeg is unavailable. - """ - ffmpeg_bin = shutil.which("ffmpeg") - if ffmpeg_bin is None: - return None - with tempfile.NamedTemporaryFile( - prefix="fw_diar_", - suffix=".wav", - delete=False, - ) as tmp: - tmp_path = tmp.name - - cmd = [ - ffmpeg_bin, - "-y", - "-v", - "error", - "-i", - src_path, - "-ac", - "1", - "-ar", - "16000", - "-f", - "wav", - tmp_path, - ] - try: - subprocess.run( - cmd, - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - except (OSError, subprocess.CalledProcessError): - with contextlib.suppress(OSError): - Path(tmp_path).unlink() - return None - return tmp_path - - -def _cleanup_temp(path: str | None) -> None: - """Remove a temporary file if it exists.""" - if path is not None: - with contextlib.suppress(OSError): - Path(path).unlink() - - def _load_audio( audio_path: str, ) -> tuple[Any, int, str | None] | None: diff --git a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/_transcribe_media.py b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/_transcribe_media.py new file mode 100755 index 00000000..6b13ff03 --- /dev/null +++ b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/_transcribe_media.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Media duration probing and the ffmpeg transcode fallback. + +Duration is probed through the ffmpeg-python binding first, then by shelling +out to ffprobe; both are optional, and an unprobeable file simply reports no +duration rather than failing the run. +""" + +from __future__ import annotations + +import contextlib +import importlib +import logging +from pathlib import Path +import shutil +import subprocess +import tempfile +from typing import TYPE_CHECKING + +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 _probe_with_ffmpeg_python( + path: str, +) -> float | None: + """Try ffmpeg-python to get duration.""" + ffmpeg_mod = _try_import("ffmpeg") + if ffmpeg_mod is None: + return None + try: + probe = ffmpeg_mod.probe(path) + fmt = probe.get("format", {}) + if "duration" in fmt: + return float(fmt["duration"]) + except (OSError, RuntimeError): + pass + return None + + +def _probe_with_ffprobe(path: str) -> float | None: + """Try ffprobe CLI to get duration.""" + ffprobe_bin = shutil.which("ffprobe") + if ffprobe_bin is None: + return None + try: + out = subprocess.check_output( + [ + ffprobe_bin, + "-v", + "error", + "-show_entries", + "format=duration", + "-of", + "default=noprint_wrappers=1:nokey=1", + path, + ], + stderr=subprocess.DEVNULL, + ) + return float(out.decode().strip()) + except ( + OSError, + subprocess.CalledProcessError, + ValueError, + ): + return None + + +def get_media_duration(path: str) -> float | None: + """Try to get media duration in seconds. + + Returns None if unavailable. + """ + result = _probe_with_ffmpeg_python(path) + if result is not None: + return result + return _probe_with_ffprobe(path) + + +def _ffmpeg_transcode_to_wav16_mono( + src_path: str, +) -> str | None: + """Transcode input to a temporary 16k mono WAV. + + Returns its path, or None if ffmpeg is unavailable. + """ + ffmpeg_bin = shutil.which("ffmpeg") + if ffmpeg_bin is None: + return None + with tempfile.NamedTemporaryFile( + prefix="fw_diar_", + suffix=".wav", + delete=False, + ) as tmp: + tmp_path = tmp.name + + cmd = [ + ffmpeg_bin, + "-y", + "-v", + "error", + "-i", + src_path, + "-ac", + "1", + "-ar", + "16000", + "-f", + "wav", + tmp_path, + ] + try: + subprocess.run( + cmd, + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except (OSError, subprocess.CalledProcessError): + with contextlib.suppress(OSError): + Path(tmp_path).unlink() + return None + return tmp_path + + +def _cleanup_temp(path: str | None) -> None: + """Remove a temporary file if it exists.""" + if path is not None: + with contextlib.suppress(OSError): + Path(path).unlink() diff --git a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/transcribe_fw.py b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/transcribe_fw.py index a6504b05..247b80b6 100755 --- a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/transcribe_fw.py +++ b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools/transcribe_fw.py @@ -11,7 +11,8 @@ from pathlib import Path import sys from typing import TYPE_CHECKING, Any -from _transcribe_diarize import diarize_segments, get_media_duration +from _transcribe_diarize import diarize_segments +from _transcribe_media import get_media_duration from _transcribe_model import download_model_with_progress from _transcribe_output import ( hhmmss, diff --git a/linux_configuration/tests/test_transcribe_diarize_audio.py b/linux_configuration/tests/test_transcribe_diarize_audio.py index 0ebad4ed..fcabdc71 100644 --- a/linux_configuration/tests/test_transcribe_diarize_audio.py +++ b/linux_configuration/tests/test_transcribe_diarize_audio.py @@ -13,6 +13,7 @@ import subprocess from typing import TYPE_CHECKING import _transcribe_diarize as diar +import _transcribe_media as media import pytest if TYPE_CHECKING: @@ -46,7 +47,7 @@ def test_transcode_returns_none_without_ffmpeg( """No ffmpeg binary means no fallback transcode.""" monkeypatch.setattr(shutil, "which", lambda _b: None) - assert diar._ffmpeg_transcode_to_wav16_mono("clip.mp4") is None + assert media._ffmpeg_transcode_to_wav16_mono("clip.mp4") is None def test_transcode_returns_the_temp_path_on_success( @@ -61,13 +62,13 @@ def test_transcode_returns_the_temp_path_on_success( monkeypatch.setattr(subprocess, "run", _record) - result = diar._ffmpeg_transcode_to_wav16_mono("clip.mp4") + result = media._ffmpeg_transcode_to_wav16_mono("clip.mp4") assert result is not None assert result.endswith(".wav") assert "16000" in commands[0] assert "-ac" in commands[0] - diar._cleanup_temp(result) + media._cleanup_temp(result) @pytest.mark.parametrize( @@ -79,14 +80,14 @@ def test_transcode_cleans_up_after_a_failure( """A failed transcode removes the temp file it had already created.""" monkeypatch.setattr(shutil, "which", lambda _b: "/usr/bin/ffmpeg") removed: list[str] = [] - monkeypatch.setattr(diar.Path, "unlink", lambda self: removed.append(str(self))) + monkeypatch.setattr(media.Path, "unlink", lambda self: removed.append(str(self))) def _boom(*_a: object, **_k: object) -> None: raise error monkeypatch.setattr(subprocess, "run", _boom) - assert diar._ffmpeg_transcode_to_wav16_mono("clip.mp4") is None + assert media._ffmpeg_transcode_to_wav16_mono("clip.mp4") is None assert len(removed) == 1 @@ -95,19 +96,19 @@ def test_cleanup_temp_removes_an_existing_file(tmp_path: Path) -> None: victim = tmp_path / "scratch.wav" victim.write_bytes(b"RIFF") - diar._cleanup_temp(str(victim)) + media._cleanup_temp(str(victim)) assert not victim.exists() def test_cleanup_temp_ignores_none() -> None: """No temp file was created, so there is nothing to remove.""" - diar._cleanup_temp(None) + media._cleanup_temp(None) def test_cleanup_temp_survives_an_already_deleted_file(tmp_path: Path) -> None: """A file removed by something else is not an error.""" - diar._cleanup_temp(str(tmp_path / "never-existed.wav")) + media._cleanup_temp(str(tmp_path / "never-existed.wav")) def test_load_audio_returns_none_without_soundfile( diff --git a/linux_configuration/tests/test_transcribe_diarize_cluster.py b/linux_configuration/tests/test_transcribe_diarize_cluster.py index 3f4b01f2..19b2a225 100644 --- a/linux_configuration/tests/test_transcribe_diarize_cluster.py +++ b/linux_configuration/tests/test_transcribe_diarize_cluster.py @@ -7,7 +7,7 @@ mocked numpy would prove nothing about whether two speakers get two labels. from __future__ import annotations -import _transcribe_diarize as diar +import _transcribe_cluster as diar import numpy as np import pytest diff --git a/linux_configuration/tests/test_transcribe_diarize_probe.py b/linux_configuration/tests/test_transcribe_diarize_probe.py index e120f6d7..1c4ec3f3 100644 --- a/linux_configuration/tests/test_transcribe_diarize_probe.py +++ b/linux_configuration/tests/test_transcribe_diarize_probe.py @@ -10,7 +10,7 @@ from __future__ import annotations import shutil import subprocess -import _transcribe_diarize as diar +import _transcribe_media as diar import pytest