mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
185 lines
6.8 KiB
Python
185 lines
6.8 KiB
Python
"""Tests for the resampling and cosine k-means used to label speakers.
|
|
|
|
numpy is a declared dependency (meta/requirements.txt), so these run against
|
|
the real library rather than a double: clustering that "works" against a
|
|
mocked numpy would prove nothing about whether two speakers get two labels.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import _transcribe_cluster as diar
|
|
import numpy as np
|
|
import pytest
|
|
|
|
_SR_16K = 16000
|
|
_TWO_SPEAKERS = 2
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# _resample_linear
|
|
# --------------------------------------------------------------------------- #
|
|
def test_resample_linear_returns_the_input_unchanged_at_the_same_rate() -> None:
|
|
"""Resampling to the rate it already has is a no-op."""
|
|
audio = np.linspace(0.0, 1.0, num=100, dtype=np.float32)
|
|
|
|
result = diar._resample_linear(audio, _SR_16K, _SR_16K)
|
|
|
|
assert result is audio
|
|
|
|
|
|
def test_resample_linear_downsamples_to_the_expected_length() -> None:
|
|
"""Halving the rate halves the sample count."""
|
|
audio = np.linspace(0.0, 1.0, num=1000, dtype=np.float32)
|
|
|
|
result = diar._resample_linear(audio, 32000, _SR_16K)
|
|
|
|
assert result.shape[0] == 500
|
|
assert result.dtype == np.float32
|
|
|
|
|
|
def test_resample_linear_upsamples_to_the_expected_length() -> None:
|
|
"""Doubling the rate doubles the sample count."""
|
|
audio = np.linspace(0.0, 1.0, num=100, dtype=np.float32)
|
|
|
|
assert diar._resample_linear(audio, 8000, _SR_16K).shape[0] == 200
|
|
|
|
|
|
def test_resample_linear_preserves_a_constant_signal() -> None:
|
|
"""Linear interpolation of a flat signal stays flat."""
|
|
audio = np.full(100, 0.5, dtype=np.float32)
|
|
|
|
result = diar._resample_linear(audio, 8000, _SR_16K)
|
|
|
|
assert np.allclose(result, 0.5)
|
|
|
|
|
|
def test_resample_linear_never_returns_an_empty_array() -> None:
|
|
"""An extreme downsample still yields at least one sample."""
|
|
audio = np.linspace(0.0, 1.0, num=10, dtype=np.float32)
|
|
|
|
assert diar._resample_linear(audio, 48000, 1).shape[0] >= 1
|
|
|
|
|
|
def test_resample_linear_requires_numpy(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Without numpy the failure is explicit rather than an AttributeError."""
|
|
monkeypatch.setattr(diar, "_try_import", lambda _n: None)
|
|
|
|
with pytest.raises(RuntimeError, match="numpy is required"):
|
|
diar._resample_linear(np.zeros(10, dtype=np.float32), 8000, _SR_16K)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# _kmeans_cosine
|
|
# --------------------------------------------------------------------------- #
|
|
def test_kmeans_cosine_separates_two_opposed_clusters() -> None:
|
|
"""Embeddings pointing in two directions get two distinct labels."""
|
|
rng = np.random.default_rng(0)
|
|
group_a = np.tile([1.0, 0.0], (10, 1)) + rng.normal(0, 0.01, (10, 2))
|
|
group_b = np.tile([0.0, 1.0], (10, 1)) + rng.normal(0, 0.01, (10, 2))
|
|
embeddings = list(np.vstack([group_a, group_b]).astype(np.float32))
|
|
|
|
labels = diar._kmeans_cosine(embeddings, k=_TWO_SPEAKERS)
|
|
|
|
assert len(set(labels.tolist())) == _TWO_SPEAKERS
|
|
assert len(set(labels[:10].tolist())) == 1
|
|
assert len(set(labels[10:].tolist())) == 1
|
|
assert labels[0] != labels[10]
|
|
|
|
|
|
def test_kmeans_cosine_ignores_magnitude() -> None:
|
|
"""Cosine clustering keys on direction, so scale must not matter."""
|
|
embeddings = [
|
|
np.array([1.0, 0.0], dtype=np.float32),
|
|
np.array([100.0, 0.0], dtype=np.float32),
|
|
np.array([0.0, 1.0], dtype=np.float32),
|
|
np.array([0.0, 50.0], dtype=np.float32),
|
|
]
|
|
|
|
labels = diar._kmeans_cosine(embeddings, k=_TWO_SPEAKERS).tolist()
|
|
|
|
assert labels[0] == labels[1]
|
|
assert labels[2] == labels[3]
|
|
assert labels[0] != labels[2]
|
|
|
|
|
|
def test_kmeans_cosine_is_deterministic_for_a_fixed_seed() -> None:
|
|
"""The same input twice gives the same labels, so runs are reproducible."""
|
|
rng = np.random.default_rng(1)
|
|
embeddings = list(rng.normal(size=(20, 8)).astype(np.float32))
|
|
|
|
first = diar._kmeans_cosine(embeddings, k=3, seed=7).tolist()
|
|
second = diar._kmeans_cosine(embeddings, k=3, seed=7).tolist()
|
|
|
|
assert first == second
|
|
|
|
|
|
def test_kmeans_cosine_returns_empty_for_no_embeddings() -> None:
|
|
"""A file with no segments produces no labels."""
|
|
assert diar._kmeans_cosine([], k=_TWO_SPEAKERS).shape == (0,)
|
|
|
|
|
|
def test_kmeans_cosine_returns_empty_for_the_wrong_dimensionality() -> None:
|
|
"""A 1-D input is not a stack of embeddings and is rejected."""
|
|
assert diar._kmeans_cosine([1.0, 2.0, 3.0], k=_TWO_SPEAKERS).shape == (0,)
|
|
|
|
|
|
def test_kmeans_cosine_pads_when_asked_for_more_speakers_than_segments() -> None:
|
|
"""Requesting 5 speakers from 2 segments still returns one label each."""
|
|
embeddings = [
|
|
np.array([1.0, 0.0], dtype=np.float32),
|
|
np.array([0.0, 1.0], dtype=np.float32),
|
|
]
|
|
|
|
labels = diar._kmeans_cosine(embeddings, k=5)
|
|
|
|
assert labels.shape == (2,)
|
|
|
|
|
|
def test_kmeans_cosine_labels_every_segment_with_one_speaker() -> None:
|
|
"""k=1 assigns every segment the same label."""
|
|
rng = np.random.default_rng(2)
|
|
embeddings = list(rng.normal(size=(6, 4)).astype(np.float32))
|
|
|
|
assert set(diar._kmeans_cosine(embeddings, k=1).tolist()) == {0}
|
|
|
|
|
|
def test_kmeans_cosine_requires_numpy(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Without numpy the failure is explicit."""
|
|
monkeypatch.setattr(diar, "_try_import", lambda _n: None)
|
|
|
|
with pytest.raises(RuntimeError, match="numpy is required"):
|
|
diar._kmeans_cosine([np.zeros(4, dtype=np.float32)], k=1)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# _run_kmeans_iterations
|
|
# --------------------------------------------------------------------------- #
|
|
def test_run_kmeans_iterations_converges_and_stops_early() -> None:
|
|
"""Already-separated data converges on the first pass and breaks out."""
|
|
features = np.array(
|
|
[[1.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.0, 1.0]], dtype=np.float32
|
|
)
|
|
centroids = np.array([[1.0, 0.0], [0.0, 1.0]], dtype=np.float32)
|
|
|
|
labels = diar._run_kmeans_iterations(np, features, centroids, 2, 50)
|
|
|
|
assert labels.tolist() == [0, 0, 1, 1]
|
|
|
|
|
|
def test_run_kmeans_iterations_keeps_an_empty_cluster_centroid() -> None:
|
|
"""A centroid that attracts no points is carried forward, not zeroed."""
|
|
features = np.array([[1.0, 0.0], [1.0, 0.0]], dtype=np.float32)
|
|
centroids = np.array([[1.0, 0.0], [0.0, 1.0]], dtype=np.float32)
|
|
|
|
labels = diar._run_kmeans_iterations(np, features, centroids, 2, 5)
|
|
|
|
assert labels.tolist() == [0, 0]
|
|
|
|
|
|
def test_run_kmeans_iterations_returns_none_when_asked_for_no_iterations() -> None:
|
|
"""A zero-iteration run never assigns labels; the loop body is skipped."""
|
|
features = np.array([[1.0, 0.0]], dtype=np.float32)
|
|
centroids = np.array([[1.0, 0.0]], dtype=np.float32)
|
|
|
|
assert diar._run_kmeans_iterations(np, features, centroids, 1, 0) is None
|