Merge pull request #36 from macaodha/fix/GH-31-negative-dimension-are-not-allowed

fix: Resolve detect Command Failure with Specific Audio Files (GH-31)
This commit is contained in:
Santiago Martinez Balvanera 2024-11-10 22:53:45 +00:00 committed by GitHub
commit 7dc28695b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 412 additions and 56 deletions

1
.gitignore vendored
View File

@ -110,5 +110,6 @@ experiments/*
!batdetect2_notebook.ipynb !batdetect2_notebook.ipynb
!batdetect2/models/*.pth.tar !batdetect2/models/*.pth.tar
!tests/data/*.wav !tests/data/*.wav
!tests/data/**/*.wav
notebooks/lightning_logs notebooks/lightning_logs
example_data/preprocessed example_data/preprocessed

View File

@ -1 +1,6 @@
__version__ = '1.0.8' import logging
numba_logger = logging.getLogger("numba")
numba_logger.setLevel(logging.WARNING)
__version__ = "1.0.8"

View File

@ -1,7 +1,5 @@
import glob import glob
import json import json
import os
import random
import numpy as np import numpy as np

View File

@ -1,4 +1,5 @@
"""Types used in the code base.""" """Types used in the code base."""
from typing import List, NamedTuple, Optional, Union from typing import List, NamedTuple, Optional, Union
import numpy as np import numpy as np
@ -17,7 +18,7 @@ except ImportError:
try: try:
from typing import NotRequired from typing import NotRequired # type: ignore
except ImportError: except ImportError:
from typing_extensions import NotRequired from typing_extensions import NotRequired

View File

