diff --git a/batdetect2/cli/__init__.py b/batdetect2/cli/__init__.py new file mode 100644 index 0000000..9111b01 --- /dev/null +++ b/batdetect2/cli/__init__.py @@ -0,0 +1,11 @@ +from batdetect2.cli.base import cli +from batdetect2.cli.compat import detect + +__all__ = [ + "cli", + "detect", +] + + +if __name__ == "__main__": + cli() diff --git a/batdetect2/cli/base.py b/batdetect2/cli/base.py new file mode 100644 index 0000000..fc379ee --- /dev/null +++ b/batdetect2/cli/base.py @@ -0,0 +1,27 @@ +"""BatDetect2 command line interface.""" + +import os + +import click + + +__all__ = [ + "cli", +] + + +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +INFO_STR = """ +BatDetect2 - Detection and Classification + Assumes audio files are mono, not stereo. + Spaces in the input paths will throw an error. Wrap in quotes. + Input files should be short in duration e.g. < 30 seconds. +""" + + +@click.group() +def cli(): + """BatDetect2 - Bat Call Detection and Classification.""" + click.echo(INFO_STR) diff --git a/batdetect2/cli.py b/batdetect2/cli/compat.py similarity index 88% rename from batdetect2/cli.py rename to batdetect2/cli/compat.py index 66aada8..9d1f977 100644 --- a/batdetect2/cli.py +++ b/batdetect2/cli/compat.py @@ -1,5 +1,4 @@ """BatDetect2 command line interface.""" -import os import click @@ -8,21 +7,7 @@ from batdetect2.detector.parameters import DEFAULT_MODEL_PATH from batdetect2.types import ProcessingConfiguration from batdetect2.utils.detector_utils import save_results_to_file -CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) - - -INFO_STR = """ -BatDetect2 - Detection and Classification - Assumes audio files are mono, not stereo. - Spaces in the input paths will throw an error. Wrap in quotes. - Input files should be short in duration e.g. < 30 seconds. -""" - - -@click.group() -def cli(): - """BatDetect2 - Bat Call Detection and Classification.""" - click.echo(INFO_STR) +from batdetect2.cli.base import cli @cli.command() @@ -147,7 +132,3 @@ def print_config(config: ProcessingConfiguration): click.echo("\nProcessing Configuration:") click.echo(f"Time Expansion Factor: {config.get('time_expansion')}") click.echo(f"Detection Threshold: {config.get('detection_threshold')}") - - -if __name__ == "__main__": - cli() diff --git a/batdetect2/data/__init__.py b/batdetect2/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/batdetect2/data/augmentations.py b/batdetect2/data/augmentations.py new file mode 100644 index 0000000..11d82f2 --- /dev/null +++ b/batdetect2/data/augmentations.py @@ -0,0 +1,304 @@ +from functools import wraps +from typing import Callable, List, Optional, Tuple + +import numpy as np +import xarray as xr +from soundevent import data +from soundevent.geometry import compute_bounds + +ClipAugmentation = Callable[[data.ClipAnnotation], data.ClipAnnotation] +AudioAugmentation = Callable[ + [xr.DataArray, data.ClipAnnotation], + Tuple[xr.DataArray, data.ClipAnnotation], +] +SpecAugmentation = Callable[ + [xr.DataArray, data.ClipAnnotation], + Tuple[xr.DataArray, data.ClipAnnotation], +] + +ClipProvider = Callable[ + [data.ClipAnnotation], Tuple[xr.DataArray, data.ClipAnnotation] +] +"""A function that provides some clip and its annotation. + +Usually this function loads a random clip from a dataset. Takes +as input a clip annotation that can be used to filter the clips +to load (in case you want to avoid loading the same clip multiple times). +""" + + +AUGMENTATION_PROBABILITY = 0.2 +MAX_DELAY = 0.005 +STRETCH_SQUEEZE_DELTA = 0.04 +MASK_MAX_TIME_PERC: float = 0.05 +MASK_MAX_FREQ_PERC: float = 0.10 + + +def maybe_apply( + augmentation: Callable, + prob: float = AUGMENTATION_PROBABILITY, +) -> Callable: + """Apply an augmentation with a given probability.""" + + @wraps(augmentation) + def _augmentation(x): + if np.random.rand() > prob: + return x + return augmentation(x) + + return _augmentation + + +def select_random_subclip( + clip_annotation: data.ClipAnnotation, + duration: Optional[float] = None, + proportion: float = 0.9, +) -> data.ClipAnnotation: + """Select a random subclip from a clip.""" + clip = clip_annotation.clip + + if duration is None: + clip_duration = clip.end_time - clip.start_time + duration = clip_duration * proportion + + start_time = np.random.uniform(clip.start_time, clip.end_time - duration) + return clip_annotation.model_copy( + update=dict( + clip=clip.model_copy( + update=dict( + start_time=start_time, + end_time=start_time + duration, + ) + ) + ) + ) + + +def combine_audio( + audio1: xr.DataArray, + audio2: xr.DataArray, + alpha: Optional[float] = None, + min_alpha: float = 0.3, + max_alpha: float = 0.7, +) -> xr.DataArray: + """Combine two audio clips.""" + + if alpha is None: + alpha = np.random.uniform(min_alpha, max_alpha) + + return alpha * audio1 + (1 - alpha) * audio2.data + + +def random_mix( + audio: xr.DataArray, + clip: data.ClipAnnotation, + provider: Optional[ClipProvider] = None, + alpha: Optional[float] = None, + min_alpha: float = 0.3, + max_alpha: float = 0.7, + join_annotations: bool = True, +) -> Tuple[xr.DataArray, data.ClipAnnotation]: + """Mix two audio clips.""" + if provider is None: + raise ValueError("No audio provider given.") + + try: + other_audio, other_clip = provider(clip) + except (StopIteration, ValueError): + raise ValueError("No more audio sources available.") + + new_audio = combine_audio( + audio, + other_audio, + alpha=alpha, + min_alpha=min_alpha, + max_alpha=max_alpha, + ) + + if join_annotations: + clip = clip.model_copy( + update=dict( + sound_events=clip.sound_events + other_clip.sound_events, + ) + ) + + return new_audio, clip + + +def add_echo( + audio: xr.DataArray, + clip: data.ClipAnnotation, + delay: Optional[float] = None, + alpha: Optional[float] = None, + min_alpha: float = 0.0, + max_alpha: float = 1.0, + max_delay: float = MAX_DELAY, +) -> Tuple[xr.DataArray, data.ClipAnnotation]: + """Add a delay to the audio.""" + if delay is None: + delay = np.random.uniform(0, max_delay) + + if alpha is None: + alpha = np.random.uniform(min_alpha, max_alpha) + + samplerate = audio.attrs["samplerate"] + offset = int(delay * samplerate) + + # NOTE: We use the copy method to avoid modifying the original audio + # data. + new_audio = audio.copy() + new_audio[offset:] += alpha * audio.data[:-offset] + return new_audio, clip + + +def scale_volume( + spec: xr.DataArray, + clip: data.ClipAnnotation, + factor: Optional[float] = None, + max_scaling: float = 2, + min_scaling: float = 0, +) -> Tuple[xr.DataArray, data.ClipAnnotation]: + """Scale the volume of a spectrogram.""" + if factor is None: + factor = np.random.uniform(min_scaling, max_scaling) + + return spec * factor, clip + + +def scale_sound_event_annotation( + sound_event_annotation: data.SoundEventAnnotation, + time_factor: float = 1, + frequency_factor: float = 1, +) -> data.SoundEventAnnotation: + sound_event = sound_event_annotation.sound_event + geometry = sound_event.geometry + + if geometry is None: + return sound_event_annotation + + start_time, low_freq, end_time, high_freq = compute_bounds(geometry) + new_geometry = data.BoundingBox( + coordinates=[ + start_time * time_factor, + low_freq * frequency_factor, + end_time * time_factor, + high_freq * frequency_factor, + ] + ) + + return sound_event_annotation.model_copy( + update=dict( + sound_event=sound_event.model_copy( + update=dict( + geometry=new_geometry, + ) + ) + ) + ) + + +def warp_spectrogram( + spec: xr.DataArray, + clip: data.ClipAnnotation, + factor: Optional[float] = None, + delta: float = STRETCH_SQUEEZE_DELTA, +) -> Tuple[xr.DataArray, data.ClipAnnotation]: + """Warp a spectrogram.""" + if factor is None: + factor = np.random.uniform(1 - delta, 1 + delta) + + start_time = clip.clip.start_time + end_time = clip.clip.end_time + duration = end_time - start_time + new_time = np.linspace( + start_time, + start_time + duration * factor, + spec.time.size, + ) + + scaled_spec = spec.interp( + time=new_time, + method="linear", + kwargs={"fill_value": 0}, + ) + scaled_spec.coords["time"] = spec.time + + scaled_clip = clip.model_copy( + update=dict( + sound_events=[ + scale_sound_event_annotation( + sound_event_annotation, + time_factor=1 / factor, + ) + for sound_event_annotation in clip.sound_events + ] + ) + ) + return scaled_spec, scaled_clip + + +def mask_axis( + array: xr.DataArray, + axis: str, + start: float, + end: float, + mask_value: float = 0, +) -> xr.DataArray: + if axis not in array.dims: + raise ValueError(f"Axis {axis} not found in array") + + coord = array[axis] + return array.where((coord < start) | (coord > end), mask_value) + + +def mask_time( + spec: xr.DataArray, + clip: data.ClipAnnotation, + max_time_mask: float = MASK_MAX_TIME_PERC, + max_num_masks: int = 3, +) -> Tuple[xr.DataArray, data.ClipAnnotation]: + """Mask a random section of the time axis.""" + + num_masks = np.random.randint(1, max_num_masks + 1) + for _ in range(num_masks): + mask_size = np.random.uniform(0, max_time_mask) + start = np.random.uniform(0, spec.time[-1] - mask_size) + end = start + mask_size + spec = mask_axis(spec, "time", start, end) + + return spec, clip + + +def mask_frequency( + spec: xr.DataArray, + clip: data.ClipAnnotation, + max_freq_mask: float = MASK_MAX_FREQ_PERC, + max_num_masks: int = 3, +) -> Tuple[xr.DataArray, data.ClipAnnotation]: + """Mask a random section of the frequency axis.""" + + num_masks = np.random.randint(1, max_num_masks + 1) + for _ in range(num_masks): + mask_size = np.random.uniform(0, max_freq_mask) + start = np.random.uniform(0, spec.frequency[-1] - mask_size) + end = start + mask_size + spec = mask_axis(spec, "frequency", start, end) + + return spec, clip + + +CLIP_AUGMENTATIONS: List[ClipAugmentation] = [ + select_random_subclip, +] + +AUDIO_AUGMENTATIONS: List[AudioAugmentation] = [ + add_echo, + random_mix, +] + +SPEC_AUGMENTATIONS: List[SpecAugmentation] = [ + scale_volume, + warp_spectrogram, + mask_time, + mask_frequency, +] diff --git a/batdetect2/data/compat.py b/batdetect2/data/compat.py new file mode 100644 index 0000000..f2398f0 --- /dev/null +++ b/batdetect2/data/compat.py @@ -0,0 +1,332 @@ +"""Compatibility functions between old and new data structures.""" + +import os +import uuid +from pathlib import Path +from typing import Callable, List, Optional, Union + +import numpy as np +from pydantic import BaseModel, Field +from soundevent import data +from soundevent.geometry import compute_bounds + +from batdetect2 import types +from batdetect2.data.labels import LabelFn + +PathLike = Union[Path, str, os.PathLike] + +__all__ = [ + "convert_to_annotation_group", + "load_annotation_project", +] + +SPECIES_TAG_KEY = "species" +ECHOLOCATION_EVENT = "Echolocation" +UNKNOWN_CLASS = "__UNKNOWN__" + +NAMESPACE = uuid.UUID("97a9776b-c0fd-4c68-accb-0b0ecd719242") + + +EventFn = Callable[[data.SoundEventAnnotation], Optional[str]] + +ClassFn = Callable[[data.Recording], int] + +IndividualFn = Callable[[data.SoundEventAnnotation], int] + + +def get_recording_class_name(recording: data.Recording) -> str: + """Get the class name for a recording.""" + tag = data.find_tag(recording.tags, SPECIES_TAG_KEY) + if tag is None: + return UNKNOWN_CLASS + return tag.value + + +def get_annotation_notes(annotation: data.ClipAnnotation) -> str: + """Get the notes for a ClipAnnotation.""" + all_notes = [ + *annotation.notes, + *annotation.clip.recording.notes, + ] + messages = [note.message for note in all_notes if note.message is not None] + return "\n".join(messages) + + +def convert_to_annotation_group( + annotation: data.ClipAnnotation, + label_fn: LabelFn = lambda _: None, + event_fn: EventFn = lambda _: ECHOLOCATION_EVENT, + class_fn: ClassFn = lambda _: 0, + individual_fn: IndividualFn = lambda _: 0, +) -> types.AudioLoaderAnnotationGroup: + """Convert a ClipAnnotation to an AudioLoaderAnnotationGroup.""" + recording = annotation.clip.recording + + start_times = [] + end_times = [] + low_freqs = [] + high_freqs = [] + class_ids = [] + x_inds = [] + y_inds = [] + individual_ids = [] + annotations: List[types.Annotation] = [] + class_id_file = class_fn(recording) + + for sound_event in annotation.sound_events: + geometry = sound_event.sound_event.geometry + + if geometry is None: + continue + + start_time, low_freq, end_time, high_freq = compute_bounds(geometry) + class_id = label_fn(sound_event) or -1 + event = event_fn(sound_event) + individual_id = individual_fn(sound_event) or -1 + + start_times.append(start_time) + end_times.append(end_time) + low_freqs.append(low_freq) + high_freqs.append(high_freq) + class_ids.append(class_id) + individual_ids.append(individual_id) + + # NOTE: This will be computed later so we just put a placeholder + # here for now. + x_inds.append(0) + y_inds.append(0) + + annotations.append( + { + "start_time": start_time, + "end_time": end_time, + "low_freq": low_freq, + "high_freq": high_freq, + "class_prob": 1.0, + "det_prob": 1.0, + "individual": "0", + "event": event, + "class_id": class_id, # type: ignore + } + ) + + return { + "id": str(recording.path), + "duration": recording.duration, + "issues": False, + "file_path": str(recording.path), + "time_exp": recording.time_expansion, + "class_name": get_recording_class_name(recording), + "notes": get_annotation_notes(annotation), + "annotated": True, + "start_times": np.array(start_times), + "end_times": np.array(end_times), + "low_freqs": np.array(low_freqs), + "high_freqs": np.array(high_freqs), + "class_ids": np.array(class_ids), + "x_inds": np.array(x_inds), + "y_inds": np.array(y_inds), + "individual_ids": np.array(individual_ids), + "annotation": annotations, + "class_id_file": class_id_file, + } + + +class Annotation(BaseModel): + """Annotation class to hold batdetect annotations.""" + + label: str = Field(alias="class") + event: str + individual: int = 0 + + start_time: float + end_time: float + low_freq: float + high_freq: float + + +class FileAnnotation(BaseModel): + """FileAnnotation class to hold batdetect annotations for a file.""" + + id: str + duration: float + time_exp: float = 1 + + label: str = Field(alias="class_name") + + annotation: List[Annotation] + + annotated: bool = False + issues: bool = False + notes: str = "" + + +def load_file_annotation(path: PathLike) -> FileAnnotation: + """Load annotation from batdetect format.""" + path = Path(path) + return FileAnnotation.model_validate_json(path.read_text()) + + +def annotation_to_sound_event( + annotation: Annotation, + recording: data.Recording, + label_key: str = "class", + event_key: str = "event", + individual_key: str = "individual", +) -> data.SoundEventAnnotation: + """Convert annotation to sound event annotation.""" + sound_event = data.SoundEvent( + uuid=uuid.uuid5( + NAMESPACE, + f"{recording.hash}_{annotation.start_time}_{annotation.end_time}", + ), + recording=recording, + geometry=data.BoundingBox( + coordinates=[ + annotation.start_time, + annotation.low_freq, + annotation.end_time, + annotation.high_freq, + ], + ), + ) + + return data.SoundEventAnnotation( + uuid=uuid.uuid5(NAMESPACE, f"{sound_event.uuid}_annotation"), + sound_event=sound_event, + tags=[ + data.Tag(key=label_key, value=annotation.label), + data.Tag(key=event_key, value=annotation.event), + data.Tag(key=individual_key, value=str(annotation.individual)), + ], + ) + + +def file_annotation_to_clip( + file_annotation: FileAnnotation, + audio_dir: PathLike = Path.cwd(), +) -> data.Clip: + """Convert file annotation to recording.""" + full_path = Path(audio_dir) / file_annotation.id + + if not full_path.exists(): + raise FileNotFoundError(f"File {full_path} not found.") + + recording = data.Recording.from_file( + full_path, + time_expansion=file_annotation.time_exp, + ) + + return data.Clip( + uuid=uuid.uuid5(NAMESPACE, f"{file_annotation.id}_clip"), + recording=recording, + start_time=0, + end_time=recording.duration, + ) + + +def file_annotation_to_clip_annotation( + file_annotation: FileAnnotation, + clip: data.Clip, + label_key: str = "class", + event_key: str = "event", + individual_key: str = "individual", +) -> data.ClipAnnotation: + """Convert file annotation to clip annotation.""" + notes = [] + if file_annotation.notes: + notes.append(data.Note(message=file_annotation.notes)) + + return data.ClipAnnotation( + uuid=uuid.uuid5(NAMESPACE, f"{file_annotation.id}_clip_annotation"), + clip=clip, + notes=notes, + tags=[data.Tag(key=label_key, value=file_annotation.label)], + sound_events=[ + annotation_to_sound_event( + annotation, + clip.recording, + label_key=label_key, + event_key=event_key, + individual_key=individual_key, + ) + for annotation in file_annotation.annotation + ], + ) + + +def file_annotation_to_annotation_task( + file_annotation: FileAnnotation, + clip: data.Clip, +) -> data.AnnotationTask: + status_badges = [] + + if file_annotation.issues: + status_badges.append( + data.StatusBadge(state=data.AnnotationState.rejected) + ) + elif file_annotation.annotated: + status_badges.append( + data.StatusBadge(state=data.AnnotationState.completed) + ) + + return data.AnnotationTask( + uuid=uuid.uuid5(uuid.NAMESPACE_URL, f"{file_annotation.id}_task"), + clip=clip, + status_badges=status_badges, + ) + + +def list_file_annotations(path: PathLike) -> List[Path]: + """List all annotations in a directory.""" + path = Path(path) + return [file for file in path.glob("*.json")] + + +def load_annotation_project( + path: PathLike, + name: Optional[str] = None, + audio_dir: PathLike = Path.cwd(), +) -> data.AnnotationProject: + """Convert annotations to annotation project.""" + paths = list_file_annotations(path) + + if name is None: + name = str(path) + + annotations = [] + tasks = [] + + for p in paths: + try: + file_annotation = load_file_annotation(p) + except FileNotFoundError: + continue + + try: + clip = file_annotation_to_clip( + file_annotation, + audio_dir=audio_dir, + ) + except FileNotFoundError: + continue + + annotations.append( + file_annotation_to_clip_annotation( + file_annotation, + clip, + ) + ) + + tasks.append( + file_annotation_to_annotation_task( + file_annotation, + clip, + ) + ) + + return data.AnnotationProject( + name=name, + clip_annotations=annotations, + tasks=tasks, + ) diff --git a/batdetect2/data/datasets.py b/batdetect2/data/datasets.py new file mode 100644 index 0000000..b215973 --- /dev/null +++ b/batdetect2/data/datasets.py @@ -0,0 +1,58 @@ +from typing import Callable, Generic, Iterable, List, TypeVar + +from soundevent import data +from torch.utils.data import Dataset + +__all__ = [ + "ClipAnnotationDataset", + "ClipDataset", +] + + +E = TypeVar("E") + + +class ClipAnnotationDataset(Dataset, Generic[E]): + + clip_annotations: List[data.ClipAnnotation] + + transform: Callable[[data.ClipAnnotation], E] + + def __init__( + self, + clip_annotations: Iterable[data.ClipAnnotation], + transform: Callable[[data.ClipAnnotation], E], + name: str = "ClipAnnotationDataset", + ): + self.clip_annotations = list(clip_annotations) + self.transform = transform + self.name = name + + def __len__(self) -> int: + return len(self.clip_annotations) + + def __getitem__(self, idx: int) -> E: + return self.transform(self.clip_annotations[idx]) + + +class ClipDataset(Dataset, Generic[E]): + + clips: List[data.Clip] + + transform: Callable[[data.Clip], E] + + def __init__( + self, + clips: Iterable[data.Clip], + transform: Callable[[data.Clip], E], + name: str = "ClipDataset", + ): + self.clips = list(clips) + self.transform = transform + self.name = name + + def __len__(self) -> int: + return len(self.clips) + + def __getitem__(self, idx: int) -> E: + return self.transform(self.clips[idx]) diff --git a/batdetect2/data/labels.py b/batdetect2/data/labels.py new file mode 100644 index 0000000..4fd41c3 --- /dev/null +++ b/batdetect2/data/labels.py @@ -0,0 +1,231 @@ +from typing import Any, Callable, List, Optional, Tuple, Union + +import numpy as np +import xarray as xr +from scipy.ndimage import gaussian_filter +from soundevent import data, geometry + +__all__ = [ + "generate_heatmaps", +] + +PositionFn = Callable[[data.SoundEvent], Tuple[float, float]] +"""Convert a sound event to a single position in time-frequency space.""" + +SizeFn = Callable[[data.SoundEvent, float, float], np.ndarray] +"""Compute the size of a sound event in time-frequency space. + +The time and frequency scales are provided as arguments to allow +modifying the size of the sound event based on the spectrogram +parameters. +""" + +LabelFn = Callable[[data.SoundEventAnnotation], Optional[str]] +"""Convert a sound event annotation to a label. + +When the label is None, this indicates that the sound event does not +belong to any of the classes of interest. +""" + +TARGET_SIGMA = 3.0 + + +GENERIC_LABEL = "__UNKNOWN__" + + +def get_lower_left_position( + sound_event: data.SoundEvent, +) -> Tuple[float, float]: + if sound_event.geometry is None: + raise ValueError("Sound event has no geometry.") + + start_time, low_freq, _, _ = geometry.compute_bounds(sound_event.geometry) + return start_time, low_freq + + +def get_bbox_size( + sound_event: data.SoundEvent, + time_scale: float = 1.0, + frequency_scale: float = 1.0, +) -> np.ndarray: + if sound_event.geometry is None: + raise ValueError("Sound event has no geometry.") + + start_time, low_freq, end_time, high_freq = geometry.compute_bounds( + sound_event.geometry + ) + + return np.array( + [ + time_scale * (end_time - start_time), + frequency_scale * (high_freq - low_freq), + ] + ) + + +def _tag_key(tag: data.Tag) -> Tuple[str, str]: + return (tag.key, tag.value) + + +def set_value_at_position( + array: xr.DataArray, + value: Any, + **query, +) -> xr.DataArray: + dims = {dim: n for n, dim in enumerate(array.dims)} + indexer: List[Union[slice, int]] = [slice(None) for _ in range(array.ndim)] + + for key, coord in query.items(): + if key not in dims: + raise ValueError(f"Dimension {key} not found in array.") + + coordinates = array.indexes[key] + indexer[dims[key]] = coordinates.get_loc(coordinates.asof(coord)) + + if isinstance(value, (tuple, list)): + value = np.array(value) + + array.data[tuple(indexer)] = value + return array + + +def generate_heatmaps( + clip_annotation: data.ClipAnnotation, + spec: xr.DataArray, + num_classes: int = 1, + label_fn: LabelFn = lambda _: None, + target_sigma: float = TARGET_SIGMA, + size_fn: SizeFn = get_bbox_size, + position_fn: PositionFn = get_lower_left_position, + class_labels: Optional[List[str]] = None, + dtype=np.float32, +) -> Tuple[xr.DataArray, xr.DataArray, xr.DataArray]: + if class_labels is None: + class_labels = [str(i) for i in range(num_classes)] + + if len(class_labels) != num_classes: + raise ValueError( + "Number of class labels must match the number of classes." + ) + + shape = dict(zip(spec.dims, spec.shape)) + + if "time" not in shape or "frequency" not in shape: + raise ValueError( + "Spectrogram must have time and frequency dimensions." + ) + + time_duration = spec.time.attrs["max"] - spec.time.attrs["min"] + freq_bandwidth = spec.frequency.attrs["max"] - spec.frequency.attrs["min"] + + # Compute the size factors + time_scale = 1 / time_duration + frequency_scale = 1 / freq_bandwidth + + # Initialize heatmaps + detection_heatmap = xr.zeros_like(spec, dtype=dtype) + class_heatmap = xr.DataArray( + data=np.zeros((num_classes, *spec.shape), dtype=dtype), + dims=["category", *spec.dims], + coords={ + "category": class_labels, + **spec.coords, + }, + ) + size_heatmap = xr.DataArray( + data=np.zeros((2, *spec.shape), dtype=dtype), + dims=["dimension", *spec.dims], + coords={ + "dimension": ["width", "height"], + **spec.coords, + }, + ) + + for sound_event_annotation in clip_annotation.sound_events: + # Get the position of the sound event + time, frequency = position_fn(sound_event_annotation.sound_event) + + # Set 1.0 at the position of the sound event in the detection heatmap + detection_heatmap = set_value_at_position( + detection_heatmap, + 1.0, + time=time, + frequency=frequency, + ) + + # Set the size of the sound event at the position in the size heatmap + size = size_fn( + sound_event_annotation.sound_event, + time_scale, + frequency_scale, + + ) + size_heatmap = set_value_at_position( + size_heatmap, + size, + time=time, + frequency=frequency, + ) + + # Get the label id for the sound event + label = label_fn(sound_event_annotation) + + if label is None or label not in class_labels: + # If the label is None or not in the class labels, we skip the + # sound event + continue + + # Set 1.0 at the position and category of the sound event in the class + # heatmap + class_heatmap = set_value_at_position( + class_heatmap, + 1.0, + time=time, + frequency=frequency, + category=label, + ) + + # Apply gaussian filters + detection_heatmap = xr.apply_ufunc( + gaussian_filter, + detection_heatmap, + target_sigma, + ) + + class_heatmap = class_heatmap.groupby("category").map( + gaussian_filter, # type: ignore + args=(target_sigma,), + ) + + # Normalize heatmaps + detection_heatmap = ( + detection_heatmap / detection_heatmap.max(dim=["time", "frequency"]) + ).fillna(0.0) + + class_heatmap = ( + class_heatmap / class_heatmap.max(dim=["time", "frequency"]) + ).fillna(0.0) + + return detection_heatmap, class_heatmap, size_heatmap + + +class Labeler: + def __init__(self, tags: List[data.Tag]): + """Create a labeler from a list of tags. + + Each tag is assigned a unique label. The labeler can then be used + to convert sound event annotations to labels. + """ + self.tags = tags + self._label_map = {_tag_key(tag): i for i, tag in enumerate(tags)} + self._inverse_label_map = {v: k for k, v in self._label_map.items()} + + def __call__( + self, sound_event_annotation: data.SoundEventAnnotation + ) -> Optional[int]: + for tag in sound_event_annotation.tags: + key = _tag_key(tag) + if key in self._label_map: + return self._label_map[key] + + return None diff --git a/batdetect2/data/preprocessing.py b/batdetect2/data/preprocessing.py new file mode 100644 index 0000000..8800a80 --- /dev/null +++ b/batdetect2/data/preprocessing.py @@ -0,0 +1,586 @@ +"""Module containing functions for preprocessing audio clips.""" + +import random +from typing import List, Optional, Tuple + +import librosa +import librosa.core.spectrum +import numpy as np +import xarray as xr +from numpy.typing import DTypeLike +from scipy.signal import resample_poly +from soundevent import audio, data + +TARGET_SAMPLERATE_HZ = 256000 +SCALE_RAW_AUDIO = False +FFT_WIN_LENGTH_S = 512 / 256000.0 +FFT_OVERLAP = 0.75 +MAX_FREQ_HZ = 120000 +MIN_FREQ_HZ = 10000 +DEFAULT_DURATION = 1 +SPEC_HEIGHT = 128 +SPEC_WIDTH = 256 +SPEC_SCALE = "pcen" +SPEC_TIME_PERIOD = DEFAULT_DURATION / SPEC_WIDTH +DENOISE_SPEC_AVG = True +MAX_SCALE_SPEC = False + + +def preprocess_audio_clip( + clip: data.Clip, + target_sampling_rate: int = TARGET_SAMPLERATE_HZ, + scale_audio: bool = SCALE_RAW_AUDIO, + fft_win_length: float = FFT_WIN_LENGTH_S, + fft_overlap: float = FFT_OVERLAP, + max_freq: int = MAX_FREQ_HZ, + min_freq: int = MIN_FREQ_HZ, + spec_scale: str = SPEC_SCALE, + denoise_spec_avg: bool = True, + max_scale_spec: bool = False, + duration: Optional[float] = DEFAULT_DURATION, + spec_height: int = SPEC_HEIGHT, + spec_time_period: float = SPEC_TIME_PERIOD, +) -> xr.DataArray: + """Preprocesses audio clip to generate spectrogram. + + Parameters + ---------- + clip + The audio clip to preprocess. + target_sampling_rate + Target sampling rate for the audio. If the audio has a different + sampling rate, it will be resampled to this rate. + scale_audio + Whether to scale the audio amplitudes to a range of [-1, 1]. + By default, the audio is not scaled. + fft_win_length + Length of the FFT window in seconds. + fft_overlap + Amount of overlap between FFT windows as a fraction of the window + length. + max_freq + Maximum frequency for spectrogram. Anything above this frequency will + be cropped. + min_freq + Minimum frequency for spectrogram. Anything below this frequency will + be cropped. + spec_scale + Scaling method for the spectrogram. Can be "pcen", "log" or + "amplitude". + denoise_spec_avg + Whether to denoise the spectrogram. Denoising is done by subtracting + the average of the spectrogram from the spectrogram and clipping + negative values to 0. + max_scale_spec + Whether to max scale the spectrogram. Max scaling is done by dividing + the spectrogram by its maximum value thus scaling values to [0, 1]. + duration + Duration of the spectrogram in seconds. If the clip duration is + different from this value, the spectrogram will be cropped or extended + to match this duration. If None, the spectrogram will have the same + duration as the clip. + spec_height + Number of frequency bins for the spectrogram. This is the height of + the final spectrogram. + spec_time_period + Time period for each spectrogram bin in seconds. The spectrogram array + will be resized (using bilinear interpolation) to have this time + period. + + Returns + ------- + xr.DataArray + Preprocessed spectrogram. + + """ + wav = load_clip_audio( + clip, + target_sampling_rate=target_sampling_rate, + scale=scale_audio, + ) + + wav = wav.assign_attrs( + recording_id=str(wav.attrs["recording_id"]), + clip_id=str(wav.attrs["clip_id"]), + path=str(wav.attrs["path"]), + ) + + spec = compute_spectrogram( + wav, + fft_win_length=fft_win_length, + fft_overlap=fft_overlap, + max_freq=max_freq, + min_freq=min_freq, + spec_scale=spec_scale, + denoise_spec_avg=denoise_spec_avg, + max_scale_spec=max_scale_spec, + ) + + if duration is not None: + spec = adjust_spec_duration(clip, spec, duration) + + duration = get_dim_width(spec, dim="time") + return resize_spectrogram( + spec, + time_bins=int(np.ceil(duration / spec_time_period)), + freq_bins=spec_height, + ) + + +def adjust_spec_duration( + clip: data.Clip, + spec: xr.DataArray, + duration: float, +) -> xr.DataArray: + current_duration = clip.end_time - clip.start_time + + if current_duration == duration: + return spec + + if current_duration > duration: + return crop_axis( + spec, + dim="time", + start=clip.start_time, + end=clip.start_time + duration, + ) + + return extend_axis( + spec, + dim="time", + start=clip.start_time, + end=clip.start_time + duration, + ) + + +def load_clip_audio( + clip: data.Clip, + target_sampling_rate: int = TARGET_SAMPLERATE_HZ, + scale: bool = SCALE_RAW_AUDIO, + dtype: DTypeLike = np.float32, +) -> xr.DataArray: + wav = audio.load_clip(clip).sel(channel=0) + + wav = resample_audio(wav, target_sampling_rate, dtype=dtype) + + if scale: + wav = scale_audio(wav) + + wav.coords["time"] = wav.time.assign_attrs( + unit="s", + long_name="Seconds since start of recording", + min=clip.start_time, + max=clip.end_time, + ) + + return wav + + +def resample_audio( + wav: xr.DataArray, + target_samplerate: int = TARGET_SAMPLERATE_HZ, + dtype: DTypeLike = np.float32, +) -> xr.DataArray: + if "samplerate" not in wav.attrs: + raise ValueError("Audio must have a 'samplerate' attribute") + + if "time" not in wav.dims: + raise ValueError("Audio must have a time dimension") + + time_axis: int = wav.get_axis_num("time") # type: ignore + original_samplerate = wav.attrs["samplerate"] + + if original_samplerate == target_samplerate: + return wav.astype(dtype) + + gcd = np.gcd(original_samplerate, target_samplerate) + resampled = resample_poly( + wav.values, + target_samplerate // gcd, + original_samplerate // gcd, + axis=time_axis, + ) + + resampled_times = np.linspace( + wav.time[0], + wav.time[-1], + len(resampled), + endpoint=False, + dtype=dtype, + ) + + return xr.DataArray( + data=resampled.astype(dtype), + dims=wav.dims, + coords={ + **wav.coords, + "time": resampled_times, + }, + attrs={ + **wav.attrs, + "samplerate": target_samplerate, + }, + ) + + +def scale_audio( + audio: xr.DataArray, + eps: float = 10e-6, +) -> xr.DataArray: + audio = audio - audio.mean() + return audio / np.add(np.abs(audio).max(), eps, dtype=audio.dtype) + + +def compute_spectrogram( + wav: xr.DataArray, + fft_win_length: float = FFT_WIN_LENGTH_S, + fft_overlap: float = FFT_OVERLAP, + max_freq: int = MAX_FREQ_HZ, + min_freq: int = MIN_FREQ_HZ, + spec_scale: str = SPEC_SCALE, + denoise_spec_avg: bool = True, + max_scale_spec: bool = False, + dtype: DTypeLike = np.float32, +) -> xr.DataArray: + spec = gen_mag_spectrogram( + wav, + window_len=fft_win_length, + overlap_perc=fft_overlap, + dtype=dtype, + ) + + spec = crop_axis( + spec, + dim="frequency", + start=min_freq, + end=max_freq, + ) + + spec = scale_spectrogram(spec, scale=spec_scale) + + if denoise_spec_avg: + spec = denoise_spectrogram(spec) + + if max_scale_spec: + spec = max_scale_spectrogram(spec) + + return spec + + +def crop_axis( + arr: xr.DataArray, + dim: str, + start: float, + end: float, + right_closed: bool = False, + left_closed: bool = True, + eps: float = 10e-6, +) -> xr.DataArray: + coord = arr.coords[dim] + + if not all(attr in coord.attrs for attr in ["min", "max"]): + raise ValueError( + f"Coordinate '{dim}' must have 'min' and 'max' attributes" + ) + + current_min = coord.attrs["min"] + current_max = coord.attrs["max"] + + if start < current_min or end > current_max: + raise ValueError( + f"Cannot select axis '{dim}' from {start} to {end}. " + f"Axis range is {current_min} to {current_max}" + ) + + slice_end = end + if not right_closed: + slice_end = end - eps + + slice_start = start + if not left_closed: + slice_start = start + eps + + arr = arr.sel({dim: slice(slice_start, slice_end)}) + + arr.coords[dim].attrs.update( + min=start, + max=end, + ) + + return arr + + +def extend_axis( + arr: xr.DataArray, + dim: str, + start: float, + end: float, + fill_value: float = 0, +) -> xr.DataArray: + coord = arr.coords[dim] + + if not all(attr in coord.attrs for attr in ["min", "max", "period"]): + raise ValueError( + f"Coordinate '{dim}' must have 'min', 'max' and 'period' attributes" + " to extend axis" + ) + + current_min = coord.attrs["min"] + current_max = coord.attrs["max"] + period = coord.attrs["period"] + + coords = coord.data + + if start < current_min: + new_coords = np.arange( + current_min, + start, + -period, + dtype=coord.dtype, + )[1:][::-1] + coords = np.concatenate([new_coords, coords]) + + if end > current_max: + new_coords = np.arange( + current_max, + end, + period, + dtype=coord.dtype, + )[1:] + coords = np.concatenate([coords, new_coords]) + + arr = arr.reindex( + {dim: coords}, + fill_value=fill_value, # type: ignore + ) + + arr.coords[dim].attrs.update( + min=start, + max=end, + ) + + return arr + + +def gen_mag_spectrogram( + audio: xr.DataArray, + window_len: float, + overlap_perc: float, + dtype: DTypeLike = np.float32, +) -> xr.DataArray: + sampling_rate = audio.attrs["samplerate"] + hop_len = window_len * (1 - overlap_perc) + nfft = int(window_len * sampling_rate) + noverlap = int(overlap_perc * nfft) + start_time = audio.time.attrs["min"] + end_time = audio.time.attrs["max"] + + # compute spec + spec, _ = librosa.core.spectrum._spectrogram( + y=audio.data, + power=1, + n_fft=nfft, + hop_length=nfft - noverlap, + center=False, + ) + + spec = xr.DataArray( + data=spec.astype(dtype), + dims=["frequency", "time"], + coords={ + "frequency": np.linspace( + 0, + sampling_rate / 2, + spec.shape[0], + endpoint=False, + dtype=dtype, + ), + "time": np.linspace( + start_time, + end_time - (window_len - hop_len), + spec.shape[1], + endpoint=False, + dtype=dtype, + ), + }, + attrs={ + **audio.attrs, + "nfft": nfft, + "noverlap": noverlap, + }, + ) + + # Add metadata to coordinates + spec.coords["time"].attrs.update( + unit="s", + long_name="Time", + min=start_time, + max=end_time - (window_len - hop_len), + period=(nfft - noverlap) / sampling_rate, + ) + spec.coords["frequency"].attrs.update( + unit="Hz", + long_name="Frequency", + period=(sampling_rate / nfft), + min=0, + max=sampling_rate / 2, + ) + + return spec + + +def denoise_spectrogram( + spec: xr.DataArray, +) -> xr.DataArray: + return xr.DataArray( + data=(spec - spec.mean("time")).clip(0), + dims=spec.dims, + coords=spec.coords, + attrs={ + **spec.attrs, + "denoised": 1, + }, + ) + + +def scale_spectrogram( + spec: xr.DataArray, + scale: str = SPEC_SCALE, + dtype: DTypeLike = np.float32, +) -> xr.DataArray: + if scale == "pcen": + return pcen(spec, dtype=dtype) + + if scale == "log": + return log_scale(spec, dtype=dtype) + + return spec + + +def log_scale( + spec: xr.DataArray, + dtype: DTypeLike = np.float32, +) -> xr.DataArray: + nfft = spec.attrs["nfft"] + sampling_rate = spec.attrs["samplerate"] + log_scaling = ( + 2.0 + * (1.0 / sampling_rate) + * (1.0 / (np.abs(np.hanning(nfft)) ** 2).sum()) + ) + return xr.DataArray( + data=np.log1p(log_scaling * spec).astype(dtype), + dims=spec.dims, + coords=spec.coords, + attrs={ + **spec.attrs, + "scale": "log", + }, + ) + + +def pcen(spec: xr.DataArray, dtype: DTypeLike = np.float32) -> xr.DataArray: + sampling_rate = spec.attrs["samplerate"] + data = librosa.pcen( + spec.data * (2**31), + sr=sampling_rate / 10, + ) + return xr.DataArray( + data=data.astype(dtype), + dims=spec.dims, + coords=spec.coords, + attrs={ + **spec.attrs, + "scale": "pcen", + }, + ) + + +def max_scale_spectrogram(spec: xr.DataArray, eps=10e-6) -> xr.DataArray: + return xr.DataArray( + data=spec / np.add(spec.max(), eps, dtype=spec.dtype), + dims=spec.dims, + coords=spec.coords, + attrs={ + **spec.attrs, + "max_scaled": 1, + }, + ) + + +def resize_spectrogram( + spec: xr.DataArray, + time_bins: int, + freq_bins: int, +) -> xr.DataArray: + new_times = np.linspace( + spec.time[0], + spec.time[-1], + time_bins, + dtype=spec.time.dtype, + endpoint=True, + ) + new_frequencies = np.linspace( + spec.frequency[0], + spec.frequency[-1], + freq_bins, + dtype=spec.frequency.dtype, + endpoint=True, + ) + + return spec.interp( + coords=dict( + time=new_times, + frequency=new_frequencies, + ), + method="linear", + ) + + +def get_dim_width(arr: xr.DataArray, dim: str) -> float: + coord = arr.coords[dim] + attrs = coord.attrs + if "min" in attrs and "max" in attrs: + return attrs["max"] - attrs["min"] + + coord_min = coord.min() + coord_max = coord.max() + return float(coord_max - coord_min) + + +class RandomClipProvider: + def __init__( + self, + clip_annotations: List[data.ClipAnnotation], + target_sampling_rate: int = TARGET_SAMPLERATE_HZ, + scale_audio: bool = SCALE_RAW_AUDIO, + ): + self.target_sampling_rate = target_sampling_rate + self.scale_audio = scale_audio + self.clip_annotations = clip_annotations + + def get_next_clip(self, clip: data.ClipAnnotation) -> data.ClipAnnotation: + tries = 0 + while True: + random_clip = random.choice(self.clip_annotations) + + if random_clip.clip != clip.clip: + return random_clip + + tries += 1 + if tries > 4: + raise ValueError("Could not find a different clip") + + def __call__( + self, + clip: data.ClipAnnotation, + ) -> Tuple[xr.DataArray, data.ClipAnnotation]: + random_clip = self.get_next_clip(clip) + + wav = load_clip_audio( + random_clip.clip, + target_sampling_rate=self.target_sampling_rate, + scale=self.scale_audio, + ) + + return wav, random_clip diff --git a/batdetect2/detector/model_helpers.py b/batdetect2/detector/model_helpers.py index f342737..01ef3da 100644 --- a/batdetect2/detector/model_helpers.py +++ b/batdetect2/detector/model_helpers.py @@ -53,7 +53,13 @@ class SelfAttention(nn.Module): class ConvBlockDownCoordF(nn.Module): def __init__( - self, in_chn, out_chn, ip_height, k_size=3, pad_size=1, stride=1 + self, + in_chn, + out_chn, + ip_height, + k_size=3, + pad_size=1, + stride=1, ): super(ConvBlockDownCoordF, self).__init__() self.coords = nn.Parameter( diff --git a/batdetect2/detector/models.py b/batdetect2/detector/models.py index d0251c2..84168bc 100644 --- a/batdetect2/detector/models.py +++ b/batdetect2/detector/models.py @@ -1,5 +1,4 @@ import torch -import torch.fft import torch.nn.functional as F from torch import nn @@ -207,7 +206,7 @@ class Net2DFastNoAttn(nn.Module): num_filts // 4, 2, kernel_size=1, padding=0 ) self.conv_classes_op = nn.Conv2d( - num_filts // 4, self.num_classes + 1, kernel_size=1, padding=0 + num_filts // 4, self.num_classes + 1, kernel_size=1, padding=0, ) if self.emb_dim > 0: diff --git a/batdetect2/detector/parameters.py b/batdetect2/detector/parameters.py index cce641e..54dab95 100644 --- a/batdetect2/detector/parameters.py +++ b/batdetect2/detector/parameters.py @@ -28,6 +28,7 @@ MAX_SCALE_SPEC = False DEFAULT_MODEL_PATH = os.path.join( os.path.dirname(os.path.dirname(__file__)), "models", + "checkpoints", "Net2DFast_UK_same.pth.tar", ) diff --git a/batdetect2/detector/post_process.py b/batdetect2/detector/post_process.py index b47eec6..1cf44fe 100644 --- a/batdetect2/detector/post_process.py +++ b/batdetect2/detector/post_process.py @@ -68,6 +68,7 @@ def run_nms( params["fft_win_length"], params["fft_overlap"], ) + print("duration", duration) top_k = int(duration * params["nms_top_k_per_sec"]) scores, y_pos, x_pos = get_topk_scores(pred_det_nms, top_k) diff --git a/batdetect2/evaluate/evaluate_models.py b/batdetect2/evaluate/evaluate_models.py index 3303c92..9c7717a 100644 --- a/batdetect2/evaluate/evaluate_models.py +++ b/batdetect2/evaluate/evaluate_models.py @@ -7,16 +7,16 @@ import copy import json import os -import torch import numpy as np import pandas as pd +import torch from sklearn.ensemble import RandomForestClassifier -from batdetect2.detector import parameters import batdetect2.train.evaluate as evl import batdetect2.train.train_utils as tu import batdetect2.utils.detector_utils as du import batdetect2.utils.plot_utils as pu +from batdetect2.detector import parameters def get_blank_annotation(ip_str): @@ -330,7 +330,8 @@ def load_gt_data(datasets, events_of_interest, class_names, classes_to_ignore): for dd in datasets: print("\n" + dd["dataset_name"]) gt_dataset = tu.load_set_of_anns( - [dd], events_of_interest=events_of_interest, verbose=True + [dd], + events_of_interest=events_of_interest, ) gt_dataset = [ parse_data(gg, class_names, classes_to_ignore, False) @@ -553,7 +554,9 @@ if __name__ == "__main__": test_dict["dataset_name"] = args["test_file"].replace(".json", "") test_dict["is_test"] = True test_dict["is_binary"] = True - test_dict["ann_path"] = os.path.join(args["ann_dir"], args["test_file"]) + test_dict["ann_path"] = os.path.join( + args["ann_dir"], args["test_file"] + ) test_dict["wav_path"] = args["data_dir"] test_sets = [test_dict] diff --git a/batdetect2/models/Net2DFast_UK_same.pth.tar b/batdetect2/models/Net2DFast_UK_same.pth.tar deleted file mode 100644 index e3704b7..0000000 Binary files a/batdetect2/models/Net2DFast_UK_same.pth.tar and /dev/null differ diff --git a/batdetect2/models/__init__.py b/batdetect2/models/__init__.py new file mode 100644 index 0000000..8d026ca --- /dev/null +++ b/batdetect2/models/__init__.py @@ -0,0 +1,91 @@ +import os +from typing import Tuple, Union + +import torch + +from batdetect2.models.encoders import ( + Net2DFast, + Net2DFastNoAttn, + Net2DFastNoCoordConv, +) +from batdetect2.models.typing import DetectionModel + +__all__ = [ + "load_model", + "Net2DFast", + "Net2DFastNoAttn", + "Net2DFastNoCoordConv", +] + +DEFAULT_MODEL_PATH = os.path.join( + os.path.dirname(os.path.dirname(__file__)), + "models", + "checkpoints", + "Net2DFast_UK_same.pth.tar", +) + + +def load_model( + model_path: str = DEFAULT_MODEL_PATH, + load_weights: bool = True, + device: Union[torch.device, str, None] = None, +) -> Tuple[DetectionModel, dict]: + """Load model from file. + + Args: + model_path (str): Path to model file. Defaults to DEFAULT_MODEL_PATH. + load_weights (bool, optional): Load weights. Defaults to True. + + Returns: + model, params: Model and parameters. + + Raises: + FileNotFoundError: Model file not found. + ValueError: Unknown model name. + """ + if device is None: + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + if not os.path.isfile(model_path): + raise FileNotFoundError("Model file not found.") + + net_params = torch.load(model_path, map_location=device) + + params = net_params["params"] + + model: DetectionModel + + if params["model_name"] == "Net2DFast": + model = Net2DFast( + params["num_filters"], + num_classes=len(params["class_names"]), + emb_dim=params["emb_dim"], + ip_height=params["ip_height"], + resize_factor=params["resize_factor"], + ) + elif params["model_name"] == "Net2DFastNoAttn": + model = Net2DFastNoAttn( + params["num_filters"], + num_classes=len(params["class_names"]), + emb_dim=params["emb_dim"], + ip_height=params["ip_height"], + resize_factor=params["resize_factor"], + ) + elif params["model_name"] == "Net2DFastNoCoordConv": + model = Net2DFastNoCoordConv( + params["num_filters"], + num_classes=len(params["class_names"]), + emb_dim=params["emb_dim"], + ip_height=params["ip_height"], + resize_factor=params["resize_factor"], + ) + else: + raise ValueError("Unknown model.") + + if load_weights: + model.load_state_dict(net_params["state_dict"]) + + model = model.to(device) + model.eval() + + return model, params diff --git a/batdetect2/models/blocks.py b/batdetect2/models/blocks.py new file mode 100644 index 0000000..d58bdb1 --- /dev/null +++ b/batdetect2/models/blocks.py @@ -0,0 +1,219 @@ +"""Module containing custom NN blocks. + +All these classes are subclasses of `torch.nn.Module` and can be used to build +complex neural network architectures. +""" + +from typing import Tuple + +import torch +import torch.nn.functional as F +from torch import nn + +__all__ = [ + "SelfAttention", + "ConvBlockDownCoordF", + "ConvBlockDownStandard", + "ConvBlockUpF", + "ConvBlockUpStandard", +] + + +class SelfAttention(nn.Module): + """Self-Attention module. + + This module implements self-attention mechanism. + """ + + def __init__(self, ip_dim: int, att_dim: int): + super().__init__() + + # Note, does not encode position information (absolute or realtive) + self.temperature = 1.0 + self.att_dim = att_dim + self.key_fun = nn.Linear(ip_dim, att_dim) + self.val_fun = nn.Linear(ip_dim, att_dim) + self.que_fun = nn.Linear(ip_dim, att_dim) + self.pro_fun = nn.Linear(att_dim, ip_dim) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = x.squeeze(2).permute(0, 2, 1) + + key = torch.matmul( + x, self.key_fun.weight.T + ) + self.key_fun.bias.unsqueeze(0).unsqueeze(0) + query = torch.matmul( + x, self.que_fun.weight.T + ) + self.que_fun.bias.unsqueeze(0).unsqueeze(0) + value = torch.matmul( + x, self.val_fun.weight.T + ) + self.val_fun.bias.unsqueeze(0).unsqueeze(0) + + kk_qq = torch.bmm(key, query.permute(0, 2, 1)) / ( + self.temperature * self.att_dim + ) + att_weights = F.softmax(kk_qq, 1) + att = torch.bmm(value.permute(0, 2, 1), att_weights) + + op = torch.matmul( + att.permute(0, 2, 1), self.pro_fun.weight.T + ) + self.pro_fun.bias.unsqueeze(0).unsqueeze(0) + op = op.permute(0, 2, 1).unsqueeze(2) + + return op + + +class ConvBlockDownCoordF(nn.Module): + """Convolutional Block with Downsampling and Coord Feature. + + This block performs convolution followed by downsampling + and concatenates with coordinate information. + """ + + def __init__( + self, + in_chn: int, + out_chn: int, + ip_height: int, + k_size: int = 3, + pad_size: int = 1, + stride: int = 1, + ): + super().__init__() + + self.coords = nn.Parameter( + torch.linspace(-1, 1, ip_height)[None, None, ..., None], + requires_grad=False, + ) + self.conv = nn.Conv2d( + in_chn + 1, + out_chn, + kernel_size=k_size, + padding=pad_size, + stride=stride, + ) + self.conv_bn = nn.BatchNorm2d(out_chn) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + freq_info = self.coords.repeat(x.shape[0], 1, 1, x.shape[3]) + x = torch.cat((x, freq_info), 1) + x = F.max_pool2d(self.conv(x), 2, 2) + x = F.relu(self.conv_bn(x), inplace=True) + return x + + +class ConvBlockDownStandard(nn.Module): + """Convolutional Block with Downsampling. + + This block performs convolution followed by downsampling. + """ + + def __init__( + self, + in_chn, + out_chn, + k_size=3, + pad_size=1, + stride=1, + ): + super(ConvBlockDownStandard, self).__init__() + self.conv = nn.Conv2d( + in_chn, + out_chn, + kernel_size=k_size, + padding=pad_size, + stride=stride, + ) + self.conv_bn = nn.BatchNorm2d(out_chn) + + def forward(self, x): + x = F.max_pool2d(self.conv(x), 2, 2) + x = F.relu(self.conv_bn(x), inplace=True) + return x + + +class ConvBlockUpF(nn.Module): + """Convolutional Block with Upsampling and Coord Feature. + + This block performs convolution followed by upsampling + and concatenates with coordinate information. + """ + + def __init__( + self, + in_chn: int, + out_chn: int, + ip_height: int, + k_size: int = 3, + pad_size: int = 1, + up_mode: str = "bilinear", + up_scale: Tuple[int, int] = (2, 2), + ): + super().__init__() + + self.up_scale = up_scale + self.up_mode = up_mode + self.coords = nn.Parameter( + torch.linspace(-1, 1, ip_height * up_scale[0])[ + None, None, ..., None + ], + requires_grad=False, + ) + self.conv = nn.Conv2d( + in_chn + 1, out_chn, kernel_size=k_size, padding=pad_size + ) + self.conv_bn = nn.BatchNorm2d(out_chn) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + op = F.interpolate( + x, + size=( + x.shape[-2] * self.up_scale[0], + x.shape[-1] * self.up_scale[1], + ), + mode=self.up_mode, + align_corners=False, + ) + freq_info = self.coords.repeat(op.shape[0], 1, 1, op.shape[3]) + op = torch.cat((op, freq_info), 1) + op = self.conv(op) + op = F.relu(self.conv_bn(op), inplace=True) + return op + + +class ConvBlockUpStandard(nn.Module): + """Convolutional Block with Upsampling. + + This block performs convolution followed by upsampling. + """ + + def __init__( + self, + in_chn: int, + out_chn: int, + k_size: int = 3, + pad_size: int = 1, + up_mode: str = "bilinear", + up_scale: Tuple[int, int] = (2, 2), + ): + super(ConvBlockUpStandard, self).__init__() + self.up_scale = up_scale + self.up_mode = up_mode + self.conv = nn.Conv2d( + in_chn, out_chn, kernel_size=k_size, padding=pad_size + ) + self.conv_bn = nn.BatchNorm2d(out_chn) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + op = F.interpolate( + x, + size=( + x.shape[-2] * self.up_scale[0], + x.shape[-1] * self.up_scale[1], + ), + mode=self.up_mode, + align_corners=False, + ) + op = self.conv(op) + op = F.relu(self.conv_bn(op), inplace=True) + return op diff --git a/batdetect2/models/detectors.py b/batdetect2/models/detectors.py new file mode 100644 index 0000000..e324c3e --- /dev/null +++ b/batdetect2/models/detectors.py @@ -0,0 +1,148 @@ +import pytorch_lightning as L +import torch +import xarray as xr +from soundevent import data +from torch import nn, optim + +from batdetect2.data.preprocessing import preprocess_audio_clip +from batdetect2.models.typing import EncoderModel, ModelOutput +from batdetect2.train import losses +from batdetect2.train.dataset import TrainExample +from batdetect2.models.post_process import ( + PostprocessConfig, + postprocess_model_outputs, +) +from batdetect2.train.preprocess import PreprocessingConfig + + +class DetectorModel(L.LightningModule): + def __init__( + self, + encoder: EncoderModel, + num_classes: int, + learning_rate: float = 1e-3, + preprocessing_config: PreprocessingConfig = PreprocessingConfig(), + postprocessing_config: PostprocessConfig = PostprocessConfig(), + ): + super().__init__() + + self.preprocessing_config = preprocessing_config + self.postprocessing_config = postprocessing_config + self.num_classes = num_classes + self.learning_rate = learning_rate + + self.encoder = encoder + + self.classifier = nn.Conv2d( + self.encoder.num_filts // 4, + self.num_classes + 1, + kernel_size=1, + padding=0, + ) + + self.bbox = nn.Conv2d( + self.encoder.num_filts // 4, + 2, + kernel_size=1, + padding=0, + ) + + def forward(self, spec: torch.Tensor) -> ModelOutput: # type: ignore + features = self.encoder(spec) + + classification_logits = self.classifier(features) + classification_probs = torch.softmax(classification_logits, dim=1) + detection_probs = classification_probs[:, :-1].sum(dim=1, keepdim=True) + + return ModelOutput( + detection_probs=detection_probs, + size_preds=self.bbox(features), + class_probs=classification_probs, + features=features, + ) + + def compute_spectrogram(self, clip: data.Clip) -> xr.DataArray: + config = self.preprocessing_config + + return preprocess_audio_clip( + clip, + target_sampling_rate=config.target_samplerate, + scale_audio=config.scale_audio, + fft_win_length=config.fft_win_length, + fft_overlap=config.fft_overlap, + max_freq=config.max_freq, + min_freq=config.min_freq, + spec_scale=config.spec_scale, + denoise_spec_avg=config.denoise_spec_avg, + max_scale_spec=config.max_scale_spec, + ) + + def process_clip(self, clip: data.Clip): + spectrogram = self.compute_spectrogram(clip) + spec_tensor = ( + torch.tensor(spectrogram.values).unsqueeze(0).unsqueeze(0) + ) + + outputs = self(spec_tensor) + + config = self.postprocessing_config + return postprocess_model_outputs( + outputs, + [clip], + nms_kernel_size=config.nms_kernel_size, + detection_threshold=config.detection_threshold, + min_freq=config.min_freq, + max_freq=config.max_freq, + top_k_per_sec=config.top_k_per_sec, + ) + + def compute_loss( + self, + outputs: ModelOutput, + batch: TrainExample, + ) -> torch.Tensor: + detection_loss = losses.focal_loss( + outputs.detection_probs, + batch.detection_heatmap, + ) + + size_loss = losses.bbox_size_loss( + outputs.size_preds, + batch.size_heatmap, + ) + + valid_mask = batch.class_heatmap.any(dim=1, keepdim=True).float() + classification_loss = losses.focal_loss( + outputs.class_probs, + batch.class_heatmap, + valid_mask=valid_mask, + ) + + return detection_loss + size_loss + classification_loss + + def training_step( # type: ignore + self, + batch: TrainExample, + ): + features = self.encoder(batch.spec) + + classification_logits = self.classifier(features) + classification_probs = torch.softmax(classification_logits, dim=1) + detection_probs = classification_probs[:, :-1].sum(dim=1, keepdim=True) + + loss = self.compute_loss( + ModelOutput( + detection_probs=detection_probs, + size_preds=self.bbox(features), + class_probs=classification_probs, + features=features, + ), + batch, + ) + self.log("train_loss", loss) + return loss + + def configure_optimizers(self): + optimizer = optim.Adam(self.parameters(), lr=self.learning_rate) + scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, 100) + return [optimizer], [scheduler] diff --git a/batdetect2/models/encoders.py b/batdetect2/models/encoders.py new file mode 100644 index 0000000..8b289ae --- /dev/null +++ b/batdetect2/models/encoders.py @@ -0,0 +1,319 @@ +from typing import Tuple + +import torch +import torch.fft +import torch.nn.functional as F +from torch import nn + +from batdetect2.models.typing import EncoderModel +from batdetect2.models.blocks import ( + ConvBlockDownCoordF, + ConvBlockDownStandard, + ConvBlockUpF, + ConvBlockUpStandard, + SelfAttention, +) + +__all__ = [ + "Net2DFast", + "Net2DFastNoAttn", + "Net2DFastNoCoordConv", +] + + +class Net2DFast(EncoderModel): + def __init__( + self, + num_filts: int, + input_height: int = 128, + ): + super().__init__() + self.num_filts = num_filts + self.input_height = input_height + self.bottleneck_height = self.input_height // 32 + + # encoder + self.conv_dn_0 = ConvBlockDownCoordF( + 1, + self.num_filts // 4, + self.input_height, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_1 = ConvBlockDownCoordF( + self.num_filts // 4, + self.num_filts // 2, + self.input_height // 2, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_2 = ConvBlockDownCoordF( + self.num_filts // 2, + self.num_filts, + self.input_height // 4, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_3 = nn.Conv2d( + self.num_filts, + self.num_filts * 2, + 3, + padding=1, + ) + self.conv_dn_3_bn = nn.BatchNorm2d(self.num_filts * 2) + + # bottleneck + self.conv_1d = nn.Conv2d( + self.num_filts * 2, + self.num_filts * 2, + (self.input_height // 8, 1), + padding=0, + ) + self.conv_1d_bn = nn.BatchNorm2d(self.num_filts * 2) + self.att = SelfAttention(self.num_filts * 2, self.num_filts * 2) + + # decoder + self.conv_up_2 = ConvBlockUpF( + self.num_filts * 2, + self.num_filts // 2, + self.input_height // 8, + ) + self.conv_up_3 = ConvBlockUpF( + self.num_filts // 2, + self.num_filts // 4, + self.input_height // 4, + ) + self.conv_up_4 = ConvBlockUpF( + self.num_filts // 4, + self.num_filts // 4, + self.input_height // 2, + ) + + self.conv_op = nn.Conv2d( + self.num_filts // 4, + self.num_filts // 4, + kernel_size=3, + padding=1, + ) + self.conv_op_bn = nn.BatchNorm2d(self.num_filts // 4) + + def pad_adjust(self, spec: torch.Tensor) -> Tuple[torch.Tensor, int, int]: + h, w = spec.shape[2:] + h_pad = (32 - h % 32) % 32 + w_pad = (32 - w % 32) % 32 + return F.pad(spec, (0, w_pad, 0, h_pad)), h_pad, w_pad + + def forward(self, spec: torch.Tensor) -> torch.Tensor: + # encoder + spec, h_pad, w_pad = self.pad_adjust(spec) + + x1 = self.conv_dn_0(spec) + x2 = self.conv_dn_1(x1) + x3 = self.conv_dn_2(x2) + x3 = F.relu_(self.conv_dn_3_bn(self.conv_dn_3(x3))) + + # bottleneck + x = F.relu_(self.conv_1d_bn(self.conv_1d(x3))) + x = self.att(x) + x = x.repeat([1, 1, self.bottleneck_height * 4, 1]) + + # decoder + x = self.conv_up_2(x + x3) + x = self.conv_up_3(x + x2) + x = self.conv_up_4(x + x1) + + # Restore original size + if h_pad > 0: + x = x[:, :, :-h_pad, :] + + if w_pad > 0: + x = x[:, :, :, :-w_pad] + + return F.relu_(self.conv_op_bn(self.conv_op(x))) + + +class Net2DFastNoAttn(EncoderModel): + def __init__( + self, + num_filts: int, + input_height: int = 128, + ): + super().__init__() + + self.num_filts = num_filts + self.input_height = input_height + self.bottleneck_height = self.input_height // 32 + + self.conv_dn_0 = ConvBlockDownCoordF( + 1, + self.num_filts // 4, + self.input_height, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_1 = ConvBlockDownCoordF( + self.num_filts // 4, + self.num_filts // 2, + self.input_height // 2, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_2 = ConvBlockDownCoordF( + self.num_filts // 2, + self.num_filts, + self.input_height // 4, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_3 = nn.Conv2d( + self.num_filts, + self.num_filts * 2, + 3, + padding=1, + ) + self.conv_dn_3_bn = nn.BatchNorm2d(self.num_filts * 2) + + self.conv_1d = nn.Conv2d( + self.num_filts * 2, + self.num_filts * 2, + (self.input_height // 8, 1), + padding=0, + ) + self.conv_1d_bn = nn.BatchNorm2d(self.num_filts * 2) + + self.conv_up_2 = ConvBlockUpF( + self.num_filts * 2, + self.num_filts // 2, + self.input_height // 8, + ) + self.conv_up_3 = ConvBlockUpF( + self.num_filts // 2, + self.num_filts // 4, + self.input_height // 4, + ) + self.conv_up_4 = ConvBlockUpF( + self.num_filts // 4, + self.num_filts // 4, + self.input_height // 2, + ) + + self.conv_op = nn.Conv2d( + self.num_filts // 4, + self.num_filts // 4, + kernel_size=3, + padding=1, + ) + self.conv_op_bn = nn.BatchNorm2d(self.num_filts // 4) + + def forward(self, spec: torch.Tensor) -> torch.Tensor: + x1 = self.conv_dn_0(spec) + x2 = self.conv_dn_1(x1) + x3 = self.conv_dn_2(x2) + x3 = F.relu_(self.conv_dn_3_bn(self.conv_dn_3(x3))) + + x = F.relu_(self.conv_1d_bn(self.conv_1d(x3))) + x = x.repeat([1, 1, self.bottleneck_height * 4, 1]) + + x = self.conv_up_2(x + x3) + x = self.conv_up_3(x + x2) + x = self.conv_up_4(x + x1) + + return F.relu_(self.conv_op_bn(self.conv_op(x))) + + +class Net2DFastNoCoordConv(EncoderModel): + def __init__( + self, + num_filts: int, + input_height: int = 128, + ): + super().__init__() + + self.num_filts = num_filts + self.input_height = input_height + self.bottleneck_height = self.input_height // 32 + + self.conv_dn_0 = ConvBlockDownStandard( + 1, + self.num_filts // 4, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_1 = ConvBlockDownStandard( + self.num_filts // 4, + self.num_filts // 2, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_2 = ConvBlockDownStandard( + self.num_filts // 2, + self.num_filts, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_3 = nn.Conv2d( + self.num_filts, + self.num_filts * 2, + 3, + padding=1, + ) + self.conv_dn_3_bn = nn.BatchNorm2d(self.num_filts * 2) + + self.conv_1d = nn.Conv2d( + self.num_filts * 2, + self.num_filts * 2, + (self.input_height // 8, 1), + padding=0, + ) + self.conv_1d_bn = nn.BatchNorm2d(self.num_filts * 2) + + self.att = SelfAttention(self.num_filts * 2, self.num_filts * 2) + + self.conv_up_2 = ConvBlockUpStandard( + self.num_filts * 2, + self.num_filts // 2, + self.input_height // 8, + ) + self.conv_up_3 = ConvBlockUpStandard( + self.num_filts // 2, + self.num_filts // 4, + self.input_height // 4, + ) + self.conv_up_4 = ConvBlockUpStandard( + self.num_filts // 4, + self.num_filts // 4, + self.input_height // 2, + ) + + self.conv_op = nn.Conv2d( + self.num_filts // 4, + self.num_filts // 4, + kernel_size=3, + padding=1, + ) + self.conv_op_bn = nn.BatchNorm2d(self.num_filts // 4) + + def forward(self, spec: torch.Tensor) -> torch.Tensor: + x1 = self.conv_dn_0(spec) + x2 = self.conv_dn_1(x1) + x3 = self.conv_dn_2(x2) + x3 = F.relu_(self.conv_dn_3_bn(self.conv_dn_3(x3))) + + x = F.relu_(self.conv_1d_bn(self.conv_1d(x3))) + x = self.att(x) + x = x.repeat([1, 1, self.bottleneck_height * 4, 1]) + + x = self.conv_up_2(x + x3) + x = self.conv_up_3(x + x2) + x = self.conv_up_4(x + x1) + + return F.relu_(self.conv_op_bn(self.conv_op(x))) diff --git a/batdetect2/models/post_process.py b/batdetect2/models/post_process.py new file mode 100644 index 0000000..08b02fa --- /dev/null +++ b/batdetect2/models/post_process.py @@ -0,0 +1,310 @@ +"""Module for postprocessing model outputs.""" + +from typing import Callable, List, Tuple, Union +from pydantic import BaseModel, Field + +import numpy as np +import torch +from soundevent import data +from torch import nn + +from batdetect2.models.typing import ModelOutput + +__all__ = [ + "postprocess_model_outputs", + "PostprocessConfig", +] + +NMS_KERNEL_SIZE = 9 +DETECTION_THRESHOLD = 0.01 +TOP_K_PER_SEC = 200 + + +class PostprocessConfig(BaseModel): + """Configuration for postprocessing model outputs.""" + + nms_kernel_size: int = Field(default=NMS_KERNEL_SIZE, gt=0) + detection_threshold: float = Field(default=DETECTION_THRESHOLD, ge=0) + min_freq: int = Field(default=10000, gt=0) + max_freq: int = Field(default=120000, gt=0) + top_k_per_sec: int = Field(default=TOP_K_PER_SEC, gt=0) + + +TagFunction = Callable[[int], List[data.Tag]] + + +def postprocess_model_outputs( + outputs: ModelOutput, + clips: List[data.Clip], + nms_kernel_size: int = NMS_KERNEL_SIZE, + detection_threshold: float = DETECTION_THRESHOLD, + min_freq: int = 10000, + max_freq: int = 120000, + top_k_per_sec: int = TOP_K_PER_SEC, +) -> List[data.ClipPrediction]: + """Postprocesses model outputs to generate clip predictions. + + This function takes the output from the model, applies non-maximum suppression, + selects the top-k scores, computes sound events from the outputs, and returns + clip predictions based on these processed outputs. + + Parameters + ---------- + outputs + Output from the model containing detection probabilities, size + predictions, class logits, and features. All tensors are expected + to have a batch dimension. + clips + List of clips for which predictions are made. The number of clips + must match the batch dimension of the model outputs. + nms_kernel_size + Size of the non-maximum suppression kernel. Default is 9. + detection_threshold + Detection threshold. Default is 0.01. + min_freq + Minimum frequency. Default is 10000. + max_freq + Maximum frequency. Default is 120000. + top_k_per_sec + Top k per second. Default is 200. + + Returns + ------- + predictions: List[data.ClipPrediction] + List of clip predictions containing predicted sound events. + + Raises + ------ + ValueError + If the number of predictions does not match the number of clips. + """ + num_predictions = len(outputs.detection_probs) + + if num_predictions == 0: + return [] + + if num_predictions != len(clips): + raise ValueError( + "Number of predictions must match the number of clips." + ) + + detection_probs = non_max_suppression( + outputs.detection_probs, + kernel_size=nms_kernel_size, + ) + + duration = clips[0].end_time - clips[0].start_time + + scores_batch, y_pos_batch, x_pos_batch = get_topk_scores( + detection_probs, + int(top_k_per_sec * duration / 2), + ) + + predictions: List[data.ClipPrediction] = [] + for scores, y_pos, x_pos, size_preds, class_probs, features, clip in zip( + scores_batch, + y_pos_batch, + x_pos_batch, + outputs.size_preds, + outputs.class_probs, + outputs.features, + clips, + ): + sound_events = compute_sound_events_from_outputs( + clip, + scores, + y_pos, + x_pos, + size_preds, + class_probs, + features, + min_freq=min_freq, + max_freq=max_freq, + detection_threshold=detection_threshold, + ) + + predictions.append( + data.ClipPrediction( + clip=clip, + sound_events=sound_events, + ) + ) + + return predictions + + +def compute_sound_events_from_outputs( + clip: data.Clip, + scores: torch.Tensor, + y_pos: torch.Tensor, + x_pos: torch.Tensor, + size_preds: torch.Tensor, + class_probs: torch.Tensor, + features: torch.Tensor, + tag_fn: TagFunction = lambda _: [], + min_freq: int = 10000, + max_freq: int = 120000, + detection_threshold: float = DETECTION_THRESHOLD, +) -> List[data.SoundEventPrediction]: + _, freq_bins, time_bins = size_preds.shape + + sorted_indices = torch.argsort(x_pos) + valid_indices = sorted_indices[ + scores[sorted_indices] > detection_threshold + ] + + scores = scores[valid_indices] + x_pos = x_pos[valid_indices] + y_pos = y_pos[valid_indices] + + predictions: List[data.SoundEventPrediction] = [] + for score, x, y in zip(scores, x_pos, y_pos): + width, height = size_preds[:, y, x] + print(width, height) + class_prob = class_probs[:, y, x] + feature = features[:, y, x] + + start_time = np.interp( + x.item(), + [0, time_bins], + [clip.start_time, clip.end_time], + ) + + end_time = np.interp( + x.item() + width.item(), + [0, time_bins], + [clip.start_time, clip.end_time], + ) + + low_freq = np.interp( + y.item(), + [0, freq_bins], + [max_freq, min_freq], + ) + + high_freq = np.interp( + y.item() - height.item(), + [0, freq_bins], + [max_freq, min_freq], + ) + + predicted_tags: List[data.PredictedTag] = [] + + for label_id, class_score in enumerate(class_prob): + corresponding_tags = tag_fn(label_id) + predicted_tags.extend( + [ + data.PredictedTag( + tag=tag, + score=class_score.item(), + ) + for tag in corresponding_tags + ] + ) + + start_time, end_time = sorted([float(start_time), float(end_time)]) + low_freq, high_freq = sorted([float(low_freq), float(high_freq)]) + + sound_event = data.SoundEvent( + recording=clip.recording, + geometry=data.BoundingBox( + coordinates=[ + start_time, + low_freq, + end_time, + high_freq, + ] + ), + features=[ + data.Feature( + name=f"batdetect2_{i}", + value=value.item(), + ) + for i, value in enumerate(feature) + ], + ) + + predictions.append( + data.SoundEventPrediction( + sound_event=sound_event, + score=score.item(), + tags=predicted_tags, + ) + ) + + return predictions + + +def non_max_suppression( + tensor: torch.Tensor, + kernel_size: Union[int, Tuple[int, int]] = NMS_KERNEL_SIZE, +) -> torch.Tensor: + """Run non-maximum suppression on a tensor. + + This function removes values from the input tensor that are not local + maxima in the neighborhood of the given kernel size. + + All non-maximum values are set to zero. + + Parameters + ---------- + tensor : torch.Tensor + Input tensor. + kernel_size : Union[int, Tuple[int, int]], optional + Size of the neighborhood to consider for non-maximum suppression. + If an integer is given, the neighborhood will be a square of the + given size. If a tuple is given, the neighborhood will be a + rectangle with the given height and width. + + Returns + ------- + torch.Tensor + Tensor with non-maximum suppressed values. + """ + if isinstance(kernel_size, int): + kernel_size_h = kernel_size + kernel_size_w = kernel_size + else: + kernel_size_h, kernel_size_w = kernel_size + + pad_h = (kernel_size_h - 1) // 2 + pad_w = (kernel_size_w - 1) // 2 + + hmax = nn.functional.max_pool2d( + tensor, + (kernel_size_h, kernel_size_w), + stride=1, + padding=(pad_h, pad_w), + ) + keep = (hmax == tensor).float() + return tensor * keep + + +def get_topk_scores( + scores: torch.Tensor, + K: int, +) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Get the top-k scores and their indices. + + Parameters + ---------- + scores : torch.Tensor + Tensor with scores. Expects input of size: `batch x 1 x height x width`. + K : int + Number of top scores to return. + + Returns + ------- + scores : torch.Tensor + Top-k scores. + ys : torch.Tensor + Y coordinates of the top-k scores. + xs : torch.Tensor + X coordinates of the top-k scores. + """ + batch, _, height, width = scores.size() + topk_scores, topk_inds = torch.topk(scores.view(batch, -1), K) + topk_inds = topk_inds % (height * width) + topk_ys = torch.div(topk_inds, width, rounding_mode="floor").long() + topk_xs = (topk_inds % width).long() + return topk_scores, topk_ys, topk_xs diff --git a/batdetect2/models/typing.py b/batdetect2/models/typing.py new file mode 100644 index 0000000..40fbd4c --- /dev/null +++ b/batdetect2/models/typing.py @@ -0,0 +1,55 @@ +from abc import ABC, abstractmethod +from typing import NamedTuple + +import torch +import torch.nn as nn + + +class ModelOutput(NamedTuple): + """Output of the detection model. + + Each of the tensors has a shape of + + `(batch_size, num_channels, spec_height, spec_width)`. + + Where `spec_height` and `spec_width` are the height and width of the + input spectrograms. + + They contain localised information of: + + 1. The probability of a bounding box detection at the given location. + 2. The predicted size of the bounding box at the given location. + 3. The probabilities of each class at the given location before softmax. + 4. Features used to make the predictions at the given location. + """ + + detection_probs: torch.Tensor + """Tensor with predict detection probabilities.""" + + size_preds: torch.Tensor + """Tensor with predicted bounding box sizes.""" + + class_probs: torch.Tensor + """Tensor with predicted class probabilities.""" + + features: torch.Tensor + """Tensor with intermediate features.""" + + +class EncoderModel(ABC, nn.Module): + + input_height: int + """Height of the input spectrogram.""" + + num_filts: int + """Dimension of the feature tensor.""" + + @abstractmethod + def forward(self, spec: torch.Tensor) -> torch.Tensor: + """Forward pass of the encoder model.""" + + +class DetectionModel(ABC, nn.Module): + @abstractmethod + def forward(self, spec: torch.Tensor) -> torch.Tensor: + """Forward pass of the detection model.""" diff --git a/batdetect2/plot.py b/batdetect2/plot.py index e436dae..0da9a36 100644 --- a/batdetect2/plot.py +++ b/batdetect2/plot.py @@ -102,6 +102,7 @@ def spectrogram( return ax + def spectrogram_with_detections( spec: Union[torch.Tensor, np.ndarray], dets: List[Annotation], diff --git a/batdetect2/plotting/__init__.py b/batdetect2/plotting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/batdetect2/plotting/common.py b/batdetect2/plotting/common.py new file mode 100644 index 0000000..82ce8c9 --- /dev/null +++ b/batdetect2/plotting/common.py @@ -0,0 +1,22 @@ +"""General plotting utilities.""" + +from typing import Optional, Tuple + +import matplotlib.pyplot as plt +from matplotlib import axes + +__all__ = [ + "create_ax", +] + + +def create_ax( + ax: Optional[axes.Axes] = None, + figsize: Tuple[int, int] = (10, 10), + **kwargs, +) -> axes.Axes: + """Create a new axis if none is provided""" + if ax is None: + _, ax = plt.subplots(figsize=figsize, **kwargs) # type: ignore + + return ax # type: ignore diff --git a/batdetect2/plotting/heatmaps.py b/batdetect2/plotting/heatmaps.py new file mode 100644 index 0000000..4927d86 --- /dev/null +++ b/batdetect2/plotting/heatmaps.py @@ -0,0 +1,27 @@ +"""Plot heatmaps""" + +from typing import Optional, Tuple + +import matplotlib.pyplot as plt +import xarray as xr +from matplotlib import axes + +from batdetect2.plotting.common import create_ax + + +def plot_heatmap( + heatmap: xr.DataArray, + ax: Optional[axes.Axes] = None, + figsize: Tuple[int, int] = (10, 10), +) -> axes.Axes: + ax = create_ax(ax, figsize=figsize) + + ax.pcolormesh( + heatmap.time, + heatmap.frequency, + heatmap, + vmax=1, + vmin=0, + ) + + return ax diff --git a/batdetect2/train/audio_dataloader.py b/batdetect2/train/audio_dataloader.py index 97fbc76..22c9070 100644 --- a/batdetect2/train/audio_dataloader.py +++ b/batdetect2/train/audio_dataloader.py @@ -1,4 +1,5 @@ """Functions and dataloaders for training and testing the model.""" + import copy from typing import List, Optional, Tuple @@ -199,8 +200,7 @@ def draw_gaussian( x0 = y0 = size // 2 # g = np.exp(- ((x - x0) ** 2 + (y - y0) ** 2) / (2 * sigma ** 2)) g = np.exp( - -((x - x0) ** 2) / (2 * sigmax**2) - - ((y - y0) ** 2) / (2 * sigmay**2) + -((x - x0) ** 2) / (2 * sigmax**2) - ((y - y0) ** 2) / (2 * sigmay**2) ) g_x = max(0, -ul[0]), min(br[0], h) - ul[0] g_y = max(0, -ul[1]), min(br[1], w) - ul[1] @@ -399,6 +399,8 @@ def echo_aug( sample_offset = ( int(echo_max_delay * np.random.random() * sampling_rate) + 1 ) + # NOTE: This seems to be wrong, as the echo should be added to the + # end of the audio, not the beginning. audio[:-sample_offset] += np.random.random() * audio[sample_offset:] return audio @@ -820,16 +822,18 @@ class AudioLoader(torch.utils.data.Dataset): # ) # create spectrogram - spec = au.generate_spectrogram( + spec, _ = au.generate_spectrogram( audio, sampling_rate, - fft_win_length=self.params["fft_win_length"], - fft_overlap=self.params["fft_overlap"], - max_freq=self.params["max_freq"], - min_freq=self.params["min_freq"], - spec_scale=self.params["spec_scale"], - denoise_spec_avg=self.params["denoise_spec_avg"], - max_scale_spec=self.params["max_scale_spec"], + params=dict( + fft_win_length=self.params["fft_win_length"], + fft_overlap=self.params["fft_overlap"], + max_freq=self.params["max_freq"], + min_freq=self.params["min_freq"], + spec_scale=self.params["spec_scale"], + denoise_spec_avg=self.params["denoise_spec_avg"], + max_scale_spec=self.params["max_scale_spec"], + ), ) rsf = self.params["resize_factor"] spec_op_shape = ( diff --git a/batdetect2/train/dataset.py b/batdetect2/train/dataset.py new file mode 100644 index 0000000..6b8df02 --- /dev/null +++ b/batdetect2/train/dataset.py @@ -0,0 +1,86 @@ +import os +from typing import NamedTuple +from pathlib import Path +from typing import Sequence, Union, Dict +from soundevent import data + +from torch.utils.data import Dataset +import torch +import xarray as xr + +from batdetect2.train.preprocess import PreprocessingConfig + + +__all__ = [ + "TrainExample", + "LabeledDataset", +] + + +PathLike = Union[Path, str, os.PathLike] + + +class TrainExample(NamedTuple): + spec: torch.Tensor + detection_heatmap: torch.Tensor + class_heatmap: torch.Tensor + size_heatmap: torch.Tensor + idx: torch.Tensor + + +def get_files(directory: PathLike, extension: str = ".nc") -> Sequence[Path]: + return list(Path(directory).glob(f"*{extension}")) + + +class LabeledDataset(Dataset): + def __init__(self, filenames: Sequence[PathLike]): + self.filenames = filenames + + def __len__(self): + return len(self.filenames) + + def __getitem__(self, idx) -> TrainExample: + data = self.load(self.filenames[idx]) + return TrainExample( + spec=data["spectrogram"], + detection_heatmap=data["detection"], + class_heatmap=data["class"], + size_heatmap=data["size"], + idx=torch.tensor(idx), + ) + + @classmethod + def from_directory(cls, directory: PathLike, extension: str = ".nc"): + return cls(get_files(directory, extension)) + + def load(self, filename: PathLike) -> Dict[str, torch.Tensor]: + dataset = xr.open_dataset(filename) + spectrogram = torch.tensor(dataset["spectrogram"].values).unsqueeze(0) + return { + "spectrogram": spectrogram, + "detection": torch.tensor(dataset["detection"].values), + "class": torch.tensor(dataset["class"].values), + "size": torch.tensor(dataset["size"].values), + } + + def get_spectrogram(self, idx): + return xr.open_dataset(self.filenames[idx])["spectrogram"] + + def get_detection_mask(self, idx): + return xr.open_dataset(self.filenames[idx])["detection"] + + def get_class_mask(self, idx): + return xr.open_dataset(self.filenames[idx])["class"] + + def get_size_mask(self, idx): + return xr.open_dataset(self.filenames[idx])["size"] + + def get_clip_annotation(self, idx): + filename = self.filenames[idx] + dataset = xr.open_dataset(filename) + clip_annotation = dataset.attrs["clip_annotation"] + return data.ClipAnnotation.model_validate_json(clip_annotation) + + def get_preprocessing_configuration(self, idx): + config = xr.open_dataset(self.filenames[idx]).attrs["configuration"] + return PreprocessingConfig.model_validate_json(config) diff --git a/batdetect2/train/light.py b/batdetect2/train/light.py new file mode 100644 index 0000000..2cbc047 --- /dev/null +++ b/batdetect2/train/light.py @@ -0,0 +1,56 @@ +import pytorch_lightning as L +from torch import Tensor, optim + +from batdetect2.models.typing import DetectionModel, ModelOutput +from batdetect2.train import losses + +from batdetect2.train.dataset import TrainExample + + +__all__ = [ + "LitDetectorModel", +] + + +class LitDetectorModel(L.LightningModule): + model: DetectionModel + + def __init__(self, model: DetectionModel, learning_rate: float = 1e-3): + super().__init__() + self.model = model + self.learning_rate = learning_rate + + def compute_loss( + self, + outputs: ModelOutput, + batch: TrainExample, + ) -> Tensor: + detection_loss = losses.focal_loss( + outputs.detection_probs, + batch.detection_heatmap, + ) + + size_loss = losses.bbox_size_loss( + outputs.size_preds, + batch.size_heatmap, + ) + + valid_mask = batch.class_heatmap.any(dim=1, keepdim=True).float() + classification_loss = losses.focal_loss( + outputs.class_probs, + batch.class_heatmap, + valid_mask=valid_mask, + ) + + return detection_loss + size_loss + classification_loss + + def training_step(self, batch: TrainExample, batch_idx: int): # type: ignore + outputs: ModelOutput = self.model(batch.spec) + loss = self.compute_loss(outputs, batch) + self.log("train_loss", loss) + return loss + + def configure_optimizers(self): + optimizer = optim.Adam(self.parameters(), lr=self.learning_rate) + scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, 100) + return [optimizer], [scheduler] diff --git a/batdetect2/train/losses.py b/batdetect2/train/losses.py index 1116c50..22a7eea 100644 --- a/batdetect2/train/losses.py +++ b/batdetect2/train/losses.py @@ -22,15 +22,15 @@ def focal_loss( gt: torch.Tensor, weights: Optional[torch.Tensor] = None, valid_mask: Optional[torch.Tensor] = None, + eps: float = 1e-5, + beta: float = 4, + alpha: float = 2, ) -> torch.Tensor: """ Focal loss adapted from CornerNet: Detecting Objects as Paired Keypoints pred (batch x c x h x w) gt (batch x c x h x w) """ - eps = 1e-5 - beta = 4 - alpha = 2 pos_inds = gt.eq(1).float() neg_inds = gt.lt(1).float() diff --git a/batdetect2/train/preprocess.py b/batdetect2/train/preprocess.py new file mode 100644 index 0000000..e7284df --- /dev/null +++ b/batdetect2/train/preprocess.py @@ -0,0 +1,209 @@ +"""Module for preprocessing data for training.""" + +import os +import warnings +from functools import partial +from pathlib import Path +from typing import Callable, Optional, Sequence, Union +from tqdm.auto import tqdm +from multiprocessing import Pool + +import xarray as xr +from pydantic import BaseModel, Field +from soundevent import data + +from batdetect2.data.labels import TARGET_SIGMA, LabelFn, generate_heatmaps +from batdetect2.data.preprocessing import ( + DENOISE_SPEC_AVG, + FFT_OVERLAP, + FFT_WIN_LENGTH_S, + MAX_FREQ_HZ, + MAX_SCALE_SPEC, + MIN_FREQ_HZ, + SCALE_RAW_AUDIO, + SPEC_SCALE, + TARGET_SAMPLERATE_HZ, + preprocess_audio_clip, +) + +PathLike = Union[Path, str, os.PathLike] +FilenameFn = Callable[[data.ClipAnnotation], str] + +__all__ = [ + "preprocess_annotations", +] + + +class PreprocessingConfig(BaseModel): + """Configuration for preprocessing data.""" + + target_samplerate: int = Field(default=TARGET_SAMPLERATE_HZ, gt=0) + + scale_audio: bool = Field(default=SCALE_RAW_AUDIO) + + fft_win_length: float = Field(default=FFT_WIN_LENGTH_S, gt=0) + + fft_overlap: float = Field(default=FFT_OVERLAP, ge=0, lt=1) + + max_freq: int = Field(default=MAX_FREQ_HZ, gt=0) + + min_freq: int = Field(default=MIN_FREQ_HZ, gt=0) + + spec_scale: str = Field(default=SPEC_SCALE) + + denoise_spec_avg: bool = DENOISE_SPEC_AVG + + max_scale_spec: bool = MAX_SCALE_SPEC + + target_sigma: float = Field(default=TARGET_SIGMA, gt=0) + + class_labels: Sequence[str] = ["bat"] + + +def generate_train_example( + clip_annotation: data.ClipAnnotation, + label_fn: LabelFn = lambda _: None, + config: Optional[PreprocessingConfig] = None, +) -> xr.Dataset: + """Generate a training example.""" + if config is None: + config = PreprocessingConfig() + + spectrogram = preprocess_audio_clip( + clip_annotation.clip, + target_sampling_rate=config.target_samplerate, + scale_audio=config.scale_audio, + fft_win_length=config.fft_win_length, + fft_overlap=config.fft_overlap, + max_freq=config.max_freq, + min_freq=config.min_freq, + spec_scale=config.spec_scale, + denoise_spec_avg=config.denoise_spec_avg, + max_scale_spec=config.max_scale_spec, + ) + + detection_heatmap, class_heatmap, size_heatmap = generate_heatmaps( + clip_annotation, + spectrogram, + target_sigma=config.target_sigma, + num_classes=len(config.class_labels), + class_labels=list(config.class_labels), + label_fn=label_fn, + ) + + dataset = xr.Dataset( + { + "spectrogram": spectrogram, + "detection": detection_heatmap, + "class": class_heatmap, + "size": size_heatmap, + } + ) + + return dataset.assign_attrs( + title=f"Training example for {clip_annotation.uuid}", + configuration=config.model_dump_json(), + clip_annotation=clip_annotation.model_dump_json(), + ) + + +def save_to_file( + dataset: xr.Dataset, + path: PathLike, +) -> None: + dataset.to_netcdf( + path, + encoding={ + "spectrogram": {"zlib": True}, + "size": {"zlib": True}, + "class": {"zlib": True}, + "detection": {"zlib": True}, + }, + ) + + +def load_config(path: PathLike, **kwargs) -> PreprocessingConfig: + """Load configuration from file.""" + + path = Path(path) + + if not path.is_file(): + warnings.warn(f"Config file not found: {path}. Using default config.") + return PreprocessingConfig(**kwargs) + + try: + return PreprocessingConfig.model_validate_json(path.read_text()) + except ValueError as e: + warnings.warn( + f"Failed to load config file: {e}. Using default config." + ) + return PreprocessingConfig(**kwargs) + + +def _get_filename(clip_annotation: data.ClipAnnotation) -> str: + return f"{clip_annotation.uuid}.nc" + + +def preprocess_single_annotation( + clip_annotation: data.ClipAnnotation, + output_dir: PathLike, + config: PreprocessingConfig, + filename_fn: FilenameFn = _get_filename, + replace: bool = False, + label_fn: LabelFn = lambda _: None, +) -> None: + output_dir = Path(output_dir) + + filename = filename_fn(clip_annotation) + path = output_dir / filename + + if path.is_file() and not replace: + return + + sample = generate_train_example( + clip_annotation, + label_fn=label_fn, + config=config, + ) + + save_to_file(sample, path) + + +def preprocess_annotations( + clip_annotations: Sequence[data.ClipAnnotation], + output_dir: PathLike, + filename_fn: FilenameFn = _get_filename, + replace: bool = False, + config_file: Optional[PathLike] = None, + label_fn: LabelFn = lambda _: None, + max_workers: Optional[int] = None, + **kwargs, +) -> None: + """Preprocess annotations and save to disk.""" + output_dir = Path(output_dir) + + if not output_dir.is_dir(): + output_dir.mkdir(parents=True) + + if config_file is not None: + config = load_config(config_file, **kwargs) + else: + config = PreprocessingConfig(**kwargs) + + with Pool(max_workers) as pool: + list( + tqdm( + pool.imap_unordered( + partial( + preprocess_single_annotation, + output_dir=output_dir, + config=config, + filename_fn=filename_fn, + replace=replace, + label_fn=label_fn, + ), + clip_annotations, + ), + total=len(clip_annotations), + ) + ) diff --git a/batdetect2/train/train.py b/batdetect2/train/train.py new file mode 100644 index 0000000..b41f230 --- /dev/null +++ b/batdetect2/train/train.py @@ -0,0 +1,82 @@ +from typing import Callable, NamedTuple, Optional + +import torch +from soundevent import data +from torch.optim import Adam +from torch.optim.lr_scheduler import CosineAnnealingLR +from torch.utils.data import DataLoader + +from batdetect2.data.datasets import ClipAnnotationDataset +from batdetect2.models.typing import DetectionModel + + +class TrainInputs(NamedTuple): + spec: torch.Tensor + detection_heatmap: torch.Tensor + class_heatmap: torch.Tensor + size_heatmap: torch.Tensor + + +def train_loop( + model: DetectionModel, + train_dataset: ClipAnnotationDataset[TrainInputs], + validation_dataset: ClipAnnotationDataset[TrainInputs], + device: Optional[torch.device] = None, + num_epochs: int = 100, + learning_rate: float = 1e-4, +): + train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True) + validation_loader = DataLoader(validation_dataset, batch_size=32) + + model.to(device) + + optimizer = Adam(model.parameters(), lr=learning_rate) + scheduler = CosineAnnealingLR( + optimizer, + num_epochs * len(train_loader), + ) + + for epoch in range(num_epochs): + train_loss = train_single_epoch( + model, + train_loader, + optimizer, + device, + scheduler, + ) + + +def train_single_epoch( + model: DetectionModel, + train_loader: DataLoader, + optimizer: Adam, + device: torch.device, + scheduler: CosineAnnealingLR, +): + model.train() + train_loss = tu.AverageMeter() + + for batch in train_loader: + optimizer.zero_grad() + + spec = batch.spec.to(device) + detection_heatmap = batch.detection_heatmap.to(device) + class_heatmap = batch.class_heatmap.to(device) + size_heatmap = batch.size_heatmap.to(device) + + outputs = model(spec) + + loss = loss_fun( + outputs, + gt_det, + gt_size, + gt_class, + det_criterion, + params, + class_inv_freq, + ) + + train_loss.update(loss.item(), data.shape[0]) + loss.backward() + optimizer.step() + scheduler.step() diff --git a/batdetect2/train/train_utils.py b/batdetect2/train/train_utils.py index acf8f42..5414d4a 100644 --- a/batdetect2/train/train_utils.py +++ b/batdetect2/train/train_utils.py @@ -1,3 +1,4 @@ +import sys import json from collections import Counter from pathlib import Path @@ -7,6 +8,11 @@ import numpy as np from batdetect2 import types +if sys.version_info >= (3, 9): + StringCounter = Counter[str] +else: + from typing import Counter as StringCounter + def write_notes_file(file_name: str, text: str): with open(file_name, "a") as da: @@ -148,7 +154,7 @@ def format_annotation( def get_class_names( data: List[types.FileAnnotation], classes_to_ignore: Optional[List[str]] = None, -) -> Tuple[Counter[str], List[float]]: +) -> Tuple[StringCounter, List[float]]: """Extracts class names and their inverse frequencies. Parameters @@ -182,7 +188,7 @@ def get_class_names( return counts, [mean_counts / counts[cc] for cc in class_names_list] -def report_class_counts(class_names: Counter[str]): +def report_class_counts(class_names: StringCounter): print("Class count:") str_len = np.max([len(cc) for cc in class_names]) + 5 for index, (class_name, count) in enumerate(class_names.most_common()): diff --git a/batdetect2/utils/audio_utils.py b/batdetect2/utils/audio_utils.py index 09e76c8..4f479d9 100644 --- a/batdetect2/utils/audio_utils.py +++ b/batdetect2/utils/audio_utils.py @@ -1,11 +1,13 @@ import warnings -from typing import Optional, Tuple, Union, overload +from typing import Optional, Tuple import librosa import librosa.core.spectrum import numpy as np import torch +from . import wavfile + __all__ = [ "load_audio", "generate_spectrogram", @@ -13,171 +15,113 @@ __all__ = [ ] -@overload -def time_to_x_coords( - time_in_file: np.ndarray, - sampling_rate: float, - fft_win_length: float, - fft_overlap: float, -) -> np.ndarray: - ... - - -@overload -def time_to_x_coords( - time_in_file: float, - sampling_rate: float, - fft_win_length: float, - fft_overlap: float, -) -> float: - ... - - -def time_to_x_coords( - time_in_file: Union[float, np.ndarray], - sampling_rate: float, - fft_win_length: float, - fft_overlap: float, -) -> Union[float, np.ndarray]: - nfft = np.floor(fft_win_length * sampling_rate) +def time_to_x_coords(time_in_file, sampling_rate, fft_win_length, fft_overlap): + nfft = np.floor(fft_win_length * sampling_rate) # int() uses floor noverlap = np.floor(fft_overlap * nfft) return (time_in_file * sampling_rate - noverlap) / (nfft - noverlap) # NOTE this is also defined in post_process -def x_coords_to_time( - x_pos: float, - sampling_rate: int, - fft_win_length: float, - fft_overlap: float, -) -> float: +def x_coords_to_time(x_pos, sampling_rate, fft_win_length, fft_overlap): nfft = np.floor(fft_win_length * sampling_rate) noverlap = np.floor(fft_overlap * nfft) return ((x_pos * (nfft - noverlap)) + noverlap) / sampling_rate - - # 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 generate_spectrogram( - audio: np.ndarray, - sampling_rate: float, - fft_win_length: float, - fft_overlap: float, - max_freq: float, - min_freq: float, - spec_scale: str, - denoise_spec_avg: bool = False, - max_scale_spec: bool = False, -) -> np.ndarray: + audio, + sampling_rate, + params, + return_spec_for_viz=False, + check_spec_size=True, +): # generate spectrogram spec = gen_mag_spectrogram( audio, sampling_rate, - window_len=fft_win_length, - overlap_perc=fft_overlap, - ) - spec = crop_spectrogram( - spec, - fft_win_length=fft_win_length, - max_freq=max_freq, - min_freq=min_freq, - ) - spec = scale_spectrogram( - spec, - sampling_rate, - spec_scale=spec_scale, - fft_win_length=fft_win_length, + params["fft_win_length"], + params["fft_overlap"], ) - if denoise_spec_avg: - spec = denoise_spectrogram(spec) - - if max_scale_spec: - spec = max_scale_spectrogram(spec) - - return spec - - -def crop_spectrogram( - spec: np.ndarray, - fft_win_length: float, - max_freq: float, - min_freq: float, -) -> np.ndarray: # crop to min/max freq - max_freq = round(max_freq * fft_win_length) - min_freq = round(min_freq * fft_win_length) + max_freq = round(params["max_freq"] * params["fft_win_length"]) + min_freq = round(params["min_freq"] * params["fft_win_length"]) if spec.shape[0] < max_freq: freq_pad = max_freq - spec.shape[0] spec = np.vstack( (np.zeros((freq_pad, spec.shape[1]), dtype=spec.dtype), spec) ) - return spec[-max_freq : spec.shape[0] - min_freq, :] + spec = spec[-max_freq : spec.shape[0] - min_freq, :] - -def denoise_spectrogram(spec: np.ndarray) -> np.ndarray: - spec = spec - np.mean(spec, 1)[:, np.newaxis] - return spec.clip(min=0) - - -def max_scale_spectrogram(spec: np.ndarray) -> np.ndarray: - return spec / (spec.max() + 10e-6) - - -def log_scale( - spec: np.ndarray, - sampling_rate: float, - fft_win_length: float, -) -> np.ndarray: - log_scaling = ( - 2.0 - * (1.0 / sampling_rate) - * ( - 1.0 - / ( - np.abs(np.hanning(int(fft_win_length * sampling_rate))) ** 2 - ).sum() + if params["spec_scale"] == "log": + log_scaling = ( + 2.0 + * (1.0 / sampling_rate) + * ( + 1.0 + / ( + np.abs( + np.hanning( + int(params["fft_win_length"] * sampling_rate) + ) + ) + ** 2 + ).sum() + ) ) - ) - return np.log1p(log_scaling * spec) + # log_scaling = (1.0 / sampling_rate)*0.1 + # log_scaling = (1.0 / sampling_rate)*10e4 + spec = np.log1p(log_scaling * spec) + elif params["spec_scale"] == "pcen": + spec = pcen(spec , sampling_rate) + elif params["spec_scale"] == "none": + pass -def scale_spectrogram( - spec: np.ndarray, - sampling_rate: float, - spec_scale: str, - fft_win_length: float, -) -> np.ndarray: - if spec_scale == "log": - return log_scale(spec, sampling_rate, fft_win_length) + if params["denoise_spec_avg"]: + spec = spec - np.mean(spec, 1)[:, np.newaxis] + spec.clip(min=0, out=spec) - if spec_scale == "pcen": - return pcen(spec, sampling_rate) + if params["max_scale_spec"]: + spec = spec / (spec.max() + 10e-6) - return spec + # needs to be divisible by specific factor - if not it should have been padded + # if check_spec_size: + # assert((int(spec.shape[0]*params['resize_factor']) % params['spec_divide_factor']) == 0) + # assert((int(spec.shape[1]*params['resize_factor']) % params['spec_divide_factor']) == 0) - -def prepare_spec_for_viz( - spec: np.ndarray, - sampling_rate: int, - fft_win_length: float, -) -> np.ndarray: # for visualization purposes - use log scaled spectrogram - return log_scale( - spec, - sampling_rate, - fft_win_length=fft_win_length, - ).astype(np.float32) + if return_spec_for_viz: + log_scaling = ( + 2.0 + * (1.0 / sampling_rate) + * ( + 1.0 + / ( + np.abs( + np.hanning( + int(params["fft_win_length"] * sampling_rate) + ) + ) + ** 2 + ).sum() + ) + ) + spec_for_viz = np.log1p(log_scaling * spec).astype(np.float32) + else: + spec_for_viz = None + + return spec, spec_for_viz def load_audio( audio_file: str, time_exp_fact: float, - target_sampling_rate: int, + target_samp_rate: int, scale: bool = False, max_duration: Optional[float] = None, -) -> Tuple[float, np.ndarray]: +) -> Tuple[int, np.ndarray]: """Load an audio file and resample it to the target sampling rate. The audio is also scaled to [-1, 1] and clipped to the maximum duration. @@ -208,82 +152,63 @@ def load_audio( """ with warnings.catch_warnings(): - audio, sampling_rate = librosa.load( + warnings.filterwarnings("ignore", category=wavfile.WavFileWarning) + # sampling_rate, audio_raw = wavfile.read(audio_file) + audio_raw, sampling_rate = librosa.load( audio_file, sr=None, dtype=np.float32, ) - if len(audio.shape) > 1: + if len(audio_raw.shape) > 1: raise ValueError("Currently does not handle stereo files") sampling_rate = sampling_rate * time_exp_fact # resample - need to do this after correcting for time expansion - audio = resample_audio(audio, sampling_rate, target_sampling_rate) - - if max_duration is not None: - audio = clip_audio(audio, target_sampling_rate, max_duration) - - # scale to [-1, 1] - if scale: - audio = scale_audio(audio) - - return target_sampling_rate, audio - - -def resample_audio( - audio: np.ndarray, - sr_orig: float, - sr_target: float, -) -> np.ndarray: - if sr_orig != sr_target: - return librosa.resample( - audio, - orig_sr=sr_orig, - target_sr=sr_target, + sampling_rate_old = sampling_rate + sampling_rate = target_samp_rate + if sampling_rate_old != sampling_rate: + audio_raw = librosa.resample( + audio_raw, + orig_sr=sampling_rate_old, + target_sr=sampling_rate, res_type="polyphase", ) - return audio - - -def clip_audio( - audio: np.ndarray, - sampling_rate: float, - max_duration: float, -) -> np.ndarray: - max_duration = int( - np.minimum( - int(sampling_rate * max_duration), - audio.shape[0], + # clipping maximum duration + if max_duration is not None: + max_duration = int( + np.minimum( + int(sampling_rate * max_duration), + audio_raw.shape[0], + ) ) - ) - return audio[:max_duration] + audio_raw = audio_raw[:max_duration] + # scale to [-1, 1] + if scale: + audio_raw = audio_raw - audio_raw.mean() + audio_raw = audio_raw / (np.abs(audio_raw).max() + 10e-6) -def scale_audio( - audio: np.ndarray, - eps: float = 10e-6, -) -> np.ndarray: - return (audio - audio.mean()) / (np.abs(audio).max() + eps) + return sampling_rate, audio_raw def pad_audio( - audio_raw: np.ndarray, - sampling_rate: float, - window_len: float, - overlap_perc: float, - resize_factor: float, - divide_factor: float, - fixed_width: Optional[int] = None, -) -> np.ndarray: + audio_raw, + fs, + ms, + overlap_perc, + resize_factor, + divide_factor, + fixed_width=None, +): # Adds zeros to the end of the raw data so that the generated sepctrogram # 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 - nfft = int(window_len * sampling_rate) + nfft = int(ms * fs) noverlap = int(overlap_perc * nfft) step = nfft - noverlap min_size = int(divide_factor * (1.0 / resize_factor)) @@ -320,23 +245,22 @@ def pad_audio( return audio_raw -def gen_mag_spectrogram( - audio: np.ndarray, - sampling_rate: float, - window_len: float, - overlap_perc: float, -) -> np.ndarray: +def gen_mag_spectrogram(x, fs, ms, overlap_perc): # Computes magnitude spectrogram by specifying time. - audio = audio.astype(np.float32) - nfft = int(window_len * sampling_rate) + + x = x.astype(np.float32) + nfft = int(ms * fs) noverlap = int(overlap_perc * nfft) + # window data + step = nfft - noverlap + # compute spec spec, _ = librosa.core.spectrum._spectrogram( - y=audio, + y=x, power=1, n_fft=nfft, - hop_length=nfft - noverlap, + hop_length=step, center=False, ) @@ -346,25 +270,24 @@ def gen_mag_spectrogram( return spec.astype(np.float32) -def gen_mag_spectrogram_pt( - audio: torch.Tensor, - sampling_rate: float, - window_len: float, - overlap_perc: float, -) -> torch.Tensor: - nfft = int(window_len * sampling_rate) +def gen_mag_spectrogram_pt(x, fs, ms, overlap_perc): + nfft = int(ms * fs) nstep = round((1.0 - overlap_perc) * nfft) - han_win = torch.hann_window(nfft, periodic=False).to(audio.device) - complex_spec = torch.stft(audio, nfft, nstep, window=han_win, center=False) + han_win = torch.hann_window(nfft, periodic=False).to(x.device) + + complex_spec = torch.stft(x, nfft, nstep, window=han_win, center=False) spec = complex_spec.pow(2.0).sum(-1) # remove DC component and flip vertically - return torch.flipud(spec[0, 1:, :]) + spec = torch.flipud(spec[0, 1:, :]) + + return spec -def pcen(spec: np.ndarray, sampling_rate: float) -> np.ndarray: +def pcen(spec_cropped, sampling_rate): # TODO should be passing hop_length too i.e. step - return librosa.pcen(spec * (2**31), sr=sampling_rate / 10).astype( + spec = librosa.pcen(spec_cropped * (2**31), sr=sampling_rate / 10).astype( np.float32 ) + return spec diff --git a/batdetect2/utils/detector_utils.py b/batdetect2/utils/detector_utils.py index a6eadfd..efd867d 100644 --- a/batdetect2/utils/detector_utils.py +++ b/batdetect2/utils/detector_utils.py @@ -437,7 +437,7 @@ def compute_spectrogram( ) # generate spectrogram - spec = au.generate_spectrogram(audio, sampling_rate, params) + spec, _ = au.generate_spectrogram(audio, sampling_rate, params) # convert to pytorch spec = torch.from_numpy(spec).to(device) @@ -746,7 +746,7 @@ def process_file( sampling_rate, audio_full = au.load_audio( audio_file, time_exp_fact=config.get("time_expansion", 1) or 1, - target_sampling_rate=config["target_samp_rate"], + target_samp_rate=config["target_samp_rate"], scale=config["scale_raw_audio"], max_duration=config.get("max_duration"), ) diff --git a/environment.yml b/environment.yml deleted file mode 100644 index a21373e..0000000 --- a/environment.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: batdetect2 -channels: - - defaults - - conda-forge - - pytorch - - nvidia -dependencies: - - python==3.10 - - matplotlib - - pandas - - scikit-learn - - numpy - - pytorch - - scipy - - torchvision - - librosa - - torchaudio diff --git a/pdm.lock b/pdm.lock deleted file mode 100644 index ce44838..0000000 --- a/pdm.lock +++ /dev/null @@ -1,1272 +0,0 @@ -# This file is @generated by PDM. -# It is not intended for manual editing. - -[metadata] -groups = ["default", "dev"] -strategy = ["cross_platform"] -lock_version = "4.4" -content_hash = "sha256:3b9b55465de7c9ad484a386587d6d93da1f8f28ec9521b4d80328fd01ce5e147" - -[[package]] -name = "annotated-types" -version = "0.6.0" -requires_python = ">=3.8" -summary = "Reusable constraint types to use with typing.Annotated" -dependencies = [ - "typing-extensions>=4.0.0; python_version < \"3.9\"", -] -files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, -] - -[[package]] -name = "appdirs" -version = "1.4.4" -summary = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -files = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] - -[[package]] -name = "attrs" -version = "22.2.0" -requires_python = ">=3.6" -summary = "Classes Without Boilerplate" -files = [ - {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, - {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, -] - -[[package]] -name = "audioread" -version = "3.0.0" -requires_python = ">=3.6" -summary = "multi-library, cross-platform audio decoding" -files = [ - {file = "audioread-3.0.0.tar.gz", hash = "sha256:121995bd207eb1fda3d566beb851d3534275925bc35a4fb6da0cb11de0f7251a"}, -] - -[[package]] -name = "certifi" -version = "2022.12.7" -requires_python = ">=3.6" -summary = "Python package for providing Mozilla's CA Bundle." -files = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, -] - -[[package]] -name = "cffi" -version = "1.15.1" -summary = "Foreign Function Interface for Python calling C code." -dependencies = [ - "pycparser", -] -files = [ - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.1.0" -requires_python = ">=3.7.0" -summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -files = [ - {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, - {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, -] - -[[package]] -name = "click" -version = "8.1.3" -requires_python = ">=3.7" -summary = "Composable command line interface toolkit" -dependencies = [ - "colorama; platform_system == \"Windows\"", -] -files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -summary = "Cross-platform colored terminal text." -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "contourpy" -version = "1.0.7" -requires_python = ">=3.8" -summary = "Python library for calculating contours of 2D quadrilateral grids" -dependencies = [ - "numpy>=1.16", -] -files = [ - {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:95c3acddf921944f241b6773b767f1cbce71d03307270e2d769fd584d5d1092d"}, - {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc1464c97579da9f3ab16763c32e5c5d5bb5fa1ec7ce509a4ca6108b61b84fab"}, - {file = "contourpy-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8acf74b5d383414401926c1598ed77825cd530ac7b463ebc2e4f46638f56cce6"}, - {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c71fdd8f1c0f84ffd58fca37d00ca4ebaa9e502fb49825484da075ac0b0b803"}, - {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f99e9486bf1bb979d95d5cffed40689cb595abb2b841f2991fc894b3452290e8"}, - {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f4d8941a9564cda3f7fa6a6cd9b32ec575830780677932abdec7bcb61717b0"}, - {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9e20e5a1908e18aaa60d9077a6d8753090e3f85ca25da6e25d30dc0a9e84c2c6"}, - {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a877ada905f7d69b2a31796c4b66e31a8068b37aa9b78832d41c82fc3e056ddd"}, - {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6381fa66866b0ea35e15d197fc06ac3840a9b2643a6475c8fff267db8b9f1e69"}, - {file = "contourpy-1.0.7-cp310-cp310-win32.whl", hash = "sha256:3c184ad2433635f216645fdf0493011a4667e8d46b34082f5a3de702b6ec42e3"}, - {file = "contourpy-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:3caea6365b13119626ee996711ab63e0c9d7496f65641f4459c60a009a1f3e80"}, - {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd7dc0e6812b799a34f6d12fcb1000539098c249c8da54f3566c6a6461d0dbad"}, - {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0f9d350b639db6c2c233d92c7f213d94d2e444d8e8fc5ca44c9706cf72193772"}, - {file = "contourpy-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e96a08b62bb8de960d3a6afbc5ed8421bf1a2d9c85cc4ea73f4bc81b4910500f"}, - {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:031154ed61f7328ad7f97662e48660a150ef84ee1bc8876b6472af88bf5a9b98"}, - {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e9ebb4425fc1b658e13bace354c48a933b842d53c458f02c86f371cecbedecc"}, - {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efb8f6d08ca7998cf59eaf50c9d60717f29a1a0a09caa46460d33b2924839dbd"}, - {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6c180d89a28787e4b73b07e9b0e2dac7741261dbdca95f2b489c4f8f887dd810"}, - {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b8d587cc39057d0afd4166083d289bdeff221ac6d3ee5046aef2d480dc4b503c"}, - {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:769eef00437edf115e24d87f8926955f00f7704bede656ce605097584f9966dc"}, - {file = "contourpy-1.0.7-cp38-cp38-win32.whl", hash = "sha256:62398c80ef57589bdbe1eb8537127321c1abcfdf8c5f14f479dbbe27d0322e66"}, - {file = "contourpy-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:57119b0116e3f408acbdccf9eb6ef19d7fe7baf0d1e9aaa5381489bc1aa56556"}, - {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30676ca45084ee61e9c3da589042c24a57592e375d4b138bd84d8709893a1ba4"}, - {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e927b3868bd1e12acee7cc8f3747d815b4ab3e445a28d2e5373a7f4a6e76ba1"}, - {file = "contourpy-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:366a0cf0fc079af5204801786ad7a1c007714ee3909e364dbac1729f5b0849e5"}, - {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ba9bb365446a22411f0673abf6ee1fea3b2cf47b37533b970904880ceb72f3"}, - {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71b0bf0c30d432278793d2141362ac853859e87de0a7dee24a1cea35231f0d50"}, - {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7281244c99fd7c6f27c1c6bfafba878517b0b62925a09b586d88ce750a016d2"}, - {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b6d0f9e1d39dbfb3977f9dd79f156c86eb03e57a7face96f199e02b18e58d32a"}, - {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7f6979d20ee5693a1057ab53e043adffa1e7418d734c1532e2d9e915b08d8ec2"}, - {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5dd34c1ae752515318224cba7fc62b53130c45ac6a1040c8b7c1a223c46e8967"}, - {file = "contourpy-1.0.7-cp39-cp39-win32.whl", hash = "sha256:c5210e5d5117e9aec8c47d9156d1d3835570dd909a899171b9535cb4a3f32693"}, - {file = "contourpy-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:60835badb5ed5f4e194a6f21c09283dd6e007664a86101431bf870d9e86266c4"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce41676b3d0dd16dbcfabcc1dc46090aaf4688fd6e819ef343dbda5a57ef0161"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a011cf354107b47c58ea932d13b04d93c6d1d69b8b6dce885e642531f847566"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31a55dccc8426e71817e3fe09b37d6d48ae40aae4ecbc8c7ad59d6893569c436"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69f8ff4db108815addd900a74df665e135dbbd6547a8a69333a68e1f6e368ac2"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efe99298ba37e37787f6a2ea868265465410822f7bea163edcc1bd3903354ea9"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a1e97b86f73715e8670ef45292d7cc033548266f07d54e2183ecb3c87598888f"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc331c13902d0f50845099434cd936d49d7a2ca76cb654b39691974cb1e4812d"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24847601071f740837aefb730e01bd169fbcaa610209779a78db7ebb6e6a7051"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abf298af1e7ad44eeb93501e40eb5a67abbf93b5d90e468d01fc0c4451971afa"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:64757f6460fc55d7e16ed4f1de193f362104285c667c112b50a804d482777edd"}, - {file = "contourpy-1.0.7.tar.gz", hash = "sha256:d8165a088d31798b59e91117d1f5fc3df8168d8b48c4acc10fc0df0d0bdbcc5e"}, -] - -[[package]] -name = "cycler" -version = "0.11.0" -requires_python = ">=3.6" -summary = "Composable style cycles" -files = [ - {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, - {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, -] - -[[package]] -name = "decorator" -version = "5.1.1" -requires_python = ">=3.5" -summary = "Decorators for Humans" -files = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, -] - -[[package]] -name = "dnspython" -version = "2.4.2" -requires_python = ">=3.8,<4.0" -summary = "DNS toolkit" -files = [ - {file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"}, - {file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"}, -] - -[[package]] -name = "email-validator" -version = "2.1.0.post1" -requires_python = ">=3.8" -summary = "A robust email address syntax and deliverability validation library." -dependencies = [ - "dnspython>=2.0.0", - "idna>=2.0.0", -] -files = [ - {file = "email_validator-2.1.0.post1-py3-none-any.whl", hash = "sha256:c973053efbeddfef924dc0bd93f6e77a1ea7ee0fce935aea7103c7a3d6d2d637"}, - {file = "email_validator-2.1.0.post1.tar.gz", hash = "sha256:a4b0bd1cf55f073b924258d19321b1f3aa74b4b5a71a42c305575dba920e1a44"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.1.1" -requires_python = ">=3.7" -summary = "Backport of PEP 654 (exception groups)" -files = [ - {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, - {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, -] - -[[package]] -name = "fonttools" -version = "4.39.3" -requires_python = ">=3.8" -summary = "Tools to manipulate font files" -files = [ - {file = "fonttools-4.39.3-py3-none-any.whl", hash = "sha256:64c0c05c337f826183637570ac5ab49ee220eec66cf50248e8df527edfa95aeb"}, - {file = "fonttools-4.39.3.zip", hash = "sha256:9234b9f57b74e31b192c3fc32ef1a40750a8fbc1cd9837a7b7bfc4ca4a5c51d7"}, -] - -[[package]] -name = "idna" -version = "3.4" -requires_python = ">=3.5" -summary = "Internationalized Domain Names in Applications (IDNA)" -files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] - -[[package]] -name = "importlib-metadata" -version = "6.1.0" -requires_python = ">=3.7" -summary = "Read metadata from Python packages" -dependencies = [ - "zipp>=0.5", -] -files = [ - {file = "importlib_metadata-6.1.0-py3-none-any.whl", hash = "sha256:ff80f3b5394912eb1b108fcfd444dc78b7f1f3e16b16188054bd01cb9cb86f09"}, - {file = "importlib_metadata-6.1.0.tar.gz", hash = "sha256:43ce9281e097583d758c2c708c4376371261a02c34682491a8e98352365aad20"}, -] - -[[package]] -name = "importlib-resources" -version = "5.12.0" -requires_python = ">=3.7" -summary = "Read resources from Python packages" -dependencies = [ - "zipp>=3.1.0; python_version < \"3.10\"", -] -files = [ - {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, - {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -requires_python = ">=3.7" -summary = "brain-dead simple config-ini parsing" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "joblib" -version = "1.2.0" -requires_python = ">=3.7" -summary = "Lightweight pipelining with Python functions" -files = [ - {file = "joblib-1.2.0-py3-none-any.whl", hash = "sha256:091138ed78f800342968c523bdde947e7a305b8594b910a0fea2ab83c3c6d385"}, - {file = "joblib-1.2.0.tar.gz", hash = "sha256:e1cee4a79e4af22881164f218d4311f60074197fb707e082e803b61f6d137018"}, -] - -[[package]] -name = "kiwisolver" -version = "1.4.4" -requires_python = ">=3.7" -summary = "A fast implementation of the Cassowary constraint solver" -files = [ - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6"}, - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c"}, - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32"}, - {file = "kiwisolver-1.4.4-cp310-cp310-win32.whl", hash = "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408"}, - {file = "kiwisolver-1.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c"}, - {file = "kiwisolver-1.4.4-cp38-cp38-win32.whl", hash = "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191"}, - {file = "kiwisolver-1.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9"}, - {file = "kiwisolver-1.4.4-cp39-cp39-win32.whl", hash = "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea"}, - {file = "kiwisolver-1.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b"}, - {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"}, -] - -[[package]] -name = "lazy-loader" -version = "0.2" -requires_python = ">=3.7" -summary = "lazy_loader" -files = [ - {file = "lazy_loader-0.2-py3-none-any.whl", hash = "sha256:c35875f815c340f823ce3271ed645045397213f961b40ad0c0d395c3f5218eeb"}, - {file = "lazy_loader-0.2.tar.gz", hash = "sha256:0edc7a5175c400acb108f283749951fefdadedeb00adcec6e88b974a9254f18a"}, -] - -[[package]] -name = "librosa" -version = "0.10.0.post2" -requires_python = ">=3.7" -summary = "Python module for audio and music processing" -dependencies = [ - "audioread>=2.1.9", - "decorator>=4.3.0", - "joblib>=0.14", - "lazy-loader>=0.1", - "msgpack>=1.0", - "numba>=0.51.0", - "numpy!=1.22.0,!=1.22.1,!=1.22.2,>=1.20.3", - "pooch<1.7,>=1.0", - "scikit-learn>=0.20.0", - "scipy>=1.2.0", - "soundfile>=0.12.1", - "soxr>=0.3.2", - "typing-extensions>=4.1.1", -] -files = [ - {file = "librosa-0.10.0.post2-py3-none-any.whl", hash = "sha256:0f3b56118cb01ea89df4b04e924c7f48c5c13d42cc55a12540eb04ae87ab5848"}, - {file = "librosa-0.10.0.post2.tar.gz", hash = "sha256:6623673da30773beaae962cb4685f188155582f25bc60fc52da968f59eea8567"}, -] - -[[package]] -name = "llvmlite" -version = "0.39.1" -requires_python = ">=3.7" -summary = "lightweight wrapper around basic LLVM functionality" -files = [ - {file = "llvmlite-0.39.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6717c7a6e93c9d2c3d07c07113ec80ae24af45cde536b34363d4bcd9188091d9"}, - {file = "llvmlite-0.39.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ddab526c5a2c4ccb8c9ec4821fcea7606933dc53f510e2a6eebb45a418d3488a"}, - {file = "llvmlite-0.39.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3f331a323d0f0ada6b10d60182ef06c20a2f01be21699999d204c5750ffd0b4"}, - {file = "llvmlite-0.39.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c00ff204afa721b0bb9835b5bf1ba7fba210eefcec5552a9e05a63219ba0dc"}, - {file = "llvmlite-0.39.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16f56eb1eec3cda3a5c526bc3f63594fc24e0c8d219375afeb336f289764c6c7"}, - {file = "llvmlite-0.39.1-cp310-cp310-win32.whl", hash = "sha256:d0bfd18c324549c0fec2c5dc610fd024689de6f27c6cc67e4e24a07541d6e49b"}, - {file = "llvmlite-0.39.1-cp310-cp310-win_amd64.whl", hash = "sha256:7ebf1eb9badc2a397d4f6a6c8717447c81ac011db00064a00408bc83c923c0e4"}, - {file = "llvmlite-0.39.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e31f4b799d530255aaf0566e3da2df5bfc35d3cd9d6d5a3dcc251663656c27b1"}, - {file = "llvmlite-0.39.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:62c0ea22e0b9dffb020601bb65cb11dd967a095a488be73f07d8867f4e327ca5"}, - {file = "llvmlite-0.39.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ffc84ade195abd4abcf0bd3b827b9140ae9ef90999429b9ea84d5df69c9058c"}, - {file = "llvmlite-0.39.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0f158e4708dda6367d21cf15afc58de4ebce979c7a1aa2f6b977aae737e2a54"}, - {file = "llvmlite-0.39.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22d36591cd5d02038912321d9ab8e4668e53ae2211da5523f454e992b5e13c36"}, - {file = "llvmlite-0.39.1-cp38-cp38-win32.whl", hash = "sha256:4c6ebace910410daf0bebda09c1859504fc2f33d122e9a971c4c349c89cca630"}, - {file = "llvmlite-0.39.1-cp38-cp38-win_amd64.whl", hash = "sha256:fb62fc7016b592435d3e3a8f680e3ea8897c3c9e62e6e6cc58011e7a4801439e"}, - {file = "llvmlite-0.39.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa9b26939ae553bf30a9f5c4c754db0fb2d2677327f2511e674aa2f5df941789"}, - {file = "llvmlite-0.39.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e4f212c018db951da3e1dc25c2651abc688221934739721f2dad5ff1dd5f90e7"}, - {file = "llvmlite-0.39.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39dc2160aed36e989610fc403487f11b8764b6650017ff367e45384dff88ffbf"}, - {file = "llvmlite-0.39.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ec3d70b3e507515936e475d9811305f52d049281eaa6c8273448a61c9b5b7e2"}, - {file = "llvmlite-0.39.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60f8dd1e76f47b3dbdee4b38d9189f3e020d22a173c00f930b52131001d801f9"}, - {file = "llvmlite-0.39.1-cp39-cp39-win32.whl", hash = "sha256:03aee0ccd81735696474dc4f8b6be60774892a2929d6c05d093d17392c237f32"}, - {file = "llvmlite-0.39.1-cp39-cp39-win_amd64.whl", hash = "sha256:3fc14e757bc07a919221f0cbaacb512704ce5774d7fcada793f1996d6bc75f2a"}, - {file = "llvmlite-0.39.1.tar.gz", hash = "sha256:b43abd7c82e805261c425d50335be9a6c4f84264e34d6d6e475207300005d572"}, -] - -[[package]] -name = "matplotlib" -version = "3.7.1" -requires_python = ">=3.8" -summary = "Python plotting package" -dependencies = [ - "contourpy>=1.0.1", - "cycler>=0.10", - "fonttools>=4.22.0", - "importlib-resources>=3.2.0; python_version < \"3.10\"", - "kiwisolver>=1.0.1", - "numpy>=1.20", - "packaging>=20.0", - "pillow>=6.2.0", - "pyparsing>=2.3.1", - "python-dateutil>=2.7", -] -files = [ - {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:95cbc13c1fc6844ab8812a525bbc237fa1470863ff3dace7352e910519e194b1"}, - {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:08308bae9e91aca1ec6fd6dda66237eef9f6294ddb17f0d0b3c863169bf82353"}, - {file = "matplotlib-3.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:544764ba51900da4639c0f983b323d288f94f65f4024dc40ecb1542d74dc0500"}, - {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d94989191de3fcc4e002f93f7f1be5da476385dde410ddafbb70686acf00ea"}, - {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99bc9e65901bb9a7ce5e7bb24af03675cbd7c70b30ac670aa263240635999a4"}, - {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb7d248c34a341cd4c31a06fd34d64306624c8cd8d0def7abb08792a5abfd556"}, - {file = "matplotlib-3.7.1-cp310-cp310-win32.whl", hash = "sha256:ce463ce590f3825b52e9fe5c19a3c6a69fd7675a39d589e8b5fbe772272b3a24"}, - {file = "matplotlib-3.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d7bc90727351fb841e4d8ae620d2d86d8ed92b50473cd2b42ce9186104ecbba"}, - {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:6eb88d87cb2c49af00d3bbc33a003f89fd9f78d318848da029383bfc08ecfbfb"}, - {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:cf0e4f727534b7b1457898c4f4ae838af1ef87c359b76dcd5330fa31893a3ac7"}, - {file = "matplotlib-3.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:46a561d23b91f30bccfd25429c3c706afe7d73a5cc64ef2dfaf2b2ac47c1a5dc"}, - {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8704726d33e9aa8a6d5215044b8d00804561971163563e6e6591f9dcf64340cc"}, - {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4cf327e98ecf08fcbb82685acaf1939d3338548620ab8dfa02828706402c34de"}, - {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:617f14ae9d53292ece33f45cba8503494ee199a75b44de7717964f70637a36aa"}, - {file = "matplotlib-3.7.1-cp38-cp38-win32.whl", hash = "sha256:7c9a4b2da6fac77bcc41b1ea95fadb314e92508bf5493ceff058e727e7ecf5b0"}, - {file = "matplotlib-3.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:14645aad967684e92fc349493fa10c08a6da514b3d03a5931a1bac26e6792bd1"}, - {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:81a6b377ea444336538638d31fdb39af6be1a043ca5e343fe18d0f17e098770b"}, - {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:28506a03bd7f3fe59cd3cd4ceb2a8d8a2b1db41afede01f66c42561b9be7b4b7"}, - {file = "matplotlib-3.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8c587963b85ce41e0a8af53b9b2de8dddbf5ece4c34553f7bd9d066148dc719c"}, - {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bf26ade3ff0f27668989d98c8435ce9327d24cffb7f07d24ef609e33d582439"}, - {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:def58098f96a05f90af7e92fd127d21a287068202aa43b2a93476170ebd99e87"}, - {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f883a22a56a84dba3b588696a2b8a1ab0d2c3d41be53264115c71b0a942d8fdb"}, - {file = "matplotlib-3.7.1-cp39-cp39-win32.whl", hash = "sha256:4f99e1b234c30c1e9714610eb0c6d2f11809c9c78c984a613ae539ea2ad2eb4b"}, - {file = "matplotlib-3.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:3ba2af245e36990facf67fde840a760128ddd71210b2ab6406e640188d69d136"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3032884084f541163f295db8a6536e0abb0db464008fadca6c98aaf84ccf4717"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a2cb34336110e0ed8bb4f650e817eed61fa064acbefeb3591f1b33e3a84fd96"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b867e2f952ed592237a1828f027d332d8ee219ad722345b79a001f49df0936eb"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:57bfb8c8ea253be947ccb2bc2d1bb3862c2bccc662ad1b4626e1f5e004557042"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:438196cdf5dc8d39b50a45cb6e3f6274edbcf2254f85fa9b895bf85851c3a613"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21e9cff1a58d42e74d01153360de92b326708fb205250150018a52c70f43c290"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d4725d70b7c03e082bbb8a34639ede17f333d7247f56caceb3801cb6ff703d"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:97cc368a7268141afb5690760921765ed34867ffb9655dd325ed207af85c7529"}, - {file = "matplotlib-3.7.1.tar.gz", hash = "sha256:7b73305f25eab4541bd7ee0b96d87e53ae9c9f1823be5659b806cd85786fe882"}, -] - -[[package]] -name = "msgpack" -version = "1.0.5" -summary = "MessagePack serializer" -files = [ - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"}, - {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"}, - {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"}, - {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"}, - {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"}, - {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"}, - {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, - {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, -] - -[[package]] -name = "numba" -version = "0.56.4" -requires_python = ">=3.7" -summary = "compiling Python code using LLVM" -dependencies = [ - "importlib-metadata; python_version < \"3.9\"", - "llvmlite<0.40,>=0.39.0dev0", - "numpy<1.24,>=1.18", - "setuptools", -] -files = [ - {file = "numba-0.56.4-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9f62672145f8669ec08762895fe85f4cf0ead08ce3164667f2b94b2f62ab23c3"}, - {file = "numba-0.56.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c602d015478b7958408d788ba00a50272649c5186ea8baa6cf71d4a1c761bba1"}, - {file = "numba-0.56.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:85dbaed7a05ff96492b69a8900c5ba605551afb9b27774f7f10511095451137c"}, - {file = "numba-0.56.4-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f4cfc3a19d1e26448032049c79fc60331b104f694cf570a9e94f4e2c9d0932bb"}, - {file = "numba-0.56.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e08e203b163ace08bad500b0c16f6092b1eb34fd1fce4feaf31a67a3a5ecf3b"}, - {file = "numba-0.56.4-cp310-cp310-win32.whl", hash = "sha256:0611e6d3eebe4cb903f1a836ffdb2bda8d18482bcd0a0dcc56e79e2aa3fefef5"}, - {file = "numba-0.56.4-cp310-cp310-win_amd64.whl", hash = "sha256:fbfb45e7b297749029cb28694abf437a78695a100e7c2033983d69f0ba2698d4"}, - {file = "numba-0.56.4-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:553da2ce74e8862e18a72a209ed3b6d2924403bdd0fb341fa891c6455545ba7c"}, - {file = "numba-0.56.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4373da9757049db7c90591e9ec55a2e97b2b36ba7ae3bf9c956a513374077470"}, - {file = "numba-0.56.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a993349b90569518739009d8f4b523dfedd7e0049e6838c0e17435c3e70dcc4"}, - {file = "numba-0.56.4-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:720886b852a2d62619ae3900fe71f1852c62db4f287d0c275a60219e1643fc04"}, - {file = "numba-0.56.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e64d338b504c9394a4a34942df4627e1e6cb07396ee3b49fe7b8d6420aa5104f"}, - {file = "numba-0.56.4-cp38-cp38-win32.whl", hash = "sha256:03fe94cd31e96185cce2fae005334a8cc712fc2ba7756e52dff8c9400718173f"}, - {file = "numba-0.56.4-cp38-cp38-win_amd64.whl", hash = "sha256:91f021145a8081f881996818474ef737800bcc613ffb1e618a655725a0f9e246"}, - {file = "numba-0.56.4-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:d0ae9270a7a5cc0ede63cd234b4ff1ce166c7a749b91dbbf45e0000c56d3eade"}, - {file = "numba-0.56.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c75e8a5f810ce80a0cfad6e74ee94f9fde9b40c81312949bf356b7304ef20740"}, - {file = "numba-0.56.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a12ef323c0f2101529d455cfde7f4135eaa147bad17afe10b48634f796d96abd"}, - {file = "numba-0.56.4-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:03634579d10a6129181129de293dd6b5eaabee86881369d24d63f8fe352dd6cb"}, - {file = "numba-0.56.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0240f9026b015e336069329839208ebd70ec34ae5bfbf402e4fcc8e06197528e"}, - {file = "numba-0.56.4-cp39-cp39-win32.whl", hash = "sha256:14dbbabf6ffcd96ee2ac827389afa59a70ffa9f089576500434c34abf9b054a4"}, - {file = "numba-0.56.4-cp39-cp39-win_amd64.whl", hash = "sha256:0da583c532cd72feefd8e551435747e0e0fbb3c0530357e6845fcc11e38d6aea"}, - {file = "numba-0.56.4.tar.gz", hash = "sha256:32d9fef412c81483d7efe0ceb6cf4d3310fde8b624a9cecca00f790573ac96ee"}, -] - -[[package]] -name = "numpy" -version = "1.23.5" -requires_python = ">=3.8" -summary = "NumPy is the fundamental package for array computing with Python." -files = [ - {file = "numpy-1.23.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c88793f78fca17da0145455f0d7826bcb9f37da4764af27ac945488116efe63"}, - {file = "numpy-1.23.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e9f4c4e51567b616be64e05d517c79a8a22f3606499941d97bb76f2ca59f982d"}, - {file = "numpy-1.23.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7903ba8ab592b82014713c491f6c5d3a1cde5b4a3bf116404e08f5b52f6daf43"}, - {file = "numpy-1.23.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e05b1c973a9f858c74367553e236f287e749465f773328c8ef31abe18f691e1"}, - {file = "numpy-1.23.5-cp310-cp310-win32.whl", hash = "sha256:522e26bbf6377e4d76403826ed689c295b0b238f46c28a7251ab94716da0b280"}, - {file = "numpy-1.23.5-cp310-cp310-win_amd64.whl", hash = "sha256:dbee87b469018961d1ad79b1a5d50c0ae850000b639bcb1b694e9981083243b6"}, - {file = "numpy-1.23.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f063b69b090c9d918f9df0a12116029e274daf0181df392839661c4c7ec9018a"}, - {file = "numpy-1.23.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0aaee12d8883552fadfc41e96b4c82ee7d794949e2a7c3b3a7201e968c7ecab9"}, - {file = "numpy-1.23.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92c8c1e89a1f5028a4c6d9e3ccbe311b6ba53694811269b992c0b224269e2398"}, - {file = "numpy-1.23.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d208a0f8729f3fb790ed18a003f3a57895b989b40ea4dce4717e9cf4af62c6bb"}, - {file = "numpy-1.23.5-cp38-cp38-win32.whl", hash = "sha256:06005a2ef6014e9956c09ba07654f9837d9e26696a0470e42beedadb78c11b07"}, - {file = "numpy-1.23.5-cp38-cp38-win_amd64.whl", hash = "sha256:ca51fcfcc5f9354c45f400059e88bc09215fb71a48d3768fb80e357f3b457e1e"}, - {file = "numpy-1.23.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8969bfd28e85c81f3f94eb4a66bc2cf1dbdc5c18efc320af34bffc54d6b1e38f"}, - {file = "numpy-1.23.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7ac231a08bb37f852849bbb387a20a57574a97cfc7b6cabb488a4fc8be176de"}, - {file = "numpy-1.23.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf837dc63ba5c06dc8797c398db1e223a466c7ece27a1f7b5232ba3466aafe3d"}, - {file = "numpy-1.23.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33161613d2269025873025b33e879825ec7b1d831317e68f4f2f0f84ed14c719"}, - {file = "numpy-1.23.5-cp39-cp39-win32.whl", hash = "sha256:af1da88f6bc3d2338ebbf0e22fe487821ea4d8e89053e25fa59d1d79786e7481"}, - {file = "numpy-1.23.5-cp39-cp39-win_amd64.whl", hash = "sha256:09b7847f7e83ca37c6e627682f145856de331049013853f344f37b0c9690e3df"}, - {file = "numpy-1.23.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:abdde9f795cf292fb9651ed48185503a2ff29be87770c3b8e2a14b0cd7aa16f8"}, - {file = "numpy-1.23.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9a909a8bae284d46bbfdefbdd4a262ba19d3bc9921b1e76126b1d21c3c34135"}, - {file = "numpy-1.23.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:01dd17cbb340bf0fc23981e52e1d18a9d4050792e8fb8363cecbf066a84b827d"}, - {file = "numpy-1.23.5.tar.gz", hash = "sha256:1b1766d6f397c18153d40015ddfc79ddb715cabadc04d2d228d4e5a8bc4ded1a"}, -] - -[[package]] -name = "nvidia-cublas-cu11" -version = "11.10.3.66" -requires_python = ">=3" -summary = "CUBLAS native runtime libraries" -dependencies = [ - "setuptools", - "wheel", -] -files = [ - {file = "nvidia_cublas_cu11-11.10.3.66-py3-none-manylinux1_x86_64.whl", hash = "sha256:d32e4d75f94ddfb93ea0a5dda08389bcc65d8916a25cb9f37ac89edaeed3bded"}, - {file = "nvidia_cublas_cu11-11.10.3.66-py3-none-win_amd64.whl", hash = "sha256:8ac17ba6ade3ed56ab898a036f9ae0756f1e81052a317bf98f8c6d18dc3ae49e"}, -] - -[[package]] -name = "nvidia-cuda-nvrtc-cu11" -version = "11.7.99" -requires_python = ">=3" -summary = "NVRTC native runtime libraries" -files = [ - {file = "nvidia_cuda_nvrtc_cu11-11.7.99-2-py3-none-manylinux1_x86_64.whl", hash = "sha256:9f1562822ea264b7e34ed5930567e89242d266448e936b85bc97a3370feabb03"}, - {file = "nvidia_cuda_nvrtc_cu11-11.7.99-py3-none-manylinux1_x86_64.whl", hash = "sha256:f7d9610d9b7c331fa0da2d1b2858a4a8315e6d49765091d28711c8946e7425e7"}, - {file = "nvidia_cuda_nvrtc_cu11-11.7.99-py3-none-win_amd64.whl", hash = "sha256:f2effeb1309bdd1b3854fc9b17eaf997808f8b25968ce0c7070945c4265d64a3"}, -] - -[[package]] -name = "nvidia-cuda-runtime-cu11" -version = "11.7.99" -requires_python = ">=3" -summary = "CUDA Runtime native Libraries" -dependencies = [ - "setuptools", - "wheel", -] -files = [ - {file = "nvidia_cuda_runtime_cu11-11.7.99-py3-none-manylinux1_x86_64.whl", hash = "sha256:cc768314ae58d2641f07eac350f40f99dcb35719c4faff4bc458a7cd2b119e31"}, - {file = "nvidia_cuda_runtime_cu11-11.7.99-py3-none-win_amd64.whl", hash = "sha256:bc77fa59a7679310df9d5c70ab13c4e34c64ae2124dd1efd7e5474b71be125c7"}, -] - -[[package]] -name = "nvidia-cudnn-cu11" -version = "8.5.0.96" -requires_python = ">=3" -summary = "cuDNN runtime libraries" -dependencies = [ - "nvidia-cublas-cu11", -] -files = [ - {file = "nvidia_cudnn_cu11-8.5.0.96-2-py3-none-manylinux1_x86_64.whl", hash = "sha256:402f40adfc6f418f9dae9ab402e773cfed9beae52333f6d86ae3107a1b9527e7"}, - {file = "nvidia_cudnn_cu11-8.5.0.96-py3-none-manylinux1_x86_64.whl", hash = "sha256:71f8111eb830879ff2836db3cccf03bbd735df9b0d17cd93761732ac50a8a108"}, -] - -[[package]] -name = "packaging" -version = "23.0" -requires_python = ">=3.7" -summary = "Core utilities for Python packages" -files = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, -] - -[[package]] -name = "pandas" -version = "1.5.3" -requires_python = ">=3.8" -summary = "Powerful data structures for data analysis, time series, and statistics" -dependencies = [ - "numpy>=1.20.3; python_version < \"3.10\"", - "numpy>=1.21.0; python_version >= \"3.10\"", - "python-dateutil>=2.8.1", - "pytz>=2020.1", -] -files = [ - {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406"}, - {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572"}, - {file = "pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996"}, - {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354"}, - {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23"}, - {file = "pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792"}, - {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7"}, - {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf"}, - {file = "pandas-1.5.3-cp38-cp38-win32.whl", hash = "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51"}, - {file = "pandas-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a"}, - {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0"}, - {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5"}, - {file = "pandas-1.5.3-cp39-cp39-win32.whl", hash = "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a"}, - {file = "pandas-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9"}, - {file = "pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1"}, -] - -[[package]] -name = "pillow" -version = "9.4.0" -requires_python = ">=3.7" -summary = "Python Imaging Library (Fork)" -files = [ - {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"}, - {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"}, - {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, - {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, - {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, - {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"}, - {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"}, - {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"}, - {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"}, - {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"}, - {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"}, - {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"}, - {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"}, - {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"}, - {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"}, - {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"}, - {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"}, -] - -[[package]] -name = "pluggy" -version = "1.0.0" -requires_python = ">=3.6" -summary = "plugin and hook calling mechanisms for python" -files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] - -[[package]] -name = "pooch" -version = "1.6.0" -requires_python = ">=3.6" -summary = "\"Pooch manages your Python library's sample data files: it automatically downloads and stores them in a local directory, with support for versioning and corruption checks.\"" -dependencies = [ - "appdirs>=1.3.0", - "packaging>=20.0", - "requests>=2.19.0", -] -files = [ - {file = "pooch-1.6.0-py3-none-any.whl", hash = "sha256:3bf0e20027096836b8dbce0152dbb785a269abeb621618eb4bdd275ff1e23c9c"}, - {file = "pooch-1.6.0.tar.gz", hash = "sha256:57d20ec4b10dd694d2b05bb64bc6b109c6e85a6c1405794ce87ed8b341ab3f44"}, -] - -[[package]] -name = "pycparser" -version = "2.21" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -summary = "C parser in Python" -files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] - -[[package]] -name = "pydantic" -version = "2.5.2" -requires_python = ">=3.7" -summary = "Data validation using Python type hints" -dependencies = [ - "annotated-types>=0.4.0", - "pydantic-core==2.14.5", - "typing-extensions>=4.6.1", -] -files = [ - {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, - {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, -] - -[[package]] -name = "pydantic-core" -version = "2.14.5" -requires_python = ">=3.7" -summary = "" -dependencies = [ - "typing-extensions!=4.7.0,>=4.6.0", -] -files = [ - {file = "pydantic_core-2.14.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd"}, - {file = "pydantic_core-2.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66"}, - {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997"}, - {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093"}, - {file = "pydantic_core-2.14.5-cp310-none-win32.whl", hash = "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720"}, - {file = "pydantic_core-2.14.5-cp310-none-win_amd64.whl", hash = "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b"}, - {file = "pydantic_core-2.14.5-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf"}, - {file = "pydantic_core-2.14.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b"}, - {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec"}, - {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124"}, - {file = "pydantic_core-2.14.5-cp38-none-win32.whl", hash = "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867"}, - {file = "pydantic_core-2.14.5-cp38-none-win_amd64.whl", hash = "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d"}, - {file = "pydantic_core-2.14.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7"}, - {file = "pydantic_core-2.14.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955"}, - {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5"}, - {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209"}, - {file = "pydantic_core-2.14.5-cp39-none-win32.whl", hash = "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6"}, - {file = "pydantic_core-2.14.5-cp39-none-win_amd64.whl", hash = "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, - {file = "pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, -] - -[[package]] -name = "pyparsing" -version = "3.0.9" -requires_python = ">=3.6.8" -summary = "pyparsing module - Classes and methods to define and execute parsing grammars" -files = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] - -[[package]] -name = "pytest" -version = "7.2.2" -requires_python = ">=3.7" -summary = "pytest: simple powerful testing with Python" -dependencies = [ - "attrs>=19.2.0", - "colorama; sys_platform == \"win32\"", - "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", - "iniconfig", - "packaging", - "pluggy<2.0,>=0.12", - "tomli>=1.0.0; python_version < \"3.11\"", -] -files = [ - {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, - {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, -] - -[[package]] -name = "python-dateutil" -version = "2.8.2" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Extensions to the standard Python datetime module" -dependencies = [ - "six>=1.5", -] -files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] - -[[package]] -name = "pytz" -version = "2023.3" -summary = "World timezone definitions, modern and historical" -files = [ - {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, - {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, -] - -[[package]] -name = "requests" -version = "2.28.2" -requires_python = ">=3.7, <4" -summary = "Python HTTP for Humans." -dependencies = [ - "certifi>=2017.4.17", - "charset-normalizer<4,>=2", - "idna<4,>=2.5", - "urllib3<1.27,>=1.21.1", -] -files = [ - {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, - {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, -] - -[[package]] -name = "scikit-learn" -version = "1.2.2" -requires_python = ">=3.8" -summary = "A set of python modules for machine learning and data mining" -dependencies = [ - "joblib>=1.1.1", - "numpy>=1.17.3", - "scipy>=1.3.2", - "threadpoolctl>=2.0.0", -] -files = [ - {file = "scikit-learn-1.2.2.tar.gz", hash = "sha256:8429aea30ec24e7a8c7ed8a3fa6213adf3814a6efbea09e16e0a0c71e1a1a3d7"}, - {file = "scikit_learn-1.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99cc01184e347de485bf253d19fcb3b1a3fb0ee4cea5ee3c43ec0cc429b6d29f"}, - {file = "scikit_learn-1.2.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e6e574db9914afcb4e11ade84fab084536a895ca60aadea3041e85b8ac963edb"}, - {file = "scikit_learn-1.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fe83b676f407f00afa388dd1fdd49e5c6612e551ed84f3b1b182858f09e987d"}, - {file = "scikit_learn-1.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2642baa0ad1e8f8188917423dd73994bf25429f8893ddbe115be3ca3183584"}, - {file = "scikit_learn-1.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ad66c3848c0a1ec13464b2a95d0a484fd5b02ce74268eaa7e0c697b904f31d6c"}, - {file = "scikit_learn-1.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9c710ff9f9936ba8a3b74a455ccf0dcf59b230caa1e9ba0223773c490cab1e51"}, - {file = "scikit_learn-1.2.2-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:2dd3ffd3950e3d6c0c0ef9033a9b9b32d910c61bd06cb8206303fb4514b88a49"}, - {file = "scikit_learn-1.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44b47a305190c28dd8dd73fc9445f802b6ea716669cfc22ab1eb97b335d238b1"}, - {file = "scikit_learn-1.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:953236889928d104c2ef14027539f5f2609a47ebf716b8cbe4437e85dce42744"}, - {file = "scikit_learn-1.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:7f69313884e8eb311460cc2f28676d5e400bd929841a2c8eb8742ae78ebf7c20"}, - {file = "scikit_learn-1.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8156db41e1c39c69aa2d8599ab7577af53e9e5e7a57b0504e116cc73c39138dd"}, - {file = "scikit_learn-1.2.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fe175ee1dab589d2e1033657c5b6bec92a8a3b69103e3dd361b58014729975c3"}, - {file = "scikit_learn-1.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d5312d9674bed14f73773d2acf15a3272639b981e60b72c9b190a0cffed5bad"}, - {file = "scikit_learn-1.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea061bf0283bf9a9f36ea3c5d3231ba2176221bbd430abd2603b1c3b2ed85c89"}, - {file = "scikit_learn-1.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:6477eed40dbce190f9f9e9d0d37e020815825b300121307942ec2110302b66a3"}, -] - -[[package]] -name = "scipy" -version = "1.9.3" -requires_python = ">=3.8" -summary = "Fundamental algorithms for scientific computing in Python" -dependencies = [ - "numpy<1.26.0,>=1.18.5", -] -files = [ - {file = "scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"}, - {file = "scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"}, - {file = "scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"}, - {file = "scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"}, - {file = "scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"}, - {file = "scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"}, -] - -[[package]] -name = "setuptools" -version = "67.6.1" -requires_python = ">=3.7" -summary = "Easily download, build, install, upgrade, and uninstall Python packages" -files = [ - {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, - {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, -] - -[[package]] -name = "six" -version = "1.16.0" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -summary = "Python 2 and 3 compatibility utilities" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "soundevent" -version = "1.3.5" -requires_python = ">=3.8" -summary = "soundevent is an open-source Python package for the computational biocoustic community, providing standardized tools for sound event analysis and data management." -dependencies = [ - "email-validator>=2.0", - "pydantic>=2.0", -] -files = [ - {file = "soundevent-1.3.5-py3-none-any.whl", hash = "sha256:514793edd87abcf82b70bf709f2d2bbaa11ebd39e9cb1c0a587a8d1d8ea50fa7"}, - {file = "soundevent-1.3.5.tar.gz", hash = "sha256:df1bd79c128e9121a1217aa161c148258d074c95e7097c4363df048c0cf4785f"}, -] - -[[package]] -name = "soundfile" -version = "0.12.1" -summary = "An audio library based on libsndfile, CFFI and NumPy" -dependencies = [ - "cffi>=1.0", -] -files = [ - {file = "soundfile-0.12.1-py2.py3-none-any.whl", hash = "sha256:828a79c2e75abab5359f780c81dccd4953c45a2c4cd4f05ba3e233ddf984b882"}, - {file = "soundfile-0.12.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d922be1563ce17a69582a352a86f28ed8c9f6a8bc951df63476ffc310c064bfa"}, - {file = "soundfile-0.12.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:bceaab5c4febb11ea0554566784bcf4bc2e3977b53946dda2b12804b4fe524a8"}, - {file = "soundfile-0.12.1-py2.py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:2dc3685bed7187c072a46ab4ffddd38cef7de9ae5eb05c03df2ad569cf4dacbc"}, - {file = "soundfile-0.12.1-py2.py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:074247b771a181859d2bc1f98b5ebf6d5153d2c397b86ee9e29ba602a8dfe2a6"}, - {file = "soundfile-0.12.1-py2.py3-none-win32.whl", hash = "sha256:59dfd88c79b48f441bbf6994142a19ab1de3b9bb7c12863402c2bc621e49091a"}, - {file = "soundfile-0.12.1-py2.py3-none-win_amd64.whl", hash = "sha256:0d86924c00b62552b650ddd28af426e3ff2d4dc2e9047dae5b3d8452e0a49a77"}, - {file = "soundfile-0.12.1.tar.gz", hash = "sha256:e8e1017b2cf1dda767aef19d2fd9ee5ebe07e050d430f77a0a7c66ba08b8cdae"}, -] - -[[package]] -name = "soxr" -version = "0.3.4" -requires_python = ">=3.6" -summary = "High quality, one-dimensional sample-rate conversion library" -dependencies = [ - "numpy", -] -files = [ - {file = "soxr-0.3.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b7b84126643c063d5daa203f7f9137e21734dabbd7e68c097607b2ef457e2f2e"}, - {file = "soxr-0.3.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:380d2d43871a68e8b1ef1702a0abe6f9e48ddb3933c7a303c45d67e121503e7c"}, - {file = "soxr-0.3.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4a1b4019c9972f57612482c4f85523d6e832e3d10935e2f070a9dcd334a4dcb"}, - {file = "soxr-0.3.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e613cee023b7c3f162b9da3f6b169cd7f58de345275be1fde9f19adc9cf144df"}, - {file = "soxr-0.3.4-cp310-cp310-win32.whl", hash = "sha256:182c02a7ba45a159a0dbb0a297335df2381ead03a65377b19663ea0ff720ecb7"}, - {file = "soxr-0.3.4-cp310-cp310-win_amd64.whl", hash = "sha256:1e95c96ce94524fae453b4331c9910d33f97506f99bae06d76a9c0649710619e"}, - {file = "soxr-0.3.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:941f7355116fe77fe6a82938fa7799a0e466a494ebc093f676969ce32b2815b1"}, - {file = "soxr-0.3.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:00fdbf24f64d8c3fb800425c383048cb24c32defac80901cde4a57fb6ce5d431"}, - {file = "soxr-0.3.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bb6d4dc807d04c536674429e2b05ae08a1efac9815c4595e41ffd6b57c2c662"}, - {file = "soxr-0.3.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff15853895b54f1b627799c6600be1ce5f7286724e7a93e4b7b9d79e5d4166f5"}, - {file = "soxr-0.3.4-cp38-cp38-win32.whl", hash = "sha256:d858becbc1fcc7b38c3436d3276290fae09403cdcbdf1d5986a18dab7023a6c3"}, - {file = "soxr-0.3.4-cp38-cp38-win_amd64.whl", hash = "sha256:068ab4df549df5783cc1eb4eb6c94f53823b164dc27134fc621fc9f5097f38cd"}, - {file = "soxr-0.3.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20130329985f9767c8417bbd125fe138790a71802b000481c386a800e2ad2bca"}, - {file = "soxr-0.3.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78090e97abfb326b7cf14ef37d08a17252b07d438388dcbbd82a6836a9d551b1"}, - {file = "soxr-0.3.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e590e75b7e5dca12bf68bfb090276f34a88fbcd793781c62d47f5d7dbe525e"}, - {file = "soxr-0.3.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3479d265574b960e12bca0878baba0862c43278915e0319d84679bb4d4fcd33"}, - {file = "soxr-0.3.4-cp39-cp39-win32.whl", hash = "sha256:83de825d6a713c7b2e76d9ec3f229a58a9ed290237e7adc05d80e8b39be995a6"}, - {file = "soxr-0.3.4-cp39-cp39-win_amd64.whl", hash = "sha256:2082f88cae89de854c3e0d62f55d0cb31eb11764f5c2a28299121fb642a22472"}, - {file = "soxr-0.3.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fe8b5f92c802f1e7793c40344f5368dc6163718c9ffa82e79ee6ad779d318ac5"}, - {file = "soxr-0.3.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0063d5f9a4e1a367084f4705301e9da131cf4d2d32aa3fe0072a1245e18088f"}, - {file = "soxr-0.3.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a680bab57adae462cdc86abcc7330beb5daa3ba5101165583eedcda88b7ba551"}, - {file = "soxr-0.3.4.tar.gz", hash = "sha256:fe68daf00e8f020977b187699903d219f9e39b9fb3d915f3f923eed8ba431449"}, -] - -[[package]] -name = "threadpoolctl" -version = "3.1.0" -requires_python = ">=3.6" -summary = "threadpoolctl" -files = [ - {file = "threadpoolctl-3.1.0-py3-none-any.whl", hash = "sha256:8b99adda265feb6773280df41eece7b2e6561b772d21ffd52e372f999024907b"}, - {file = "threadpoolctl-3.1.0.tar.gz", hash = "sha256:a335baacfaa4400ae1f0d8e3a58d6674d2f8828e3716bb2802c44955ad391380"}, -] - -[[package]] -name = "tomli" -version = "2.0.1" -requires_python = ">=3.7" -summary = "A lil' TOML parser" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "torch" -version = "1.13.1" -requires_python = ">=3.7.0" -summary = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" -dependencies = [ - "nvidia-cublas-cu11==11.10.3.66; platform_system == \"Linux\"", - "nvidia-cuda-nvrtc-cu11==11.7.99; platform_system == \"Linux\"", - "nvidia-cuda-runtime-cu11==11.7.99; platform_system == \"Linux\"", - "nvidia-cudnn-cu11==8.5.0.96; platform_system == \"Linux\"", - "typing-extensions", -] -files = [ - {file = "torch-1.13.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:fd12043868a34a8da7d490bf6db66991108b00ffbeecb034228bfcbbd4197143"}, - {file = "torch-1.13.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d9fe785d375f2e26a5d5eba5de91f89e6a3be5d11efb497e76705fdf93fa3c2e"}, - {file = "torch-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:98124598cdff4c287dbf50f53fb455f0c1e3a88022b39648102957f3445e9b76"}, - {file = "torch-1.13.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:393a6273c832e047581063fb74335ff50b4c566217019cc6ace318cd79eb0566"}, - {file = "torch-1.13.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:0122806b111b949d21fa1a5f9764d1fd2fcc4a47cb7f8ff914204fd4fc752ed5"}, - {file = "torch-1.13.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:727dbf00e2cf858052364c0e2a496684b9cb5aa01dc8a8bc8bbb7c54502bdcdd"}, - {file = "torch-1.13.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:df8434b0695e9ceb8cc70650afc1310d8ba949e6db2a0525ddd9c3b2b181e5fe"}, - {file = "torch-1.13.1-cp38-cp38-win_amd64.whl", hash = "sha256:5e1e722a41f52a3f26f0c4fcec227e02c6c42f7c094f32e49d4beef7d1e213ea"}, - {file = "torch-1.13.1-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:33e67eea526e0bbb9151263e65417a9ef2d8fa53cbe628e87310060c9dcfa312"}, - {file = "torch-1.13.1-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:eeeb204d30fd40af6a2d80879b46a7efbe3cf43cdbeb8838dd4f3d126cc90b2b"}, - {file = "torch-1.13.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:50ff5e76d70074f6653d191fe4f6a42fdbe0cf942fbe2a3af0b75eaa414ac038"}, - {file = "torch-1.13.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:2c3581a3fd81eb1f0f22997cddffea569fea53bafa372b2c0471db373b26aafc"}, - {file = "torch-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:0aa46f0ac95050c604bcf9ef71da9f1172e5037fdf2ebe051962d47b123848e7"}, - {file = "torch-1.13.1-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:6930791efa8757cb6974af73d4996b6b50c592882a324b8fb0589c6a9ba2ddaf"}, - {file = "torch-1.13.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:e0df902a7c7dd6c795698532ee5970ce898672625635d885eade9976e5a04949"}, -] - -[[package]] -name = "torchaudio" -version = "0.13.1" -summary = "An audio package for PyTorch" -dependencies = [ - "torch==1.13.1", -] -files = [ - {file = "torchaudio-0.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5e0f3dc6699506521364266704e6bf89d0d0579fd435d12c5c2f5858d52de4fa"}, - {file = "torchaudio-0.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ec72a17d4d2178829e7780682999b535cf57fe160d0c20b0d6bdc1ad1a87c4dd"}, - {file = "torchaudio-0.13.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:62e9b5c260a27231d905588b72d2e2984ff9cdbb557af86eb178982fd265198d"}, - {file = "torchaudio-0.13.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:2e47562cdcdd47cb8ed86a3cf053b7067cc9e88340f4550ae73d790ddbc12f21"}, - {file = "torchaudio-0.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5f2fc60206aa687eadc8cfb7c167784678936fbad13ccc583794fba3d6f77e1b"}, - {file = "torchaudio-0.13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:42ce5c66d304bc2cd68338916b8223e322e09a84dcbd9228814ef36bc477a37b"}, - {file = "torchaudio-0.13.1-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:b093b3e7661c85168ec9dde2cf97345965ea0931d3d2a7e78bd409221e6d6998"}, - {file = "torchaudio-0.13.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:167f77ef385592a5af6f4e2ad1630a42ca1b70f905762fcd62e13dd4f163bdcf"}, - {file = "torchaudio-0.13.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3c48bcff00eae8180f87f58d1c9e7e9fd8c4cb7eb3ea8817935fb6048d152bc7"}, - {file = "torchaudio-0.13.1-cp38-cp38-win_amd64.whl", hash = "sha256:5de44b6b96a8d7a05650ef7377b2386650ddce92551d7dc02e05e7002aee5fd2"}, - {file = "torchaudio-0.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9d2170540de32ae031aab3936129868e896ea041617b6d6692dde6aa2dfb0a23"}, - {file = "torchaudio-0.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:91fcfbf47000402d12bff2624e6220a0fd3b8ca8ee6ff51edf5945ec39ab0a7f"}, - {file = "torchaudio-0.13.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:32592088b48dfcd2ca247ad5d081a9e0c61de0caabb993d68bac779326456d8d"}, - {file = "torchaudio-0.13.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:3023aeb5c191047bef1681a3741bffd4a2164b58a64cad24dd37da5e1ac2d1f1"}, - {file = "torchaudio-0.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:418fbf93ba77b9742b05b76561be4fe7e8ded27cfe414828624765986b30ce5a"}, -] - -[[package]] -name = "torchvision" -version = "0.14.1" -requires_python = ">=3.7" -summary = "image and video datasets and models for torch deep learning" -dependencies = [ - "numpy", - "pillow!=8.3.*,>=5.3.0", - "requests", - "torch==1.13.1", - "typing-extensions", -] -files = [ - {file = "torchvision-0.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb05dd9dd3af5428fee525400759daf8da8e4caec45ddd6908cfb36571f6433"}, - {file = "torchvision-0.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d0766ea92affa7af248e327dd85f7c9cfdf51a57530b43212d4e1858548e9d7"}, - {file = "torchvision-0.14.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:6d7b35653113664ea3fdcb71f515cfbf29d2fe393000fd8aaff27a1284de6908"}, - {file = "torchvision-0.14.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:8a9eb773a2fa8f516e404ac09c059fb14e6882c48fdbb9c946327d2ce5dba6cd"}, - {file = "torchvision-0.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:13986f0c15377ff23039e1401012ccb6ecf71024ce53def27139e4eac5a57592"}, - {file = "torchvision-0.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:68ed03359dcd3da9cd21b8ab94da21158df8a6a0c5bad0bf4a42f0e448d28cb3"}, - {file = "torchvision-0.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:30fcf0e9fe57d4ac4ce6426659a57dce199637ccb6c70be1128670f177692624"}, - {file = "torchvision-0.14.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0ed02aefd09bf1114d35f1aa7dce55aa61c2c7e57f9aa02dce362860be654e85"}, - {file = "torchvision-0.14.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a541e49fc3c4e90e49e6988428ab047415ed52ea97d0c0bfd147d8bacb8f4df8"}, - {file = "torchvision-0.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:6099b3191dc2516099a32ae38a5fb349b42e863872a13545ab1a524b6567be60"}, - {file = "torchvision-0.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5e744f56e5f5b452deb5fc0f3f2ba4d2f00612d14d8da0dbefea8f09ac7690b"}, - {file = "torchvision-0.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:758b20d079e810b4740bd60d1eb16e49da830e3360f9be379eb177ee221fa5d4"}, - {file = "torchvision-0.14.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:83045507ef8d3c015d4df6be79491375b2f901352cfca6e72b4723e9c4f9a55d"}, - {file = "torchvision-0.14.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:eaed58cf454323ed9222d4e0dd5fb897064f454b400696e03a5200e65d3a1e76"}, - {file = "torchvision-0.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:b337e1245ca4353623dd563c03cd8f020c2496a7c5d12bba4d2e381999c766e0"}, -] - -[[package]] -name = "typing-extensions" -version = "4.8.0" -requires_python = ">=3.8" -summary = "Backported and Experimental Type Hints for Python 3.8+" -files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, -] - -[[package]] -name = "urllib3" -version = "1.26.15" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -summary = "HTTP library with thread-safe connection pooling, file post, and more." -files = [ - {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, - {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, -] - -[[package]] -name = "wheel" -version = "0.40.0" -requires_python = ">=3.7" -summary = "A built-package format for Python" -files = [ - {file = "wheel-0.40.0-py3-none-any.whl", hash = "sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247"}, - {file = "wheel-0.40.0.tar.gz", hash = "sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873"}, -] - -[[package]] -name = "zipp" -version = "3.15.0" -requires_python = ">=3.7" -summary = "Backport of pathlib-compatible object wrapper for zip files" -files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, -] diff --git a/pyproject.toml b/pyproject.toml index 73007af..f116a06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,9 @@ +[tool] +rye = { dev-dependencies = [ + "ipykernel>=6.29.4", + "setuptools>=69.5.1", + "pytest>=8.1.1", +] } [tool.pdm] [tool.pdm.dev-dependencies] dev = [ @@ -22,12 +28,17 @@ dependencies = [ "torch>=1.13.1", "torchaudio", "torchvision", - "click", - "soundevent>=1.3.5", - "click", + "soundevent[audio,geometry,plot]>=1.3.5", "click>=8.1.7", + "netcdf4>=1.6.5", + "tqdm>=4.66.2", + "pytorch-lightning>=2.2.2", + "cf-xarray>=0.9.0", + "onnx>=1.16.0", + "lightning[extra]>=2.2.2", + "tensorboard>=2.16.2", ] -requires-python = ">=3.8,<3.12" +requires-python = ">=3.9" readme = "README.md" license = { text = "CC-by-nc-4" } classifiers = [ @@ -65,6 +76,9 @@ line-length = 79 profile = "black" line_length = 79 +[tool.ruff] +line-length = 79 + [[tool.mypy.overrides]] module = [ "librosa", diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000..930cdda --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,518 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false + +-e file:. +absl-py==2.1.0 + # via tensorboard +aiobotocore==2.12.3 + # via s3fs +aiohttp==3.9.5 + # via aiobotocore + # via fsspec + # via lightning + # via s3fs +aioitertools==0.11.0 + # via aiobotocore +aiosignal==1.3.1 + # via aiohttp +annotated-types==0.6.0 + # via pydantic +antlr4-python3-runtime==4.9.3 + # via hydra-core + # via omegaconf +anyio==4.3.0 + # via starlette +arrow==1.3.0 + # via lightning +asttokens==2.4.1 + # via stack-data +async-timeout==4.0.3 + # via aiohttp + # via redis +attrs==23.2.0 + # via aiohttp +audioread==3.0.1 + # via librosa +backcall==0.2.0 + # via ipython +backoff==2.2.1 + # via lightning +beautifulsoup4==4.12.3 + # via lightning +bitsandbytes==0.41.0 + # via lightning +blessed==1.20.0 + # via inquirer +boto3==1.34.69 + # via lightning-cloud +botocore==1.34.69 + # via aiobotocore + # via boto3 + # via s3transfer +certifi==2024.2.2 + # via netcdf4 + # via requests +cf-xarray==0.9.0 + # via batdetect2 +cffi==1.16.0 + # via soundfile +cftime==1.6.3 + # via netcdf4 +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via batdetect2 + # via lightning + # via lightning-cloud + # via uvicorn +comm==0.2.2 + # via ipykernel +contourpy==1.1.1 + # via matplotlib +croniter==1.4.1 + # via lightning +cycler==0.12.1 + # via matplotlib +cython==3.0.10 + # via soundevent +dateutils==0.6.12 + # via lightning +debugpy==1.8.1 + # via ipykernel +decorator==5.1.1 + # via ipython + # via librosa +deepdiff==6.7.1 + # via lightning +dnspython==2.6.1 + # via email-validator +docker==6.1.3 + # via lightning +docstring-parser==0.16 + # via jsonargparse +editor==1.6.6 + # via inquirer +email-validator==2.1.1 + # via soundevent +exceptiongroup==1.2.1 + # via anyio + # via pytest +executing==2.0.1 + # via stack-data +fastapi==0.110.2 + # via lightning + # via lightning-cloud +filelock==3.13.4 + # via torch + # via triton +fonttools==4.51.0 + # via matplotlib +frozenlist==1.4.1 + # via aiohttp + # via aiosignal +fsspec==2023.12.2 + # via lightning + # via lightning-fabric + # via pytorch-lightning + # via s3fs + # via torch +grpcio==1.62.2 + # via tensorboard +h11==0.14.0 + # via uvicorn +hydra-core==1.3.2 + # via lightning +idna==3.7 + # via anyio + # via email-validator + # via requests + # via yarl +importlib-metadata==7.1.0 + # via jupyter-client + # via markdown +importlib-resources==6.4.0 + # via matplotlib + # via typeshed-client +iniconfig==2.0.0 + # via pytest +inquirer==3.2.4 + # via lightning +ipykernel==6.29.4 +ipython==8.12.3 + # via ipykernel +jedi==0.19.1 + # via ipython +jinja2==3.1.3 + # via lightning + # via torch +jmespath==1.0.1 + # via boto3 + # via botocore +joblib==1.4.0 + # via librosa + # via scikit-learn +jsonargparse==4.28.0 + # via lightning +jupyter-client==8.6.1 + # via ipykernel +jupyter-core==5.7.2 + # via ipykernel + # via jupyter-client +kiwisolver==1.4.5 + # via matplotlib +lazy-loader==0.4 + # via librosa +librosa==0.10.1 + # via batdetect2 +lightning==2.2.2 + # via batdetect2 +lightning-api-access==0.0.5 + # via lightning +lightning-cloud==0.5.65 + # via lightning +lightning-fabric==2.2.2 + # via lightning +lightning-utilities==0.11.2 + # via lightning + # via lightning-fabric + # via pytorch-lightning + # via torchmetrics +llvmlite==0.41.1 + # via numba +markdown==3.6 + # via tensorboard +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.5 + # via jinja2 + # via werkzeug +matplotlib==3.7.5 + # via batdetect2 + # via lightning + # via soundevent +matplotlib-inline==0.1.7 + # via ipykernel + # via ipython +mdurl==0.1.2 + # via markdown-it-py +mpmath==1.3.0 + # via sympy +msgpack==1.0.8 + # via librosa +multidict==6.0.5 + # via aiohttp + # via yarl +nest-asyncio==1.6.0 + # via ipykernel +netcdf4==1.6.5 + # via batdetect2 +networkx==3.1 + # via torch +numba==0.58.1 + # via librosa +numpy==1.24.4 + # via batdetect2 + # via cftime + # via contourpy + # via librosa + # via lightning + # via lightning-fabric + # via matplotlib + # via netcdf4 + # via numba + # via onnx + # via pandas + # via pytorch-lightning + # via scikit-learn + # via scipy + # via shapely + # via soxr + # via tensorboard + # via tensorboardx + # via torchmetrics + # via torchvision + # via xarray +nvidia-cublas-cu12==12.1.3.1 + # via nvidia-cudnn-cu12 + # via nvidia-cusolver-cu12 + # via torch +nvidia-cuda-cupti-cu12==12.1.105 + # via torch +nvidia-cuda-nvrtc-cu12==12.1.105 + # via torch +nvidia-cuda-runtime-cu12==12.1.105 + # via torch +nvidia-cudnn-cu12==8.9.2.26 + # via torch +nvidia-cufft-cu12==11.0.2.54 + # via torch +nvidia-curand-cu12==10.3.2.106 + # via torch +nvidia-cusolver-cu12==11.4.5.107 + # via torch +nvidia-cusparse-cu12==12.1.0.106 + # via nvidia-cusolver-cu12 + # via torch +nvidia-nccl-cu12==2.19.3 + # via torch +nvidia-nvjitlink-cu12==12.4.127 + # via nvidia-cusolver-cu12 + # via nvidia-cusparse-cu12 +nvidia-nvtx-cu12==12.1.105 + # via torch +omegaconf==2.3.0 + # via hydra-core + # via lightning +onnx==1.16.0 + # via batdetect2 +ordered-set==4.1.0 + # via deepdiff +packaging==24.0 + # via docker + # via hydra-core + # via ipykernel + # via lazy-loader + # via lightning + # via lightning-fabric + # via lightning-utilities + # via matplotlib + # via pooch + # via pytest + # via pytorch-lightning + # via tensorboardx + # via torchmetrics + # via xarray +pandas==2.0.3 + # via batdetect2 + # via xarray +parso==0.8.4 + # via jedi +pexpect==4.9.0 + # via ipython +pickleshare==0.7.5 + # via ipython +pillow==10.3.0 + # via matplotlib + # via torchvision +platformdirs==4.2.0 + # via jupyter-core + # via pooch +pluggy==1.4.0 + # via pytest +pooch==1.8.1 + # via librosa +prompt-toolkit==3.0.43 + # via ipython +protobuf==5.26.1 + # via onnx + # via tensorboard + # via tensorboardx +psutil==5.9.8 + # via ipykernel + # via lightning +ptyprocess==0.7.0 + # via pexpect +pure-eval==0.2.2 + # via stack-data +pycparser==2.22 + # via cffi +pydantic==2.7.0 + # via fastapi + # via lightning + # via soundevent +pydantic-core==2.18.1 + # via pydantic +pygments==2.17.2 + # via ipython + # via rich +pyjwt==2.8.0 + # via lightning-cloud +pyparsing==3.1.2 + # via matplotlib +pytest==8.1.1 +python-dateutil==2.9.0.post0 + # via arrow + # via botocore + # via croniter + # via dateutils + # via jupyter-client + # via matplotlib + # via pandas +python-multipart==0.0.9 + # via lightning + # via lightning-cloud +pytorch-lightning==2.2.2 + # via batdetect2 + # via lightning +pytz==2024.1 + # via dateutils + # via pandas +pyyaml==6.0.1 + # via jsonargparse + # via lightning + # via omegaconf + # via pytorch-lightning +pyzmq==26.0.0 + # via ipykernel + # via jupyter-client +readchar==4.0.6 + # via inquirer +redis==5.0.4 + # via lightning +requests==2.31.0 + # via docker + # via fsspec + # via lightning + # via lightning-cloud + # via pooch +rich==13.7.1 + # via lightning + # via lightning-cloud +runs==1.2.2 + # via editor +s3fs==2023.12.2 + # via lightning +s3transfer==0.10.1 + # via boto3 +scikit-learn==1.3.2 + # via batdetect2 + # via librosa +scipy==1.10.1 + # via batdetect2 + # via librosa + # via scikit-learn + # via soundevent +setuptools==69.5.1 + # via lightning-utilities + # via readchar + # via tensorboard +shapely==2.0.3 + # via soundevent +six==1.16.0 + # via asttokens + # via blessed + # via lightning-cloud + # via python-dateutil + # via tensorboard +sniffio==1.3.1 + # via anyio +soundevent==1.3.5 + # via batdetect2 +soundfile==0.12.1 + # via librosa + # via soundevent +soupsieve==2.5 + # via beautifulsoup4 +soxr==0.3.7 + # via librosa +stack-data==0.6.3 + # via ipython +starlette==0.37.2 + # via fastapi + # via lightning +sympy==1.12 + # via torch +tensorboard==2.16.2 + # via batdetect2 +tensorboard-data-server==0.7.2 + # via tensorboard +tensorboardx==2.6.2.2 + # via lightning +threadpoolctl==3.4.0 + # via scikit-learn +tomli==2.0.1 + # via pytest +torch==2.2.2 + # via batdetect2 + # via lightning + # via lightning-fabric + # via pytorch-lightning + # via torchaudio + # via torchmetrics + # via torchvision +torchaudio==2.2.2 + # via batdetect2 +torchmetrics==1.3.2 + # via lightning + # via pytorch-lightning +torchvision==0.17.2 + # via batdetect2 +tornado==6.4 + # via ipykernel + # via jupyter-client +tqdm==4.66.2 + # via batdetect2 + # via lightning + # via pytorch-lightning +traitlets==5.14.2 + # via comm + # via ipykernel + # via ipython + # via jupyter-client + # via jupyter-core + # via lightning + # via matplotlib-inline +triton==2.2.0 + # via torch +types-python-dateutil==2.9.0.20240316 + # via arrow +typeshed-client==2.5.1 + # via jsonargparse +typing-extensions==4.11.0 + # via aioitertools + # via anyio + # via fastapi + # via ipython + # via jsonargparse + # via librosa + # via lightning + # via lightning-fabric + # via lightning-utilities + # via pydantic + # via pydantic-core + # via pytorch-lightning + # via starlette + # via torch + # via typeshed-client + # via uvicorn +tzdata==2024.1 + # via pandas +urllib3==1.26.18 + # via botocore + # via docker + # via lightning + # via lightning-cloud + # via requests +uvicorn==0.29.0 + # via lightning + # via lightning-cloud +wcwidth==0.2.13 + # via blessed + # via prompt-toolkit +websocket-client==1.7.0 + # via docker + # via lightning + # via lightning-cloud +websockets==11.0.3 + # via lightning +werkzeug==3.0.2 + # via tensorboard +wrapt==1.16.0 + # via aiobotocore +xarray==2023.1.0 + # via cf-xarray + # via soundevent +xmod==1.8.1 + # via editor + # via runs +yarl==1.9.4 + # via aiohttp +zipp==3.18.1 + # via importlib-metadata + # via importlib-resources diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 0000000..f2594d1 --- /dev/null +++ b/requirements.lock @@ -0,0 +1,448 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false + +-e file:. +absl-py==2.1.0 + # via tensorboard +aiobotocore==2.12.3 + # via s3fs +aiohttp==3.9.5 + # via aiobotocore + # via fsspec + # via lightning + # via s3fs +aioitertools==0.11.0 + # via aiobotocore +aiosignal==1.3.1 + # via aiohttp +annotated-types==0.6.0 + # via pydantic +antlr4-python3-runtime==4.9.3 + # via hydra-core + # via omegaconf +anyio==4.3.0 + # via starlette +arrow==1.3.0 + # via lightning +async-timeout==4.0.3 + # via aiohttp + # via redis +attrs==23.2.0 + # via aiohttp +audioread==3.0.1 + # via librosa +backoff==2.2.1 + # via lightning +beautifulsoup4==4.12.3 + # via lightning +bitsandbytes==0.41.0 + # via lightning +blessed==1.20.0 + # via inquirer +boto3==1.34.69 + # via lightning-cloud +botocore==1.34.69 + # via aiobotocore + # via boto3 + # via s3transfer +certifi==2024.2.2 + # via netcdf4 + # via requests +cf-xarray==0.9.0 + # via batdetect2 +cffi==1.16.0 + # via soundfile +cftime==1.6.3 + # via netcdf4 +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via batdetect2 + # via lightning + # via lightning-cloud + # via uvicorn +contourpy==1.1.1 + # via matplotlib +croniter==1.4.1 + # via lightning +cycler==0.12.1 + # via matplotlib +cython==3.0.10 + # via soundevent +dateutils==0.6.12 + # via lightning +decorator==5.1.1 + # via librosa +deepdiff==6.7.1 + # via lightning +dnspython==2.6.1 + # via email-validator +docker==6.1.3 + # via lightning +docstring-parser==0.16 + # via jsonargparse +editor==1.6.6 + # via inquirer +email-validator==2.1.1 + # via soundevent +exceptiongroup==1.2.1 + # via anyio +fastapi==0.110.2 + # via lightning + # via lightning-cloud +filelock==3.13.4 + # via torch + # via triton +fonttools==4.51.0 + # via matplotlib +frozenlist==1.4.1 + # via aiohttp + # via aiosignal +fsspec==2023.12.2 + # via lightning + # via lightning-fabric + # via pytorch-lightning + # via s3fs + # via torch +grpcio==1.62.2 + # via tensorboard +h11==0.14.0 + # via uvicorn +hydra-core==1.3.2 + # via lightning +idna==3.7 + # via anyio + # via email-validator + # via requests + # via yarl +importlib-metadata==7.1.0 + # via markdown +importlib-resources==6.4.0 + # via matplotlib + # via typeshed-client +inquirer==3.2.4 + # via lightning +jinja2==3.1.3 + # via lightning + # via torch +jmespath==1.0.1 + # via boto3 + # via botocore +joblib==1.4.0 + # via librosa + # via scikit-learn +jsonargparse==4.28.0 + # via lightning +kiwisolver==1.4.5 + # via matplotlib +lazy-loader==0.4 + # via librosa +librosa==0.10.1 + # via batdetect2 +lightning==2.2.2 + # via batdetect2 +lightning-api-access==0.0.5 + # via lightning +lightning-cloud==0.5.65 + # via lightning +lightning-fabric==2.2.2 + # via lightning +lightning-utilities==0.11.2 + # via lightning + # via lightning-fabric + # via pytorch-lightning + # via torchmetrics +llvmlite==0.41.1 + # via numba +markdown==3.6 + # via tensorboard +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.5 + # via jinja2 + # via werkzeug +matplotlib==3.7.5 + # via batdetect2 + # via lightning + # via soundevent +mdurl==0.1.2 + # via markdown-it-py +mpmath==1.3.0 + # via sympy +msgpack==1.0.8 + # via librosa +multidict==6.0.5 + # via aiohttp + # via yarl +netcdf4==1.6.5 + # via batdetect2 +networkx==3.1 + # via torch +numba==0.58.1 + # via librosa +numpy==1.24.4 + # via batdetect2 + # via cftime + # via contourpy + # via librosa + # via lightning + # via lightning-fabric + # via matplotlib + # via netcdf4 + # via numba + # via onnx + # via pandas + # via pytorch-lightning + # via scikit-learn + # via scipy + # via shapely + # via soxr + # via tensorboard + # via tensorboardx + # via torchmetrics + # via torchvision + # via xarray +nvidia-cublas-cu12==12.1.3.1 + # via nvidia-cudnn-cu12 + # via nvidia-cusolver-cu12 + # via torch +nvidia-cuda-cupti-cu12==12.1.105 + # via torch +nvidia-cuda-nvrtc-cu12==12.1.105 + # via torch +nvidia-cuda-runtime-cu12==12.1.105 + # via torch +nvidia-cudnn-cu12==8.9.2.26 + # via torch +nvidia-cufft-cu12==11.0.2.54 + # via torch +nvidia-curand-cu12==10.3.2.106 + # via torch +nvidia-cusolver-cu12==11.4.5.107 + # via torch +nvidia-cusparse-cu12==12.1.0.106 + # via nvidia-cusolver-cu12 + # via torch +nvidia-nccl-cu12==2.19.3 + # via torch +nvidia-nvjitlink-cu12==12.4.127 + # via nvidia-cusolver-cu12 + # via nvidia-cusparse-cu12 +nvidia-nvtx-cu12==12.1.105 + # via torch +omegaconf==2.3.0 + # via hydra-core + # via lightning +onnx==1.16.0 + # via batdetect2 +ordered-set==4.1.0 + # via deepdiff +packaging==24.0 + # via docker + # via hydra-core + # via lazy-loader + # via lightning + # via lightning-fabric + # via lightning-utilities + # via matplotlib + # via pooch + # via pytorch-lightning + # via tensorboardx + # via torchmetrics + # via xarray +pandas==2.0.3 + # via batdetect2 + # via xarray +pillow==10.3.0 + # via matplotlib + # via torchvision +platformdirs==4.2.0 + # via pooch +pooch==1.8.1 + # via librosa +protobuf==5.26.1 + # via onnx + # via tensorboard + # via tensorboardx +psutil==5.9.8 + # via lightning +pycparser==2.22 + # via cffi +pydantic==2.7.0 + # via fastapi + # via lightning + # via soundevent +pydantic-core==2.18.1 + # via pydantic +pygments==2.17.2 + # via rich +pyjwt==2.8.0 + # via lightning-cloud +pyparsing==3.1.2 + # via matplotlib +python-dateutil==2.9.0.post0 + # via arrow + # via botocore + # via croniter + # via dateutils + # via matplotlib + # via pandas +python-multipart==0.0.9 + # via lightning + # via lightning-cloud +pytorch-lightning==2.2.2 + # via batdetect2 + # via lightning +pytz==2024.1 + # via dateutils + # via pandas +pyyaml==6.0.1 + # via jsonargparse + # via lightning + # via omegaconf + # via pytorch-lightning +readchar==4.0.6 + # via inquirer +redis==5.0.4 + # via lightning +requests==2.31.0 + # via docker + # via fsspec + # via lightning + # via lightning-cloud + # via pooch +rich==13.7.1 + # via lightning + # via lightning-cloud +runs==1.2.2 + # via editor +s3fs==2023.12.2 + # via lightning +s3transfer==0.10.1 + # via boto3 +scikit-learn==1.3.2 + # via batdetect2 + # via librosa +scipy==1.10.1 + # via batdetect2 + # via librosa + # via scikit-learn + # via soundevent +setuptools==69.5.1 + # via lightning-utilities + # via readchar + # via tensorboard +shapely==2.0.3 + # via soundevent +six==1.16.0 + # via blessed + # via lightning-cloud + # via python-dateutil + # via tensorboard +sniffio==1.3.1 + # via anyio +soundevent==1.3.5 + # via batdetect2 +soundfile==0.12.1 + # via librosa + # via soundevent +soupsieve==2.5 + # via beautifulsoup4 +soxr==0.3.7 + # via librosa +starlette==0.37.2 + # via fastapi + # via lightning +sympy==1.12 + # via torch +tensorboard==2.16.2 + # via batdetect2 +tensorboard-data-server==0.7.2 + # via tensorboard +tensorboardx==2.6.2.2 + # via lightning +threadpoolctl==3.4.0 + # via scikit-learn +torch==2.2.2 + # via batdetect2 + # via lightning + # via lightning-fabric + # via pytorch-lightning + # via torchaudio + # via torchmetrics + # via torchvision +torchaudio==2.2.2 + # via batdetect2 +torchmetrics==1.3.2 + # via lightning + # via pytorch-lightning +torchvision==0.17.2 + # via batdetect2 +tqdm==4.66.2 + # via batdetect2 + # via lightning + # via pytorch-lightning +traitlets==5.14.3 + # via lightning +triton==2.2.0 + # via torch +types-python-dateutil==2.9.0.20240316 + # via arrow +typeshed-client==2.5.1 + # via jsonargparse +typing-extensions==4.11.0 + # via aioitertools + # via anyio + # via fastapi + # via jsonargparse + # via librosa + # via lightning + # via lightning-fabric + # via lightning-utilities + # via pydantic + # via pydantic-core + # via pytorch-lightning + # via starlette + # via torch + # via typeshed-client + # via uvicorn +tzdata==2024.1 + # via pandas +urllib3==1.26.18 + # via botocore + # via docker + # via lightning + # via lightning-cloud + # via requests +uvicorn==0.29.0 + # via lightning + # via lightning-cloud +wcwidth==0.2.13 + # via blessed +websocket-client==1.7.0 + # via docker + # via lightning + # via lightning-cloud +websockets==11.0.3 + # via lightning +werkzeug==3.0.2 + # via tensorboard +wrapt==1.16.0 + # via aiobotocore +xarray==2023.1.0 + # via cf-xarray + # via soundevent +xmod==1.8.1 + # via editor + # via runs +yarl==1.9.4 + # via aiohttp +zipp==3.18.1 + # via importlib-metadata + # via importlib-resources diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index cac4479..0000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -librosa==0.9.2 -matplotlib==3.6.2 -numpy==1.23.4 -pandas==1.5.2 -scikit_learn==1.2.0 -scipy==1.9.3 -torch==1.13.0 -torchaudio==0.13.0 -torchvision==0.14.0 -click diff --git a/tests/test_data/__init__.py b/tests/test_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_data/test_batdetect.py b/tests/test_data/test_batdetect.py new file mode 100644 index 0000000..1ead0b8 --- /dev/null +++ b/tests/test_data/test_batdetect.py @@ -0,0 +1,19 @@ +"""Test suite for loading batdetect annotations.""" + +from pathlib import Path + +from soundevent import data + +from batdetect2.data.compat import load_annotation_project + +ROOT_DIR = Path(__file__).parent.parent.parent + + +def test_load_example_annotation_project(): + path = ROOT_DIR / "example_data" / "anns" + audio_dir = ROOT_DIR / "example_data" / "audio" + project = load_annotation_project(path, audio_dir=audio_dir) + assert isinstance(project, data.AnnotationProject) + assert project.name == str(path) + assert len(project.clip_annotations) == 3 + assert len(project.tasks) == 3 diff --git a/tests/test_features.py b/tests/test_features.py index e34d499..f3eabf1 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -133,17 +133,16 @@ def test_compute_max_power_bb(max_power: int): audio = np.zeros((int(duration * samplerate),)) # Add a signal during the time and frequency range of interest - audio[ - int(start_time * samplerate) : int(end_time * samplerate) - ] = 0.5 * librosa.tone( - max_power, sr=samplerate, duration=end_time - start_time + audio[int(start_time * samplerate) : int(end_time * samplerate)] = ( + 0.5 + * librosa.tone( + max_power, sr=samplerate, duration=end_time - start_time + ) ) # Add a more powerful signal outside frequency range of interest - audio[ - int(start_time * samplerate) : int(end_time * samplerate) - ] += 2 * librosa.tone( - 80_000, sr=samplerate, duration=end_time - start_time + audio[int(start_time * samplerate) : int(end_time * samplerate)] += ( + 2 * librosa.tone(80_000, sr=samplerate, duration=end_time - start_time) ) params = api.get_config( @@ -152,7 +151,7 @@ def test_compute_max_power_bb(max_power: int): target_samp_rate=samplerate, ) - spec = au.generate_spectrogram( + spec, _ = au.generate_spectrogram( audio, samplerate, params, @@ -221,17 +220,17 @@ def test_compute_max_power(): audio = np.zeros((int(duration * samplerate),)) # Add a signal during the time and frequency range of interest - audio[ - int(start_time * samplerate) : int(end_time * samplerate) - ] = 0.5 * librosa.tone( - 3_500, sr=samplerate, duration=end_time - start_time + audio[int(start_time * samplerate) : int(end_time * samplerate)] = ( + 0.5 + * librosa.tone(3_500, sr=samplerate, duration=end_time - start_time) ) # Add a more powerful signal outside frequency range of interest - audio[ - int(start_time * samplerate) : int(end_time * samplerate) - ] += 2 * librosa.tone( - max_power, sr=samplerate, duration=end_time - start_time + audio[int(start_time * samplerate) : int(end_time * samplerate)] += ( + 2 + * librosa.tone( + max_power, sr=samplerate, duration=end_time - start_time + ) ) params = api.get_config( @@ -240,7 +239,7 @@ def test_compute_max_power(): target_samp_rate=samplerate, ) - spec = au.generate_spectrogram( + spec, _ = au.generate_spectrogram( audio, samplerate, params, diff --git a/tests/test_migration/__init__.py b/tests/test_migration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_migration/test_preprocessing.py b/tests/test_migration/test_preprocessing.py new file mode 100644 index 0000000..6027898 --- /dev/null +++ b/tests/test_migration/test_preprocessing.py @@ -0,0 +1,120 @@ +from pathlib import Path + +import numpy as np +import pytest +from soundevent import data + +from batdetect2.data import preprocessing +from batdetect2.utils import audio_utils + +ROOT_DIR = Path(__file__).parent.parent.parent +EXAMPLE_AUDIO = ROOT_DIR / "example_data" / "audio" +TEST_AUDIO = ROOT_DIR / "tests" / "data" + + +TEST_FILES = [ + EXAMPLE_AUDIO / "20170701_213954-MYOMYS-LR_0_0.5.wav", + EXAMPLE_AUDIO / "20180530_213516-EPTSER-LR_0_0.5.wav", + EXAMPLE_AUDIO / "20180627_215323-RHIFER-LR_0_0.5.wav", + TEST_AUDIO / "20230322_172000_selec2.wav", +] + + +@pytest.mark.parametrize("audio_file", TEST_FILES) +@pytest.mark.parametrize("scale", [True, False]) +def test_audio_loading_hasnt_changed( + audio_file, + scale, +): + time_expansion = 1 + target_sampling_rate = 256_000 + recording = data.Recording.from_file( + audio_file, + time_expansion=time_expansion, + ) + clip = data.Clip( + recording=recording, + start_time=0, + end_time=recording.duration, + ) + + _, audio_original = audio_utils.load_audio( + audio_file, + time_expansion, + target_samp_rate=target_sampling_rate, + scale=scale, + ) + audio_new = preprocessing.load_clip_audio( + clip, + target_sampling_rate=target_sampling_rate, + scale=scale, + dtype=np.float32, + ) + + assert audio_original.shape == audio_new.shape + assert audio_original.dtype == audio_new.dtype + assert np.isclose(audio_original, audio_new.data).all() + + +@pytest.mark.parametrize("audio_file", TEST_FILES) +@pytest.mark.parametrize("spec_scale", ["log", "pcen", "amplitude"]) +@pytest.mark.parametrize("denoise_spec_avg", [True, False]) +@pytest.mark.parametrize("max_scale_spec", [True, False]) +@pytest.mark.parametrize("fft_win_length", [512 / 256_000, 1024 / 256_000]) +def test_spectrogram_generation_hasnt_changed( + audio_file, + spec_scale, + denoise_spec_avg, + max_scale_spec, + fft_win_length, +): + time_expansion = 1 + target_sampling_rate = 256_000 + min_freq = 10_000 + max_freq = 120_000 + fft_overlap = 0.75 + recording = data.Recording.from_file( + audio_file, + time_expansion=time_expansion, + ) + clip = data.Clip( + recording=recording, + start_time=0, + end_time=recording.duration, + ) + audio = preprocessing.load_clip_audio( + clip, + target_sampling_rate=target_sampling_rate, + ) + + spec_original, _ = audio_utils.generate_spectrogram( + audio.data, + sampling_rate=target_sampling_rate, + params=dict( + fft_win_length=fft_win_length, + fft_overlap=fft_overlap, + max_freq=max_freq, + min_freq=min_freq, + spec_scale=spec_scale, + denoise_spec_avg=denoise_spec_avg, + max_scale_spec=max_scale_spec, + ), + ) + + new_spec = preprocessing.compute_spectrogram( + audio, + fft_win_length=fft_win_length, + fft_overlap=fft_overlap, + max_freq=max_freq, + min_freq=min_freq, + spec_scale=spec_scale, + denoise_spec_avg=denoise_spec_avg, + max_scale_spec=max_scale_spec, + dtype=np.float32, + ) + + assert spec_original.shape == new_spec.shape + assert spec_original.dtype == new_spec.dtype + + # NOTE: The original spectrogram is flipped vertically + assert np.isclose(spec_original, np.flipud(new_spec.data)).all()