diff --git a/.github/workflows/deploy_to_hf.yml b/.github/workflows/deploy_to_hf.yml new file mode 100644 index 0000000000000000000000000000000000000000..eef8faa34fc94d1940baa72d8c9aa8f82f944ac1 --- /dev/null +++ b/.github/workflows/deploy_to_hf.yml @@ -0,0 +1,55 @@ + +name: Deploy to Hugging Face Spaces + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Check out the repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install Poetry + run: | + curl -sSL https://install.python-poetry.org | python3 - + echo "${{ runner.tool_cache }}/poetry/bin" >> $GITHUB_PATH + + - name: Export requirements.txt + run: poetry export -f requirements.txt --output requirements.txt --without-hashes + + - name: Create packages.txt + run: | + echo "portaudio19-dev" > packages.txt + + - name: Prepend YAML header to README + run: | + cat hf_space_metadata.yml README.md > new_readme.md + mv new_readme.md README.md + + - name: Install Hugging Face CLI + run: | + python -m pip install --upgrade pip + pip install huggingface_hub + + - name: Configure Hugging Face CLI + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + run: | + huggingface-cli login --token $HF_TOKEN + + - name: Deploy to Spaces + env: + HF_USERNAME: ${{ secrets.HF_USERNAME }} + SPACE_NAME: ${{ secrets.SPACE_NAME }} + run: | + huggingface-cli upload atsushieee/improvisation-lab . --repo-type=space diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..f7f544079caa727224b95a768a08cf35d54ce702 --- /dev/null +++ b/.gitignore @@ -0,0 +1,165 @@ +# Ignore config.yml +config.yml + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..c745a0490e94f4a81bd3834beeb577e6e20541ee --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +.PHONY: install +install: + poetry install + +.PHONY: run +run: + poetry run python main.py + +.PHONY: lint +lint: + poetry run pflake8 improvisation_lab scripts tests main.py + poetry run mypy improvisation_lab scripts tests main.py + poetry run pydocstyle improvisation_lab scripts tests main.py + +.PHONY: format +format: + poetry run black improvisation_lab scripts tests main.py + poetry run isort improvisation_lab scripts tests main.py + +.PHONY: test +test: + poetry run pytest -vs tests + +.PHONY: pitch-demo-web pitch-demo-direct +pitch-demo-web: + poetry run python scripts/pitch_detection_demo.py --input web + +pitch-demo-direct: + poetry run python scripts/pitch_detection_demo.py --input direct + +# Target alias (Default: input voice via web) +.PHONY: pitch-demo +pitch-demo: pitch-demo-web diff --git a/README.md b/README.md index 3da79897c7374ed3c03067f08a6c39556ca9b3d9..8939db1631a951cc955ff930e0d9ddf6f53490b8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,148 @@ --- title: Improvisation Lab -emoji: 🌖 -colorFrom: purple -colorTo: blue +emoji: 🎵 +python_version: 3.11 +colorFrom: blue +colorTo: purple sdk: gradio sdk_version: 5.7.1 -app_file: app.py +app_file: main.py pinned: false +license: mit --- +# Improvisation Lab -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +A Python package for generating musical improvisation melodies based on music theory principles. The package specializes in creating natural-sounding melodic phrases that follow chord progressions while respecting musical rules, with real-time pitch detection for practice feedback. + +Improvisation Lab Demo + +https://github.com/user-attachments/assets/a4207f7e-166c-4f50-9c19-5bf5269fd04e + + +## Features + +- Generate melodic phrases based on scales and chord progressions +- Support for multiple scale types: + - Major + - Natural minor + - Harmonic minor + - Diminished +- Support for various chord types: + - Major 7th (maj7) + - Minor 7th (min7) + - Dominant 7th (dom7) + - Half-diminished (min7b5) + - Diminished 7th (dim7) +- Intelligent note selection based on: + - Chord tones vs non-chord tones + - Scale degrees + - Previous note context +- Real-time pitch detection with FCPE (Fast Context-aware Pitch Estimation) +- Web-based and direct microphone input support + +## Prerequisites + +- Python 3.11 or higher +- A working microphone +- [Poetry](https://python-poetry.org/) for dependency management + +## Installation +```bash +make install +``` + +## Quick Start +1. Create your configuration file: + +```bash +cp config.yml.example config.yml +``` + +2. (Optional) Edit `config.yml` to customize settings like audio parameters and song selection + +3. Run the script to start the melody generation and playback (default is web interface): + +```bash +make run +``` + +- To run the console interface, use: + +```bash +poetry run python main.py --app_type console +``` + +4. Follow the displayed melody phrases and sing along with real-time feedback + +### Configuration + +The application can be customized through `config.yml` with the following options: + +#### Audio Settings +- `sample_rate`: Audio sampling rate (default: 44100 Hz) +- `buffer_duration`: Duration of audio processing buffer (default: 0.2 seconds) +- `note_duration`: How long to display each note during practice (default: 3 seconds) +- `pitch_detector`: Configuration for the pitch detection algorithm + - `hop_length`: Hop length for the pitch detection algorithm (default: 512) + - `threshold`: Threshold for the pitch detection algorithm (default: 0.006) + - `f0_min`: Minimum frequency for the pitch detection algorithm (default: 80 Hz) + - `f0_max`: Maximum frequency for the pitch detection algorithm (default: 880 Hz) + - `device`: Device to use for the pitch detection algorithm (default: "cpu") + +#### Song Selection +- `selected_song`: Name of the song to practice +- `chord_progressions`: Dictionary of songs and their progressions + - Format: `[scale_root, scale_type, chord_root, chord_type, duration]` + - Example: + ```yaml + fly_me_to_the_moon: + - ["A", "natural_minor", "A", "min7", 4] + - ["A", "natural_minor", "D", "min7", 4] + - ["C", "major", "G", "dom7", 4] + ``` + + +## How It Works + +### Melody Generation +The melody generation follows these principles: +1. Notes are selected based on their relationship to the current chord and scale +2. Chord tones have more freedom in movement +3. Non-chord tones are restricted to moving to adjacent scale notes +4. Phrases are connected naturally by considering the previous note +5. All generated notes stay within the specified scale + +### Real-time Feedback +Pitch Detection Demo: + +https://github.com/user-attachments/assets/fd9e6e3f-85f1-42be-a6c8-b757da478854 + +The application provides real-time feedback by: +1. Capturing audio from your microphone +2. Detecting the pitch using FCPE (Fast Context-aware Pitch Estimation) +3. Converting the frequency to the nearest musical note +4. Displaying both the target note and your sung note in real-time + +## Development +### Running Lint +```bash +make lint +``` + +### Running Format +```bash +make format +``` + +### Running Tests +```bash +make test +``` + +## License + +MIT License + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/config.yml.example b/config.yml.example new file mode 100644 index 0000000000000000000000000000000000000000..8c80a18c578863ac482eb0efef3a822aa948ddb8 --- /dev/null +++ b/config.yml.example @@ -0,0 +1,20 @@ +audio: + sample_rate: 44100 + buffer_duration: 0.2 + note_duration: 1.0 + pitch_detector: + hop_length: 512 + threshold: 0.006 + f0_min: 80 + f0_max: 880 + device: "cpu" + +selected_song: "fly_me_to_the_moon" + +chord_progressions: + fly_me_to_the_moon: + - ["A", "natural_minor", "A", "min7", 4] + - ["A", "natural_minor", "D", "min7", 4] + - ["C", "major", "G", "dom7", 4] + - ["C", "major", "C", "maj7", 2] + - ["F", "major", "C", "dom7", 2] diff --git a/hf_space_metadata.yml b/hf_space_metadata.yml new file mode 100644 index 0000000000000000000000000000000000000000..16d1214b85b4c877aea5483edaf4159f1e80ac41 --- /dev/null +++ b/hf_space_metadata.yml @@ -0,0 +1,12 @@ +--- +title: Improvisation Lab +emoji: 🎵 +python_version: 3.11 +colorFrom: blue +colorTo: purple +sdk: gradio +sdk_version: 5.7.1 +app_file: main.py +pinned: false +license: mit +--- diff --git a/improvisation_lab/__init__.py b/improvisation_lab/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b1adb3a232953b72f073f25be6b7fe78c05db31b --- /dev/null +++ b/improvisation_lab/__init__.py @@ -0,0 +1 @@ +"""Improvisation Lab - A Python package for musical improvisation.""" diff --git a/improvisation_lab/application/__init__.py b/improvisation_lab/application/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..beb529f1dd03e6afe4126a7eeea6166c959966dd --- /dev/null +++ b/improvisation_lab/application/__init__.py @@ -0,0 +1 @@ +"""Application layer for the Improvisation Lab.""" diff --git a/improvisation_lab/application/melody_practice/__init__.py b/improvisation_lab/application/melody_practice/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..30a2585b9889ec11616fbba805d965a71dfd56a1 --- /dev/null +++ b/improvisation_lab/application/melody_practice/__init__.py @@ -0,0 +1,6 @@ +"""Application layer for melody practice.""" + +from improvisation_lab.application.melody_practice.app_factory import \ + MelodyPracticeAppFactory + +__all__ = ["MelodyPracticeAppFactory"] diff --git a/improvisation_lab/application/melody_practice/app_factory.py b/improvisation_lab/application/melody_practice/app_factory.py new file mode 100644 index 0000000000000000000000000000000000000000..0c5331d8e26caa954703764ffcb8e53e8a5be471 --- /dev/null +++ b/improvisation_lab/application/melody_practice/app_factory.py @@ -0,0 +1,28 @@ +"""Factory class for creating melody practice applications.""" + +from improvisation_lab.application.melody_practice.console_app import \ + ConsoleMelodyPracticeApp +from improvisation_lab.application.melody_practice.web_app import \ + WebMelodyPracticeApp +from improvisation_lab.config import Config +from improvisation_lab.service import MelodyPracticeService + + +class MelodyPracticeAppFactory: + """Factory class for creating melody practice applications.""" + + @staticmethod + def create_app(app_type: str, service: MelodyPracticeService, config: Config): + """Create a melody practice application. + + Args: + app_type: Type of application to create. + service: MelodyPracticeService instance. + config: Config instance. + """ + if app_type == "web": + return WebMelodyPracticeApp(service, config) + elif app_type == "console": + return ConsoleMelodyPracticeApp(service, config) + else: + raise ValueError(f"Unknown app type: {app_type}") diff --git a/improvisation_lab/application/melody_practice/base_app.py b/improvisation_lab/application/melody_practice/base_app.py new file mode 100644 index 0000000000000000000000000000000000000000..dda495b9c227ee46b31413c8a5d4d2dc1ee0a4a1 --- /dev/null +++ b/improvisation_lab/application/melody_practice/base_app.py @@ -0,0 +1,53 @@ +"""Base class for melody practice applications.""" + +from abc import ABC, abstractmethod +from typing import List, Optional + +import numpy as np + +from improvisation_lab.config import Config +from improvisation_lab.domain.composition import PhraseData +from improvisation_lab.presentation.melody_practice import ViewTextManager +from improvisation_lab.service import MelodyPracticeService + + +class BaseMelodyPracticeApp(ABC): + """Base class for melody practice applications.""" + + def __init__(self, service: MelodyPracticeService, config: Config): + """Initialize the application. + + Args: + service: MelodyPracticeService instance. + config: Config instance. + """ + self.service = service + self.config = config + self.phrases: Optional[List[PhraseData]] = None + self.current_phrase_idx: int = 0 + self.current_note_idx: int = 0 + self.is_running: bool = False + self.text_manager = ViewTextManager() + + @abstractmethod + def _process_audio_callback(self, audio_data: np.ndarray): + """Process incoming audio data and update the application state. + + Args: + audio_data: Audio data to process. + """ + pass + + @abstractmethod + def _advance_to_next_note(self): + """Advance to the next note or phrase.""" + pass + + @abstractmethod + def launch(self, **kwargs): + """Launch the application. + + Args: + **kwargs: Additional keyword arguments for the launch method. + """ + pass diff --git a/improvisation_lab/application/melody_practice/console_app.py b/improvisation_lab/application/melody_practice/console_app.py new file mode 100644 index 0000000000000000000000000000000000000000..1bb67acf1ea7e336f46a674c72329e0729378194 --- /dev/null +++ b/improvisation_lab/application/melody_practice/console_app.py @@ -0,0 +1,82 @@ +"""Console application for melody practice.""" + +import time + +import numpy as np + +from improvisation_lab.application.melody_practice.base_app import \ + BaseMelodyPracticeApp +from improvisation_lab.config import Config +from improvisation_lab.infrastructure.audio import DirectAudioProcessor +from improvisation_lab.presentation.melody_practice import ConsoleMelodyView +from improvisation_lab.service import MelodyPracticeService + + +class ConsoleMelodyPracticeApp(BaseMelodyPracticeApp): + """Main application class for melody practice.""" + + def __init__(self, service: MelodyPracticeService, config: Config): + """Initialize the application using console UI. + + Args: + service: MelodyPracticeService instance. + config: Config instance. + """ + super().__init__(service, config) + + self.audio_processor = DirectAudioProcessor( + sample_rate=config.audio.sample_rate, + callback=self._process_audio_callback, + buffer_duration=config.audio.buffer_duration, + ) + + self.ui = ConsoleMelodyView(self.text_manager, config.selected_song) + + def _process_audio_callback(self, audio_data: np.ndarray): + """Process incoming audio data and update the application state. + + Args: + audio_data: Audio data to process. + """ + if self.phrases is None: + return + current_phrase = self.phrases[self.current_phrase_idx] + current_note = current_phrase.notes[self.current_note_idx] + + result = self.service.process_audio(audio_data, current_note) + self.ui.display_pitch_result(result) + + # Progress to next note if current note is complete + if result.remaining_time <= 0: + self._advance_to_next_note() + + def _advance_to_next_note(self): + """Advance to the next note or phrase.""" + if self.phrases is None: + return + self.current_note_idx += 1 + if self.current_note_idx >= len(self.phrases[self.current_phrase_idx].notes): + self.current_note_idx = 0 + self.current_phrase_idx += 1 + self.ui.display_phrase_info(self.current_phrase_idx, self.phrases) + if self.current_phrase_idx >= len(self.phrases): + self.current_phrase_idx = 0 + + def launch(self): + """Launch the application.""" + self.ui.launch() + self.phrases = self.service.generate_melody() + self.current_phrase_idx = 0 + self.current_note_idx = 0 + self.is_running = True + + if not self.audio_processor.is_recording: + try: + self.audio_processor.start_recording() + self.ui.display_phrase_info(self.current_phrase_idx, self.phrases) + while True: + time.sleep(0.1) + except KeyboardInterrupt: + print("\nStopping...") + finally: + self.audio_processor.stop_recording() diff --git a/improvisation_lab/application/melody_practice/web_app.py b/improvisation_lab/application/melody_practice/web_app.py new file mode 100644 index 0000000000000000000000000000000000000000..33b05f79a5bf911a934b757f185ed0515fe7c0cf --- /dev/null +++ b/improvisation_lab/application/melody_practice/web_app.py @@ -0,0 +1,120 @@ +"""Web application for melody practice.""" + +import numpy as np + +from improvisation_lab.application.melody_practice.base_app import \ + BaseMelodyPracticeApp +from improvisation_lab.config import Config +from improvisation_lab.infrastructure.audio import WebAudioProcessor +from improvisation_lab.presentation.melody_practice import WebMelodyView +from improvisation_lab.service import MelodyPracticeService + + +class WebMelodyPracticeApp(BaseMelodyPracticeApp): + """Main application class for melody practice.""" + + def __init__(self, service: MelodyPracticeService, config: Config): + """Initialize the application using web UI. + + Args: + service: MelodyPracticeService instance. + config: Config instance. + """ + super().__init__(service, config) + + self.audio_processor = WebAudioProcessor( + sample_rate=config.audio.sample_rate, + callback=self._process_audio_callback, + buffer_duration=config.audio.buffer_duration, + ) + + # UIをコールバック関数と共に初期化 + self.ui = WebMelodyView( + on_generate_melody=self.start, + on_end_practice=self.stop, + on_audio_input=self.handle_audio, + song_name=config.selected_song, + ) + + def _process_audio_callback(self, audio_data: np.ndarray): + """Process incoming audio data and update the application state. + + Args: + audio_data: Audio data to process. + """ + if not self.is_running or not self.phrases: + return + + current_phrase = self.phrases[self.current_phrase_idx] + current_note = current_phrase.notes[self.current_note_idx] + + result = self.service.process_audio(audio_data, current_note) + + # Update status display + self.text_manager.update_pitch_result(result) + + # Progress to next note if current note is complete + if result.remaining_time <= 0: + self._advance_to_next_note() + + self.text_manager.update_phrase_text(self.current_phrase_idx, self.phrases) + + def _advance_to_next_note(self): + """Advance to the next note or phrase.""" + if self.phrases is None: + return + self.current_note_idx += 1 + if self.current_note_idx >= len(self.phrases[self.current_phrase_idx].notes): + self.current_note_idx = 0 + self.current_phrase_idx += 1 + if self.current_phrase_idx >= len(self.phrases): + self.current_phrase_idx = 0 + + def handle_audio(self, audio: tuple[int, np.ndarray]) -> tuple[str, str]: + """Handle audio input from Gradio interface. + + Args: + audio: Audio data to process. + + Returns: + tuple[str, str]: The current phrase text and result text. + """ + if not self.is_running: + return "Not running", "Start the session first" + + self.audio_processor.process_audio(audio) + return self.text_manager.phrase_text, self.text_manager.result_text + + def start(self) -> tuple[str, str]: + """Start a new practice session. + + Returns: + tuple[str, str]: The current phrase text and result text. + """ + self.phrases = self.service.generate_melody() + self.current_phrase_idx = 0 + self.current_note_idx = 0 + self.is_running = True + + if not self.audio_processor.is_recording: + self.text_manager.initialize_text() + self.audio_processor.start_recording() + + self.text_manager.update_phrase_text(self.current_phrase_idx, self.phrases) + return self.text_manager.phrase_text, self.text_manager.result_text + + def stop(self) -> tuple[str, str]: + """Stop the current practice session. + + Returns: + tuple[str, str]: The current phrase text and result text. + """ + self.is_running = False + if self.audio_processor.is_recording: + self.audio_processor.stop_recording() + self.text_manager.terminate_text() + return self.text_manager.phrase_text, self.text_manager.result_text + + def launch(self, **kwargs): + """Launch the application.""" + self.ui.launch(**kwargs) diff --git a/improvisation_lab/config.py b/improvisation_lab/config.py new file mode 100644 index 0000000000000000000000000000000000000000..37376c640417824d5754f939005f99fb363aade2 --- /dev/null +++ b/improvisation_lab/config.py @@ -0,0 +1,89 @@ +"""Configuration module for audio settings and chord progressions.""" + +from dataclasses import dataclass, field +from pathlib import Path + +import yaml + + +@dataclass +class PitchDetectorConfig: + """Configuration settings for pitch detection.""" + + sample_rate: int = 44100 + hop_length: int = 512 + decoder_mode: str = "local_argmax" + threshold: float = 0.006 + f0_min: int = 80 + f0_max: int = 880 + interp_uv: bool = False + device: str = "cpu" + + +@dataclass +class AudioConfig: + """Configuration class for audio-related settings.""" + + sample_rate: int = 44100 + buffer_duration: float = 0.2 + note_duration: float = 1.0 + pitch_detector: PitchDetectorConfig = field(default_factory=PitchDetectorConfig) + + @classmethod + def from_yaml(cls, yaml_data: dict) -> "AudioConfig": + """Create AudioConfig instance from YAML data.""" + config = cls( + sample_rate=yaml_data.get("sample_rate", cls.sample_rate), + buffer_duration=yaml_data.get("buffer_duration", cls.buffer_duration), + note_duration=yaml_data.get("note_duration", cls.note_duration), + ) + + if "pitch_detector" in yaml_data: + pitch_detector_data = yaml_data["pitch_detector"] + # The sample rate must be set explicitly + # Use the sample rate specified in the audio config + pitch_detector_data["sample_rate"] = config.sample_rate + config.pitch_detector = PitchDetectorConfig(**pitch_detector_data) + + return config + + +@dataclass +class Config: + """Application configuration handler.""" + + audio: AudioConfig + selected_song: str + chord_progressions: dict + + def __init__(self, config_path: str | Path = "config.yml"): + """Initialize Config instance. + + Args: + config_path: Path to YAML configuration file (default: 'config.yml'). + """ + self.config_path = Path(config_path) + self._load_config() + + def _load_config(self): + if self.config_path.exists(): + with open(self.config_path, "r") as f: + yaml_data = yaml.safe_load(f) + self.audio = AudioConfig.from_yaml(yaml_data.get("audio", {})) + self.selected_song = yaml_data.get( + "selected_song", "fly_me_to_the_moon" + ) + self.chord_progressions = yaml_data.get("chord_progressions", {}) + else: + self.audio = AudioConfig() + self.selected_song = "fly_me_to_the_moon" + self.chord_progressions = { + # opening 4 bars of Fly Me to the Moon + "fly_me_to_the_moon": [ + ("A", "natural_minor", "A", "min7", 8), + ("A", "natural_minor", "D", "min7", 8), + ("C", "major", "G", "dom7", 8), + ("C", "major", "C", "maj7", 4), + ("F", "major", "C", "dom7", 4), + ] + } diff --git a/improvisation_lab/domain/__init__.py b/improvisation_lab/domain/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f4f112ac444eb0a9797357f96b09ef45ac17366c --- /dev/null +++ b/improvisation_lab/domain/__init__.py @@ -0,0 +1 @@ +"""Package containing domain logic.""" diff --git a/improvisation_lab/domain/analysis/__init__.py b/improvisation_lab/domain/analysis/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d49ac5ecfeaacb1e24f2e0e9b2bc7aa624523d83 --- /dev/null +++ b/improvisation_lab/domain/analysis/__init__.py @@ -0,0 +1,5 @@ +"""Module for music analysis.""" + +from improvisation_lab.domain.analysis.pitch_detector import PitchDetector + +__all__ = ["PitchDetector"] diff --git a/improvisation_lab/domain/analysis/pitch_detector.py b/improvisation_lab/domain/analysis/pitch_detector.py new file mode 100644 index 0000000000000000000000000000000000000000..e88d551a98a86a36834371ce0a7abc4e375290f5 --- /dev/null +++ b/improvisation_lab/domain/analysis/pitch_detector.py @@ -0,0 +1,61 @@ +"""PitchDetector class for real-time pitch detection using FCPE.""" + +import numpy as np +import torch +from torchfcpe import spawn_bundled_infer_model + +from improvisation_lab.config import PitchDetectorConfig + + +class PitchDetector: + """Class for real-time pitch detection using FCPE.""" + + def __init__(self, config: PitchDetectorConfig): + """Initialize pitch detector. + + Args: + config: Configuration settings for pitch detection. + """ + self.sample_rate = config.sample_rate + self.hop_length = config.hop_length + self.decoder_mode = config.decoder_mode + self.threshold = config.threshold + self.f0_min = config.f0_min + self.f0_max = config.f0_max + self.interp_uv = config.interp_uv + self.model = spawn_bundled_infer_model(device=config.device) + + def detect_pitch(self, audio_frame: np.ndarray) -> float: + """Detect pitch from audio frame. + + Args: + audio_frame: Numpy array of audio samples + + Returns: + Frequency in Hz + """ + audio_length = len(audio_frame) + f0_target_length = (audio_length // self.hop_length) + 1 + + # Convert to torch tensor and reshape to match expected dimensions + # Add batch and channel dimensions + audio_tensor = torch.from_numpy(audio_frame).float() + audio_tensor = audio_tensor.unsqueeze(0).unsqueeze(-1) + + pitch = self.model.infer( + audio_tensor, + sr=self.sample_rate, + decoder_mode=self.decoder_mode, + threshold=self.threshold, + f0_min=self.f0_min, + f0_max=self.f0_max, + interp_uv=self.interp_uv, + output_interp_target_length=f0_target_length, + ) + + # Extract the middle frequency value from the pitch tensor + # Taking the middle value helps avoid potential inaccuracies at the edges + # of the audio frame, providing a more stable frequency estimate. + middle_index = pitch.size(1) // 2 + frequency = pitch[0, middle_index, 0].item() + return frequency diff --git a/improvisation_lab/domain/composition/__init__.py b/improvisation_lab/domain/composition/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6f1e1986382be9850fcd44d6c3c4836797663696 --- /dev/null +++ b/improvisation_lab/domain/composition/__init__.py @@ -0,0 +1,6 @@ +"""Module for melody improvisation generation.""" + +from improvisation_lab.domain.composition.melody_composer import ( + MelodyComposer, PhraseData) + +__all__ = ["PhraseData", "MelodyComposer"] diff --git a/improvisation_lab/domain/composition/melody_composer.py b/improvisation_lab/domain/composition/melody_composer.py new file mode 100644 index 0000000000000000000000000000000000000000..8d9f54fb3d77bb1402b3b40def4922a2d62a7c01 --- /dev/null +++ b/improvisation_lab/domain/composition/melody_composer.py @@ -0,0 +1,71 @@ +"""Module for handling melody generation and playback.""" + +from dataclasses import dataclass +from typing import List, Optional + +from improvisation_lab.domain.composition.phrase_generator import \ + PhraseGenerator +from improvisation_lab.domain.music_theory import ChordTone + + +@dataclass +class PhraseData: + """Data structure containing information about a melodic phrase.""" + + notes: List[str] + chord_name: str + scale_info: str + length: int + + +class MelodyComposer: + """Class responsible for generating melodic phrases based on chord progressions.""" + + def __init__(self): + """Initialize MelodyPlayer with a melody generator.""" + self.phrase_generator = PhraseGenerator() + + def generate_phrases( + self, progression: List[tuple[str, str, str, str, int]] + ) -> List[PhraseData]: + """Generate a sequence of melodic phrases based on a chord progression. + + Args: + progression: + List of tuples containing (scale_root, scale_type, chord_root, + chord_type, length) for each chord in the progression. + + Returns: + List of PhraseData objects containing the generated melodic phrases. + """ + phrases: List[PhraseData] = [] + prev_note: Optional[str] = None + prev_note_was_chord_tone = False + + for scale_root, scale_type, chord_root, chord_type, length in progression: + phrase = self.phrase_generator.generate_phrase( + scale_root=scale_root, + scale_type=scale_type, + chord_root=chord_root, + chord_type=chord_type, + prev_note=prev_note, + prev_note_was_chord_tone=prev_note_was_chord_tone, + length=length, + ) + + # Update information for the next phrase + prev_note = phrase[-1] + prev_note_was_chord_tone = self.phrase_generator.is_chord_tone( + prev_note, ChordTone.get_chord_tones(chord_root, chord_type) + ) + + phrases.append( + PhraseData( + notes=phrase, + chord_name=f"{chord_root}{chord_type}", + scale_info=f"{scale_root} {scale_type}", + length=length, + ) + ) + + return phrases diff --git a/improvisation_lab/domain/composition/phrase_generator.py b/improvisation_lab/domain/composition/phrase_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..50c9eab55d5dcb0e23b9c4151a7682cd1b05384d --- /dev/null +++ b/improvisation_lab/domain/composition/phrase_generator.py @@ -0,0 +1,188 @@ +"""Module for generating improvised melody phrases. + +This module provides functionality to generate natural melody phrases +based on given scales and chord progressions, following music theory principles. +""" + +import random + +from improvisation_lab.domain.music_theory import ChordTone, Notes, Scale + + +class PhraseGenerator: + """Class for generating improvised melody phrases. + + This class generates melody phrases based on given scales and chord progressions, + following music theory rules. + The next note selection depends on whether the current note is a chord tone or not, + with chord tones having more freedom in movement + while non-chord tones move to adjacent notes. + """ + + def is_chord_tone(self, note: str, chord_tones: list[str]) -> bool: + """Check if a note is a chord tone. + + Args: + note: The note to check. + chord_tones: The list of chord tones. + + Returns: + True if the note is a chord tone, False otherwise. + """ + return note in chord_tones + + def get_adjacent_notes(self, note: str, scale_notes: list[str]) -> list[str]: + """Get adjacent notes to a given note. + + Args: + note: The note to get adjacent notes to. + scale_notes: The list of notes in the scale. + + Returns: + The list of adjacent notes in order (lower note first, then higher note). + """ + length_scale_notes = len(scale_notes) + if note in scale_notes: + note_index = scale_notes.index(note) + return [ + scale_notes[(note_index - 1) % length_scale_notes], + scale_notes[(note_index + 1) % length_scale_notes], + ] + + return [ + self._find_closest_note_in_direction(note, scale_notes, -1), + self._find_closest_note_in_direction(note, scale_notes, 1), + ] + + def _find_closest_note_in_direction( + self, note: str, scale_notes: list[str], direction: int + ) -> str: + """Find the closest note in a given direction within the scale. + + Args: + start_index: Starting index in the chromatic scale. + all_notes: List of all notes (chromatic scale). + scale_notes: List of notes in the target scale. + direction: Direction to search (-1 for lower, 1 for higher). + + Returns: + The closest note in the given direction that exists in the scale. + """ + all_notes = [note.value for note in Notes] # Chromatic scale + note_index = all_notes.index(note) + + current_index = note_index + while True: + current_index = (current_index + direction) % 12 + current_note = all_notes[current_index] + if current_note in scale_notes: + return current_note + if current_index == note_index: # If we've gone full circle + break + return all_notes[current_index] + + def get_next_note( + self, current_note: str, scale_notes: list[str], chord_tones: list[str] + ) -> str: + """Get the next note based on the current note, scale, and chord tones. + + Args: + current_note: The current note. + scale_notes: The list of notes in the scale. + chord_tones: The list of chord tones. + + Returns: + The next note. + """ + is_current_chord_tone = self.is_chord_tone(current_note, chord_tones) + + if is_current_chord_tone: + # For chord tones, freely move to any scale note + available_notes = [note for note in scale_notes if note != current_note] + return random.choice(available_notes) + # For non-chord tones, move to adjacent notes only + adjacent_notes = self.get_adjacent_notes(current_note, scale_notes) + return random.choice(adjacent_notes) + + def select_first_note( + self, + scale_notes: list[str], + chord_tones: list[str], + prev_note: str | None = None, + prev_note_was_chord_tone: bool = False, + ) -> str: + """Select the first note of a phrase. + + Args: + scale_notes: The list of notes in the scale. + chord_tones: The list of chord tones. + prev_note: The last note of the previous phrase (default: None). + prev_note_was_chord_tone: + Whether the previous note was a chord tone (default: False). + + Returns: + The selected first note. + """ + # For the first phrase, randomly select from scale notes + if prev_note is None: + return random.choice(scale_notes) + + # Case: previous note was a chord tone, can move freely + if prev_note_was_chord_tone: + available_notes = [note for note in scale_notes if note != prev_note] + return random.choice(available_notes) + + # Case: previous note was not a chord tone + if prev_note in chord_tones: + # If it's a chord tone in the current chord, can move freely + available_notes = [note for note in scale_notes if note != prev_note] + return random.choice(available_notes) + + # If it's not a chord tone, can only move to adjacent notes + adjacent_notes = self.get_adjacent_notes(prev_note, scale_notes) + return random.choice(adjacent_notes) + + def generate_phrase( + self, + scale_root: str, + scale_type: str, + chord_root: str, + chord_type: str, + prev_note: str | None = None, + prev_note_was_chord_tone: bool = False, + length=8, + ) -> list[str]: + """Generate a phrase of notes. + + Args: + scale_root: The root note of the scale. + scale_type: The type of scale (e.g., "major", "natural_minor"). + chord_root: The root note of the chord. + chord_type: The type of chord (e.g., "maj", "maj7"). + prev_note: The last note of the previous phrase (default: None). + prev_note_was_chord_tone: + Whether the previous note was a chord tone (default: False). + length: The length of the phrase (default: 8). + + Returns: + A list of note names in the phrase. + """ + # Get scale notes and chord tones + scale_notes = Scale.get_scale_notes(scale_root, scale_type) + chord_tones = ChordTone.get_chord_tones(chord_root, chord_type) + + # Generate the phrase + phrase = [] + + # Select the first note + current_note = self.select_first_note( + scale_notes, chord_tones, prev_note, prev_note_was_chord_tone + ) + phrase.append(current_note) + + # Generate remaining notes + for _ in range(length - 1): + current_note = self.get_next_note(current_note, scale_notes, chord_tones) + phrase.append(current_note) + + return phrase diff --git a/improvisation_lab/domain/music_theory.py b/improvisation_lab/domain/music_theory.py new file mode 100644 index 0000000000000000000000000000000000000000..e9f9de4bf205d8b25526e42504238899fa00c56e --- /dev/null +++ b/improvisation_lab/domain/music_theory.py @@ -0,0 +1,172 @@ +"""Module containing basic music theory concepts and constants.""" + +from enum import Enum + +import numpy as np + + +class Notes(str, Enum): + """Enumeration of musical notes in chromatic scale. + + This class represents the twelve notes of the chromatic scale + and provides methods for note manipulation and validation. + It inherits from both str and Enum to provide string-like behavior + while maintaining the benefits of enumeration. + + The str inheritance allows direct string operations on the note values, + while Enum ensures type safety and provides a defined set of valid notes. + + Examples: + >>> note = Notes.C + >>> isinstance(note, str) # True + >>> note.lower() # 'c' + >>> note + 'm' # 'Cm' + """ + + C = "C" + C_SHARP = "C#" + D = "D" + D_SHARP = "D#" + E = "E" + F = "F" + F_SHARP = "F#" + G = "G" + G_SHARP = "G#" + A = "A" + A_SHARP = "A#" + B = "B" + + @classmethod + def get_note_index(cls, note: str) -> int: + """Get the index of a note in the chromatic scale. + + Args: + note (str): The note name to find the index for. + + Returns: + int: The index of the note in the chromatic scale (0-11). + """ + return list(cls).index(cls(note)) + + @classmethod + def get_chromatic_scale(cls, note: str) -> list[str]: + """Return all notes in chromatic order. + + Args: + note (str): The note name to start the chromatic scale from. + + Returns: + list[str]: A list of note names in chromatic order, + starting from C (e.g., ["C", "C#", "D", ...]). + """ + start_idx = cls.get_note_index(note) + all_notes = [note.value for note in cls] + return all_notes[start_idx:] + all_notes[:start_idx] + + @classmethod + def convert_frequency_to_note(cls, frequency: float) -> str: + """Convert a frequency in Hz to the nearest note name on a piano keyboard. + + Args: + frequency: The frequency in Hz. + + Returns: + The name of the nearest note. + """ + A4_frequency = 440.0 + # Calculate the number of semitones from A4 (440Hz) + n = 12 * np.log2(frequency / A4_frequency) + + # Round to the nearest semitone + n = round(n) + + # Calculate octave and index of note name with respect to A4 + octave = 4 + (n + 9) // 12 + note_idx = (n + 9) % 12 + + note = cls.get_chromatic_scale(cls.C)[note_idx] + return f"{note}{octave}" + + @classmethod + def convert_frequency_to_base_note(cls, frequency: float) -> str: + """Convert frequency to base note name without octave number. + + Args: + frequency: Frequency in Hz + + Returns: + Base note name (e.g., 'C', 'C#', 'D') + """ + note_with_octave = cls.convert_frequency_to_note(frequency) + return note_with_octave[:-1] # Remove the octave number + + +class Scale: + """Musical scale representation and operations. + + This class handles scale-related operations including scale generation + and scale note calculations. + """ + + SCALES = { + "major": [0, 2, 4, 5, 7, 9, 11], + "natural_minor": [0, 2, 3, 5, 7, 8, 10], + "harmonic_minor": [0, 2, 3, 5, 7, 8, 11], + "diminished": [0, 2, 3, 5, 6, 8, 9, 11], + } + + @classmethod + def get_scale_notes(cls, root_note: str, scale_type: str) -> list[str]: + """Generate scale notes from root note and scale type. + + Args: + root_note: The root note of the scale. + scale_type: The type of scale (e.g., "major", "natural_minor"). + + Returns: + A list of note names in the scale. + + Raises: + ValueError: If root_note is invalid or scale_type is not recognized. + """ + if scale_type not in cls.SCALES: + raise ValueError(f"Invalid scale type: {scale_type}") + + scale_pattern = cls.SCALES[scale_type] + chromatic = Notes.get_chromatic_scale(root_note) + return [chromatic[interval % 12] for interval in scale_pattern] + + +class ChordTone: + """Musical chord tone representation and operations. + + This class handles chord tone-related operations + including chord tone generation and chord tone calculation. + """ + + CHORD_TONES = { + "maj": [0, 4, 7, 9], + "maj7": [0, 4, 7, 11], + "min7": [0, 3, 7, 10], + "min7(b5)": [0, 3, 6, 10], + "dom7": [0, 4, 7, 10], + "dim7": [0, 3, 6, 9], + } + + @classmethod + def get_chord_tones(cls, root_note: str, chord_type: str) -> list[str]: + """Generate chord tones from root note and chord type. + + Args: + root_note: The root note of the chord. + chord_type: The type of chord (e.g., "maj", "maj7"). + + Returns: + A list of note names in the chord. + """ + if chord_type not in cls.CHORD_TONES: + raise ValueError(f"Invalid chord type: {chord_type}") + + chord_pattern = cls.CHORD_TONES[chord_type] + chromatic = Notes.get_chromatic_scale(root_note) + return [chromatic[interval] for interval in chord_pattern] diff --git a/improvisation_lab/infrastructure/__init__.py b/improvisation_lab/infrastructure/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2206fd73890808900f3a59494193a0cd5b0dc7cc --- /dev/null +++ b/improvisation_lab/infrastructure/__init__.py @@ -0,0 +1 @@ +"""Infrastructure layer for handling external dependencies and implementations.""" diff --git a/improvisation_lab/infrastructure/audio/__init__.py b/improvisation_lab/infrastructure/audio/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9353cc3a8da3a6b851e481d56e4dc59e94149738 --- /dev/null +++ b/improvisation_lab/infrastructure/audio/__init__.py @@ -0,0 +1,10 @@ +"""Audio infrastructure components.""" + +from improvisation_lab.infrastructure.audio.audio_processor import \ + AudioProcessor +from improvisation_lab.infrastructure.audio.direct_processor import \ + DirectAudioProcessor +from improvisation_lab.infrastructure.audio.web_processor import \ + WebAudioProcessor + +__all__ = ["AudioProcessor", "DirectAudioProcessor", "WebAudioProcessor"] diff --git a/improvisation_lab/infrastructure/audio/audio_processor.py b/improvisation_lab/infrastructure/audio/audio_processor.py new file mode 100644 index 0000000000000000000000000000000000000000..6e98f2f6f29e87e2523aee67761c2665e78a3a64 --- /dev/null +++ b/improvisation_lab/infrastructure/audio/audio_processor.py @@ -0,0 +1,53 @@ +"""Module providing abstract base class for audio input handling.""" + +from abc import ABC, abstractmethod +from typing import Callable + +import numpy as np + + +class AudioProcessor(ABC): + """Abstract base class for audio input handling.""" + + def __init__( + self, + sample_rate: int, + callback: Callable[[np.ndarray], None] | None = None, + buffer_duration: float = 0.2, + ): + """Initialize AudioInput. + + Args: + sample_rate: Audio sample rate in Hz + callback: Optional callback function to process audio data + buffer_duration: Duration of audio buffer in seconds + """ + self.sample_rate = sample_rate + self.is_recording = False + self._callback = callback + self._buffer = np.array([], dtype=np.float32) + self._buffer_size = int(sample_rate * buffer_duration) + + def _append_to_buffer(self, audio_data: np.ndarray) -> None: + """Append new audio data to the buffer.""" + # Convert stereo to mono if necessary + if audio_data.ndim > 1: + audio_data = np.mean(audio_data, axis=1) + self._buffer = np.concatenate([self._buffer, audio_data]) + + def _process_buffer(self) -> None: + """Process buffer data if it has reached the desired size.""" + if len(self._buffer) >= self._buffer_size: + if self._callback is not None: + self._callback(self._buffer[: self._buffer_size]) + self._buffer = self._buffer[self._buffer_size :] + + @abstractmethod + def start_recording(self): + """Start recording audio.""" + pass + + @abstractmethod + def stop_recording(self): + """Stop recording audio.""" + pass diff --git a/improvisation_lab/infrastructure/audio/direct_processor.py b/improvisation_lab/infrastructure/audio/direct_processor.py new file mode 100644 index 0000000000000000000000000000000000000000..6d8fa1355eeb3f682f0b77f5cdaf20f2415afc08 --- /dev/null +++ b/improvisation_lab/infrastructure/audio/direct_processor.py @@ -0,0 +1,104 @@ +"""Module for handling microphone input and audio processing. + +This module provides functionality for real-time audio capture from a microphone, +with support for buffering and callback-based processing of audio data. +""" + +from typing import Callable + +import numpy as np +import pyaudio + +from improvisation_lab.infrastructure.audio.audio_processor import \ + AudioProcessor + + +class DirectAudioProcessor(AudioProcessor): + """Handle real-time audio input from microphone. + + This class provides functionality to: + 1. Capture audio from the default microphone + 2. Buffer the incoming audio data + 3. Process the buffered data through a user-provided callback function + + The audio processing is done in chunks, with the chunk size determined by + the buffer_duration parameter. This allows for efficient real-time + processing of audio data, such as pitch detection. + """ + + def __init__( + self, + sample_rate: int, + callback: Callable[[np.ndarray], None] | None = None, + buffer_duration: float = 0.2, + ): + """Initialize MicInput. + + Args: + sample_rate: Audio sample rate in Hz + callback: Optional callback function to process audio data + buffer_duration: Duration of audio buffer in seconds before processing + """ + super().__init__(sample_rate, callback, buffer_duration) + self.audio = None + self._stream = None + + def _audio_callback( + self, in_data: bytes, frame_count: int, time_info: dict, status: int + ) -> tuple[bytes, int]: + """Process incoming audio data. + + This callback is automatically called by PyAudio + when new audio data is available. + The audio data is converted to a numpy array and: + 1. Stored in the internal buffer + 2. Passed to the user-provided callback function if one exists + + Note: + This method follows PyAudio's callback function specification. + It must accept four arguments (in_data, frame_count, time_info, status) + and return a tuple of (bytes, status_flag). + These arguments are automatically provided by PyAudio + when calling this callback. + + Args: + in_data: Raw audio input data as bytes + frame_count: Number of frames in the input + time_info: Dictionary with timing information + status: Stream status flag + + Returns: + Tuple of (input_data, pyaudio.paContinue) + """ + # Convert bytes to numpy array (float32 format) + audio_data = np.frombuffer(in_data, dtype=np.float32) + self._append_to_buffer(audio_data) + self._process_buffer() + return (in_data, pyaudio.paContinue) + + def start_recording(self): + """Start recording from microphone.""" + if self.is_recording: + raise RuntimeError("Recording is already in progress") + + self.audio = pyaudio.PyAudio() + self._stream = self.audio.open( + format=pyaudio.paFloat32, + channels=1, + rate=self.sample_rate, + input=True, + stream_callback=self._audio_callback, + ) + self.is_recording = True + + def stop_recording(self): + """Stop recording from microphone.""" + if not self.is_recording: + raise RuntimeError("Recording is not in progress") + + self._stream.stop_stream() + self._stream.close() + self.audio.terminate() + self.is_recording = False + self._stream = None + self.audio = None diff --git a/improvisation_lab/infrastructure/audio/web_processor.py b/improvisation_lab/infrastructure/audio/web_processor.py new file mode 100644 index 0000000000000000000000000000000000000000..8006f83113b6c4754e04e8bc3e8da6ec15117a1a --- /dev/null +++ b/improvisation_lab/infrastructure/audio/web_processor.py @@ -0,0 +1,112 @@ +"""Module for handling audio input through Gradio interface.""" + +from typing import Callable + +import numpy as np +from scipy import signal + +from improvisation_lab.infrastructure.audio.audio_processor import \ + AudioProcessor + + +class WebAudioProcessor(AudioProcessor): + """Handle audio input from Gradio interface.""" + + def __init__( + self, + sample_rate: int, + callback: Callable[[np.ndarray], None] | None = None, + buffer_duration: float = 0.2, + ): + """Initialize GradioAudioInput. + + Args: + sample_rate: Audio sample rate in Hz + callback: Optional callback function to process audio data + buffer_duration: Duration of audio buffer in seconds + """ + super().__init__(sample_rate, callback, buffer_duration) + + def _resample_audio( + self, audio_data: np.ndarray, original_sr: int, target_sr: int + ) -> np.ndarray: + """Resample audio data to target sample rate. + + In the case of Gradio, + the sample rate of the audio data may not match the target sample rate. + + Args: + audio_data: numpy array of audio samples + original_sr: Original sample rate in Hz + target_sr: Target sample rate in Hz + + Returns: + Resampled audio data with target sample rate + """ + number_of_samples = round(len(audio_data) * float(target_sr) / original_sr) + resampled_data = signal.resample(audio_data, number_of_samples) + return resampled_data + + def _normalize_audio(self, audio_data: np.ndarray) -> np.ndarray: + """Normalize audio data to range [-1, 1] by dividing by maximum absolute value. + + Args: + audio_data: numpy array of audio samples + + Returns: + Normalized audio data with values between -1 and 1 + """ + if len(audio_data) == 0: + return audio_data + max_abs = np.max(np.abs(audio_data)) + return audio_data if max_abs == 0 else audio_data / max_abs + + def _remove_low_amplitude_noise(self, audio_data: np.ndarray) -> np.ndarray: + """Remove low amplitude noise from audio data. + + Applies a threshold to remove low amplitude signals that are likely noise. + + Args: + audio_data: Audio data as numpy array + + Returns: + Audio data with low amplitude noise removed + """ + # [TODO] Set appropriate threshold + threshold = 20.0 + audio_data[np.abs(audio_data) < threshold] = 0 + return audio_data + + def process_audio(self, audio_input: tuple[int, np.ndarray]) -> None: + """Process incoming audio data from Gradio. + + Args: + audio_input: Tuple of (sample_rate, audio_data) + where audio_data is a (samples, channels) array + """ + if not self.is_recording: + return + + input_sample_rate, audio_data = audio_input + if input_sample_rate != self.sample_rate: + audio_data = self._resample_audio( + audio_data, input_sample_rate, self.sample_rate + ) + audio_data = self._remove_low_amplitude_noise(audio_data) + audio_data = self._normalize_audio(audio_data) + + self._append_to_buffer(audio_data) + self._process_buffer() + + def start_recording(self): + """Start accepting audio input from Gradio.""" + if self.is_recording: + raise RuntimeError("Recording is already in progress") + self.is_recording = True + + def stop_recording(self): + """Stop accepting audio input from Gradio.""" + if not self.is_recording: + raise RuntimeError("Recording is not in progress") + self.is_recording = False + self._buffer = np.array([], dtype=np.float32) diff --git a/improvisation_lab/presentation/__init__.py b/improvisation_lab/presentation/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f66942a4be9c18905be6ae0c5e468a94302c2f28 --- /dev/null +++ b/improvisation_lab/presentation/__init__.py @@ -0,0 +1 @@ +"""Presentation layer for the application.""" diff --git a/improvisation_lab/presentation/melody_practice/__init__.py b/improvisation_lab/presentation/melody_practice/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ba09645d02ed943f50c31c94affa609902185e74 --- /dev/null +++ b/improvisation_lab/presentation/melody_practice/__init__.py @@ -0,0 +1,14 @@ +"""Presentation layer for melody practice. + +This package contains modules for handling the user interface +and text management for melody practice applications. +""" + +from improvisation_lab.presentation.melody_practice.console_melody_view import \ + ConsoleMelodyView +from improvisation_lab.presentation.melody_practice.view_text_manager import \ + ViewTextManager +from improvisation_lab.presentation.melody_practice.web_melody_view import \ + WebMelodyView + +__all__ = ["WebMelodyView", "ViewTextManager", "ConsoleMelodyView"] diff --git a/improvisation_lab/presentation/melody_practice/console_melody_view.py b/improvisation_lab/presentation/melody_practice/console_melody_view.py new file mode 100644 index 0000000000000000000000000000000000000000..6f6895d4025159c370086ef150e4aba6696543e6 --- /dev/null +++ b/improvisation_lab/presentation/melody_practice/console_melody_view.py @@ -0,0 +1,56 @@ +"""Console-based melody practice view. + +This module provides a console interface for visualizing +and interacting with melody practice sessions. +""" + +from typing import List + +from improvisation_lab.domain.composition import PhraseData +from improvisation_lab.presentation.melody_practice.view_text_manager import \ + ViewTextManager +from improvisation_lab.service.melody_practice_service import PitchResult + + +class ConsoleMelodyView: + """Console-based implementation of melody visualization.""" + + def __init__(self, text_manager: ViewTextManager, song_name: str): + """Initialize the console view with a text manager and song name. + + Args: + text_manager: Text manager for updating and displaying text. + song_name: Name of the song to be practiced. + """ + self.text_manager = text_manager + self.song_name = song_name + + def launch(self): + """Run the console interface.""" + print("\n" + f"Generating melody for {self.song_name}:") + print("Sing each note for 1 second!") + + def display_phrase_info(self, phrase_number: int, phrases_data: List[PhraseData]): + """Display phrase information in console. + + Args: + phrase_number: Number of the phrase. + phrases_data: List of phrase data. + """ + self.text_manager.update_phrase_text(phrase_number, phrases_data) + print("\n" + "-" * 50) + print("\n" + self.text_manager.phrase_text + "\n") + + def display_pitch_result(self, pitch_result: PitchResult): + """Display note status in console. + + Args: + pitch_result: The result of the pitch detection. + """ + self.text_manager.update_pitch_result(pitch_result) + print(f"{self.text_manager.result_text:<80}", end="\r", flush=True) + + def display_practice_end(self): + """Display practice end message in console.""" + self.text_manager.terminate_text() + print(self.text_manager.phrase_text) diff --git a/improvisation_lab/presentation/melody_practice/view_text_manager.py b/improvisation_lab/presentation/melody_practice/view_text_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..2230a784ff87f6132218785d4aabbdf75f38d81b --- /dev/null +++ b/improvisation_lab/presentation/melody_practice/view_text_manager.py @@ -0,0 +1,70 @@ +"""Text management for melody practice. + +This class manages the text displayed +in both the web and console versions of the melody practice. +""" + +from typing import List + +from improvisation_lab.domain.composition import PhraseData +from improvisation_lab.service.melody_practice_service import PitchResult + + +class ViewTextManager: + """Displayed text management for melody practice.""" + + def __init__(self): + """Initialize the text manager.""" + self.initialize_text() + + def initialize_text(self): + """Initialize the text.""" + self.phrase_text = "No phrase data" + self.result_text = "Ready to start... (waiting for audio)" + + def terminate_text(self): + """Terminate the text.""" + self.phrase_text = "Session Stopped" + self.result_text = "Practice ended" + + def set_waiting_for_audio(self): + """Set the text to waiting for audio.""" + self.result_text = "Waiting for audio..." + + def update_pitch_result(self, pitch_result: PitchResult): + """Update the pitch result text. + + Args: + pitch_result: The result of the pitch detection. + """ + result_text = ( + f"Target: {pitch_result.target_note} | " + f"Your note: {pitch_result.current_base_note or '---'}" + ) + if pitch_result.current_base_note is not None: + result_text += f" | Remaining: {pitch_result.remaining_time:.1f}s" + self.result_text = result_text + + def update_phrase_text(self, current_phrase_idx: int, phrases: List[PhraseData]): + """Update the phrase text. + + Args: + current_phrase_idx: The index of the current phrase. + phrases: The list of phrases. + """ + if not phrases: + self.phrase_text = "No phrase data" + return self.phrase_text + + current_phrase = phrases[current_phrase_idx] + self.phrase_text = ( + f"Phrase {current_phrase_idx + 1}: " + f"{current_phrase.chord_name}\n" + f"{' -> '.join(current_phrase.notes)}" + ) + + if current_phrase_idx < len(phrases) - 1: + next_phrase = phrases[current_phrase_idx + 1] + self.phrase_text += ( + f"\nNext: {next_phrase.chord_name} ({next_phrase.notes[0]})" + ) diff --git a/improvisation_lab/presentation/melody_practice/web_melody_view.py b/improvisation_lab/presentation/melody_practice/web_melody_view.py new file mode 100644 index 0000000000000000000000000000000000000000..deb898bb1a5b7abcf87d407d6ceb11cc0795b4c8 --- /dev/null +++ b/improvisation_lab/presentation/melody_practice/web_melody_view.py @@ -0,0 +1,99 @@ +"""Web-based melody practice view. + +This module provides a web interface using Gradio for visualizing +and interacting with melody practice sessions. +""" + +from typing import Any, Callable + +import gradio as gr + + +class WebMelodyView: + """Handles the user interface for the melody practice application.""" + + def __init__( + self, + on_generate_melody: Callable[[], tuple[str, str]], + on_end_practice: Callable[[], tuple[str, str]], + on_audio_input: Callable[[Any], tuple[str, str]], + song_name: str, + ): + """Initialize the UI with callback functions. + + Args: + on_generate_melody: Function to call when start button is clicked + on_end_practice: Function to call when stop button is clicked + on_audio_input: Function to process audio input + song_name: Name of the song to be practiced + """ + self.on_generate_melody = on_generate_melody + self.on_end_practice = on_end_practice + self.on_audio_input = on_audio_input + self.song_name = song_name + + def _build_interface(self) -> gr.Blocks: + """Create and configure the Gradio interface. + + Returns: + gr.Blocks: The Gradio interface. + """ + with gr.Blocks() as app: + self._add_header() + self.generate_melody_button = gr.Button("Generate Melody") + with gr.Row(): + self.phrase_info_box = gr.Textbox(label="Phrase Information", value="") + self.pitch_result_box = gr.Textbox(label="Pitch Result", value="") + self._add_audio_input() + self.end_practice_button = gr.Button("End Practice") + + self._add_buttons_callbacks() + + return app + + def _add_header(self): + """Create the header section of the UI.""" + gr.Markdown(f"# {self.song_name} Melody Practice\nSing each note for 1 second!") + + def _add_buttons_callbacks(self): + """Create the control buttons section.""" + # Connect button callbacks + self.generate_melody_button.click( + fn=self.on_generate_melody, + outputs=[self.phrase_info_box, self.pitch_result_box], + ) + + self.end_practice_button.click( + fn=self.on_end_practice, + outputs=[self.phrase_info_box, self.pitch_result_box], + ) + + def _add_audio_input(self): + """Create the audio input section.""" + audio_input = gr.Audio( + label="Audio Input", + sources=["microphone"], + streaming=True, + type="numpy", + show_label=True, + ) + + # Attention: have to specify inputs explicitly, + # otherwise the callback function is not called + audio_input.stream( + fn=self.on_audio_input, + inputs=audio_input, + outputs=[self.phrase_info_box, self.pitch_result_box], + show_progress=False, + stream_every=0.1, + ) + + def launch(self, **kwargs): + """Launch the Gradio application. + + Args: + **kwargs: Additional keyword arguments for the launch method. + """ + app = self._build_interface() + app.queue() + app.launch(**kwargs) diff --git a/improvisation_lab/service/__init__.py b/improvisation_lab/service/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3d38d2d9f28fd550d769d519c50bac21f28a5169 --- /dev/null +++ b/improvisation_lab/service/__init__.py @@ -0,0 +1,6 @@ +"""Service layer for the Improvisation Lab.""" + +from improvisation_lab.service.melody_practice_service import \ + MelodyPracticeService + +__all__ = ["MelodyPracticeService"] diff --git a/improvisation_lab/service/melody_practice_service.py b/improvisation_lab/service/melody_practice_service.py new file mode 100644 index 0000000000000000000000000000000000000000..cf8ae156915defd9f0f225d3a17c975bb0fa4d50 --- /dev/null +++ b/improvisation_lab/service/melody_practice_service.py @@ -0,0 +1,128 @@ +"""Service for practicing melodies.""" + +import time +from dataclasses import dataclass + +import numpy as np + +from improvisation_lab.config import Config +from improvisation_lab.domain.analysis import PitchDetector +from improvisation_lab.domain.composition import MelodyComposer, PhraseData +from improvisation_lab.domain.music_theory import Notes + + +@dataclass +class PitchResult: + """Result of pitch detection.""" + + target_note: str + current_base_note: str | None + is_correct: bool + remaining_time: float + + +class MelodyPracticeService: + """Service for generating and processing melodies.""" + + def __init__(self, config: Config): + """Initialize MelodyPracticeService with configuration.""" + self.config = config + self.melody_composer = MelodyComposer() + self.pitch_detector = PitchDetector(config.audio.pitch_detector) + + self.correct_pitch_start_time: float | None = None + + def generate_melody(self) -> list[PhraseData]: + """Generate a melody based on the configured chord progression. + + Returns: + List of PhraseData instances representing the generated melody. + """ + selected_progression = self.config.chord_progressions[self.config.selected_song] + return self.melody_composer.generate_phrases(selected_progression) + + def process_audio(self, audio_data: np.ndarray, target_note: str) -> PitchResult: + """Process audio data to detect pitch and provide feedback. + + Args: + audio_data: Audio data as a numpy array. + target_note: The target note to display. + Returns: + PitchResult containing the target note, detected note, correctness, + and remaining time. + """ + frequency = self.pitch_detector.detect_pitch(audio_data) + + if frequency <= 0: # if no voice detected, reset the correct pitch start time + return self._create_no_voice_result(target_note) + + note_name = Notes.convert_frequency_to_base_note(frequency) + if note_name != target_note: + return self._create_incorrect_pitch_result(target_note, note_name) + + return self._create_correct_pitch_result(target_note, note_name) + + def _create_no_voice_result(self, target_note: str) -> PitchResult: + """Create result for no voice detected case. + + Args: + target_note: The target note to display. + + Returns: + PitchResult for no voice detected case. + """ + self.correct_pitch_start_time = None + return PitchResult( + target_note=target_note, + current_base_note=None, + is_correct=False, + remaining_time=self.config.audio.note_duration, + ) + + def _create_incorrect_pitch_result( + self, target_note: str, detected_note: str + ) -> PitchResult: + """Create result for incorrect pitch case, reset the correct pitch start time. + + Args: + target_note: The target note to display. + detected_note: The detected note. + + Returns: + PitchResult for incorrect pitch case. + """ + self.correct_pitch_start_time = None + return PitchResult( + target_note=target_note, + current_base_note=detected_note, + is_correct=False, + remaining_time=self.config.audio.note_duration, + ) + + def _create_correct_pitch_result( + self, target_note: str, detected_note: str + ) -> PitchResult: + """Create result for correct pitch case. + + Args: + target_note: The target note to display. + detected_note: The detected note. + + Returns: + PitchResult for correct pitch case. + """ + current_time = time.time() + # Note is completed if the correct pitch is sustained for the duration of a note + if self.correct_pitch_start_time is None: + self.correct_pitch_start_time = current_time + remaining_time = self.config.audio.note_duration + else: + elapsed_time = current_time - self.correct_pitch_start_time + remaining_time = max(0, self.config.audio.note_duration - elapsed_time) + + return PitchResult( + target_note=target_note, + current_base_note=detected_note, + is_correct=True, + remaining_time=remaining_time, + ) diff --git a/main.py b/main.py new file mode 100644 index 0000000000000000000000000000000000000000..df717bbdd2b3d0140fd082c60f2dcf3a2afc09c1 --- /dev/null +++ b/main.py @@ -0,0 +1,33 @@ +"""Main application module for melody practice. + +This module initializes and launches the melody practice application +using either a web or console interface. +""" + +import argparse + +from improvisation_lab.application.melody_practice import \ + MelodyPracticeAppFactory +from improvisation_lab.config import Config +from improvisation_lab.service import MelodyPracticeService + + +def main(): + """Run the application.""" + parser = argparse.ArgumentParser(description="Run the melody practice application") + parser.add_argument( + "--app_type", + choices=["web", "console"], + default="web", + help="Type of application to run (web or console)", + ) + args = parser.parse_args() + + config = Config() + service = MelodyPracticeService(config) + app = MelodyPracticeAppFactory.create_app(args.app_type, service, config) + app.launch() + + +if __name__ == "__main__": + main() diff --git a/packages.txt b/packages.txt new file mode 100644 index 0000000000000000000000000000000000000000..aaf59da0e781a7ca2fa58094096ebf819698bc72 --- /dev/null +++ b/packages.txt @@ -0,0 +1 @@ +portaudio19-dev diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000000000000000000000000000000000000..9498203b48de55c41d46f415474927447232d759 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,2356 @@ +[[package]] +name = "aiofiles" +version = "23.2.1" +description = "File support for asyncio." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "anyio" +version = "4.6.2.post1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.9" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "audioop-lts" +version = "0.2.1" +description = "LTS Port of Python audioop" +category = "main" +optional = false +python-versions = ">=3.13" + +[[package]] +name = "black" +version = "24.10.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.9" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.7.0" + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" + +[[package]] +name = "einops" +version = "0.8.0" +description = "A new flavour of deep learning operations" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "fastapi" +version = "0.115.6" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.42.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "ffmpy" +version = "0.4.0" +description = "A simple Python wrapper for FFmpeg" +category = "main" +optional = false +python-versions = "<4.0.0,>=3.8.1" + +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] + +[[package]] +name = "flake8" +version = "7.0.0" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.8.1" + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.2.0,<3.3.0" + +[[package]] +name = "fsspec" +version = "2024.10.0" +description = "File-system specification" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +dev = ["pre-commit", "ruff"] +doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] +test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] +test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] +tqdm = ["tqdm"] + +[[package]] +name = "gradio" +version = "5.7.1" +description = "Python library for easily interacting with trained machine learning models" +category = "main" +optional = false +python-versions = ">=3.10" + +[package.dependencies] +aiofiles = ">=22.0,<24.0" +anyio = ">=3.0,<5.0" +audioop-lts = {version = "<1.0", markers = "python_version >= \"3.13\""} +fastapi = ">=0.115.2,<1.0" +ffmpy = "*" +gradio-client = "1.5.0" +httpx = ">=0.24.1" +huggingface-hub = ">=0.25.1" +jinja2 = "<4.0" +markupsafe = ">=2.0,<3.0" +numpy = ">=1.0,<3.0" +orjson = ">=3.0,<4.0" +packaging = "*" +pandas = ">=1.0,<3.0" +pillow = ">=8.0,<12.0" +pydantic = ">=2.0" +pydub = "*" +python-multipart = "0.0.12" +pyyaml = ">=5.0,<7.0" +ruff = {version = ">=0.2.2", markers = "sys_platform != \"emscripten\""} +safehttpx = ">=0.1.1,<1.0" +semantic-version = ">=2.0,<3.0" +starlette = {version = ">=0.40.0,<1.0", markers = "sys_platform != \"emscripten\""} +tomlkit = "0.12.0" +typer = {version = ">=0.12,<1.0", markers = "sys_platform != \"emscripten\""} +typing-extensions = ">=4.0,<5.0" +urllib3 = {version = ">=2.0,<3.0", markers = "sys_platform == \"emscripten\""} +uvicorn = {version = ">=0.14.0", markers = "sys_platform != \"emscripten\""} + +[package.extras] +oauth = ["authlib", "itsdangerous"] + +[[package]] +name = "gradio-client" +version = "1.5.0" +description = "Python library for easily interacting with trained machine learning models" +category = "main" +optional = false +python-versions = ">=3.10" + +[package.dependencies] +fsspec = "*" +httpx = ">=0.24.1" +huggingface-hub = ">=0.19.3" +packaging = "*" +typing-extensions = ">=4.0,<5.0" +websockets = ">=10.0,<13.0" + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "httpcore" +version = "1.0.7" +description = "A minimal low-level HTTP client." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.0" +description = "The next generation HTTP client." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = ">=1.0.0,<2.0.0" +idna = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "huggingface-hub" +version = "0.26.3" +description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" +category = "main" +optional = false +python-versions = ">=3.8.0" + +[package.dependencies] +filelock = "*" +fsspec = ">=2023.5.0" +packaging = ">=20.9" +pyyaml = ">=5.1" +requests = "*" +tqdm = ">=4.42.1" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +cli = ["InquirerPy (==0.3.4)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] +hf-transfer = ["hf-transfer (>=0.1.4)"] +inference = ["aiohttp"] +quality = ["libcst (==1.4.0)", "mypy (==1.5.1)", "ruff (>=0.5.0)"] +tensorflow = ["graphviz", "pydot", "tensorflow"] +tensorflow-testing = ["keras (<3.0)", "tensorflow"] +testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] +torch = ["safetensors[torch]", "torch"] +typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.8.0" + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "local-attention" +version = "1.9.15" +description = "Local attention, window with lookback, for language modeling" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +einops = ">=0.8.0" +torch = "*" + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "mpmath" +version = "1.3.0" +description = "Python library for arbitrary-precision floating-point arithmetic" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] +docs = ["sphinx"] +gmpy = ["gmpy2 (>=2.1.0a4)"] +tests = ["pytest (>=4.6)"] + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "networkx" +version = "3.4.2" +description = "Python package for creating and manipulating graphs and networks" +category = "main" +optional = false +python-versions = ">=3.10" + +[package.extras] +default = ["matplotlib (>=3.7)", "numpy (>=1.24)", "pandas (>=2.0)", "scipy (>=1.10,!=1.11.0,!=1.11.1)"] +developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] +doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.15)", "sphinx (>=7.3)", "sphinx-gallery (>=0.16)", "texext (>=0.6.7)"] +example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=1.9)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] +extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] +test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +category = "main" +optional = false +python-versions = ">=3.9" + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.1.3.1" +description = "CUBLAS native runtime libraries" +category = "main" +optional = false +python-versions = ">=3" + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.1.105" +description = "CUDA profiling tools runtime libs." +category = "main" +optional = false +python-versions = ">=3" + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.1.105" +description = "NVRTC native runtime libraries" +category = "main" +optional = false +python-versions = ">=3" + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.1.105" +description = "CUDA Runtime native Libraries" +category = "main" +optional = false +python-versions = ">=3" + +[[package]] +name = "nvidia-cudnn-cu12" +version = "8.9.2.26" +description = "cuDNN runtime libraries" +category = "main" +optional = false +python-versions = ">=3" + +[package.dependencies] +nvidia-cublas-cu12 = "*" + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.0.2.54" +description = "CUFFT native runtime libraries" +category = "main" +optional = false +python-versions = ">=3" + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.2.106" +description = "CURAND native runtime libraries" +category = "main" +optional = false +python-versions = ">=3" + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.4.5.107" +description = "CUDA solver native runtime libraries" +category = "main" +optional = false +python-versions = ">=3" + +[package.dependencies] +nvidia-cublas-cu12 = "*" +nvidia-cusparse-cu12 = "*" +nvidia-nvjitlink-cu12 = "*" + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.1.0.106" +description = "CUSPARSE native runtime libraries" +category = "main" +optional = false +python-versions = ">=3" + +[package.dependencies] +nvidia-nvjitlink-cu12 = "*" + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.19.3" +description = "NVIDIA Collective Communication Library (NCCL) Runtime" +category = "main" +optional = false +python-versions = ">=3" + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.4.127" +description = "Nvidia JIT LTO Library" +category = "main" +optional = false +python-versions = ">=3" + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.1.105" +description = "NVIDIA Tools Extension" +category = "main" +optional = false +python-versions = ">=3" + +[[package]] +name = "orjson" +version = "3.10.12" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "pandas" +version = "2.2.3" +description = "Powerful data structures for data analysis, time series, and statistics" +category = "main" +optional = false +python-versions = ">=3.9" + +[package.dependencies] +numpy = [ + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "pillow" +version = "11.0.0" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.9" + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pyaudio" +version = "0.2.14" +description = "Cross-platform audio I/O with PortAudio" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +test = ["numpy"] + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "pydantic" +version = "2.10.3" +description = "Data validation using Python type hints" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.27.1" +typing-extensions = ">=4.12.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.27.1" +description = "Core functionality for Pydantic validation and serialization" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydocstyle" +version = "6.3.0" +description = "Python docstring style checker" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +snowballstemmer = ">=2.2.0" + +[package.extras] +toml = ["tomli (>=1.2.3)"] + +[[package]] +name = "pydub" +version = "0.25.1" +description = "Manipulate audio with an simple and easy high level interface" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyproject-flake8" +version = "7.0.0" +description = "pyproject-flake8 (`pflake8`), a monkey patching wrapper to connect flake8 with pyproject.toml configuration" +category = "dev" +optional = false +python-versions = ">=3.8.1" + +[package.dependencies] +flake8 = "7.0.0" + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-multipart" +version = "0.0.12" +description = "A streaming multipart parser for Python" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "pytz" +version = "2024.2" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.8.0" + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "ruff" +version = "0.8.1" +description = "An extremely fast Python linter and code formatter, written in Rust." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "safehttpx" +version = "0.1.6" +description = "A small Python library created to help developers protect their applications from Server Side Request Forgery (SSRF) attacks." +category = "main" +optional = false +python-versions = ">3.9" + +[package.dependencies] +httpx = "*" + +[package.extras] +dev = ["pytest"] + +[[package]] +name = "scipy" +version = "1.14.1" +description = "Fundamental algorithms for scientific computing in Python" +category = "main" +optional = false +python-versions = ">=3.10" + +[package.dependencies] +numpy = ">=1.23.5,<2.3" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<=7.3.7)", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "semantic-version" +version = "2.10.0" +description = "A library implementing the 'SemVer' scheme." +category = "main" +optional = false +python-versions = ">=2.7" + +[package.extras] +dev = ["Django (>=1.11)", "check-manifest", "colorama (<=0.4.1)", "coverage", "flake8", "nose2", "readme-renderer (<25.0)", "tox", "wheel", "zest.releaser[recommended]"] +doc = ["Sphinx", "sphinx-rtd-theme"] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "starlette" +version = "0.41.3" +description = "The little ASGI library that shines." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[[package]] +name = "sympy" +version = "1.13.1" +description = "Computer algebra system (CAS) in Python" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +mpmath = ">=1.1.0,<1.4" + +[package.extras] +dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] + +[[package]] +name = "tomlkit" +version = "0.12.0" +description = "Style preserving TOML library" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "torch" +version = "2.2.2" +description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" +category = "main" +optional = false +python-versions = ">=3.8.0" + +[package.dependencies] +filelock = "*" +fsspec = "*" +jinja2 = "*" +networkx = "*" +nvidia-cublas-cu12 = {version = "12.1.3.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-cupti-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-nvrtc-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-runtime-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cudnn-cu12 = {version = "8.9.2.26", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cufft-cu12 = {version = "11.0.2.54", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-curand-cu12 = {version = "10.3.2.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cusolver-cu12 = {version = "11.4.5.107", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cusparse-cu12 = {version = "12.1.0.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-nccl-cu12 = {version = "2.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-nvtx-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +sympy = "*" +triton = {version = "2.2.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.12\""} +typing-extensions = ">=4.8.0" + +[package.extras] +opt-einsum = ["opt-einsum (>=3.3)"] +optree = ["optree (>=0.9.1)"] + +[[package]] +name = "torchaudio" +version = "2.2.2" +description = "An audio package for PyTorch" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +torch = "2.2.2" + +[[package]] +name = "torchfcpe" +version = "0.0.4" +description = "The official Pytorch implementation of Fast Context-based Pitch Estimation (FCPE)" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +einops = "*" +local-attention = "*" +numpy = "*" +torch = "*" +torchaudio = "*" + +[[package]] +name = "tqdm" +version = "4.67.1" +description = "Fast, Extensible Progress Meter" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "triton" +version = "2.2.0" +description = "A language and compiler for custom Deep Learning operations" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +filelock = "*" + +[package.extras] +build = ["cmake (>=3.20)", "lit"] +tests = ["autopep8", "flake8", "isort", "numpy", "pytest", "scipy (>=1.7.1)", "torch"] +tutorials = ["matplotlib", "pandas", "tabulate", "torch"] + +[[package]] +name = "typer" +version = "0.15.1" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20240917" +description = "Typing stubs for PyYAML" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +category = "main" +optional = false +python-versions = ">=2" + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uvicorn" +version = "0.32.1" +description = "The lightning-fast ASGI server." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "websockets" +version = "12.0" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +category = "main" +optional = false +python-versions = ">=3.8" + +[metadata] +lock-version = "1.1" +python-versions = "^3.11" +content-hash = "b9edd415dc842818ea23160f70cba66c34fa2f4eb48c8635945689c06c2c5150" + +[metadata.files] +aiofiles = [ + {file = "aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107"}, + {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"}, +] +annotated-types = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] +anyio = [ + {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, + {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, +] +audioop-lts = [ + {file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a"}, + {file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e"}, + {file = "audioop_lts-0.2.1-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:4a8dd6a81770f6ecf019c4b6d659e000dc26571b273953cef7cd1d5ce2ff3ae6"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cd3c0b6f2ca25c7d2b1c3adeecbe23e65689839ba73331ebc7d893fcda7ffe"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff3f97b3372c97782e9c6d3d7fdbe83bce8f70de719605bd7ee1839cd1ab360a"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a351af79edefc2a1bd2234bfd8b339935f389209943043913a919df4b0f13300"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aeb6f96f7f6da80354330470b9134d81b4cf544cdd1c549f2f45fe964d28059"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c589f06407e8340e81962575fcffbba1e92671879a221186c3d4662de9fe804e"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fbae5d6925d7c26e712f0beda5ed69ebb40e14212c185d129b8dfbfcc335eb48"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_i686.whl", hash = "sha256:d2d5434717f33117f29b5691fbdf142d36573d751716249a288fbb96ba26a281"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f626a01c0a186b08f7ff61431c01c055961ee28769591efa8800beadd27a2959"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:05da64e73837f88ee5c6217d732d2584cf638003ac72df124740460531e95e47"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:56b7a0a4dba8e353436f31a932f3045d108a67b5943b30f85a5563f4d8488d77"}, + {file = "audioop_lts-0.2.1-cp313-abi3-win32.whl", hash = "sha256:6e899eb8874dc2413b11926b5fb3857ec0ab55222840e38016a6ba2ea9b7d5e3"}, + {file = "audioop_lts-0.2.1-cp313-abi3-win_amd64.whl", hash = "sha256:64562c5c771fb0a8b6262829b9b4f37a7b886c01b4d3ecdbae1d629717db08b4"}, + {file = "audioop_lts-0.2.1-cp313-abi3-win_arm64.whl", hash = "sha256:c45317debeb64002e980077642afbd977773a25fa3dfd7ed0c84dccfc1fafcb0"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3827e3fce6fee4d69d96a3d00cd2ab07f3c0d844cb1e44e26f719b34a5b15455"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:161249db9343b3c9780ca92c0be0d1ccbfecdbccac6844f3d0d44b9c4a00a17f"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5b7b4ff9de7a44e0ad2618afdc2ac920b91f4a6d3509520ee65339d4acde5abf"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72e37f416adb43b0ced93419de0122b42753ee74e87070777b53c5d2241e7fab"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534ce808e6bab6adb65548723c8cbe189a3379245db89b9d555c4210b4aaa9b6"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2de9b6fb8b1cf9f03990b299a9112bfdf8b86b6987003ca9e8a6c4f56d39543"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f24865991b5ed4b038add5edbf424639d1358144f4e2a3e7a84bc6ba23e35074"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bdb3b7912ccd57ea53197943f1bbc67262dcf29802c4a6df79ec1c715d45a78"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:120678b208cca1158f0a12d667af592e067f7a50df9adc4dc8f6ad8d065a93fb"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:54cd4520fc830b23c7d223693ed3e1b4d464997dd3abc7c15dce9a1f9bd76ab2"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:d6bd20c7a10abcb0fb3d8aaa7508c0bf3d40dfad7515c572014da4b979d3310a"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f0ed1ad9bd862539ea875fb339ecb18fcc4148f8d9908f4502df28f94d23491a"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e1af3ff32b8c38a7d900382646e91f2fc515fd19dea37e9392275a5cbfdbff63"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:f51bb55122a89f7a0817d7ac2319744b4640b5b446c4c3efcea5764ea99ae509"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f0f2f336aa2aee2bce0b0dcc32bbba9178995454c7b979cf6ce086a8801e14c7"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0"}, + {file = "audioop_lts-0.2.1.tar.gz", hash = "sha256:e81268da0baa880431b68b1308ab7257eb33f356e57a5f9b1f915dfb13dd1387"}, +] +black = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] +certifi = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] +charset-normalizer = [ + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] +click = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] +colorama = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +einops = [ + {file = "einops-0.8.0-py3-none-any.whl", hash = "sha256:9572fb63046264a862693b0a87088af3bdc8c068fde03de63453cbbde245465f"}, + {file = "einops-0.8.0.tar.gz", hash = "sha256:63486517fed345712a8385c100cb279108d9d47e6ae59099b07657e983deae85"}, +] +fastapi = [ + {file = "fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"}, + {file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"}, +] +ffmpy = [ + {file = "ffmpy-0.4.0-py3-none-any.whl", hash = "sha256:39c0f20c5b465e7f8d29a5191f3a7d7675a8c546d9d985de8921151cd9b59e14"}, + {file = "ffmpy-0.4.0.tar.gz", hash = "sha256:131b57794e802ad555f579007497f7a3d0cab0583d37496c685b8acae4837b1d"}, +] +filelock = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] +flake8 = [ + {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, + {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, +] +fsspec = [ + {file = "fsspec-2024.10.0-py3-none-any.whl", hash = "sha256:03b9a6785766a4de40368b88906366755e2819e758b83705c88cd7cb5fe81871"}, + {file = "fsspec-2024.10.0.tar.gz", hash = "sha256:eda2d8a4116d4f2429db8550f2457da57279247dd930bb12f821b58391359493"}, +] +gradio = [ + {file = "gradio-5.7.1-py3-none-any.whl", hash = "sha256:6b6c788445ea676b9589e57c62b2ebdad67b089585c56744c56012d0915d1001"}, +] +gradio-client = [ + {file = "gradio_client-1.5.0-py3-none-any.whl", hash = "sha256:4412edd555461771425355a8791ec4c437913dee515cb415444cb9a87d00bd9c"}, + {file = "gradio_client-1.5.0.tar.gz", hash = "sha256:21d6a667ad96f38327a7cc2ea5425700175a9bb679ce40247ee30239f60c4046"}, +] +h11 = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] +httpcore = [ + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, +] +httpx = [ + {file = "httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc"}, + {file = "httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0"}, +] +huggingface-hub = [ + {file = "huggingface_hub-0.26.3-py3-none-any.whl", hash = "sha256:e66aa99e569c2d5419240a9e553ad07245a5b1300350bfbc5a4945cf7432991b"}, + {file = "huggingface_hub-0.26.3.tar.gz", hash = "sha256:90e1fe62ffc26757a073aaad618422b899ccf9447c2bba8c902a90bef5b42e1d"}, +] +idna = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] +iniconfig = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] +isort = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] +jinja2 = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] +local-attention = [ + {file = "local_attention-1.9.15-py3-none-any.whl", hash = "sha256:d3055bdb87c1a8a68c6795b849b453ea9c2adb0dc426d72f71bd53f84be6e0db"}, + {file = "local_attention-1.9.15.tar.gz", hash = "sha256:8c322d9141af2860624917d9d60b502f1a1654ac3cf1052c6ff03f5ab785d5e9"}, +] +markdown-it-py = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] +markupsafe = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] +mccabe = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] +mdurl = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] +mpmath = [ + {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, + {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, +] +mypy = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] +mypy-extensions = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] +networkx = [ + {file = "networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}, + {file = "networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}, +] +numpy = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] +nvidia-cublas-cu12 = [ + {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728"}, + {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-win_amd64.whl", hash = "sha256:2b964d60e8cf11b5e1073d179d85fa340c120e99b3067558f3cf98dd69d02906"}, +] +nvidia-cuda-cupti-cu12 = [ + {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e"}, + {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:bea8236d13a0ac7190bd2919c3e8e6ce1e402104276e6f9694479e48bb0eb2a4"}, +] +nvidia-cuda-nvrtc-cu12 = [ + {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2"}, + {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:0a98a522d9ff138b96c010a65e145dc1b4850e9ecb75a0172371793752fd46ed"}, +] +nvidia-cuda-runtime-cu12 = [ + {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40"}, + {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:dfb46ef84d73fababab44cf03e3b83f80700d27ca300e537f85f636fac474344"}, +] +nvidia-cudnn-cu12 = [ + {file = "nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9"}, +] +nvidia-cufft-cu12 = [ + {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56"}, + {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-win_amd64.whl", hash = "sha256:d9ac353f78ff89951da4af698f80870b1534ed69993f10a4cf1d96f21357e253"}, +] +nvidia-curand-cu12 = [ + {file = "nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0"}, + {file = "nvidia_curand_cu12-10.3.2.106-py3-none-win_amd64.whl", hash = "sha256:75b6b0c574c0037839121317e17fd01f8a69fd2ef8e25853d826fec30bdba74a"}, +] +nvidia-cusolver-cu12 = [ + {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd"}, + {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-win_amd64.whl", hash = "sha256:74e0c3a24c78612192a74fcd90dd117f1cf21dea4822e66d89e8ea80e3cd2da5"}, +] +nvidia-cusparse-cu12 = [ + {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c"}, + {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-win_amd64.whl", hash = "sha256:b798237e81b9719373e8fae8d4f091b70a0cf09d9d85c95a557e11df2d8e9a5a"}, +] +nvidia-nccl-cu12 = [ + {file = "nvidia_nccl_cu12-2.19.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:a9734707a2c96443331c1e48c717024aa6678a0e2a4cb66b2c364d18cee6b48d"}, +] +nvidia-nvjitlink-cu12 = [ + {file = "nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4abe7fef64914ccfa909bc2ba39739670ecc9e820c83ccc7a6ed414122599b83"}, + {file = "nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57"}, + {file = "nvidia_nvjitlink_cu12-12.4.127-py3-none-win_amd64.whl", hash = "sha256:fd9020c501d27d135f983c6d3e244b197a7ccad769e34df53a42e276b0e25fa1"}, +] +nvidia-nvtx-cu12 = [ + {file = "nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5"}, + {file = "nvidia_nvtx_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:65f4d98982b31b60026e0e6de73fbdfc09d08a96f4656dd3665ca616a11e1e82"}, +] +orjson = [ + {file = "orjson-3.10.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ece01a7ec71d9940cc654c482907a6b65df27251255097629d0dea781f255c6d"}, + {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34ec9aebc04f11f4b978dd6caf697a2df2dd9b47d35aa4cc606cabcb9df69d7"}, + {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd6ec8658da3480939c79b9e9e27e0db31dffcd4ba69c334e98c9976ac29140e"}, + {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17e6baf4cf01534c9de8a16c0c611f3d94925d1701bf5f4aff17003677d8ced"}, + {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6402ebb74a14ef96f94a868569f5dccf70d791de49feb73180eb3c6fda2ade56"}, + {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0000758ae7c7853e0a4a6063f534c61656ebff644391e1f81698c1b2d2fc8cd2"}, + {file = "orjson-3.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:888442dcee99fd1e5bd37a4abb94930915ca6af4db50e23e746cdf4d1e63db13"}, + {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c1f7a3ce79246aa0e92f5458d86c54f257fb5dfdc14a192651ba7ec2c00f8a05"}, + {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:802a3935f45605c66fb4a586488a38af63cb37aaad1c1d94c982c40dcc452e85"}, + {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1da1ef0113a2be19bb6c557fb0ec2d79c92ebd2fed4cfb1b26bab93f021fb885"}, + {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a3273e99f367f137d5b3fecb5e9f45bcdbfac2a8b2f32fbc72129bbd48789c2"}, + {file = "orjson-3.10.12-cp310-none-win32.whl", hash = "sha256:475661bf249fd7907d9b0a2a2421b4e684355a77ceef85b8352439a9163418c3"}, + {file = "orjson-3.10.12-cp310-none-win_amd64.whl", hash = "sha256:87251dc1fb2b9e5ab91ce65d8f4caf21910d99ba8fb24b49fd0c118b2362d509"}, + {file = "orjson-3.10.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a734c62efa42e7df94926d70fe7d37621c783dea9f707a98cdea796964d4cf74"}, + {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:750f8b27259d3409eda8350c2919a58b0cfcd2054ddc1bd317a643afc646ef23"}, + {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb52c22bfffe2857e7aa13b4622afd0dd9d16ea7cc65fd2bf318d3223b1b6252"}, + {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:440d9a337ac8c199ff8251e100c62e9488924c92852362cd27af0e67308c16ef"}, + {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e15c06491c69997dfa067369baab3bf094ecb74be9912bdc4339972323f252"}, + {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:362d204ad4b0b8724cf370d0cd917bb2dc913c394030da748a3bb632445ce7c4"}, + {file = "orjson-3.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b57cbb4031153db37b41622eac67329c7810e5f480fda4cfd30542186f006ae"}, + {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:165c89b53ef03ce0d7c59ca5c82fa65fe13ddf52eeb22e859e58c237d4e33b9b"}, + {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5dee91b8dfd54557c1a1596eb90bcd47dbcd26b0baaed919e6861f076583e9da"}, + {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a4e1cfb72de6f905bdff061172adfb3caf7a4578ebf481d8f0530879476c07"}, + {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:038d42c7bc0606443459b8fe2d1f121db474c49067d8d14c6a075bbea8bf14dd"}, + {file = "orjson-3.10.12-cp311-none-win32.whl", hash = "sha256:03b553c02ab39bed249bedd4abe37b2118324d1674e639b33fab3d1dafdf4d79"}, + {file = "orjson-3.10.12-cp311-none-win_amd64.whl", hash = "sha256:8b8713b9e46a45b2af6b96f559bfb13b1e02006f4242c156cbadef27800a55a8"}, + {file = "orjson-3.10.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53206d72eb656ca5ac7d3a7141e83c5bbd3ac30d5eccfe019409177a57634b0d"}, + {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8010afc2150d417ebda810e8df08dd3f544e0dd2acab5370cfa6bcc0662f8f"}, + {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed459b46012ae950dd2e17150e838ab08215421487371fa79d0eced8d1461d70"}, + {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dcb9673f108a93c1b52bfc51b0af422c2d08d4fc710ce9c839faad25020bb69"}, + {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22a51ae77680c5c4652ebc63a83d5255ac7d65582891d9424b566fb3b5375ee9"}, + {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910fdf2ac0637b9a77d1aad65f803bac414f0b06f720073438a7bd8906298192"}, + {file = "orjson-3.10.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24ce85f7100160936bc2116c09d1a8492639418633119a2224114f67f63a4559"}, + {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a76ba5fc8dd9c913640292df27bff80a685bed3a3c990d59aa6ce24c352f8fc"}, + {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ff70ef093895fd53f4055ca75f93f047e088d1430888ca1229393a7c0521100f"}, + {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4244b7018b5753ecd10a6d324ec1f347da130c953a9c88432c7fbc8875d13be"}, + {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16135ccca03445f37921fa4b585cff9a58aa8d81ebcb27622e69bfadd220b32c"}, + {file = "orjson-3.10.12-cp312-none-win32.whl", hash = "sha256:2d879c81172d583e34153d524fcba5d4adafbab8349a7b9f16ae511c2cee8708"}, + {file = "orjson-3.10.12-cp312-none-win_amd64.whl", hash = "sha256:fc23f691fa0f5c140576b8c365bc942d577d861a9ee1142e4db468e4e17094fb"}, + {file = "orjson-3.10.12-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47962841b2a8aa9a258b377f5188db31ba49af47d4003a32f55d6f8b19006543"}, + {file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6334730e2532e77b6054e87ca84f3072bee308a45a452ea0bffbbbc40a67e296"}, + {file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:accfe93f42713c899fdac2747e8d0d5c659592df2792888c6c5f829472e4f85e"}, + {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7974c490c014c48810d1dede6c754c3cc46598da758c25ca3b4001ac45b703f"}, + {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3f250ce7727b0b2682f834a3facff88e310f52f07a5dcfd852d99637d386e79e"}, + {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f31422ff9486ae484f10ffc51b5ab2a60359e92d0716fcce1b3593d7bb8a9af6"}, + {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f29c5d282bb2d577c2a6bbde88d8fdcc4919c593f806aac50133f01b733846e"}, + {file = "orjson-3.10.12-cp313-none-win32.whl", hash = "sha256:f45653775f38f63dc0e6cd4f14323984c3149c05d6007b58cb154dd080ddc0dc"}, + {file = "orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825"}, + {file = "orjson-3.10.12-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7d69af5b54617a5fac5c8e5ed0859eb798e2ce8913262eb522590239db6c6763"}, + {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ed119ea7d2953365724a7059231a44830eb6bbb0cfead33fcbc562f5fd8f935"}, + {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5fc1238ef197e7cad5c91415f524aaa51e004be5a9b35a1b8a84ade196f73f"}, + {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43509843990439b05f848539d6f6198d4ac86ff01dd024b2f9a795c0daeeab60"}, + {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f72e27a62041cfb37a3de512247ece9f240a561e6c8662276beaf4d53d406db4"}, + {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a904f9572092bb6742ab7c16c623f0cdccbad9eeb2d14d4aa06284867bddd31"}, + {file = "orjson-3.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:855c0833999ed5dc62f64552db26f9be767434917d8348d77bacaab84f787d7b"}, + {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:897830244e2320f6184699f598df7fb9db9f5087d6f3f03666ae89d607e4f8ed"}, + {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:0b32652eaa4a7539f6f04abc6243619c56f8530c53bf9b023e1269df5f7816dd"}, + {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:36b4aa31e0f6a1aeeb6f8377769ca5d125db000f05c20e54163aef1d3fe8e833"}, + {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5535163054d6cbf2796f93e4f0dbc800f61914c0e3c4ed8499cf6ece22b4a3da"}, + {file = "orjson-3.10.12-cp38-none-win32.whl", hash = "sha256:90a5551f6f5a5fa07010bf3d0b4ca2de21adafbbc0af6cb700b63cd767266cb9"}, + {file = "orjson-3.10.12-cp38-none-win_amd64.whl", hash = "sha256:703a2fb35a06cdd45adf5d733cf613cbc0cb3ae57643472b16bc22d325b5fb6c"}, + {file = "orjson-3.10.12-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f29de3ef71a42a5822765def1febfb36e0859d33abf5c2ad240acad5c6a1b78d"}, + {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de365a42acc65d74953f05e4772c974dad6c51cfc13c3240899f534d611be967"}, + {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a5a0158648a67ff0004cb0df5df7dcc55bfc9ca154d9c01597a23ad54c8d0c"}, + {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c47ce6b8d90fe9646a25b6fb52284a14ff215c9595914af63a5933a49972ce36"}, + {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0eee4c2c5bfb5c1b47a5db80d2ac7aaa7e938956ae88089f098aff2c0f35d5d8"}, + {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d3081bbe8b86587eb5c98a73b97f13d8f9fea685cf91a579beddacc0d10566"}, + {file = "orjson-3.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c23a6e90383884068bc2dba83d5222c9fcc3b99a0ed2411d38150734236755"}, + {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5472be7dc3269b4b52acba1433dac239215366f89dc1d8d0e64029abac4e714e"}, + {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7319cda750fca96ae5973efb31b17d97a5c5225ae0bc79bf5bf84df9e1ec2ab6"}, + {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:74d5ca5a255bf20b8def6a2b96b1e18ad37b4a122d59b154c458ee9494377f80"}, + {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ff31d22ecc5fb85ef62c7d4afe8301d10c558d00dd24274d4bbe464380d3cd69"}, + {file = "orjson-3.10.12-cp39-none-win32.whl", hash = "sha256:c22c3ea6fba91d84fcb4cda30e64aff548fcf0c44c876e681f47d61d24b12e6b"}, + {file = "orjson-3.10.12-cp39-none-win_amd64.whl", hash = "sha256:be604f60d45ace6b0b33dd990a66b4526f1a7a186ac411c942674625456ca548"}, + {file = "orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff"}, +] +packaging = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] +pandas = [ + {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, + {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, + {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, + {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, + {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, + {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, + {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, +] +pathspec = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] +pillow = [ + {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, + {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, + {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, + {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, + {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, + {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, + {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, + {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, + {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, + {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, + {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"}, + {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"}, + {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"}, + {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"}, + {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"}, + {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"}, + {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"}, + {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, + {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, + {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, + {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, + {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, +] +platformdirs = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] +pluggy = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] +pyaudio = [ + {file = "PyAudio-0.2.14-cp310-cp310-win32.whl", hash = "sha256:126065b5e82a1c03ba16e7c0404d8f54e17368836e7d2d92427358ad44fefe61"}, + {file = "PyAudio-0.2.14-cp310-cp310-win_amd64.whl", hash = "sha256:2a166fc88d435a2779810dd2678354adc33499e9d4d7f937f28b20cc55893e83"}, + {file = "PyAudio-0.2.14-cp311-cp311-win32.whl", hash = "sha256:506b32a595f8693811682ab4b127602d404df7dfc453b499c91a80d0f7bad289"}, + {file = "PyAudio-0.2.14-cp311-cp311-win_amd64.whl", hash = "sha256:bbeb01d36a2f472ae5ee5e1451cacc42112986abe622f735bb870a5db77cf903"}, + {file = "PyAudio-0.2.14-cp312-cp312-win32.whl", hash = "sha256:5fce4bcdd2e0e8c063d835dbe2860dac46437506af509353c7f8114d4bacbd5b"}, + {file = "PyAudio-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:12f2f1ba04e06ff95d80700a78967897a489c05e093e3bffa05a84ed9c0a7fa3"}, + {file = "PyAudio-0.2.14-cp38-cp38-win32.whl", hash = "sha256:858caf35b05c26d8fc62f1efa2e8f53d5fa1a01164842bd622f70ddc41f55000"}, + {file = "PyAudio-0.2.14-cp38-cp38-win_amd64.whl", hash = "sha256:2dac0d6d675fe7e181ba88f2de88d321059b69abd52e3f4934a8878e03a7a074"}, + {file = "PyAudio-0.2.14-cp39-cp39-win32.whl", hash = "sha256:f745109634a7c19fa4d6b8b7d6967c3123d988c9ade0cd35d4295ee1acdb53e9"}, + {file = "PyAudio-0.2.14-cp39-cp39-win_amd64.whl", hash = "sha256:009f357ee5aa6bc8eb19d69921cd30e98c42cddd34210615d592a71d09c4bd57"}, + {file = "PyAudio-0.2.14.tar.gz", hash = "sha256:78dfff3879b4994d1f4fc6485646a57755c6ee3c19647a491f790a0895bd2f87"}, +] +pycodestyle = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] +pydantic = [ + {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, + {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, +] +pydantic-core = [ + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, + {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, + {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, + {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, + {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, + {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, + {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, + {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, + {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, + {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, + {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, + {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, + {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, + {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, + {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, + {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, + {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, +] +pydocstyle = [ + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, +] +pydub = [ + {file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"}, + {file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"}, +] +pyflakes = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] +pygments = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] +pyproject-flake8 = [ + {file = "pyproject_flake8-7.0.0-py3-none-any.whl", hash = "sha256:611e91b49916e6d0685f88423ad4baff490888278a258975403c0dee6eb6072e"}, + {file = "pyproject_flake8-7.0.0.tar.gz", hash = "sha256:5b953592336bc04d86e8942fdca1014256044a3445c8b6ca9467d08636749158"}, +] +pytest = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] +pytest-mock = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] +python-dateutil = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] +python-multipart = [ + {file = "python_multipart-0.0.12-py3-none-any.whl", hash = "sha256:43dcf96cf65888a9cd3423544dd0d75ac10f7aa0c3c28a175bbcd00c9ce1aebf"}, + {file = "python_multipart-0.0.12.tar.gz", hash = "sha256:045e1f98d719c1ce085ed7f7e1ef9d8ccc8c02ba02b5566d5f7521410ced58cb"}, +] +pytz = [ + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, +] +pyyaml = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] +requests = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] +rich = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] +ruff = [ + {file = "ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5"}, + {file = "ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087"}, + {file = "ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209"}, + {file = "ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871"}, + {file = "ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1"}, + {file = "ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5"}, + {file = "ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d"}, + {file = "ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26"}, + {file = "ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1"}, + {file = "ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c"}, + {file = "ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa"}, + {file = "ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540"}, + {file = "ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9"}, + {file = "ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5"}, + {file = "ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790"}, + {file = "ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6"}, + {file = "ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737"}, + {file = "ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f"}, +] +safehttpx = [ + {file = "safehttpx-0.1.6-py3-none-any.whl", hash = "sha256:407cff0b410b071623087c63dd2080c3b44dc076888d8c5823c00d1e58cb381c"}, + {file = "safehttpx-0.1.6.tar.gz", hash = "sha256:b356bfc82cee3a24c395b94a2dbeabbed60aff1aa5fa3b5fe97c4f2456ebce42"}, +] +scipy = [ + {file = "scipy-1.14.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3"}, + {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d"}, + {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69"}, + {file = "scipy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad"}, + {file = "scipy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8"}, + {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37"}, + {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2"}, + {file = "scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2"}, + {file = "scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc"}, + {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310"}, + {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066"}, + {file = "scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1"}, + {file = "scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e"}, + {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d"}, + {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e"}, + {file = "scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06"}, + {file = "scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84"}, + {file = "scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417"}, +] +semantic-version = [ + {file = "semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177"}, + {file = "semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c"}, +] +shellingham = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] +six = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] +sniffio = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] +starlette = [ + {file = "starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7"}, + {file = "starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835"}, +] +sympy = [ + {file = "sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8"}, + {file = "sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f"}, +] +tomlkit = [ + {file = "tomlkit-0.12.0-py3-none-any.whl", hash = "sha256:926f1f37a1587c7a4f6c7484dae538f1345d96d793d9adab5d3675957b1d0766"}, + {file = "tomlkit-0.12.0.tar.gz", hash = "sha256:01f0477981119c7d8ee0f67ebe0297a7c95b14cf9f4b102b45486deb77018716"}, +] +torch = [ + {file = "torch-2.2.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:bc889d311a855dd2dfd164daf8cc903a6b7273a747189cebafdd89106e4ad585"}, + {file = "torch-2.2.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:15dffa4cc3261fa73d02f0ed25f5fa49ecc9e12bf1ae0a4c1e7a88bbfaad9030"}, + {file = "torch-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:11e8fe261233aeabd67696d6b993eeb0896faa175c6b41b9a6c9f0334bdad1c5"}, + {file = "torch-2.2.2-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:b2e2200b245bd9f263a0d41b6a2dab69c4aca635a01b30cca78064b0ef5b109e"}, + {file = "torch-2.2.2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:877b3e6593b5e00b35bbe111b7057464e76a7dd186a287280d941b564b0563c2"}, + {file = "torch-2.2.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:ad4c03b786e074f46606f4151c0a1e3740268bcf29fbd2fdf6666d66341c1dcb"}, + {file = "torch-2.2.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:32827fa1fbe5da8851686256b4cd94cc7b11be962862c2293811c94eea9457bf"}, + {file = "torch-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:f9ef0a648310435511e76905f9b89612e45ef2c8b023bee294f5e6f7e73a3e7c"}, + {file = "torch-2.2.2-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:95b9b44f3bcebd8b6cd8d37ec802048c872d9c567ba52c894bba90863a439059"}, + {file = "torch-2.2.2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:49aa4126ede714c5aeef7ae92969b4b0bbe67f19665106463c39f22e0a1860d1"}, + {file = "torch-2.2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:cf12cdb66c9c940227ad647bc9cf5dba7e8640772ae10dfe7569a0c1e2a28aca"}, + {file = "torch-2.2.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:89ddac2a8c1fb6569b90890955de0c34e1724f87431cacff4c1979b5f769203c"}, + {file = "torch-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:451331406b760f4b1ab298ddd536486ab3cfb1312614cfe0532133535be60bea"}, + {file = "torch-2.2.2-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:eb4d6e9d3663e26cd27dc3ad266b34445a16b54908e74725adb241aa56987533"}, + {file = "torch-2.2.2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:bf9558da7d2bf7463390b3b2a61a6a3dbb0b45b161ee1dd5ec640bf579d479fc"}, + {file = "torch-2.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cd2bf7697c9e95fb5d97cc1d525486d8cf11a084c6af1345c2c2c22a6b0029d0"}, + {file = "torch-2.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b421448d194496e1114d87a8b8d6506bce949544e513742b097e2ab8f7efef32"}, + {file = "torch-2.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:3dbcd563a9b792161640c0cffe17e3270d85e8f4243b1f1ed19cca43d28d235b"}, + {file = "torch-2.2.2-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:31f4310210e7dda49f1fb52b0ec9e59382cfcb938693f6d5378f25b43d7c1d29"}, + {file = "torch-2.2.2-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:c795feb7e8ce2e0ef63f75f8e1ab52e7fd5e1a4d7d0c31367ade1e3de35c9e95"}, + {file = "torch-2.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:a6e5770d68158d07456bfcb5318b173886f579fdfbf747543901ce718ea94782"}, + {file = "torch-2.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:67dcd726edff108e2cd6c51ff0e416fd260c869904de95750e80051358680d24"}, + {file = "torch-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:539d5ef6c4ce15bd3bd47a7b4a6e7c10d49d4d21c0baaa87c7d2ef8698632dfb"}, + {file = "torch-2.2.2-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:dff696de90d6f6d1e8200e9892861fd4677306d0ef604cb18f2134186f719f82"}, + {file = "torch-2.2.2-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:3a4dd910663fd7a124c056c878a52c2b0be4a5a424188058fe97109d4436ee42"}, +] +torchaudio = [ + {file = "torchaudio-2.2.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b1d58201d108e85db3e35b84319f33884f61f327c38ead86913218c8c1acc3dd"}, + {file = "torchaudio-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a520e14ea0ba89d9dc27922eb4609f9eac5c01c279830e0f216b9c9e017d438b"}, + {file = "torchaudio-2.2.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:f05d14f6cd0bc3498de19eb1b87420c06895911acf7eca08da37a21a4d42dbbe"}, + {file = "torchaudio-2.2.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:b0f38e7d3548914d78aafc27ff00f7701b1a50bfcddc58965f545fc92ccd4a66"}, + {file = "torchaudio-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ad5c6ef0d8fac69221d02fd384b07373f59605d7a09f20c6fe67132c6574ece2"}, + {file = "torchaudio-2.2.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:f1a81a518a3e86c004125eb891fc433ce8fb2343295b5d612d0f37b24e131efd"}, + {file = "torchaudio-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:01482fc85117f85ee44f8aa8e9c11b1c022326173e0748789ed42b219102937f"}, + {file = "torchaudio-2.2.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:02e3dc45408d83371d9832ee4520f13f887f5da4cd0931ebde6aaf2a1723d340"}, + {file = "torchaudio-2.2.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:0a03a48b6d55d17d48f419a7f1d0d4018d48a04c76585c16a9b5e69281f92f94"}, + {file = "torchaudio-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:45ff277ced4a3f8cdc0474df16ebfb177633337040e5ac82d1fd46e4e6b57f85"}, + {file = "torchaudio-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:da3cc523696166ea525d2b3377d789da5388f36d94a20a324b09df00f1c43458"}, + {file = "torchaudio-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fc7aac4f4b24e9b3fa03a2a7933363f7e5c484835ccb2a20cf164a0e5e715b7"}, + {file = "torchaudio-2.2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:2b20b3b2f0d71b626cfa651cb290010f0cae6c2f6d5cb33f39ce34f99877fd9d"}, + {file = "torchaudio-2.2.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:9db0338bd3a78e60c745b6b5c366e4c9b88eb210e1fdd617d3f62f1a0b859ea4"}, + {file = "torchaudio-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:468e46c1dcf4a8c5d5ef68dae934a67a83f544034d1be7322cc58f721ff0e487"}, + {file = "torchaudio-2.2.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:8ce4df065a949911d2b6782aa4c13687efadea23ffc7c7a6f15f7e7ae5c89524"}, + {file = "torchaudio-2.2.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b3b8abe26b067e9c4a6e3dba156b91d7a85247e88dda70b7c43859f55b978ddc"}, + {file = "torchaudio-2.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:53cf1089ac8082d626627e1a7e5bfd82f879f7d8129a36d7360243338fd0dfb3"}, + {file = "torchaudio-2.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:4f756a6e667dd8841bf21a07ead3efedaa7a27d55852779c266f6f2a1064c994"}, + {file = "torchaudio-2.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:de887ac20208ad50786c22c82a3da641376c5e01d1c2ac6dafbccd6ee3d30c93"}, + {file = "torchaudio-2.2.2-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:db70b13a871a49487bd9042bf04b12f74aed77b1a87d2fbeb68d09d9b64bc528"}, + {file = "torchaudio-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b78a84a189bf3da4b966375cebdecc584a4dc5f60e0bde721d73401ed5cad45"}, + {file = "torchaudio-2.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:0ee438a5854874ce6e2fd89cae7ea60977f68a82b851719dddb3f7779c9e85ab"}, + {file = "torchaudio-2.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:723f4e57b5d0c120357ca60cd55b4e6cfac845bc0ecccb4b417a44aa4ebc526b"}, + {file = "torchaudio-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:2da5a53d7fb3f1e83d552c06ad143338a3ab12f517ccdf7e107592dbd51deb83"}, +] +torchfcpe = [ + {file = "torchfcpe-0.0.4-py3-none-any.whl", hash = "sha256:f042c463d850d76c6f4899a0b84f0b694bb560adf05f4de951097a756d17472d"}, +] +tqdm = [ + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, +] +triton = [ + {file = "triton-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2294514340cfe4e8f4f9e5c66c702744c4a117d25e618bd08469d0bfed1e2e5"}, + {file = "triton-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da58a152bddb62cafa9a857dd2bc1f886dbf9f9c90a2b5da82157cd2b34392b0"}, + {file = "triton-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af58716e721460a61886668b205963dc4d1e4ac20508cc3f623aef0d70283d5"}, + {file = "triton-2.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8fe46d3ab94a8103e291bd44c741cc294b91d1d81c1a2888254cbf7ff846dab"}, + {file = "triton-2.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8ce26093e539d727e7cf6f6f0d932b1ab0574dc02567e684377630d86723ace"}, + {file = "triton-2.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:227cc6f357c5efcb357f3867ac2a8e7ecea2298cd4606a8ba1e931d1d5a947df"}, +] +typer = [ + {file = "typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847"}, + {file = "typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a"}, +] +types-pyyaml = [ + {file = "types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587"}, + {file = "types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570"}, +] +typing-extensions = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] +tzdata = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] +urllib3 = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] +uvicorn = [ + {file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"}, + {file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"}, +] +websockets = [ + {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, + {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, + {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, + {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, + {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, + {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, + {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, + {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, + {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, + {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, + {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, + {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, + {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, + {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, + {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, + {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, + {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, + {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, + {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, + {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, + {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, + {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, + {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, + {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, + {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..672601dc01e762e71bbc3a4efe2b62c9c2e001dd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,45 @@ +[tool.poetry] +name = "improvisation-lab" +version = "0.2.0" +description = "" +authors = ["atsushieee "] +readme = "README.md" +packages = [ + {include = "improvisation_lab"}, + {include = "scripts"} +] + +[tool.poetry.dependencies] +python = "^3.11" +torch = "2.2.2" +torchfcpe = "^0.0.4" +numpy = "1.26.4" +pyaudio = "^0.2.14" +pyyaml = "^6.0.2" +types-pyyaml = "^6.0.12.20240917" +scipy = "^1.14.1" +gradio = "5.7.1" + + +[tool.poetry.group.dev.dependencies] +mypy = "^1.13.0" +black = "^24.10.0" +isort = "^5.13.2" +pydocstyle = "^6.3.0" +pytest = "^8.3.3" +pyproject-flake8 = "^7.0.0" +pytest-mock = "^3.14.0" + +[tool.flake8] +max-line-length = 88 +extend-ignore = "E203" + +[tool.black] +line-length = 88 + +[tool.mypy] +ignore_missing_imports = "True" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..ed35d32b8bf6e25078e884cc7f482ab1a208d9f4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,77 @@ +aiofiles==23.2.1 ; python_version >= "3.11" and python_version < "4.0" +annotated-types==0.7.0 ; python_version >= "3.11" and python_version < "4.0" +anyio==4.6.2.post1 ; python_version >= "3.11" and python_version < "4.0" +audioop-lts==0.2.1 ; python_version >= "3.13" and python_version < "4.0" +certifi==2024.8.30 ; python_version >= "3.11" and python_version < "4.0" +charset-normalizer==3.4.0 ; python_version >= "3.11" and python_version < "4.0" +click==8.1.7 ; python_version >= "3.11" and python_version < "4.0" and sys_platform != "emscripten" +colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and platform_system == "Windows" +einops==0.8.0 ; python_version >= "3.11" and python_version < "4.0" +fastapi==0.115.6 ; python_version >= "3.11" and python_version < "4.0" +ffmpy==0.4.0 ; python_version >= "3.11" and python_version < "4.0" +filelock==3.16.1 ; python_version >= "3.11" and python_version < "4.0" +fsspec==2024.10.0 ; python_version >= "3.11" and python_version < "4.0" +gradio-client==1.5.0 ; python_version >= "3.11" and python_version < "4.0" +gradio==5.7.1 ; python_version >= "3.11" and python_version < "4.0" +h11==0.14.0 ; python_version >= "3.11" and python_version < "4.0" +httpcore==1.0.7 ; python_version >= "3.11" and python_version < "4.0" +httpx==0.28.0 ; python_version >= "3.11" and python_version < "4.0" +huggingface-hub==0.26.3 ; python_version >= "3.11" and python_version < "4.0" +idna==3.10 ; python_version >= "3.11" and python_version < "4.0" +jinja2==3.1.4 ; python_version >= "3.11" and python_version < "4.0" +local-attention==1.9.15 ; python_version >= "3.11" and python_version < "4.0" +markdown-it-py==3.0.0 ; python_version >= "3.11" and python_version < "4.0" and sys_platform != "emscripten" +markupsafe==2.1.5 ; python_version >= "3.11" and python_version < "4.0" +mdurl==0.1.2 ; python_version >= "3.11" and python_version < "4.0" and sys_platform != "emscripten" +mpmath==1.3.0 ; python_version >= "3.11" and python_version < "4.0" +networkx==3.4.2 ; python_version >= "3.11" and python_version < "4.0" +numpy==1.26.4 ; python_version >= "3.11" and python_version < "4.0" +nvidia-cublas-cu12==12.1.3.1 ; platform_system == "Linux" and platform_machine == "x86_64" and python_version >= "3.11" and python_version < "4.0" +nvidia-cuda-cupti-cu12==12.1.105 ; platform_system == "Linux" and platform_machine == "x86_64" and python_version >= "3.11" and python_version < "4.0" +nvidia-cuda-nvrtc-cu12==12.1.105 ; platform_system == "Linux" and platform_machine == "x86_64" and python_version >= "3.11" and python_version < "4.0" +nvidia-cuda-runtime-cu12==12.1.105 ; platform_system == "Linux" and platform_machine == "x86_64" and python_version >= "3.11" and python_version < "4.0" +nvidia-cudnn-cu12==8.9.2.26 ; platform_system == "Linux" and platform_machine == "x86_64" and python_version >= "3.11" and python_version < "4.0" +nvidia-cufft-cu12==11.0.2.54 ; platform_system == "Linux" and platform_machine == "x86_64" and python_version >= "3.11" and python_version < "4.0" +nvidia-curand-cu12==10.3.2.106 ; platform_system == "Linux" and platform_machine == "x86_64" and python_version >= "3.11" and python_version < "4.0" +nvidia-cusolver-cu12==11.4.5.107 ; platform_system == "Linux" and platform_machine == "x86_64" and python_version >= "3.11" and python_version < "4.0" +nvidia-cusparse-cu12==12.1.0.106 ; platform_system == "Linux" and platform_machine == "x86_64" and python_version >= "3.11" and python_version < "4.0" +nvidia-nccl-cu12==2.19.3 ; platform_system == "Linux" and platform_machine == "x86_64" and python_version >= "3.11" and python_version < "4.0" +nvidia-nvjitlink-cu12==12.4.127 ; platform_system == "Linux" and platform_machine == "x86_64" and python_version >= "3.11" and python_version < "4.0" +nvidia-nvtx-cu12==12.1.105 ; platform_system == "Linux" and platform_machine == "x86_64" and python_version >= "3.11" and python_version < "4.0" +orjson==3.10.12 ; python_version >= "3.11" and python_version < "4.0" +packaging==24.2 ; python_version >= "3.11" and python_version < "4.0" +pandas==2.2.3 ; python_version >= "3.11" and python_version < "4.0" +pillow==11.0.0 ; python_version >= "3.11" and python_version < "4.0" +pyaudio==0.2.14 ; python_version >= "3.11" and python_version < "4.0" +pydantic-core==2.27.1 ; python_version >= "3.11" and python_version < "4.0" +pydantic==2.10.3 ; python_version >= "3.11" and python_version < "4.0" +pydub==0.25.1 ; python_version >= "3.11" and python_version < "4.0" +pygments==2.18.0 ; python_version >= "3.11" and python_version < "4.0" and sys_platform != "emscripten" +python-dateutil==2.9.0.post0 ; python_version >= "3.11" and python_version < "4.0" +python-multipart==0.0.12 ; python_version >= "3.11" and python_version < "4.0" +pytz==2024.2 ; python_version >= "3.11" and python_version < "4.0" +pyyaml==6.0.2 ; python_version >= "3.11" and python_version < "4.0" +requests==2.32.3 ; python_version >= "3.11" and python_version < "4.0" +rich==13.9.4 ; python_version >= "3.11" and python_version < "4.0" and sys_platform != "emscripten" +ruff==0.8.1 ; python_version >= "3.11" and python_version < "4.0" and sys_platform != "emscripten" +safehttpx==0.1.6 ; python_version >= "3.11" and python_version < "4.0" +scipy==1.14.1 ; python_version >= "3.11" and python_version < "4.0" +semantic-version==2.10.0 ; python_version >= "3.11" and python_version < "4.0" +shellingham==1.5.4 ; python_version >= "3.11" and python_version < "4.0" and sys_platform != "emscripten" +six==1.17.0 ; python_version >= "3.11" and python_version < "4.0" +sniffio==1.3.1 ; python_version >= "3.11" and python_version < "4.0" +starlette==0.41.3 ; python_version >= "3.11" and python_version < "4.0" +sympy==1.13.1 ; python_version >= "3.11" and python_version < "4.0" +tomlkit==0.12.0 ; python_version >= "3.11" and python_version < "4.0" +torch==2.2.2 ; python_version >= "3.11" and python_version < "4.0" +torchaudio==2.2.2 ; python_version >= "3.11" and python_version < "4.0" +torchfcpe==0.0.4 ; python_version >= "3.11" and python_version < "4.0" +tqdm==4.67.1 ; python_version >= "3.11" and python_version < "4.0" +triton==2.2.0 ; platform_system == "Linux" and platform_machine == "x86_64" and python_version < "3.12" and python_version >= "3.11" +typer==0.15.1 ; python_version >= "3.11" and python_version < "4.0" and sys_platform != "emscripten" +types-pyyaml==6.0.12.20240917 ; python_version >= "3.11" and python_version < "4.0" +typing-extensions==4.12.2 ; python_version >= "3.11" and python_version < "4.0" +tzdata==2024.2 ; python_version >= "3.11" and python_version < "4.0" +urllib3==2.2.3 ; python_version >= "3.11" and python_version < "4.0" +uvicorn==0.32.1 ; python_version >= "3.11" and python_version < "4.0" and sys_platform != "emscripten" +websockets==12.0 ; python_version >= "3.11" and python_version < "4.0" diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..82ddcd5ad1294ea03c22cbfc1fc62487a47142de --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Scripts package for improvisation-lab.""" diff --git a/scripts/pitch_detection_demo.py b/scripts/pitch_detection_demo.py new file mode 100644 index 0000000000000000000000000000000000000000..13c8113abc16ec95d9a3cbd28b744397955fcc87 --- /dev/null +++ b/scripts/pitch_detection_demo.py @@ -0,0 +1,141 @@ +"""Script for demonstrating pitch detection functionality.""" + +import argparse +import time + +import gradio as gr + +from improvisation_lab.config import Config +from improvisation_lab.domain.analysis import PitchDetector +from improvisation_lab.domain.music_theory import Notes +from improvisation_lab.infrastructure.audio import (DirectAudioProcessor, + WebAudioProcessor) + + +def create_process_audio(pitch_detector: PitchDetector): + """Create audio processing callback function. + + Args: + pitch_detector: PitchDetector instance + + Returns: + Callback function for processing audio data + """ + + def process_audio(audio_data): + frequency = pitch_detector.detect_pitch(audio_data) + if frequency > 0: # voice detected + note_name = Notes.convert_frequency_to_note(frequency) + print( + f"\rFrequency: {frequency:6.1f} Hz | Note: {note_name:<5}", + end="", + flush=True, + ) + else: # no voice detected + print("\rNo voice detected ", end="", flush=True) + + return process_audio + + +def run_direct_audio_demo(config: Config): + """Run pitch detection demo using microphone input. + + Args: + config: Configuration object + """ + pitch_detector = PitchDetector(config.audio.pitch_detector) + mic_input = DirectAudioProcessor( + sample_rate=config.audio.sample_rate, + buffer_duration=config.audio.buffer_duration, + ) + + print("Starting pitch detection demo (Microphone)...") + print("Sing or hum a note!") + print("-" * 50) + + try: + mic_input._callback = create_process_audio(pitch_detector) + mic_input.start_recording() + while True: + time.sleep(0.1) + except KeyboardInterrupt: + print("\nStopping...") + finally: + mic_input.stop_recording() + + +def run_web_audio_demo(config: Config): + """Run pitch detection demo using Gradio interface. + + Args: + config: Configuration object + """ + pitch_detector = PitchDetector(config.audio.pitch_detector) + audio_input = WebAudioProcessor( + sample_rate=config.audio.sample_rate, + buffer_duration=config.audio.buffer_duration, + ) + + print("Starting pitch detection demo (Gradio)...") + result = {"text": "No voice detected"} + + def process_audio(audio_data): + frequency = pitch_detector.detect_pitch(audio_data) + if frequency > 0: + note_name = Notes.convert_frequency_to_note(frequency) + result["text"] = f"Frequency: {frequency:6.1f} Hz | Note: {note_name}" + else: + result["text"] = "No voice detected" + + audio_input._callback = process_audio + + def handle_audio(audio): + """Handle audio input from Gradio.""" + if audio is None: + return result["text"] + if not audio_input.is_recording: + audio_input.start_recording() + audio_input.process_audio(audio) + return result["text"] + + interface = gr.Interface( + fn=handle_audio, + inputs=gr.Audio( + sources=["microphone"], + streaming=True, + type="numpy", + ), + outputs=gr.Text(label="Detection Result"), + live=True, + title="Pitch Detection Demo", + allow_flagging="never", + stream_every=0.05, + ) + interface.queue() + interface.launch( + share=False, + debug=True, + ) + + +def main(): + """Run the pitch detection demo.""" + parser = argparse.ArgumentParser(description="Run pitch detection demo") + parser.add_argument( + "--input", + choices=["direct", "web"], + default="web", + help="Input method (direct: microphone or web: browser)", + ) + args = parser.parse_args() + + config = Config() + + if args.input == "web": + run_web_audio_demo(config) + else: + run_direct_audio_demo(config) + + +if __name__ == "__main__": + main() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fbf2649db75d8661f026bed1759288a0d253ffb2 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test package for improvisation-lab.""" diff --git a/tests/application/__init__.py b/tests/application/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bad5751adbdf1f26404effbc1dfe2a8cdc464860 --- /dev/null +++ b/tests/application/__init__.py @@ -0,0 +1 @@ +"""Tests for the application layer.""" diff --git a/tests/application/melody_practice/__init__.py b/tests/application/melody_practice/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..75cd668d286c76d9fd833a8b66f0b7413ba1b41e --- /dev/null +++ b/tests/application/melody_practice/__init__.py @@ -0,0 +1 @@ +"""Tests for the melody practice application layer.""" diff --git a/tests/application/melody_practice/test_app_factory.py b/tests/application/melody_practice/test_app_factory.py new file mode 100644 index 0000000000000000000000000000000000000000..aa0b9da3c79e32d5b0f41942d3faf85ac3cd9cec --- /dev/null +++ b/tests/application/melody_practice/test_app_factory.py @@ -0,0 +1,34 @@ +"""Tests for the MelodyPracticeAppFactory class.""" + +import pytest + +from improvisation_lab.application.melody_practice.app_factory import \ + MelodyPracticeAppFactory +from improvisation_lab.application.melody_practice.console_app import \ + ConsoleMelodyPracticeApp +from improvisation_lab.application.melody_practice.web_app import \ + WebMelodyPracticeApp +from improvisation_lab.config import Config +from improvisation_lab.service import MelodyPracticeService + + +class TestMelodyPracticeAppFactory: + @pytest.fixture + def init_module(self): + self.config = Config() + self.service = MelodyPracticeService(self.config) + + @pytest.mark.usefixtures("init_module") + def test_create_web_app(self): + app = MelodyPracticeAppFactory.create_app("web", self.service, self.config) + assert isinstance(app, WebMelodyPracticeApp) + + @pytest.mark.usefixtures("init_module") + def test_create_console_app(self): + app = MelodyPracticeAppFactory.create_app("console", self.service, self.config) + assert isinstance(app, ConsoleMelodyPracticeApp) + + @pytest.mark.usefixtures("init_module") + def test_create_app_invalid_type(self): + with pytest.raises(ValueError): + MelodyPracticeAppFactory.create_app("invalid", self.service, self.config) diff --git a/tests/application/melody_practice/test_console_app.py b/tests/application/melody_practice/test_console_app.py new file mode 100644 index 0000000000000000000000000000000000000000..605bf66ea3a4b3367013160cf7736332577137b2 --- /dev/null +++ b/tests/application/melody_practice/test_console_app.py @@ -0,0 +1,71 @@ +"""Tests for the ConsoleMelodyPracticeApp class.""" + +from unittest.mock import Mock, patch + +import pytest + +from improvisation_lab.application.melody_practice.console_app import \ + ConsoleMelodyPracticeApp +from improvisation_lab.config import Config +from improvisation_lab.infrastructure.audio import DirectAudioProcessor +from improvisation_lab.presentation.melody_practice.console_melody_view import \ + ConsoleMelodyView +from improvisation_lab.service import MelodyPracticeService + + +class TestConsoleMelodyPracticeApp: + @pytest.fixture + def init_module(self): + """Initialize ConsoleMelodyPracticeApp for testing.""" + config = Config() + service = MelodyPracticeService(config) + self.app = ConsoleMelodyPracticeApp(service, config) + self.app.ui = Mock(spec=ConsoleMelodyView) + self.app.audio_processor = Mock(spec=DirectAudioProcessor) + self.app.audio_processor.is_recording = False + + @pytest.mark.usefixtures("init_module") + @patch.object(DirectAudioProcessor, "start_recording", return_value=None) + @patch("time.sleep", side_effect=KeyboardInterrupt) + def test_launch(self, mock_start_recording, mock_sleep): + """Test launching the application. + + Args: + mock_start_recording: Mock object for start_recording method. + mock_sleep: Mock object for sleep method. + """ + self.app.launch() + assert self.app.is_running + assert self.app.current_phrase_idx == 0 + assert self.app.current_note_idx == 0 + self.app.ui.launch.assert_called_once() + self.app.ui.display_phrase_info.assert_called_once_with(0, self.app.phrases) + mock_start_recording.assert_called_once() + + @pytest.mark.usefixtures("init_module") + def test_process_audio_callback(self): + """Test processing audio callback.""" + audio_data = Mock() + self.app.phrases = [Mock(notes=["C", "E", "G"]), Mock(notes=["C", "E", "G"])] + self.app.current_phrase_idx = 0 + self.app.current_note_idx = 2 + + with patch.object( + self.app.service, "process_audio", return_value=Mock(remaining_time=0) + ) as mock_process_audio: + self.app._process_audio_callback(audio_data) + mock_process_audio.assert_called_once_with(audio_data, "G") + self.app.ui.display_pitch_result.assert_called_once() + self.app.ui.display_phrase_info.assert_called_once_with(1, self.app.phrases) + + @pytest.mark.usefixtures("init_module") + def test_advance_to_next_note(self): + """Test advancing to the next note.""" + self.app.phrases = [Mock(notes=["C", "E", "G"])] + self.app.current_phrase_idx = 0 + self.app.current_note_idx = 2 + + self.app._advance_to_next_note() + assert self.app.current_note_idx == 0 + assert self.app.current_phrase_idx == 0 + self.app.ui.display_phrase_info.assert_called_once_with(1, self.app.phrases) diff --git a/tests/application/melody_practice/test_web_app.py b/tests/application/melody_practice/test_web_app.py new file mode 100644 index 0000000000000000000000000000000000000000..18cd54e79a1b5a3c5ae5aaa7a2ffc7548d5f2999 --- /dev/null +++ b/tests/application/melody_practice/test_web_app.py @@ -0,0 +1,94 @@ +"""Tests for the WebMelodyPracticeApp class.""" + +from unittest.mock import Mock, patch + +import pytest + +from improvisation_lab.application.melody_practice.web_app import \ + WebMelodyPracticeApp +from improvisation_lab.config import Config +from improvisation_lab.infrastructure.audio import WebAudioProcessor +from improvisation_lab.presentation.melody_practice.web_melody_view import \ + WebMelodyView +from improvisation_lab.service import MelodyPracticeService + + +class TestWebMelodyPracticeApp: + @pytest.fixture + def init_module(self): + """Initialize WebMelodyPracticeApp for testing.""" + config = Config() + service = MelodyPracticeService(config) + self.app = WebMelodyPracticeApp(service, config) + self.app.ui = Mock(spec=WebMelodyView) + self.app.audio_processor = Mock(spec=WebAudioProcessor) + + @pytest.mark.usefixtures("init_module") + def test_launch(self): + """Test launching the application.""" + with patch.object(self.app.ui, "launch", return_value=None) as mock_launch: + self.app.launch() + mock_launch.assert_called_once() + + @pytest.mark.usefixtures("init_module") + def test_process_audio_callback(self): + """Test processing audio callback.""" + audio_data = Mock() + self.app.is_running = True + self.app.phrases = [Mock(notes=["C", "E", "G"]), Mock(notes=["C", "E", "G"])] + self.app.current_phrase_idx = 0 + self.app.current_note_idx = 2 + + mock_result = Mock() + mock_result.target_note = "G" + mock_result.current_base_note = "G" + mock_result.remaining_time = 0.0 + + with patch.object( + self.app.service, "process_audio", return_value=mock_result + ) as mock_process_audio: + self.app._process_audio_callback(audio_data) + mock_process_audio.assert_called_once_with(audio_data, "G") + assert ( + self.app.text_manager.result_text + == "Target: G | Your note: G | Remaining: 0.0s" + ) + + @pytest.mark.usefixtures("init_module") + def test_handle_audio(self): + """Test handling audio input.""" + audio_data = (48000, Mock()) + self.app.is_running = True + with patch.object( + self.app.audio_processor, "process_audio", return_value=None + ) as mock_process_audio: + phrase_text, result_text = self.app.handle_audio(audio_data) + mock_process_audio.assert_called_once_with(audio_data) + assert phrase_text == self.app.text_manager.phrase_text + assert result_text == self.app.text_manager.result_text + + @pytest.mark.usefixtures("init_module") + def test_start(self): + """Test starting the application.""" + self.app.audio_processor.is_recording = False + with patch.object( + self.app.audio_processor, "start_recording", return_value=None + ) as mock_start_recording: + phrase_text, result_text = self.app.start() + mock_start_recording.assert_called_once() + assert self.app.is_running + assert phrase_text == self.app.text_manager.phrase_text + assert result_text == self.app.text_manager.result_text + + @pytest.mark.usefixtures("init_module") + def test_stop(self): + """Test stopping the application.""" + self.app.audio_processor.is_recording = True + with patch.object( + self.app.audio_processor, "stop_recording", return_value=None + ) as mock_stop_recording: + phrase_text, result_text = self.app.stop() + mock_stop_recording.assert_called_once() + assert not self.app.is_running + assert phrase_text == self.app.text_manager.phrase_text + assert result_text == self.app.text_manager.result_text diff --git a/tests/domain/__init__.py b/tests/domain/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f77fb2b8baeedd61aa97cb227163a895d8bcd602 --- /dev/null +++ b/tests/domain/__init__.py @@ -0,0 +1 @@ +"""Test package for domain layer of improvisation-lab.""" diff --git a/tests/domain/analysis/__init__.py b/tests/domain/analysis/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6004c098a54625e8504b03e46b5b9a71556326c3 --- /dev/null +++ b/tests/domain/analysis/__init__.py @@ -0,0 +1 @@ +"""Test package for music analysis module.""" diff --git a/tests/domain/analysis/test_pitch_detector.py b/tests/domain/analysis/test_pitch_detector.py new file mode 100644 index 0000000000000000000000000000000000000000..2865787cc4435782c39b887f01f63845c92503a1 --- /dev/null +++ b/tests/domain/analysis/test_pitch_detector.py @@ -0,0 +1,49 @@ +import numpy as np +import pytest + +from improvisation_lab.config import PitchDetectorConfig +from improvisation_lab.domain.analysis.pitch_detector import PitchDetector + + +class TestPitchDetector: + + @pytest.fixture + def init_module(self) -> None: + """Initialization.""" + config = PitchDetectorConfig() + self.pitch_detector = PitchDetector(config) + + @pytest.mark.usefixtures("init_module") + def test_detect_pitch_sine_wave(self): + """Test pitch detection with a simple sine wave.""" + # Create a sine wave at 440 Hz (A4 note) + duration = 0.2 # seconds + # Array of sr * duration equally spaced values dividing the range 0 to duration. + t = np.linspace(0, duration, int(self.pitch_detector.sample_rate * duration)) + frequency = 440.0 + # Generates sine waves for a specified time + audio_data = np.sin(2 * np.pi * frequency * t).astype(np.float32) + + # Detect pitch + detected_freq = self.pitch_detector.detect_pitch(audio_data) + + # Check if detected frequency is close to 440 Hz + assert abs(detected_freq - 440.0) < 1.5 # Allow 1.5 Hz tolerance + + def test_custom_parameters(self): + """Test pitch detection with custom parameters.""" + custom_config = PitchDetectorConfig( + sample_rate=22050, + f0_min=100, + f0_max=800, + threshold=0.01, + ) + detector = PitchDetector(custom_config) + + duration = 0.2 + t = np.linspace(0, duration, int(detector.sample_rate * duration)) + frequency = 440.0 + audio_data = np.sin(2 * np.pi * frequency * t).astype(np.float32) + + detected_freq = detector.detect_pitch(audio_data) + assert abs(detected_freq - 440.0) < 1.5 diff --git a/tests/domain/composition/__init__.py b/tests/domain/composition/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..afceb945fcee0c203c5647a79d01c01584d062c4 --- /dev/null +++ b/tests/domain/composition/__init__.py @@ -0,0 +1 @@ +"""Test package for melody jam module.""" diff --git a/tests/domain/composition/test_melody_composer.py b/tests/domain/composition/test_melody_composer.py new file mode 100644 index 0000000000000000000000000000000000000000..898a59173a978ed736d3830d4eb128e1d7c0c62c --- /dev/null +++ b/tests/domain/composition/test_melody_composer.py @@ -0,0 +1,84 @@ +"""Tests for melody composer module.""" + +import pytest + +from improvisation_lab.domain.composition.melody_composer import MelodyComposer + + +class TestMelodyComposer: + @pytest.fixture + def init_module(self): + """Initialize test module.""" + self.melody_composer = MelodyComposer() + + @pytest.mark.usefixtures("init_module") + def test_generate_phrases_basic(self): + """Test basic phrase generation functionality.""" + progression = [ + ("C", "major", "C", "maj7", 4), + ("A", "natural_minor", "A", "min7", 4), + ] + + phrases = self.melody_composer.generate_phrases(progression) + + # Test number of phrases + assert len(phrases) == 2 + + # Test first phrase structure + first_phrase = phrases[0] + assert len(first_phrase.notes) == 4 + assert first_phrase.chord_name == "Cmaj7" + assert first_phrase.scale_info == "C major" + assert first_phrase.length == 4 + + # Test second phrase structure + second_phrase = phrases[1] + assert len(second_phrase.notes) == 4 + assert second_phrase.chord_name == "Amin7" + assert second_phrase.scale_info == "A natural_minor" + assert second_phrase.length == 4 + + @pytest.mark.usefixtures("init_module") + def test_generate_phrases_empty_progression(self): + """Test handling of empty progression.""" + phrases = self.melody_composer.generate_phrases([]) + assert len(phrases) == 0 + + @pytest.mark.usefixtures("init_module") + def test_phrase_connection(self): + """Test that phrases are properly connected.""" + progression = [ + ("C", "major", "C", "maj7", 2), + ("C", "major", "F", "maj7", 2), + ] + + # Get chord tones for both chords + first_chord_tones = ["C", "E", "G", "B"] # Cmaj7 + second_chord_tones = ["F", "A", "C", "E"] # Fmaj7 + + # Run multiple times to ensure random selection works correctly + for _ in range(10): + phrases = self.melody_composer.generate_phrases(progression) + + # Get the last note of first phrase + last_note = phrases[0].notes[-1] + # Get the first note of second phrase + first_note = phrases[1].notes[0] + + # Check if the last note is a chord tone of either chord + is_first_chord_tone = self.melody_composer.phrase_generator.is_chord_tone( + last_note, first_chord_tones + ) + is_second_chord_tone = self.melody_composer.phrase_generator.is_chord_tone( + last_note, second_chord_tones + ) + + # If the last note is not a chord tone of either chord + if not is_first_chord_tone and not is_second_chord_tone: + # Then first note should be adjacent + adjacent_notes = ( + self.melody_composer.phrase_generator.get_adjacent_notes( + last_note, ["C", "D", "E", "F", "G", "A", "B"] + ) + ) + assert first_note in adjacent_notes diff --git a/tests/domain/composition/test_phrase_generator.py b/tests/domain/composition/test_phrase_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..42cff3e90704ba8397535400ae67fa542a7f10c5 --- /dev/null +++ b/tests/domain/composition/test_phrase_generator.py @@ -0,0 +1,208 @@ +import pytest + +from improvisation_lab.domain.composition.phrase_generator import \ + PhraseGenerator +from improvisation_lab.domain.music_theory import Scale + + +class TestPhraseGenerator: + + @pytest.fixture + def init_module(self) -> None: + """Initialization.""" + self.phrase_generator = PhraseGenerator() + + @pytest.mark.usefixtures("init_module") + def test_is_chord_tone(self): + """Test that is_chord_tone correctly identifies chord tones.""" + test_cases = [ + ("C", ["C", "E", "G"], True), + ("D", ["C", "E", "G"], False), + ("E", ["C", "E", "G", "B"], True), + ("F", ["C", "E", "G", "B"], False), + ("G#", ["G#", "C", "D#"], True), + ("A", ["G#", "C", "D#"], False), + ] + + for note, chord_tones, expected in test_cases: + assert self.phrase_generator.is_chord_tone(note, chord_tones) == expected + + @pytest.mark.usefixtures("init_module") + def test_get_adjacent_notes(self): + """Test that get_adjacent_notes returns correct adjacent notes.""" + test_cases = [ + ("C", ["C", "D", "E", "F", "G", "A", "B"], ["B", "D"]), + ("C#", ["C", "D", "E", "F", "G", "A", "B"], ["C", "D"]), + ("B", ["C", "D", "E", "F", "G", "A", "B"], ["A", "C"]), + ] + + for note, scale_notes, expected in test_cases: + result = self.phrase_generator.get_adjacent_notes(note, scale_notes) + assert sorted(result) == sorted(expected) + + @pytest.mark.usefixtures("init_module") + def test_find_closest_note_in_direction(self): + """Test that _find_closest_note_in_direction finds correct notes.""" + test_cases = [ + ( + "C", + ["C", "D", "E", "F", "G", "A", "B"], + 1, # direction (higher) + "D", # expected + ), + ("C#", ["C", "D", "E", "F", "G", "A", "B"], -1, "C"), # direction (lower) + ("B", ["C", "D", "E", "F", "G", "A", "A#"], 1, "C"), # direction (lower) + ] + + for note, scale_notes, direction, expected in test_cases: + result = self.phrase_generator._find_closest_note_in_direction( + note, scale_notes, direction + ) + assert result == expected + + @pytest.mark.usefixtures("init_module") + def test_get_next_note(self): + """Test that get_next_note returns correct next note based on chord tones.""" + scale_notes = ["C", "D", "E", "F", "G", "A", "A#"] + + # Case 1: Current note is a chord tone + current_note = "C" + chord_tones = ["C", "E", "G", "A#"] # C7 + + # Run multiple times to ensure random selection works correctly + for _ in range(10): + result = self.phrase_generator.get_next_note( + current_note, scale_notes, chord_tones + ) + # Should be able to move to any scale note except current note + assert result in scale_notes + assert result != current_note + + # Case 2: Current note is not a chord tone + current_note = "D" # Not in C major triad + expected_adjacent = ["C", "E"] # Only adjacent notes in scale + + # Run multiple times to ensure random selection works correctly + for _ in range(10): + result = self.phrase_generator.get_next_note( + current_note, scale_notes, chord_tones + ) + # Should only move to adjacent notes + assert result in expected_adjacent + + # Case 3: Edge case - note at the end of scale + current_note = "B" + chord_tones = ["A", "C", "E", "G"] # Am7 + expected_adjacent = ["A#", "C"] + + for _ in range(10): + result = self.phrase_generator.get_next_note( + current_note, scale_notes, chord_tones + ) + assert result in expected_adjacent + + @pytest.mark.usefixtures("init_module") + def test_select_first_note(self): + """Test that select_first_note returns correct first note. + + Tests the selection of the first note based on previous note and conditions. + """ + scale_notes = ["C", "D", "E", "F", "G", "A", "B"] + chord_tones = ["C", "E", "G"] # C major triad + + # Case 1: prev_note is None + for _ in range(10): + result = self.phrase_generator.select_first_note(scale_notes, chord_tones) + assert result in scale_notes + + # Case 2: prev_note exists and was a chord tone + prev_note = "E" + for _ in range(10): + result = self.phrase_generator.select_first_note( + scale_notes, + chord_tones, + prev_note=prev_note, + prev_note_was_chord_tone=True, + ) + assert result in scale_notes + assert result != prev_note + + # Case 3: prev_note exists, wasn't a chord tone, but is in current chord tones + prev_note = "C" + for _ in range(10): + result = self.phrase_generator.select_first_note( + scale_notes, + chord_tones, + prev_note=prev_note, + prev_note_was_chord_tone=False, + ) + assert result in scale_notes + assert result != prev_note + + # Case 4: prev_note exists, wasn't a chord tone and isn't in current chord tones + prev_note = "C#" + expected_adjacent = ["C", "D"] # Adjacent notes in scale + for _ in range(10): + result = self.phrase_generator.select_first_note( + scale_notes, + chord_tones, + prev_note=prev_note, + prev_note_was_chord_tone=False, + ) + assert result in expected_adjacent + + @pytest.mark.usefixtures("init_module") + def test_generate_phrase(self): + """Test that generate_phrase generates valid melody phrases.""" + # Case 1: First phrase (no previous note) + phrase = self.phrase_generator.generate_phrase( + scale_root="C", + scale_type="major", + chord_root="C", + chord_type="maj7", + length=8, + ) + assert len(phrase) == 8 + assert all(note in Scale.get_scale_notes("C", "major") for note in phrase) + + # Case 2: Phrase after a chord tone + phrase = self.phrase_generator.generate_phrase( + scale_root="A", + scale_type="natural_minor", + chord_root="A", + chord_type="min7", + prev_note="A", + prev_note_was_chord_tone=True, + length=6, + ) + assert len(phrase) == 6 + assert all( + note in Scale.get_scale_notes("A", "natural_minor") for note in phrase + ) + + # Case 3: Phrase after a non-chord tone + for _ in range(10): + phrase = self.phrase_generator.generate_phrase( + scale_root="D", + scale_type="harmonic_minor", + chord_root="A", + chord_type="dom7", + prev_note="G#", + prev_note_was_chord_tone=False, + length=4, + ) + assert len(phrase) == 4 + scale_notes = Scale.get_scale_notes("D", "harmonic_minor") + assert phrase[0] == "G" or phrase[0] == "A" + assert all(note in scale_notes for note in phrase) + + # Case 4: Different lengths + for length in [4, 8, 12]: + phrase = self.phrase_generator.generate_phrase( + scale_root="G", + scale_type="major", + chord_root="G", + chord_type="maj7", + length=length, + ) + assert len(phrase) == length diff --git a/tests/domain/test_music_theory.py b/tests/domain/test_music_theory.py new file mode 100644 index 0000000000000000000000000000000000000000..7e7ae470b07b4ce18ede67febb904c78e1622940 --- /dev/null +++ b/tests/domain/test_music_theory.py @@ -0,0 +1,109 @@ +"""Tests for music theory related classes.""" + +import pytest + +from improvisation_lab.domain.music_theory import ChordTone, Notes, Scale + + +class TestNotes: + def test_notes_values_are_valid(self): + """Test that initializing Notes with invalid note name raises ValueError.""" + with pytest.raises(ValueError): + Notes("H") + + def test_get_note_index_returns_correct_index(self): + assert Notes.get_note_index("C") == 0 + assert Notes.get_note_index("G") == 7 + + def test_get_chromatic_scale_returns_ordered_notes(self): + """Test that get_chromatic_scale returns notes in chromatic order.""" + assert Notes.get_chromatic_scale("C") == [ + "C", + "C#", + "D", + "D#", + "E", + "F", + "F#", + "G", + "G#", + "A", + "A#", + "B", + ] + assert Notes.get_chromatic_scale("G") == [ + "G", + "G#", + "A", + "A#", + "B", + "C", + "C#", + "D", + "D#", + "E", + "F", + "F#", + ] + + def test_get_chromatic_scale_raises_error_for_invalid_note(self): + """Test that get_chromatic_scale raises ValueError for invalid notes.""" + with pytest.raises(ValueError): + Notes.get_chromatic_scale("Z") + + def test_frequency_to_note_name(self): + """Test that frequency_to_note_name returns correct note names.""" + test_cases = [ + (440.0, "A4"), + (261.63, "C4"), + (329.63, "E4"), + (493.88, "B4"), + (880.0, "A5"), + ] + + for frequency, expected_note in test_cases: + assert Notes.convert_frequency_to_note(frequency) == expected_note + + def test_convert_frequency_to_base_note(self): + """Test that convert_frequency_to_base_note returns correct base note names.""" + test_cases = [ + (440.0, "A"), # A4 -> A + (261.63, "C"), # C4 -> C + (329.63, "E"), # E4 -> E + (493.88, "B"), # B4 -> B + (880.0, "A"), # A5 -> A + ] + + for frequency, expected_note in test_cases: + assert Notes.convert_frequency_to_base_note(frequency) == expected_note + + +class TestScale: + def test_get_scale_notes_returns_correct_notes(self): + assert Scale.get_scale_notes("A", "major") == [ + "A", + "B", + "C#", + "D", + "E", + "F#", + "G#", + ] + + +class TestChordTone: + """Tests for ChordTone class.""" + + def test_get_chord_tones_returns_correct_notes(self): + """Test that get_chord_tones returns correct chord tones.""" + test_cases = [ + ("C", "maj", ["C", "E", "G", "A"]), + ("D", "min7", ["D", "F", "A", "C"]), + ("G", "dom7", ["G", "B", "D", "F"]), + ("A", "dim7", ["A", "C", "D#", "F#"]), + ("F", "maj7", ["F", "A", "C", "E"]), + ("B", "min7(b5)", ["B", "D", "F", "A"]), + ] + + for root, chord_type, expected in test_cases: + assert ChordTone.get_chord_tones(root, chord_type) == expected diff --git a/tests/infrastructure/__init__.py b/tests/infrastructure/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4a1d8bb1203697c61675e35e6c76185afd527c7f --- /dev/null +++ b/tests/infrastructure/__init__.py @@ -0,0 +1 @@ +"""Test package for infrastructure layer components.""" diff --git a/tests/infrastructure/audio/__init__.py b/tests/infrastructure/audio/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ed790b60401b20016b574daaf2ad7536d1e62cbb --- /dev/null +++ b/tests/infrastructure/audio/__init__.py @@ -0,0 +1 @@ +"""Test package for audio infrastructure components.""" diff --git a/tests/infrastructure/audio/test_direct_processor.py b/tests/infrastructure/audio/test_direct_processor.py new file mode 100644 index 0000000000000000000000000000000000000000..24bb2cb47b444c605a85e235fde858ebe5fe9cad --- /dev/null +++ b/tests/infrastructure/audio/test_direct_processor.py @@ -0,0 +1,128 @@ +from unittest.mock import Mock, patch + +import numpy as np +import pyaudio +import pytest + +from improvisation_lab.infrastructure.audio import DirectAudioProcessor + + +class TestMicInput: + @pytest.fixture + def init_module(self): + self.mic_input = DirectAudioProcessor(sample_rate=44100) + + @pytest.mark.usefixtures("init_module") + @patch("pyaudio.PyAudio") + def test_start_recording(self, mock_pyaudio): + """Test recording start functionality.""" + self.mic_input.start_recording() + + assert self.mic_input.is_recording + # Verify that the PyAudio settings are correct + mock_pyaudio.return_value.open.assert_called_once_with( + format=pyaudio.paFloat32, + channels=1, + rate=44100, + input=True, + stream_callback=self.mic_input._audio_callback, + ) + + @pytest.mark.usefixtures("init_module") + def test_start_recording_when_already_recording(self): + """Test that starting recording when already recording raises RuntimeError.""" + self.mic_input.is_recording = True + + with pytest.raises(RuntimeError) as exc_info: + self.mic_input.start_recording() + + assert str(exc_info.value) == "Recording is already in progress" + + @pytest.mark.usefixtures("init_module") + def test_audio_callback(self): + """Test that the audio callback is called with the correct data.""" + # Create sample audio data that matches the buffer size + buffer_duration = 0.2 + sample_rate = 44100 + buffer_size = int(sample_rate * buffer_duration) + test_data = np.array([0.1] * buffer_size, dtype=np.float32) + test_bytes = test_data.tobytes() + + # Create a mock callback + mock_callback = Mock() + self.mic_input._callback = mock_callback + + # Call the audio callback + result = self.mic_input._audio_callback(test_bytes, len(test_data), {}, 0) + + # Verify the callback was called with the correct data + # First element of call_args is the first argument of the callback function + np.testing.assert_array_almost_equal(mock_callback.call_args[0][0], test_data) + + # pyaudio.paContinue is an integer constant representing the stream status + # 0: continue, 1: complete, 2: error + assert result == (test_bytes, pyaudio.paContinue) + + @pytest.mark.usefixtures("init_module") + @patch("pyaudio.PyAudio") + def test_stop_recording(self, mock_pyaudio): + """Test recording stop functionality.""" + # First start recording to set up the stream + self.mic_input.start_recording() + + # Now test stopping + self.mic_input.stop_recording() + + # Verify recording state + assert not self.mic_input.is_recording + assert self.mic_input._stream is None + assert self.mic_input.audio is None + + # Verify that stream methods were called + mock_stream = mock_pyaudio.return_value.open.return_value + mock_stream.stop_stream.assert_called_once() + mock_stream.close.assert_called_once() + mock_pyaudio.return_value.terminate.assert_called_once() + + @pytest.mark.usefixtures("init_module") + def test_stop_recording_not_recording(self): + """Test that stopping when not recording raises an error.""" + with pytest.raises(RuntimeError, match="Recording is not in progress"): + self.mic_input.stop_recording() + + @pytest.mark.usefixtures("init_module") + def test_append_to_buffer(self): + """Test appending data to the buffer.""" + initial_data = np.array([0.1, 0.2], dtype=np.float32) + new_data = np.array([0.3, 0.4], dtype=np.float32) + expected_data = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) + + self.mic_input._buffer = initial_data + self.mic_input._append_to_buffer(new_data) + + np.testing.assert_array_almost_equal(self.mic_input._buffer, expected_data) + + @pytest.mark.usefixtures("init_module") + def test_process_buffer(self): + """Test processing buffer when it reaches the desired size.""" + # Setup buffer with more data than buffer_size + buffer_size = self.mic_input._buffer_size + test_data = np.array([0.1] * (buffer_size + 2), dtype=np.float32) + self.mic_input._buffer = test_data + + # Setup mock callback + mock_callback = Mock() + self.mic_input._callback = mock_callback + + # Process buffer + self.mic_input._process_buffer() + + # Verify callback was called with correct data + np.testing.assert_array_almost_equal( + mock_callback.call_args[0][0], test_data[:buffer_size] + ) + + # Verify remaining data in buffer + np.testing.assert_array_almost_equal( + self.mic_input._buffer, test_data[buffer_size:] + ) diff --git a/tests/infrastructure/audio/test_web_processor.py b/tests/infrastructure/audio/test_web_processor.py new file mode 100644 index 0000000000000000000000000000000000000000..7d6c36880371111401c6b8e3fa00369f0647b5b2 --- /dev/null +++ b/tests/infrastructure/audio/test_web_processor.py @@ -0,0 +1,186 @@ +"""Tests for GradioAudioInput class.""" + +from unittest.mock import Mock + +import numpy as np +import pytest + +from improvisation_lab.infrastructure.audio import WebAudioProcessor + + +class TestGradioAudioInput: + @pytest.fixture + def init_module(self): + """Initialize test module.""" + self.sample_rate = 44100 + self.buffer_duration = 0.2 + self.audio_input = WebAudioProcessor( + sample_rate=self.sample_rate, + buffer_duration=self.buffer_duration, + ) + + @pytest.mark.usefixtures("init_module") + def test_initialization(self): + """Test initialization of GradioAudioInput.""" + assert self.audio_input.sample_rate == 44100 + assert not self.audio_input.is_recording + assert self.audio_input._callback is None + assert len(self.audio_input._buffer) == 0 + assert self.audio_input._buffer_size == int(44100 * 0.2) + + @pytest.mark.usefixtures("init_module") + def test_start_recording(self): + """Test recording start functionality.""" + self.audio_input.start_recording() + assert self.audio_input.is_recording + + @pytest.mark.usefixtures("init_module") + def test_start_recording_when_already_recording(self): + """Test that starting recording when already recording raises RuntimeError.""" + self.audio_input.is_recording = True + with pytest.raises(RuntimeError, match="Recording is already in progress"): + self.audio_input.start_recording() + + @pytest.mark.usefixtures("init_module") + def test_stop_recording(self): + """Test recording stop functionality.""" + self.audio_input.is_recording = True + self.audio_input._buffer = np.array([0.1, 0.2], dtype=np.float32) + + self.audio_input.stop_recording() + + assert not self.audio_input.is_recording + assert len(self.audio_input._buffer) == 0 + + @pytest.mark.usefixtures("init_module") + def test_stop_recording_when_not_recording(self): + """Test that stopping recording when not recording raises RuntimeError.""" + with pytest.raises(RuntimeError, match="Recording is not in progress"): + self.audio_input.stop_recording() + + @pytest.mark.usefixtures("init_module") + def test_append_to_buffer(self): + """Test appending data to the buffer.""" + initial_data = np.array([0.1, 0.2], dtype=np.float32) + new_data = np.array([0.3, 0.4], dtype=np.float32) + expected_data = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) + + self.audio_input._buffer = initial_data + self.audio_input._append_to_buffer(new_data) + + np.testing.assert_array_almost_equal(self.audio_input._buffer, expected_data) + + @pytest.mark.usefixtures("init_module") + def test_process_buffer(self): + """Test processing buffer when it reaches the desired size.""" + # Setup buffer with more data than buffer_size + buffer_size = self.audio_input._buffer_size + test_data = np.array([0.1] * (buffer_size + 2), dtype=np.float32) + self.audio_input._buffer = test_data + + # Setup mock callback + mock_callback = Mock() + self.audio_input._callback = mock_callback + + # Process buffer + self.audio_input._process_buffer() + + # Verify callback was called with correct data + np.testing.assert_array_almost_equal( + mock_callback.call_args[0][0], test_data[:buffer_size] + ) + + # Verify remaining data in buffer + np.testing.assert_array_almost_equal( + self.audio_input._buffer, test_data[buffer_size:] + ) + + @pytest.mark.usefixtures("init_module") + def test_resample_audio(self): + """Test audio resampling functionality.""" + # Create test data at 48000 Hz + duration = 0.1 # seconds + original_sr = 48000 + target_sr = 44100 + t = np.linspace(0, duration, int(original_sr * duration)) + test_data = np.sin(2 * np.pi * 440 * t).astype(np.float32) # 440 Hz sine wave + + # Resample to 44100 Hz + resampled_data = self.audio_input._resample_audio( + test_data, original_sr, target_sr + ) + + # Check that the length is correct + expected_length = round(len(test_data) * float(target_sr) / original_sr) + assert len(resampled_data) == expected_length + + # Check that the data is still a float32 array + assert resampled_data.dtype == np.float32 + + # Check that the signal maintains similar characteristics + assert np.allclose( + np.mean(np.abs(test_data)), np.mean(np.abs(resampled_data)), rtol=0.1 + ) + + @pytest.mark.usefixtures("init_module") + def test_normalize_audio_normal_case(self): + """Test audio normalization with non-zero data.""" + # Test data with known max value + test_data = np.array([0.5, -1.0, 0.25], dtype=np.float32) + normalized_data = self.audio_input._normalize_audio(test_data) + + # Check that the maximum absolute value is 1.0 + assert np.max(np.abs(normalized_data)) == 1.0 + # Check that the relative relationships are preserved + np.testing.assert_array_almost_equal( + normalized_data, np.array([0.5, -1.0, 0.25], dtype=np.float32) + ) + + @pytest.mark.usefixtures("init_module") + def test_normalize_audio_empty_array(self): + """Test audio normalization with empty array.""" + test_data = np.array([], dtype=np.float32) + normalized_data = self.audio_input._normalize_audio(test_data) + + assert len(normalized_data) == 0 + assert normalized_data.dtype == np.float32 + + @pytest.mark.usefixtures("init_module") + def test_normalize_audio_zero_array(self): + """Test audio normalization with array of zeros.""" + test_data = np.zeros(5, dtype=np.float32) + normalized_data = self.audio_input._normalize_audio(test_data) + + # Should return the same array of zeros without division + np.testing.assert_array_equal(normalized_data, test_data) + assert normalized_data.dtype == np.float32 + + @pytest.mark.usefixtures("init_module") + def test_remove_low_amplitude_noise_normal_case(self): + """Test noise removal with mixed amplitude signals.""" + # Test data with both high and low amplitude signals + test_data = np.array([5.0, -25.0, 0.5, 30.0, 15.0, 1.0], dtype=np.float32) + processed_data = self.audio_input._remove_low_amplitude_noise(test_data) + + # Values below threshold (20.0) should be zero + expected_data = np.array([0.0, -25.0, 0.0, 30.0, 0.0, 0.0], dtype=np.float32) + np.testing.assert_array_equal(processed_data, expected_data) + + @pytest.mark.usefixtures("init_module") + def test_remove_low_amplitude_noise_all_below_threshold(self): + """Test noise removal when all signals are below threshold.""" + test_data = np.array([1.0, -5.0, 0.5, 10.0, 15.0], dtype=np.float32) + processed_data = self.audio_input._remove_low_amplitude_noise(test_data) + + # All values should be zero as they're below threshold + expected_data = np.zeros_like(test_data) + np.testing.assert_array_equal(processed_data, expected_data) + + @pytest.mark.usefixtures("init_module") + def test_remove_low_amplitude_noise_empty_array(self): + """Test noise removal with empty array.""" + test_data = np.array([], dtype=np.float32) + processed_data = self.audio_input._remove_low_amplitude_noise(test_data) + + assert len(processed_data) == 0 + assert processed_data.dtype == np.float32 diff --git a/tests/presentation/__init__.py b/tests/presentation/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..35e8e6dae312b5ef186109cb521db140985ae132 --- /dev/null +++ b/tests/presentation/__init__.py @@ -0,0 +1 @@ +"""Test Package for Presentation Layer.""" diff --git a/tests/presentation/melody_practice/__init__.py b/tests/presentation/melody_practice/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a6c6034ae65af80d24ee201d90238b10f899c186 --- /dev/null +++ b/tests/presentation/melody_practice/__init__.py @@ -0,0 +1 @@ +"""Test Package for Melody Practice Presentation Layer.""" diff --git a/tests/presentation/melody_practice/test_console_melody_view.py b/tests/presentation/melody_practice/test_console_melody_view.py new file mode 100644 index 0000000000000000000000000000000000000000..c78c15762d14671cdbe24da4a7b9be85dd6b2f2f --- /dev/null +++ b/tests/presentation/melody_practice/test_console_melody_view.py @@ -0,0 +1,54 @@ +import pytest + +from improvisation_lab.domain.composition import PhraseData +from improvisation_lab.presentation.melody_practice.console_melody_view import \ + ConsoleMelodyView +from improvisation_lab.presentation.melody_practice.view_text_manager import \ + ViewTextManager +from improvisation_lab.service.melody_practice_service import PitchResult + + +class TestConsoleMelodyView: + """Tests for the ConsoleMelodyView class.""" + + @pytest.fixture + def init_module(self): + self.text_manager = ViewTextManager() + self.console_view = ConsoleMelodyView(self.text_manager, "Test Song") + + @pytest.mark.usefixtures("init_module") + def test_launch(self, capsys): + self.console_view.launch() + captured = capsys.readouterr() + assert "Generating melody for Test Song:" in captured.out + assert "Sing each note for 1 second!" in captured.out + + @pytest.mark.usefixtures("init_module") + def test_display_phrase_info(self, capsys): + phrases_data = [ + PhraseData( + notes=["C", "E", "G"], + chord_name="Cmaj7", + scale_info="C major", + length=4, + ) + ] + self.console_view.display_phrase_info(0, phrases_data) + captured = capsys.readouterr() + assert "Phrase 1: Cmaj7" in captured.out + assert "C -> E -> G" in captured.out + + @pytest.mark.usefixtures("init_module") + def test_display_pitch_result(self, capsys): + pitch_result = PitchResult( + target_note="C", current_base_note="A", is_correct=False, remaining_time=2.5 + ) + self.console_view.display_pitch_result(pitch_result) + captured = capsys.readouterr() + assert "Target: C | Your note: A | Remaining: 2.5s" in captured.out + + @pytest.mark.usefixtures("init_module") + def test_display_practice_end(self, capsys): + self.console_view.display_practice_end() + captured = capsys.readouterr() + assert "Session Stopped" in captured.out diff --git a/tests/presentation/melody_practice/test_view_text_manager.py b/tests/presentation/melody_practice/test_view_text_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..338c2426a22a7f91893fcb8853a8c1cef17005db --- /dev/null +++ b/tests/presentation/melody_practice/test_view_text_manager.py @@ -0,0 +1,73 @@ +"""Tests for the ViewTextManager class.""" + +import pytest + +from improvisation_lab.domain.composition import PhraseData +from improvisation_lab.presentation.melody_practice.view_text_manager import \ + ViewTextManager +from improvisation_lab.service.melody_practice_service import PitchResult + + +class TestViewTextManager: + + @pytest.fixture + def init_module(self): + self.text_manager = ViewTextManager() + + @pytest.mark.usefixtures("init_module") + def test_initialize_text(self): + self.text_manager.initialize_text() + assert self.text_manager.phrase_text == "No phrase data" + assert self.text_manager.result_text == "Ready to start... (waiting for audio)" + + @pytest.mark.usefixtures("init_module") + def test_terminate_text(self): + self.text_manager.terminate_text() + assert self.text_manager.phrase_text == "Session Stopped" + assert self.text_manager.result_text == "Practice ended" + + @pytest.mark.usefixtures("init_module") + def test_set_waiting_for_audio(self): + self.text_manager.set_waiting_for_audio() + assert self.text_manager.result_text == "Waiting for audio..." + + @pytest.mark.usefixtures("init_module") + def test_update_pitch_result(self): + pitch_result = PitchResult( + target_note="C", current_base_note="A", is_correct=False, remaining_time=2.5 + ) + self.text_manager.update_pitch_result(pitch_result) + assert ( + self.text_manager.result_text + == "Target: C | Your note: A | Remaining: 2.5s" + ) + + @pytest.mark.usefixtures("init_module") + def test_update_phrase_text_no_phrases(self): + result = self.text_manager.update_phrase_text(0, []) + assert result == "No phrase data" + assert self.text_manager.phrase_text == "No phrase data" + + @pytest.mark.usefixtures("init_module") + def test_update_phrase_text_with_phrases(self): + phrases = [ + PhraseData( + notes=["C", "E", "G"], + chord_name="Cmaj7", + scale_info="C major", + length=4, + ), + PhraseData( + notes=["A", "C", "E"], + chord_name="Amin7", + scale_info="A minor", + length=4, + ), + ] + self.text_manager.update_phrase_text(0, phrases) + expected_text = "Phrase 1: Cmaj7\nC -> E -> G\nNext: Amin7 (A)" + assert self.text_manager.phrase_text == expected_text + + self.text_manager.update_phrase_text(1, phrases) + expected_text = "Phrase 2: Amin7\nA -> C -> E" + assert self.text_manager.phrase_text == expected_text diff --git a/tests/presentation/melody_practice/test_web_melody_view.py b/tests/presentation/melody_practice/test_web_melody_view.py new file mode 100644 index 0000000000000000000000000000000000000000..e6cc634e002a8dd68fe54cd595c4d2f34a3dfd10 --- /dev/null +++ b/tests/presentation/melody_practice/test_web_melody_view.py @@ -0,0 +1,65 @@ +import warnings +from unittest.mock import Mock, patch + +import gradio as gr +import pytest + +from improvisation_lab.presentation.melody_practice.web_melody_view import \ + WebMelodyView + + +class TestWebMelodyView: + + @pytest.fixture + def init_module(self): + self.start_callback = Mock(return_value=("Phrase Info", "Note Status")) + self.stop_callback = Mock(return_value=("Session Stopped", "Practice ended")) + self.audio_callback = Mock( + return_value=("Audio Phrase Info", "Audio Note Status") + ) + self.web_view = WebMelodyView( + on_generate_melody=self.start_callback, + on_end_practice=self.stop_callback, + on_audio_input=self.audio_callback, + song_name="Test Song", + ) + + @pytest.mark.usefixtures("init_module") + def test_build_interface(self): + warnings.simplefilter("ignore", category=DeprecationWarning) + app = self.web_view._build_interface() + assert isinstance(app, gr.Blocks) + + @pytest.mark.usefixtures("init_module") + @patch("gradio.Markdown") + def test_create_header(self, mock_markdown): + self.web_view._add_header() + mock_markdown.assert_called_once_with( + "# Test Song Melody Practice\nSing each note for 1 second!" + ) + + @pytest.mark.usefixtures("init_module") + def test_create_status_section(self): + self.web_view._build_interface() + assert isinstance(self.web_view.phrase_info_box, gr.Textbox) + assert isinstance(self.web_view.pitch_result_box, gr.Textbox) + + @pytest.mark.usefixtures("init_module") + def test_create_control_buttons(self): + self.web_view._build_interface() + self.web_view.on_generate_melody() + self.start_callback.assert_called_once() + self.web_view.on_end_practice() + self.stop_callback.assert_called_once() + + @pytest.mark.usefixtures("init_module") + def test_create_audio_input(self): + self.web_view._build_interface() + self.web_view.on_audio_input() + self.audio_callback.assert_called_once() + + @pytest.mark.usefixtures("init_module") + def test_launch(self, mocker): + mocker.patch.object(gr.Blocks, "launch", return_value=None) + self.web_view.launch() + gr.Blocks.launch.assert_called_once() diff --git a/tests/service/__init__.py b/tests/service/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bad5751adbdf1f26404effbc1dfe2a8cdc464860 --- /dev/null +++ b/tests/service/__init__.py @@ -0,0 +1 @@ +"""Tests for the application layer.""" diff --git a/tests/service/test_melody_practice_service.py b/tests/service/test_melody_practice_service.py new file mode 100644 index 0000000000000000000000000000000000000000..a650df628c4f65ac97168e487fccd3d5aac9df8d --- /dev/null +++ b/tests/service/test_melody_practice_service.py @@ -0,0 +1,110 @@ +"""Tests for MelodyPracticeService.""" + +import time + +import numpy as np +import pytest + +from improvisation_lab.config import Config +from improvisation_lab.service.melody_practice_service import ( + MelodyPracticeService, PitchResult) + + +class TestMelodyService: + @pytest.fixture + def init_module(self): + """Create MelodyService instance for testing.""" + config = Config() + self.service = MelodyPracticeService(config) + + @pytest.mark.usefixtures("init_module") + def test_generate_melody(self): + """Test melody generation.""" + phrases = self.service.generate_melody() + assert len(phrases) > 0 + assert all(hasattr(phrase, "notes") for phrase in phrases) + assert all(hasattr(phrase, "chord_name") for phrase in phrases) + assert all(hasattr(phrase, "scale_info") for phrase in phrases) + assert all(hasattr(phrase, "length") for phrase in phrases) + + @pytest.mark.usefixtures("init_module") + def test_process_audio_no_voice(self): + """Test processing audio with no voice detected.""" + audio_data = np.zeros(1024, dtype=np.float32) + result = self.service.process_audio(audio_data, target_note="A") + + assert isinstance(result, PitchResult) + assert result.current_base_note is None + assert not result.is_correct + + @pytest.mark.usefixtures("init_module") + def test_process_audio_with_voice(self): + """Test processing audio with voice detected.""" + sample_rate = 44100 + duration = 0.1 + t = np.linspace(0, duration, int(sample_rate * duration)) + audio_data = np.sin(2 * np.pi * 440 * t) + + result = self.service.process_audio(audio_data, target_note="A") + + assert isinstance(result, PitchResult) + assert result.current_base_note == "A" + assert result.is_correct + + @pytest.mark.usefixtures("init_module") + def test_process_audio_incorrect_pitch(self): + """Test processing audio with incorrect pitch.""" + sample_rate = 44100 + duration = 0.1 + t = np.linspace(0, duration, int(sample_rate * duration)) + # Generate 440Hz (A4) when target is C4 + audio_data = np.sin(2 * np.pi * 440 * t) + + result = self.service.process_audio(audio_data, target_note="C") + + assert isinstance(result, PitchResult) + assert result.current_base_note == "A" + assert not result.is_correct + assert result.remaining_time == self.service.config.audio.note_duration + + @pytest.mark.usefixtures("init_module") + def test_correct_pitch_timing(self): + """Test timing behavior with correct pitch.""" + sample_rate = 44100 + duration = 0.1 + t = np.linspace(0, duration, int(sample_rate * duration)) + audio_data = np.sin(2 * np.pi * 440 * t) + + # First detection + result1 = self.service.process_audio(audio_data, target_note="A") + initial_time = self.service.correct_pitch_start_time + assert result1.is_correct + assert result1.remaining_time == self.service.config.audio.note_duration + + # Wait a bit + time.sleep(0.5) + + # Second detection + result2 = self.service.process_audio(audio_data, target_note="A") + assert result2.is_correct + assert result2.remaining_time < self.service.config.audio.note_duration + assert initial_time == self.service.correct_pitch_start_time + + @pytest.mark.usefixtures("init_module") + def test_correct_pitch_completion(self): + """Test completion of correct pitch duration.""" + sample_rate = 44100 + duration = 0.1 + t = np.linspace(0, duration, int(sample_rate * duration)) + audio_data = np.sin(2 * np.pi * 440 * t) + + # First detection + result1 = self.service.process_audio(audio_data, target_note="A") + assert result1.remaining_time == self.service.config.audio.note_duration + + # Wait for full duration + time.sleep(self.service.config.audio.note_duration + 0.1) + + # Final detection + result2 = self.service.process_audio(audio_data, target_note="A") + assert result2.remaining_time == 0 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000000000000000000000000000000000000..066e17ee49e86235d88f87ce25e3c95be8d9a873 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,86 @@ +"""Tests for configuration module.""" + +import pytest +import yaml + +from improvisation_lab.config import AudioConfig, Config + + +class TestConfig: + @pytest.fixture + def sample_config_file(self, tmp_path): + """Create a sample config file for testing.""" + config_data = { + "audio": { + "sample_rate": 48000, + "buffer_duration": 0.3, + "note_duration": 4, + }, + "selected_song": "test_song", + "chord_progressions": { + "test_song": [ + ["C", "major", "C", "maj7", 4], + ] + }, + } + config_file = tmp_path / "test_config.yml" + with open(config_file, "w") as f: + yaml.dump(config_data, f) + return config_file + + def test_load_config_from_file(self, sample_config_file): + """Test loading configuration from a YAML file.""" + config = Config(config_path=sample_config_file) + + assert config.audio.sample_rate == 48000 + assert config.audio.buffer_duration == 0.3 + assert config.audio.note_duration == 4 + assert config.selected_song == "test_song" + assert "test_song" in config.chord_progressions + + def test_load_config_with_defaults(self): + """Test loading configuration with default values when file doesn't exist.""" + config = Config(config_path="nonexistent.yml") + + assert config.audio.sample_rate == 44100 + assert config.audio.buffer_duration == 0.2 + assert config.audio.note_duration == 1.0 + assert config.selected_song == "fly_me_to_the_moon" + assert "fly_me_to_the_moon" in config.chord_progressions + + def test_audio_config_from_yaml(self): + """Test creating AudioConfig from YAML data.""" + yaml_data = { + "sample_rate": 48000, + "buffer_duration": 0.3, + "note_duration": 4, + } + audio_config = AudioConfig.from_yaml(yaml_data) + + assert audio_config.sample_rate == 48000 + assert audio_config.buffer_duration == 0.3 + assert audio_config.note_duration == 4 + + def test_pitch_detector_config_from_yaml(self): + """Test creating PitchDetector config from YAML data.""" + yaml_data = { + "sample_rate": 44100, + "buffer_duration": 0.2, + "note_duration": 3, + "pitch_detector": { + "hop_length": 256, + "threshold": 0.01, + "f0_min": 100, + "f0_max": 800, + "device": "cpu", + }, + } + audio_config = AudioConfig.from_yaml(yaml_data) + + assert audio_config.pitch_detector.hop_length == 256 + assert audio_config.pitch_detector.threshold == 0.01 + assert audio_config.pitch_detector.f0_min == 100 + assert audio_config.pitch_detector.f0_max == 800 + assert audio_config.pitch_detector.device == "cpu" + # 未指定のパラメータはデフォルト値を保持 + assert audio_config.pitch_detector.decoder_mode == "local_argmax"