@ -6,6 +6,8 @@ import librosa.core.spectrum
import numpy as np import numpy as np
import torch import torch
from batdetect2.detector import parameters
from . import wavfile from . import wavfile
__all__ = [ __all__ = [
@ -15,20 +17,44 @@ __all__ = [
] ]
def time_to_x_coords(time_in_file, sampling_rate, fft_win_length, fft_overlap): def time_to_x_coords(
nfft = np.floor(fft_win_length * sampling_rate) # int() uses floor time_in_file: float,
noverlap = np.floor(fft_overlap * nfft) samplerate: float = parameters.TARGET_SAMPLERATE_HZ,
return (time_in_file * sampling_rate - noverlap) / (nfft - noverlap) window_duration: float = parameters.FFT_WIN_LENGTH_S,
window_overlap: float = parameters.FFT_OVERLAP,
) -> float:
nfft = np.floor(window_duration * samplerate) # int() uses floor
noverlap = np.floor(window_overlap * nfft)
return (time_in_file * samplerate - noverlap) / (nfft - noverlap)
# NOTE this is also defined in post_process def x_coords_to_time(
def x_coords_to_time(x_pos, sampling_rate, fft_win_length, fft_overlap): x_pos: int,
nfft = np.floor(fft_win_length * sampling_rate) samplerate: float = parameters.TARGET_SAMPLERATE_HZ,
noverlap = np.floor(fft_overlap * nfft) window_duration: float = parameters.FFT_WIN_LENGTH_S,
return ((x_pos * (nfft - noverlap)) + noverlap) / sampling_rate window_overlap: float = parameters.FFT_OVERLAP,
) -> float:
n_fft = np.floor(window_duration * samplerate)
n_overlap = np.floor(window_overlap * n_fft)
n_step = n_fft - n_overlap
return ((x_pos * n_step) + n_overlap) / samplerate
# return (1.0 - fft_overlap) * fft_win_length * (x_pos + 0.5) # 0.5 is for center of temporal window # return (1.0 - fft_overlap) * fft_win_length * (x_pos + 0.5) # 0.5 is for center of temporal window
def x_coord_to_sample(
x_pos: int,
samplerate: float = parameters.TARGET_SAMPLERATE_HZ,
window_duration: float = parameters.FFT_WIN_LENGTH_S,
window_overlap: float = parameters.FFT_OVERLAP,
resize_factor: float = parameters.RESIZE_FACTOR,
) -> int:
n_fft = np.floor(window_duration * samplerate)
n_overlap = np.floor(window_overlap * n_fft)
n_step = n_fft - n_overlap
x_pos = int(x_pos / resize_factor)
return int((x_pos * n_step) + n_overlap)
def generate_spectrogram( def generate_spectrogram(
audio, audio,
sampling_rate, sampling_rate,
@ -184,55 +210,118 @@ def load_audio(
return sampling_rate, audio_raw return sampling_rate, audio_raw
def compute_spectrogram_width(
length: int,
samplerate: int = parameters.TARGET_SAMPLERATE_HZ,
window_duration: float = parameters.FFT_WIN_LENGTH_S,
window_overlap: float = parameters.FFT_OVERLAP,
resize_factor: float = parameters.RESIZE_FACTOR,
) -> int:
n_fft = int(window_duration * samplerate)
n_overlap = int(window_overlap * n_fft)
n_step = n_fft - n_overlap
width = (length - n_overlap) // n_step
return int(width * resize_factor)
def pad_audio( def pad_audio(
audio_raw, audio: np.ndarray,
fs, samplerate: int = parameters.TARGET_SAMPLERATE_HZ,
ms, window_duration: float = parameters.FFT_WIN_LENGTH_S,
overlap_perc, window_overlap: float = parameters.FFT_OVERLAP,
resize_factor, resize_factor: float = parameters.RESIZE_FACTOR,
divide_factor, divide_factor: int = parameters.SPEC_DIVIDE_FACTOR,
fixed_width=None, fixed_width: Optional[int] = None,
): ):
# Adds zeros to the end of the raw data so that the generated sepctrogram """Pad audio to be evenly divisible by `divide_factor`.
# will be evenly divisible by `divide_factor`
# Also deals with very short audio clips and fixed_width during training
# This code could be clearer, clean up This function pads the audio signal with zeros to ensure that the
nfft = int(ms * fs) generated spectrogram length will be evenly divisible by `divide_factor`.
noverlap = int(overlap_perc * nfft) This is important for the model to work correctly.
step = nfft - noverlap
min_size = int(divide_factor * (1.0 / resize_factor))
spec_width = (audio_raw.shape[0] - noverlap) // step
spec_width_rs = spec_width * resize_factor
if fixed_width is not None and spec_width < fixed_width: This `divide_factor` comes from the model architecture as it downscales
# too small the spectrogram by this factor, so the input must be divisible by this
# used during training to ensure all the batches are the same size integer number.
diff = fixed_width * step + noverlap - audio_raw.shape[0]
audio_raw = np.hstack( Parameters
(audio_raw, np.zeros(diff, dtype=audio_raw.dtype)) ----------
audio : np.ndarray
The audio signal.
samplerate : int
The sampling rate of the audio signal.
window_size : float
The window size in seconds used for the spectrogram computation.
window_overlap : float
The overlap between windows in the spectrogram computation.
resize_factor : float
This factor is used to resize the spectrogram after the STFT
computation. Default is 0.5 which means that the spectrogram will be
reduced by half. Important to take into account for the final size of
the spectrogram.
divide_factor : int
The factor by which the spectrogram will be divided.
fixed_width : int, optional
If provided, the audio will be padded or cut so that the resulting
spectrogram width will be equal to this value.
Returns
-------
np.ndarray
The padded audio signal.
"""
spec_width = compute_spectrogram_width(
audio.shape[0],
samplerate=samplerate,
window_duration=window_duration,
window_overlap=window_overlap,
resize_factor=resize_factor,
)
if fixed_width:
target_samples = x_coord_to_sample(
fixed_width,
samplerate=samplerate,
window_duration=window_duration,
window_overlap=window_overlap,
resize_factor=resize_factor,
) )
elif fixed_width is not None and spec_width > fixed_width: if spec_width < fixed_width:
# too big # need to be at least min_size
# used during training to ensure all the batches are the same size diff = target_samples - audio.shape[0]
diff = fixed_width * step + noverlap - audio_raw.shape[0] return np.hstack((audio, np.zeros(diff, dtype=audio.dtype)))
audio_raw = audio_raw[:diff]
elif ( if spec_width > fixed_width:
spec_width_rs < min_size return audio[:target_samples]
or (np.floor(spec_width_rs) % divide_factor) != 0
): return audio
# need to be at least min_size
div_amt = np.ceil(spec_width_rs / float(divide_factor)) min_width = int(divide_factor / resize_factor)
div_amt = np.maximum(1, div_amt)
target_size = int(div_amt * divide_factor * (1.0 / resize_factor)) if spec_width < min_width:
diff = target_size * step + noverlap - audio_raw.shape[0] target_samples = x_coord_to_sample(
audio_raw = np.hstack( min_width,
(audio_raw, np.zeros(diff, dtype=audio_raw.dtype)) samplerate=samplerate,
window_duration=window_duration,
window_overlap=window_overlap,
resize_factor=resize_factor,
) )
diff = target_samples - audio.shape[0]
return np.hstack((audio, np.zeros(diff, dtype=audio.dtype)))
return audio_raw if (spec_width % divide_factor) == 0:
return audio
target_width = int(np.ceil(spec_width / divide_factor)) * divide_factor
target_samples = x_coord_to_sample(
target_width,
samplerate=samplerate,
window_duration=window_duration,
window_overlap=window_overlap,
resize_factor=resize_factor,
)
diff = target_samples - audio.shape[0]
return np.hstack((audio, np.zeros(diff, dtype=audio.dtype)))
def gen_mag_spectrogram(x, fs, ms, overlap_perc): def gen_mag_spectrogram(x, fs, ms, overlap_perc):
@ -247,7 +336,11 @@ def gen_mag_spectrogram(x, fs, ms, overlap_perc):
# compute spec # compute spec
spec, _ = librosa.core.spectrum._spectrogram( spec, _ = librosa.core.spectrum._spectrogram(
y=x, power=1, n_fft=nfft, hop_length=step, center=False y=x,
power=1,
n_fft=nfft,
hop_length=step,
center=False,
) )
# remove DC component and flip vertical orientation # remove DC component and flip vertical orientation

View File

@ -11,7 +11,7 @@ import torch.nn.functional as F
try: try:
from numpy.exceptions import AxisError from numpy.exceptions import AxisError
except ImportError: except ImportError:
from numpy import AxisError from numpy import AxisError # type: ignore
import batdetect2.detector.compute_features as feats import batdetect2.detector.compute_features as feats
import batdetect2.detector.post_process as pp import batdetect2.detector.post_process as pp
@ -759,7 +759,7 @@ def process_file(
# Get original sampling rate # Get original sampling rate
file_samp_rate = librosa.get_samplerate(audio_file) file_samp_rate = librosa.get_samplerate(audio_file)
orig_samp_rate = file_samp_rate * config.get("time_expansion", 1) or 1 orig_samp_rate = file_samp_rate * (config.get("time_expansion") or 1)
# load audio file # load audio file
sampling_rate, audio_full = au.load_audio( sampling_rate, audio_full = au.load_audio(

View File

@ -53,6 +53,8 @@ batdetect2 = "batdetect2.cli:cli"
[tool.uv] [tool.uv]
dev-dependencies = [ dev-dependencies = [
"debugpy>=1.8.8",
"hypothesis>=6.118.7",
"pyright>=1.1.388", "pyright>=1.1.388",
"pytest>=7.2.2", "pytest>=7.2.2",
"ruff>=0.7.3", "ruff>=0.7.3",

17
tests/conftest.py Normal file
View File

@ -0,0 +1,17 @@
from pathlib import Path
import pytest
@pytest.fixture
def data_dir() -> Path:
dir = Path(__file__).parent / "data"
assert dir.exists()
return dir
@pytest.fixture
def contrib_dir(data_dir) -> Path:
dir = data_dir / "contrib"
assert dir.exists()
return dir

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

136
tests/test_audio_utils.py Normal file
View File

@ -0,0 +1,136 @@
import numpy as np
import torch
import torch.nn.functional as F
from hypothesis import given
from hypothesis import strategies as st
from batdetect2.detector import parameters
from batdetect2.utils import audio_utils, detector_utils
@given(duration=st.floats(min_value=0.1, max_value=2))
def test_can_compute_correct_spectrogram_width(duration: float):
samplerate = parameters.TARGET_SAMPLERATE_HZ
params = parameters.DEFAULT_SPECTROGRAM_PARAMETERS
length = int(duration * samplerate)
audio = np.random.rand(length)
spectrogram, _ = audio_utils.generate_spectrogram(
audio,
samplerate,
params,
)
# convert to pytorch
spectrogram = torch.from_numpy(spectrogram)
# add batch and channel dimensions
spectrogram = spectrogram.unsqueeze(0).unsqueeze(0)
# resize the spec
resize_factor = params["resize_factor"]
spec_op_shape = (
int(params["spec_height"] * resize_factor),
int(spectrogram.shape[-1] * resize_factor),
)
spectrogram = F.interpolate(
spectrogram,
size=spec_op_shape,
mode="bilinear",
align_corners=False,
)
expected_width = audio_utils.compute_spectrogram_width(
length,
samplerate=parameters.TARGET_SAMPLERATE_HZ,
window_duration=params["fft_win_length"],
window_overlap=params["fft_overlap"],
resize_factor=params["resize_factor"],
)
assert spectrogram.shape[-1] == expected_width
@given(duration=st.floats(min_value=0.1, max_value=2))
def test_pad_audio_without_fixed_size(duration: float):
# Test the pad_audio function
# This function is used to pad audio with zeros to a specific length
# It is used in the generate_spectrogram function
# The function is tested with a simplepas
samplerate = parameters.TARGET_SAMPLERATE_HZ
params = parameters.DEFAULT_SPECTROGRAM_PARAMETERS
length = int(duration * samplerate)
audio = np.random.rand(length)
# pad the audio to be divisible by divide factor
padded_audio = audio_utils.pad_audio(
audio,
samplerate=samplerate,
window_duration=params["fft_win_length"],
window_overlap=params["fft_overlap"],
resize_factor=params["resize_factor"],
divide_factor=params["spec_divide_factor"],
)
# check that the padded audio is divisible by the divide factor
expected_width = audio_utils.compute_spectrogram_width(
len(padded_audio),
samplerate=parameters.TARGET_SAMPLERATE_HZ,
window_duration=params["fft_win_length"],
window_overlap=params["fft_overlap"],
resize_factor=params["resize_factor"],
)
assert expected_width % params["spec_divide_factor"] == 0
@given(duration=st.floats(min_value=0.1, max_value=2))
def test_computed_spectrograms_are_actually_divisible_by_the_spec_divide_factor(
duration: float,
):
samplerate = parameters.TARGET_SAMPLERATE_HZ
params = parameters.DEFAULT_SPECTROGRAM_PARAMETERS
length = int(duration * samplerate)
audio = np.random.rand(length)
_, spectrogram, _ = detector_utils.compute_spectrogram(
audio,
samplerate,
params,
torch.device("cpu"),
)
assert spectrogram.shape[-1] % params["spec_divide_factor"] == 0
@given(
duration=st.floats(min_value=0.1, max_value=2),
width=st.integers(min_value=128, max_value=1024),
)
def test_pad_audio_with_fixed_width(duration: float, width: int):
samplerate = parameters.TARGET_SAMPLERATE_HZ
params = parameters.DEFAULT_SPECTROGRAM_PARAMETERS
length = int(duration * samplerate)
audio = np.random.rand(length)
# pad the audio to be divisible by divide factor
padded_audio = audio_utils.pad_audio(
audio,
samplerate=samplerate,
window_duration=params["fft_win_length"],
window_overlap=params["fft_overlap"],
resize_factor=params["resize_factor"],
divide_factor=params["spec_divide_factor"],
fixed_width=width,
)
# check that the padded audio is divisible by the divide factor
expected_width = audio_utils.compute_spectrogram_width(
len(padded_audio),
samplerate=parameters.TARGET_SAMPLERATE_HZ,
window_duration=params["fft_win_length"],
window_overlap=params["fft_overlap"],
resize_factor=params["resize_factor"],
)
assert expected_width == width

42
tests/test_contrib.py Normal file
View File

@ -0,0 +1,42 @@
"""Test suite to ensure user provided files are correctly processed."""
from pathlib import Path
from click.testing import CliRunner
from batdetect2.cli import cli
runner = CliRunner()
def test_files_negative_dimensions_are_not_allowed(
contrib_dir: Path,
tmp_path: Path,
):
"""This test stems from issue #31.
A user provided a set of files which which batdetect2 cli failed and
generated the following error message:
[2272] "Error processing file!: negative dimensions are not allowed"
This test ensures that the error message is not generated when running
batdetect2 cli with the same set of files.
"""
path = contrib_dir / "jeff37"
assert path.exists()
results_dir = tmp_path / "results"
result = runner.invoke(
cli,
[
"detect",
str(path),
str(results_dir),
"0.3",
],
)
assert result.exit_code == 0
assert results_dir.exists()
assert len(list(results_dir.glob("*.csv"))) == 5
assert len(list(results_dir.glob("*.json"))) == 5

61
uv.lock generated
View File

@ -6,6 +6,15 @@ resolution-markers = [
"python_full_version >= '3.12'", "python_full_version >= '3.12'",
] ]
[[package]]
name = "attrs"
version = "24.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 },
]
[[package]] [[package]]
name = "audioread" name = "audioread"
version = "3.0.1" version = "3.0.1"
@ -34,6 +43,8 @@ dependencies = [
[package.dependency-groups] [package.dependency-groups]
dev = [ dev = [
{ name = "debugpy" },
{ name = "hypothesis" },
{ name = "pyright" }, { name = "pyright" },
{ name = "pytest" }, { name = "pytest" },
{ name = "ruff" }, { name = "ruff" },
@ -55,6 +66,8 @@ requires-dist = [
[package.metadata.dependency-groups] [package.metadata.dependency-groups]
dev = [ dev = [
{ name = "debugpy", specifier = ">=1.8.8" },
{ name = "hypothesis", specifier = ">=6.118.7" },
{ name = "pyright", specifier = ">=1.1.388" }, { name = "pyright", specifier = ">=1.1.388" },
{ name = "pytest", specifier = ">=7.2.2" }, { name = "pytest", specifier = ">=7.2.2" },
{ name = "ruff", specifier = ">=0.7.3" }, { name = "ruff", specifier = ">=0.7.3" },
@ -283,6 +296,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 },
] ]
[[package]]
name = "debugpy"
version = "1.8.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/5e/7667b95c9d7ddb25c047143a3a47685f9be2a5d3d177a85a730b22dc6e5c/debugpy-1.8.8.zip", hash = "sha256:e6355385db85cbd666be703a96ab7351bc9e6c61d694893206f8001e22aee091", size = 4928684 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/79/677d71c342d5f24baf81d262c9e0c19cac3b17b4e4587c0574eaa3964ab1/debugpy-1.8.8-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:e59b1607c51b71545cb3496876544f7186a7a27c00b436a62f285603cc68d1c6", size = 2088337 },
{ url = "https://files.pythonhosted.org/packages/11/b3/4119fa89b66bcc64a3b186ea52ee7c22bccc5d1765ee890887678b0e3e76/debugpy-1.8.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6531d952b565b7cb2fbd1ef5df3d333cf160b44f37547a4e7cf73666aca5d8d", size = 3567953 },
{ url = "https://files.pythonhosted.org/packages/e8/4a/01f70b44af27c13d720446ce9bf14467c90411e90e6c6ffbb7c45845d23d/debugpy-1.8.8-cp310-cp310-win32.whl", hash = "sha256:b01f4a5e5c5fb1d34f4ccba99a20ed01eabc45a4684f4948b5db17a319dfb23f", size = 5128658 },
{ url = "https://files.pythonhosted.org/packages/2b/a5/c4210f3842db0911a49b3030bfc217e0772bfd33d7aa50996bc762e8a334/debugpy-1.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:535f4fb1c024ddca5913bb0eb17880c8f24ba28aa2c225059db145ee557035e9", size = 5157545 },
{ url = "https://files.pythonhosted.org/packages/38/55/6b5596ea6d5490e17abc2896f1fbe83d31205a22629805daccd30686721c/debugpy-1.8.8-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:c399023146e40ae373753a58d1be0a98bf6397fadc737b97ad612886b53df318", size = 2187057 },
{ url = "https://files.pythonhosted.org/packages/3f/f7/c2ee07f6335c3620c1435aef2c4d3d4853f6b7fb0789aa2c52a84498ef90/debugpy-1.8.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09cc7b162586ea2171eea055985da2702b0723f6f907a423c9b2da5996ad67ba", size = 3139844 },
{ url = "https://files.pythonhosted.org/packages/0d/68/01d335338b68bdebab11de573f4631c7bf0404666ccbf474621123497702/debugpy-1.8.8-cp311-cp311-win32.whl", hash = "sha256:eea8821d998ebeb02f0625dd0d76839ddde8cbf8152ebbe289dd7acf2cdc6b98", size = 5049405 },
{ url = "https://files.pythonhosted.org/packages/22/1d/3f69460b4b8f01dace3882513de71a446eb37ee57fe2112be948fadebde8/debugpy-1.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:d4483836da2a533f4b1454dffc9f668096ac0433de855f0c22cdce8c9f7e10c4", size = 5075025 },
{ url = "https://files.pythonhosted.org/packages/c2/04/8e79824c4d9100049bda056aeaf8f2765d1325a4521a87f8bb373c977236/debugpy-1.8.8-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:0cc94186340be87b9ac5a707184ec8f36547fb66636d1029ff4f1cc020e53996", size = 2514549 },
{ url = "https://files.pythonhosted.org/packages/a5/6b/c336d1eba1aedc9f654aefcdfe47ec41657d149f28ca1477c5f9009681c6/debugpy-1.8.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64674e95916e53c2e9540a056e5f489e0ad4872645399d778f7c598eacb7b7f9", size = 4229617 },
{ url = "https://files.pythonhosted.org/packages/63/9c/d9276c41e9e14164b31bcba789c87a355c091d0fc2d4e4e36a4881c9aa54/debugpy-1.8.8-cp312-cp312-win32.whl", hash = "sha256:5c6e885dbf12015aed73770f29dec7023cb310d0dc2ba8bfbeb5c8e43f80edc9", size = 5167033 },
{ url = "https://files.pythonhosted.org/packages/6d/1c/fd4bc22196b2d0defaa9f644ea4d676d0cb53b6434091b5fa2d4e49c85f2/debugpy-1.8.8-cp312-cp312-win_amd64.whl", hash = "sha256:19ffbd84e757a6ca0113574d1bf5a2298b3947320a3e9d7d8dc3377f02d9f864", size = 5209968 },
{ url = "https://files.pythonhosted.org/packages/3d/c8/7b1b654f7c21bac0e77272ee503b00f75e8acc8753efa542d4495591c741/debugpy-1.8.8-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:53709d4ec586b525724819dc6af1a7703502f7e06f34ded7157f7b1f963bb854", size = 2089581 },
{ url = "https://files.pythonhosted.org/packages/2d/87/57eb80944ce75f383946d79d9dd3ff0e0cd7c737f446be11661e3b963fbf/debugpy-1.8.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a9c013077a3a0000e83d97cf9cc9328d2b0bbb31f56b0e99ea3662d29d7a6a2", size = 3562815 },
{ url = "https://files.pythonhosted.org/packages/45/e1/23f65fbf5564cd8b3f126ab4a82c8a1a4728bdfd1b7fb0e2a856f794790e/debugpy-1.8.8-cp39-cp39-win32.whl", hash = "sha256:ffe94dd5e9a6739a75f0b85316dc185560db3e97afa6b215628d1b6a17561cb2", size = 5121656 },
{ url = "https://files.pythonhosted.org/packages/7c/f8/751ea54bb878fe965010d0492776671a7aab045937118b356027235e59ce/debugpy-1.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:5c0e5a38c7f9b481bf31277d2f74d2109292179081f11108e668195ef926c0f9", size = 5175678 },
{ url = "https://files.pythonhosted.org/packages/03/99/ec2190d03df5dbd610418919bd1c3d8e6f61d0a97894e11ade6d3260cfb8/debugpy-1.8.8-py2.py3-none-any.whl", hash = "sha256:ec684553aba5b4066d4de510859922419febc710df7bba04fe9e7ef3de15d34f", size = 5157124 },
]
[[package]] [[package]]
name = "decorator" name = "decorator"
version = "5.1.1" version = "5.1.1"
@ -360,6 +398,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/b2/454d6e7f0158951d8a78c2e1eb4f69ae81beb8dca5fee9809c6c99e9d0d0/fsspec-2024.10.0-py3-none-any.whl", hash = "sha256:03b9a6785766a4de40368b88906366755e2819e758b83705c88cd7cb5fe81871", size = 179641 }, { url = "https://files.pythonhosted.org/packages/c6/b2/454d6e7f0158951d8a78c2e1eb4f69ae81beb8dca5fee9809c6c99e9d0d0/fsspec-2024.10.0-py3-none-any.whl", hash = "sha256:03b9a6785766a4de40368b88906366755e2819e758b83705c88cd7cb5fe81871", size = 179641 },
] ]
[[package]]
name = "hypothesis"
version = "6.118.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "sortedcontainers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/16/31/7cbfc717e2f529472695ab97d508a9b995f8e463a9b8a699762cdaa48ee3/hypothesis-6.118.7.tar.gz", hash = "sha256:604328f5d766a056182f54b4826f9b2d5f664f42bff68fd81b4d9d6c44b2398b", size = 410787 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/4c/b87dc5c9ca9a4cc0c6828a90c2d1de6089f844e0c5badcdeac14fdb386c3/hypothesis-6.118.7-py3-none-any.whl", hash = "sha256:5fe1d80f46d81c6160ef762e4e11a61bb4eb6838a8fb7bd3c5a2542fb107bc38", size = 471912 },
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.10" version = "3.10"
@ -1260,6 +1312,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 },
] ]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 },
]
[[package]] [[package]]
name = "soundfile" name = "soundfile"
version = "0.12.1" version = "0.12.1"