Download src/tests/test_audio_decode.py from CIIL/Anuvani-Diarization: direct link, hf CLI and curl.
- Browser
- Download file 15.4 kB
-
https://huggingface.co/CIIL/Anuvani-Diarization/resolve/main/src/tests/test_audio_decode.py
- Command line
-
hf download hf://CIIL/Anuvani-Diarization/src/tests/test_audio_decode.py
-
curl -L -o test_audio_decode.py https://huggingface.co/CIIL/Anuvani-Diarization/resolve/main/src/tests/test_audio_decode.py
15.4 kB
| # Copyright (c) 2026 The Anuvani-Diarization authors | |
| # SPDX-License-Identifier: MIT OR Apache-2.0 | |
| """diar-audio against soundfile 0.14.0 (libsndfile 1.2.2) and the release's `load_audio`: decoded float32 samples, | |
| sample rates, channel means and error messages, compared bit for bit. | |
| The engine's input path is `decode_mono` (Rust), then torchaudio's resample when the rate is not 16 kHz (the same | |
| call the release makes, run by the network backend), then `validate` (Rust). `engine_load_audio` below composes | |
| exactly that, so the end-to-end comparison with the release's `load_audio` covers every branch. | |
| """ | |
| import json | |
| import os | |
| import sys | |
| import unicodedata | |
| from multiprocessing import get_context | |
| from pathlib import Path, PurePosixPath | |
| import numpy as np | |
| import pytest | |
| import soundfile as sf | |
| from hypothesis import given, strategies as st | |
| from hypothesis.extra import numpy as hnp | |
| from conftest import PROJ | |
| TEST_AUDIO = PROJ / "artifacts/test_audio" | |
| def load_audio_ref(): | |
| sys.path.insert(0, str(PROJ / "tests")) | |
| import refpkg | |
| refpkg.load() | |
| import torch | |
| # what load_bundle sets before any load_audio | |
| torch.set_num_threads(2) | |
| from anuvani_diar_ref.frontend import load_audio | |
| return load_audio | |
| class EngineError(Exception): | |
| """An error the engine reports as the string "ExceptionName: message".""" | |
| def outcome(fn, *a): | |
| try: | |
| return fn(*a), None | |
| except EngineError as e: | |
| return None, str(e) | |
| # the release's failure is part of the contract | |
| except Exception as e: | |
| return None, f"{type(e).__name__}: {e}" | |
| def engine_load_audio(core, path): | |
| """decode_mono (Rust) -> torchaudio resample if needed (the release's own call) -> validate (Rust).""" | |
| import torch | |
| import torchaudio.functional as F | |
| name = str(path) | |
| audio, sr, err = core.decode_mono(name) | |
| if audio is None: | |
| raise EngineError(err) | |
| if sr != 16000: | |
| audio = F.resample(torch.from_numpy(audio).unsqueeze(0), sr, 16000).squeeze(0).numpy().astype(np.float32) | |
| sr = 16000 | |
| err = core.validate_audio(name, audio) | |
| if err: | |
| raise EngineError(err) | |
| return audio, sr | |
| def same_load(core, load_audio_ref, path): | |
| ref, ref_err = outcome(load_audio_ref, Path(path)) | |
| got, got_err = outcome(engine_load_audio, core, Path(path)) | |
| if ref_err or got_err: | |
| return ref_err is not None and got_err == ref_err, (ref_err, got_err) | |
| (ra, rsr), (ga, gsr) = ref, got | |
| return (rsr == gsr and ra.dtype == ga.dtype and ra.shape == ga.shape and ra.tobytes() == ga.tobytes()), None | |
| # ---------------------------------------------------------------- synthetic files: every format and subtype | |
| def _signal(rng, n, ch, kind): | |
| x = (rng.standard_normal((n, ch)) * 0.3).astype(np.float64) | |
| if kind == "clip": | |
| # full scale | |
| x[: n // 3] = np.sign(x[: n // 3]) | |
| if kind == "edge": | |
| x = np.clip(x, -1, 1) | |
| if n > 8: | |
| x[0] = 1.0; x[1] = -1.0; x[2] = 0.0; x[3] = -0.0; x[4] = 1e-40; x[5] = 3.0; x[6] = -3.0 | |
| return np.clip(x, -1, 1) if kind != "edge" else x | |
| # (format, subtype, extension, rates) | |
| SPECS = [ | |
| ("WAV", "PCM_U8", "wav", (16000, 8000)), ("WAV", "PCM_16", "wav", (16000, 44100)), ("WAV", "PCM_24", "wav", (16000, 48000)), | |
| ("WAV", "PCM_32", "wav", (16000,)), ("WAV", "FLOAT", "wav", (16000, 22050)), ("WAV", "DOUBLE", "wav", (16000,)), | |
| ("WAV", "ULAW", "wav", (16000, 8000)), ("WAV", "ALAW", "wav", (8000,)), ("WAV", "IMA_ADPCM", "wav", (16000,)), | |
| ("WAV", "MS_ADPCM", "wav", (16000,)), ("WAV", "GSM610", "wav", (8000,)), | |
| ("FLAC", "PCM_S8", "flac", (16000,)), ("FLAC", "PCM_16", "flac", (16000, 44100)), ("FLAC", "PCM_24", "flac", (16000, 48000)), | |
| ("OGG", "VORBIS", "ogg", (16000, 44100)), ("OGG", "OPUS", "opus", (16000, 48000)), | |
| ("MP3", "MPEG_LAYER_III", "mp3", (16000, 44100)), | |
| ("AIFF", "PCM_16", "aiff", (16000,)), ("AIFF", "FLOAT", "aiff", (16000,)), ("AU", "PCM_16", "au", (16000,)), | |
| ("W64", "PCM_16", "w64", (16000,)), ("CAF", "PCM_16", "caf", (16000,)), ("RF64", "FLOAT", "rf64", (16000,)), | |
| ] | |
| CHANNELS = (1, 2, 3, 6, 8) | |
| LENGTHS = (1, 7, 4801, 3 * 16000 + 5) | |
| def synthetic(tmp_path_factory): | |
| d = tmp_path_factory.mktemp("audio") | |
| rng = np.random.default_rng(20261007) | |
| files = [] | |
| for fmt, sub, ext, rates in SPECS: | |
| for ch in CHANNELS: | |
| if sub == "GSM610" and ch > 1: | |
| continue | |
| for n in LENGTHS: | |
| for sr in rates: | |
| kind = ("plain", "clip", "edge")[(len(files)) % 3] | |
| x = _signal(rng, n, ch, kind if sub in ("FLOAT", "DOUBLE") else ("clip" if kind == "clip" else "plain")) | |
| p = d / f"{fmt}_{sub}_{ch}ch_{n}_{sr}.{ext}" | |
| try: | |
| sf.write(p, x if ch > 1 else x[:, 0], sr, subtype=sub, format=fmt) | |
| except Exception: | |
| # a combination libsndfile cannot write | |
| continue | |
| files.append(p) | |
| # float files carrying NaN / inf (the release rejects them after the mean) | |
| for name, val in (("nan", np.nan), ("posinf", np.inf), ("neginf", -np.inf)): | |
| x = _signal(rng, 32000, 2, "plain"); x[777, 1] = val | |
| p = d / f"WAV_FLOAT_special_{name}.wav"; sf.write(p, x, 16000, subtype="FLOAT"); files.append(p) | |
| # >= 8 channels: numpy's 8-accumulator pairwise sum | |
| for ch in (9, 12, 16, 33): | |
| x = _signal(rng, 9000, ch, "plain") | |
| p = d / f"WAV_FLOAT_{ch}ch.wav"; sf.write(p, x, 16000, subtype="FLOAT"); files.append(p) | |
| # empty files at both kinds of rate | |
| for sr in (16000, 8000): | |
| p = d / f"empty_{sr}.wav"; sf.write(p, np.zeros(0, np.float32), sr, subtype="PCM_16"); files.append(p) | |
| # not audio / unreadable | |
| (d / "text.wav").write_text("not a wav file\n"); files.append(d / "text.wav") | |
| trunc = d / "truncated.flac"; good = files[[str(f) for f in files].index(str(d / "FLAC_PCM_16_2ch_48005_16000.flac"))] | |
| trunc.write_bytes(good.read_bytes()[: len(good.read_bytes()) // 2]); files.append(trunc) | |
| (d / "zero_bytes.wav").write_bytes(b""); files.append(d / "zero_bytes.wav") | |
| files.append(d / "does_not_exist.wav") | |
| # a directory | |
| files.append(d) | |
| return files | |
| def test_synthetic_matrix_is_broad(synthetic): | |
| assert len(synthetic) > 250, len(synthetic) | |
| def test_sf_read_matches_soundfile(core, synthetic): | |
| core.sndfile_init() | |
| bad = [] | |
| for p in synthetic: | |
| ref, ref_err = outcome(sf.read, str(p), -1, 0, None, "float32", False) | |
| got, got_err = core.sf_read_f32(str(p)) | |
| if ref_err or got is None: | |
| if ref_err != got_err: | |
| bad.append((p.name, ref_err, got_err)) | |
| continue | |
| ra, rsr = ref | |
| if not (rsr == got_err and ra.dtype == got.dtype and ra.shape == got.shape and ra.tobytes() == got.tobytes()): | |
| bad.append((p.name, "data", ra.shape, getattr(got, "shape", None))) | |
| assert not bad, bad[:10] | |
| def test_load_audio_matches_release(core, load_audio_ref, synthetic): | |
| bad = [(p.name, info) for p in synthetic for ok, info in [same_load(core, load_audio_ref, p)] if not ok] | |
| assert not bad, bad[:10] | |
| def test_robustness_files_match_release(core, load_audio_ref): | |
| items = json.loads((TEST_AUDIO / "MANIFEST.json").read_text()) | |
| bad = [(i["sid"], info) for i in items for ok, info in [same_load(core, load_audio_ref, i["path"])] if not ok] | |
| assert len(items) == 28 and not bad, bad | |
| # ---------------------------------------------------------------- channel mean | |
| special = st.sampled_from([0.0, -0.0, 1.0, -1.0, 1e-45, -1e-45, 1e-38, 3.4e38, -3.4e38, np.inf, -np.inf, np.nan, 0.1, 1 / 3]) | |
| def test_channel_mean_matches_numpy(core, x): | |
| got = core.channel_mean(np.ascontiguousarray(x)) | |
| ref = x.mean(axis=1).astype(np.float32) if x.shape[0] else np.zeros(0, np.float32) | |
| na, nb = np.isnan(ref), np.isnan(got) | |
| assert np.array_equal(na, nb) and ref[~na].tobytes() == got[~nb].tobytes() | |
| def test_channel_mean_long_rows(core): | |
| rng = np.random.default_rng(3) | |
| for ch in (127, 128, 129, 255, 1000, 9000): | |
| x = rng.standard_normal((5, ch)).astype(np.float32) | |
| assert core.channel_mean(x).tobytes() == x.mean(axis=1).astype(np.float32).tobytes(), ch | |
| # ---------------------------------------------------------------- names and messages | |
| def test_repr_every_code_point(core): | |
| bad = [] | |
| for cp in range(0x110000): | |
| if 0xD800 <= cp <= 0xDFFF: | |
| continue | |
| s = chr(cp) | |
| if core.py_repr_str(s) != repr(s): | |
| bad.append(hex(cp)) | |
| assert not bad, (len(bad), bad[:20]) | |
| def test_repr_strings(core, s): | |
| if "\x00" in s: | |
| return | |
| assert core.py_repr_str(s) == repr(s) | |
| def test_pathlib(core, p): | |
| assert core.pathlib_str(p) == str(PurePosixPath(p)) | |
| assert core.pathlib_stem(p) == PurePosixPath(p).stem | |
| # ---------------------------------------------------------------- every replay input (slow) | |
| def _check_one(item): | |
| from anuvani_diar import _core as core | |
| import refpkg | |
| refpkg.load() | |
| import torch | |
| torch.set_num_threads(2) | |
| from anuvani_diar_ref.frontend import load_audio | |
| ok, info = same_load(core, load_audio, item["path"]) | |
| return item["set"] + "/" + item["sid"], ok, info | |
| def test_every_replay_input_decodes_identically(): | |
| items = json.loads((PROJ / "artifacts/oracle_audio_full_inputs.json").read_text()) | |
| with get_context("spawn").Pool(16, initializer=os.nice, initargs=(5,)) as pool: | |
| res = pool.map(_check_one, items, chunksize=4) | |
| bad = [r for r in res if not r[1]] | |
| assert len(res) == 1973 and not bad, bad[:10] | |
| # ---------------------------------------------------------------- damaged files (seeded mutations) | |
| MUTANT_BASES = [("WAV", "PCM_16", "wav", 2, 16000), ("WAV", "FLOAT", "wav", 1, 16000), ("FLAC", "PCM_16", "flac", 1, 16000), | |
| ("FLAC", "PCM_24", "flac", 2, 44100), ("OGG", "VORBIS", "ogg", 2, 16000), ("MP3", "MPEG_LAYER_III", "mp3", 1, 16000), | |
| ("AIFF", "PCM_16", "aiff", 1, 8000), ("W64", "PCM_16", "w64", 1, 16000), ("CAF", "PCM_16", "caf", 2, 16000), | |
| ("AU", "PCM_16", "au", 1, 16000), ("RF64", "FLOAT", "rf64", 1, 16000)] | |
| EDGE_WORDS = (0, 1, 2, 0x7F, 0xFF, 0x7FFF, 0xFFFF, 0x7FFFFFFF, 0x80000000, 0xFFFFFFFF) | |
| PLAIN_RATES = (8000, 16000, 22050, 44100, 48000) | |
| def mutate(data, rng): | |
| """One seeded damage: bits flipped in the header or anywhere, a cut, a header word set to an edge value, | |
| bytes inserted or removed, or junk appended.""" | |
| b = bytearray(data) | |
| kind = int(rng.integers(6)) | |
| if kind == 0: | |
| for _ in range(int(rng.integers(1, 4))): | |
| i = int(rng.integers(min(len(b), 64))) | |
| b[i] ^= 1 << int(rng.integers(8)) | |
| elif kind == 1: | |
| for _ in range(int(rng.integers(1, 8))): | |
| i = int(rng.integers(len(b))) | |
| b[i] ^= 1 << int(rng.integers(8)) | |
| elif kind == 2: | |
| b = b[: int(rng.integers(len(b)))] | |
| elif kind == 3: | |
| i = 4 * int(rng.integers(min(len(b), 128) // 4)) | |
| b[i:i + 4] = int(EDGE_WORDS[int(rng.integers(len(EDGE_WORDS)))]).to_bytes(4, "little" if rng.integers(2) else "big") | |
| elif kind == 4: | |
| i = int(rng.integers(len(b))) | |
| if rng.integers(2): | |
| b[i:i] = rng.integers(0, 256, int(rng.integers(1, 16)), dtype=np.uint8).tobytes() | |
| else: | |
| del b[i:i + int(rng.integers(1, 16))] | |
| else: | |
| b += rng.integers(0, 256, int(rng.integers(1, 4096)), dtype=np.uint8).tobytes() | |
| return bytes(b) | |
| def mutants(tmp_path_factory): | |
| d = tmp_path_factory.mktemp("mutants") | |
| rng = np.random.default_rng(20261008) | |
| files = [] | |
| for fmt, sub, ext, ch, sr in MUTANT_BASES: | |
| x = _signal(rng, 8000, ch, "plain") | |
| base = d / f"base_{fmt}_{sub}.{ext}" | |
| sf.write(base, x if ch > 1 else x[:, 0], sr, subtype=sub, format=fmt) | |
| data = base.read_bytes() | |
| for k in range(250): | |
| p = d / f"m_{fmt}_{sub}_{k:03d}.{ext}" | |
| p.write_bytes(mutate(data, rng)) | |
| files.append(p) | |
| return files | |
| def test_damaged_files_decode_as_soundfile(core, mutants): | |
| """2,750 damaged files: the same samples and rate as soundfile, or the same error text.""" | |
| core.sndfile_init() | |
| bad, kinds = [], set() | |
| for p in mutants: | |
| ref, ref_err = outcome(sf.read, str(p), -1, 0, None, "float32", False) | |
| got, got_err = core.sf_read_f32(str(p)) | |
| if ref_err or got is None: | |
| kinds.add((ref_err or "").split(":")[0]) | |
| if ref_err != got_err: | |
| bad.append((p.name, ref_err, got_err)) | |
| continue | |
| ra, rsr = ref | |
| if not (rsr == got_err and ra.dtype == got.dtype and ra.shape == got.shape and ra.tobytes() == got.tobytes()): | |
| bad.append((p.name, "data", ra.shape, getattr(got, "shape", None))) | |
| assert not bad, bad[:10] | |
| # the suite does reach the error paths | |
| assert "LibsndfileError" in kinds | |
| def test_damaged_files_load_as_the_release(core, load_audio_ref, mutants): | |
| """The whole input path (decode, channel mean, resample at the usual rates, validation) on the damaged files; | |
| files whose damage gives an unusual rate are compared at decoding only (above), since resampling an arbitrary | |
| rate is the release's own torchaudio call either way.""" | |
| bad, n = [], 0 | |
| for p in mutants: | |
| got, sr = core.sf_read_f32(str(p)) | |
| if got is not None and sr not in PLAIN_RATES: | |
| continue | |
| n += 1 | |
| ok, info = same_load(core, load_audio_ref, p) | |
| if not ok: | |
| bad.append((p.name, info)) | |
| assert n > 1500 and not bad, (n, bad[:10]) | |
| def test_memory_error_text_matches_numpy(core): | |
| from numpy.core._exceptions import _ArrayMemoryError | |
| sizes = set() | |
| for k in range(0, 62): | |
| for d in (-3, -1, 0, 1, 3): | |
| sizes.add((1 << k) + d) | |
| for unit in range(1, 7): | |
| for v in (999.4, 999.5, 999.6, 1023.4, 1023.5, 1023.6, 1.0, 1.005, 9.995, 99.95): | |
| sizes.add(int(v * (1 << (10 * unit)))) | |
| rng = np.random.default_rng(5) | |
| sizes.update(int(x) for x in rng.integers(1, 1 << 62, 3000, dtype=np.int64)) | |
| bad = [] | |
| # numpy refuses larger sizes before allocating | |
| for nb in sorted(s for s in sizes if 0 < s < 1 << 63): | |
| for ch in (1, 2): | |
| frames = nb // (4 * ch) | |
| if frames <= 0: | |
| continue | |
| shape = (frames,) if ch == 1 else (frames, ch) | |
| want = str(_ArrayMemoryError(shape, np.dtype("float32"))) | |
| got = core.numpy_memory_error(frames, ch) | |
| if got != want: | |
| bad.append((frames, ch, want, got)) | |
| assert not bad, bad[:5] | |