diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c46716e..2bb2c37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,36 +3,36 @@ name: ci on: push jobs: - test: - runs-on: [self-hosted, gpu] - steps: - - uses: actions/checkout@v3 + # test: + # runs-on: [self-hosted, gpu] + # steps: + # - uses: actions/checkout@v4 - - name: test gpu is available - run: nvidia-smi + # - name: test gpu is available + # run: nvidia-smi - - name: build image - run: make build + # - name: build image + # run: make build - - name: test-no-docker - run: make test-no-docker + # - name: test-no-docker + # run: make test-no-docker - - name: test - run: make test + # - name: test + # run: make test - - name: test-no-gpu - run: make test-no-gpu + # - name: test-no-gpu + # run: make test-no-gpu publish-to-pypi-and-github-release: if: "startsWith(github.ref, 'refs/tags/')" runs-on: ubuntu-latest - needs: test + # needs: test steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' - name: Install pypa/build run: python -m pip install --upgrade setuptools build twine diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..4949071 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,47 @@ +stages: + # - build + - test + +variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + PRE_COMMIT_HOME: "$CI_PROJECT_DIR/.cache/pre-commit" + RUFF_CACHE_DIR: "$CI_PROJECT_DIR/.cache/ruff_cache" + MYPY_CACHE_DIR: "$CI_PROJECT_DIR/.cache/mypy_cache" + +cache: + key: $CI_PROJECT_NAME + paths: + - .cache/ + + +test: + stage: test + needs: [] + tags: + - u60-docker-gpu + image: tandav/pitch-detectors:12.4.1-cudnn-devel-ubuntu22.0-python3.12 + variables: + PITCH_DETECTORS_SPICE_MODEL_PATH: /models/spice_model + PITCH_DETECTORS_PENN_CHECKPOINT_PATH: /models/fcnf0++.pt + script: + - export $(grep -v '^#' $S3_ENV | xargs) && python scripts/download_models.py + - pytest --cov pitch_detectors --cov-report term --cov-report xml --junitxml report.xml + coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' + artifacts: + when: always + expire_in: 1 week + reports: + coverage_report: + coverage_format: cobertura + path: coverage.xml + junit: report.xml + +lint: + stage: test + needs: [] + image: python:3.12@sha256:fce9bc7648ef917a5ab67176cf1c7eb41b110452e259736144bc22f32f3aa622 + variables: + PIP_INDEX_URL: https://pypi.tandav.me/index/ + script: + - pip install .[dev] + - pre-commit run --all-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d53331..1ae565a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.6.0 hooks: - id: check-added-large-files - id: check-yaml @@ -18,47 +18,46 @@ repos: - id: detect-private-key - id: double-quote-string-fixer - id: name-tests-test - - id: requirements-txt-fixer - repo: https://github.com/asottile/add-trailing-comma - rev: v2.3.0 + rev: v3.1.0 hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.1.0 + rev: v3.16.0 hooks: - id: pyupgrade - - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.7.0 + - repo: https://github.com/hhatto/autopep8 + rev: v2.3.1 hooks: - id: autopep8 - repo: https://github.com/PyCQA/autoflake - rev: v1.7.6 + rev: v2.3.1 hooks: - id: autoflake - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.254 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.5.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/PyCQA/pylint - rev: v2.17.0 + rev: v3.2.4 hooks: - id: pylint additional_dependencies: ["pylint-per-file-ignores"] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.982 + rev: v1.10.1 hooks: - id: mypy additional_dependencies: [types-redis, types-tabulate, pydantic] - - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.302 - hooks: - - id: pyright + # - repo: https://github.com/RobertCraigie/pyright-python + # rev: v1.1.369 + # hooks: + # - id: pyright diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index eaf93c0..0000000 --- a/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04 - -# https://github.com/NVIDIA/nvidia-docker/wiki/Usage -# https://github.com/NVIDIA/nvidia-docker/issues/531 -ENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility - -RUN apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common && \ - add-apt-repository -y ppa:deadsnakes/ppa && \ - apt-get install -y python3.10-dev python3.10-venv libsndfile-dev libasound-dev portaudio19-dev - -# https://pythonspeed.com/articles/activate-virtualenv-dockerfile/ -ENV VIRTUAL_ENV=/venv -RUN python3.10 -m venv $VIRTUAL_ENV -ENV PATH="$VIRTUAL_ENV/bin:$PATH" - -WORKDIR /app -COPY pyproject.toml . - -RUN --mount=type=cache,target=/root/.cache/pip \ - pip install --upgrade pip setuptools wheel && \ - pip install .[dev] - -COPY pitch_detectors /app/pitch_detectors - -RUN --mount=type=cache,target=/root/.cache/pip \ - pip install --no-deps . - -COPY tests /app/tests -COPY scripts/ /app/scripts -COPY data /app/data diff --git a/Makefile b/Makefile deleted file mode 100644 index 395ae66..0000000 --- a/Makefile +++ /dev/null @@ -1,56 +0,0 @@ -IMAGE = tandav/pitch-detectors:11.8.0-cudnn8-devel-ubuntu22.04 - -.PHONY: build -build: - DOCKER_BUILDKIT=1 docker build --progress=plain -t $(IMAGE) . - -.PHONY: push -push: - docker push $(IMAGE) - docker push tandav/pitch-detectors:latest - -# python -m pitch_detectors.util ld_library_path - -.PHONY: test-no-docker -test-no-docker: - /home/tandav/.cache/.virtualenvs/pitch-detectors/bin/python -m pytest -c no_docker_pytest.ini -x -v --cov pitch_detectors - -.PHONY: test -test: build - docker run --rm -t --gpus all \ - -e PITCH_DETECTORS_GPU=true \ - -e PITCH_DETECTORS_GPU_MEMORY_LIMIT=true \ - -v /home/tandav/docs/bhairava/libmv/data/fcnf0++.pt:/fcnf0++.pt:ro \ - -v /home/tandav/docs/bhairava/libmv/data/spice_model:/spice_model:ro \ - $(IMAGE) \ - pytest -x -v --cov pitch_detectors - -.PHONY: test-no-gpu -test-no-gpu: build - docker run --rm -t \ - -e PITCH_DETECTORS_GPU=false \ - -v /home/tandav/docs/bhairava/libmv/data/fcnf0++.pt:/fcnf0++.pt:ro \ - -v /home/tandav/docs/bhairava/libmv/data/spice_model:/spice_model:ro \ - $(IMAGE) \ - pytest -v --cov pitch_detectors - -.PHONY: evaluation -evaluation: build - eval "$$(cat .env)"; \ - docker run --rm -t --gpus all \ - -e PITCH_DETECTORS_GPU=true \ - -e REDIS_URL=$$REDIS_URL \ - -v /media/tandav/sg8tb1/downloads-archive/f0-datasets:/app/f0-datasets:ro \ - $(IMAGE) \ - python -m pitch_detectors.evaluation - -.PHONY: table -table: - eval "$$(cat .env)"; \ - REDIS_URL=$$REDIS_URL \ - python -m pitch_detectors.evaluation.table - -.PHONY: bumpver -bumpver: - # usage: make bumpver PART=minor - bumpver update --no-fetch --$(PART) diff --git a/README.md b/README.md index 1a915b7..9f7da83 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![pipeline status](https://gitlab.tandav.me/pitchtrack/pitch-detectors/badges/master/pipeline.svg)](https://gitlab.tandav.me/pitchtrack/pitch-detectors/-/commits/master) + # pitch-detectors collection of pitch (f0, fundamental frequency) detection algorithms with unified interface @@ -50,3 +52,8 @@ plt.show() ## additional features - [ ] robust (vote + median) ensemble algorithm using all models - [ ] json import/export + +## notes: +Tests are running in subprocess (using `scripts/run_algorithm.py`) to avoid pytorch cuda import caching. +It's difficult to disable gpu after it has been initialized. (https://github.com/pytorch/pytorch/issues/9158) +It is also difficult to set correct PATH and LD_LIBRARY_PATH without a subprocess. diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..80ac361 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,51 @@ +version: '3' +dotenv: ['.env'] +vars: + BASE_IMAGE: tandav/pitch-detectors-base:12.4.1-cudnn-devel-ubuntu22.0-python3.12 + IMAGE: tandav/pitch-detectors:12.4.1-cudnn-devel-ubuntu22.0-python3.12 + +tasks: + build-base: + cmd: docker build --tag {{.BASE_IMAGE}} --file docker/base.dockerfile . + + push-base: + cmd: docker push {{.BASE_IMAGE}} + + build: + cmd: docker build --build-arg="BASE_IMAGE={{.BASE_IMAGE}}" --tag {{.IMAGE}} --file docker/pitch-detectors.dockerfile . + + push: + cmd: docker push {{.IMAGE}} + + test: + deps: [build] + cmd: > + docker run --rm -t --gpus all + -v /media/tandav/sg8tb1/downloads-archive/libmv-data/spice_model:/spice_model:ro + -v /media/tandav/sg8tb1/downloads-archive/libmv-data/fcnf0++.pt:/fcnf0++.pt:ro + {{.IMAGE}} + pytest -v + + test-no-docker: + cmd: pytest -v + + freeze: + cmd: docker run --rm -t --gpus all {{.IMAGE}} /venv/bin/pip freeze > freeze.txt + + bumpver: + desc: 'Bump version. Pass --. Usage example: task bumpver -- --minor' + cmds: + - bumpver update --no-fetch {{.CLI_ARGS}} + + evaluation: + deps: [build] + cmd: > + docker run --rm -t --gpus all + -e PITCH_DETECTORS_GPU=true + -e REDIS_URL={{.REDIS_URL}} + -v /media/tandav/sg8tb1/downloads-archive/f0-datasets:/app/f0-datasets:ro + {{.IMAGE}} + python -m pitch_detectors.evaluation + + table: + cmd: python -m pitch_detectors.evaluation.table diff --git a/docker/base.dockerfile b/docker/base.dockerfile new file mode 100644 index 0000000..fa18eeb --- /dev/null +++ b/docker/base.dockerfile @@ -0,0 +1,21 @@ +FROM nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04@sha256:0a1cb6e7bd047a1067efe14efdf0276352d5ca643dfd77963dab1a4f05a003a4 + +# https://github.com/NVIDIA/nvidia-docker/wiki/Usage +# https://github.com/NVIDIA/nvidia-docker/issues/531 +ENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility +ENV DEBIAN_FRONTEND=noninteractive + +ARG PYTHON_VERSION=3.12 + +RUN apt-get update && \ + apt-get install -y software-properties-common && \ + add-apt-repository -y ppa:deadsnakes/ppa && \ + apt-get install -y python${PYTHON_VERSION}-dev python${PYTHON_VERSION}-venv libsndfile-dev libasound-dev portaudio19-dev + +# this is only need for crepe @ git+https://github.com/tandav/crepe +RUN apt-get install -y git + +# https://pythonspeed.com/articles/activate-virtualenv-dockerfile/ +ENV VIRTUAL_ENV=/venv +RUN python${PYTHON_VERSION} -m venv $VIRTUAL_ENV +ENV PATH="$VIRTUAL_ENV/bin:$PATH" diff --git a/docker/pitch-detectors.dockerfile b/docker/pitch-detectors.dockerfile new file mode 100644 index 0000000..d29c176 --- /dev/null +++ b/docker/pitch-detectors.dockerfile @@ -0,0 +1,18 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} + +WORKDIR /app +COPY pyproject.toml . +ENV PIP_INDEX_URL=https://pypi.tandav.me/index/ +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --upgrade pip setuptools wheel && \ + pip install .[dev] + +COPY pitch_detectors /app/pitch_detectors + +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --no-deps . + +COPY tests /app/tests +COPY scripts/ /app/scripts +COPY data /app/data diff --git a/freeze.txt b/freeze.txt new file mode 100644 index 0000000..b889a1c --- /dev/null +++ b/freeze.txt @@ -0,0 +1,150 @@ +absl-py==2.1.0 +aiobotocore==2.13.1 +aiohttp==3.9.5 +aioitertools==0.11.0 +aiosignal==1.3.1 +AMFM_decompy==1.0.11 +annotated-types==0.7.0 +apprise==1.8.0 +astunparse==1.6.3 +attrs==23.2.0 +audioread==3.0.1 +botocore==1.34.131 +bumpver==2023.1129 +certifi==2024.6.2 +cffi==1.16.0 +cfgv==3.4.0 +charset-normalizer==3.3.2 +click==8.1.7 +colorama==0.4.6 +colortool==0.6.0 +contourpy==1.2.1 +coverage==7.5.4 +crepe @ git+https://github.com/tandav/crepe@ca79a30daa5e4de061c5a7a26ecc8065011c3697 +cycler==0.12.1 +Cython==3.0.10 +decorator==5.1.1 +distlib==0.3.8 +dsplib==0.9.0 +filelock==3.15.4 +flatbuffers==24.3.25 +fonttools==4.53.0 +frozenlist==1.4.1 +fsspec==2024.6.1 +future==1.0.0 +gast==0.6.0 +google-pasta==0.2.0 +grpcio==1.64.1 +h5py==3.11.0 +hmmlearn==0.3.2 +huggingface-hub==0.23.4 +identify==2.5.36 +idna==3.7 +imageio==2.34.2 +iniconfig==2.0.0 +Jinja2==3.1.4 +jmespath==1.0.1 +joblib==1.4.2 +keras==3.4.1 +kiwisolver==1.4.5 +lazy_loader==0.4 +lexid==2021.1006 +libclang==18.1.1 +librosa==0.10.2.post1 +llvmlite==0.43.0 +looseversion==1.3.0 +Markdown==3.6 +markdown-it-py==3.0.0 +MarkupSafe==2.1.5 +matplotlib==3.9.0 +mdurl==0.1.2 +mido==1.3.2 +mir_eval==0.7 +ml-dtypes==0.3.2 +mpmath==1.3.0 +msgpack==1.0.8 +multidict==6.0.5 +musiclib==2.1.0 +namex==0.0.8 +networkx==3.3 +nodeenv==1.9.1 +numba==0.60.0 +numpy==1.26.4 +nvidia-cublas-cu12==12.1.3.1 +nvidia-cuda-cupti-cu12==12.1.105 +nvidia-cuda-nvrtc-cu12==12.1.105 +nvidia-cuda-runtime-cu12==12.1.105 +nvidia-cudnn-cu12==8.9.2.26 +nvidia-cufft-cu12==11.0.2.54 +nvidia-curand-cu12==10.3.2.106 +nvidia-cusolver-cu12==11.4.5.107 +nvidia-cusparse-cu12==12.1.0.106 +nvidia-nccl-cu12==2.20.5 +nvidia-nvjitlink-cu12==12.5.40 +nvidia-nvtx-cu12==12.1.105 +oauthlib==3.2.2 +opseq==0.1.2 +opt-einsum==3.3.0 +optree==0.11.0 +packaging==23.2 +penn==0.0.13 +pillow==10.3.0 +pitch-detectors @ file:///app +platformdirs==4.2.2 +pluggy==1.5.0 +pooch==1.8.2 +praat-parselmouth==0.4.3 +pre-commit==3.7.1 +protobuf==4.25.3 +pycparser==2.22 +pydantic==2.7.4 +pydantic_core==2.18.4 +Pygments==2.18.0 +pyparsing==3.1.2 +pyreaper==0.0.10 +pysptk==1.0.1 +pytest==8.2.2 +pytest-cov==5.0.0 +pytest-env==1.1.3 +pytest-order==1.2.1 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +pyworld==0.3.4 +PyYAML==6.0.1 +redis==5.0.7 +requests==2.32.3 +requests-oauthlib==2.0.0 +resampy==0.4.3 +rich==13.7.1 +s3fs==2024.6.1 +scikit-learn==1.5.0 +scipy==1.14.0 +setuptools==70.1.1 +six==1.16.0 +soundfile==0.12.1 +soxr==0.3.7 +svg.py==1.4.3 +sympy==1.12.1 +tabulate==0.9.0 +tensorboard==2.16.2 +tensorboard-data-server==0.7.2 +tensorflow==2.16.2 +tensorflow-hub==0.16.1 +termcolor==2.4.0 +tf_keras==2.16.0 +threadpoolctl==3.5.0 +toml==0.10.2 +torch==2.3.1 +torch-yin==0.1.3 +torchaudio==2.3.1 +torchcrepe==0.0.23 +torchutil==0.0.13 +tqdm==4.66.4 +typing_extensions==4.12.2 +urllib3==2.2.2 +virtualenv==20.26.3 +Werkzeug==3.0.3 +wheel==0.43.0 +wrapt==1.16.0 +yapecs==0.0.8 +yarl==1.9.4 diff --git a/no_docker_pytest.ini b/no_docker_pytest.ini deleted file mode 100644 index 8dacef1..0000000 --- a/no_docker_pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -[pytest] -env = - PITCH_DETECTORS_PENN_CHECKPOINT_PATH=/home/tandav/docs/bhairava/libmv/data/fcnf0++.pt - PITCH_DETECTORS_SPICE_MODEL_PATH=/home/tandav/docs/bhairava/libmv/data/spice_model diff --git a/pitch_detectors/algorithms/ensemble.py b/pitch_detectors/algorithms/ensemble.py index b497b73..ac7140a 100644 --- a/pitch_detectors/algorithms/ensemble.py +++ b/pitch_detectors/algorithms/ensemble.py @@ -9,7 +9,6 @@ from pitch_detectors.schemas import F0 PDT: TypeAlias = type[PitchDetector] -AlgoDict: TypeAlias = dict[PDT, PitchDetector] | dict[PDT, F0] | dict def vote_and_median( @@ -63,7 +62,7 @@ def __init__( self, a: np.ndarray, fs: int, - algorithms: tuple[PDT, ...], + algorithms: tuple[PDT, ...] | None = None, algorithms_kwargs: dict[PDT, dict[str, Any]] | None = None, gpu: bool | None = None, vote_and_median_kwargs: dict[str, Any] | None = None, @@ -72,10 +71,15 @@ def __init__( TorchGPU.__init__(self, gpu) PitchDetector.__init__(self, a, fs) + if algorithms is None: + from pitch_detectors.algorithms import ALGORITHMS as algorithms_ + else: + algorithms_ = algorithms + self._algorithms = {} algorithms_kwargs = algorithms_kwargs or {} - for cls in algorithms: + for cls in algorithms_: self._algorithms[cls] = cls(a, fs, **algorithms_kwargs.get(cls, {})) f0 = vote_and_median( diff --git a/pitch_detectors/algorithms/penn.py b/pitch_detectors/algorithms/penn.py index 621a12b..697e9fe 100644 --- a/pitch_detectors/algorithms/penn.py +++ b/pitch_detectors/algorithms/penn.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import numpy as np @@ -28,13 +29,14 @@ def __init__( if checkpoint is None: checkpoint = os.environ.get('PITCH_DETECTORS_PENN_CHECKPOINT_PATH', '/fcnf0++.pt') + checkpoint_path = Path(checkpoint) f0, periodicity = from_audio( audio=torch.tensor(a.reshape(1, -1)), sample_rate=fs, fmin=hz_min, fmax=hz_max, - checkpoint=checkpoint, + checkpoint=checkpoint_path, gpu=0 if self.gpu else None, ) if self.gpu: diff --git a/pitch_detectors/algorithms/reaper.py b/pitch_detectors/algorithms/reaper.py index ac2697c..3db0a98 100644 --- a/pitch_detectors/algorithms/reaper.py +++ b/pitch_detectors/algorithms/reaper.py @@ -15,9 +15,9 @@ def __init__( hz_max: float = config.HZ_MAX, ): import pyreaper - from dsplib.scale import minmax_scaler + from dsplib.scale import minmax_scaler_array int16_info = np.iinfo(np.int16) - a = minmax_scaler(a, np.min(a), np.max(a), int16_info.min, int16_info.max).round().astype(np.int16) + a = minmax_scaler_array(a, np.min(a), np.max(a), int16_info.min, int16_info.max).round().astype(np.int16) super().__init__(a, fs) pm_times, pm, f0_times, f0, corr = pyreaper.reaper(self.a, fs=self.fs, minf0=hz_min, maxf0=hz_max, frame_period=0.01) f0[f0 == -1] = np.nan diff --git a/pitch_detectors/algorithms/spice.py b/pitch_detectors/algorithms/spice.py index 31b8d08..f572bd1 100644 --- a/pitch_detectors/algorithms/spice.py +++ b/pitch_detectors/algorithms/spice.py @@ -7,7 +7,13 @@ class Spice(TensorflowGPU, PitchDetector): - """https://ai.googleblog.com/2019/11/spice-self-supervised-pitch-estimation.html""" + """ + https://ai.googleblog.com/2019/11/spice-self-supervised-pitch-estimation.html + https://blog.tensorflow.org/2020/06/estimating-pitch-with-spice-and-tensorflow-hub.html + https://github.com/tensorflow/docs/blob/master/site/en/hub/tutorials/spice.ipynb + https://www.kaggle.com/models/google/spice + https://www.kaggle.com/models/google/spice/tensorFlow1/spice/2 + """ def __init__( self, @@ -25,21 +31,20 @@ def __init__( TensorflowGPU.__init__(self, gpu) PitchDetector.__init__(self, a, fs) - import tensorflow as tf import tensorflow_hub as hub if spice_model_path is None: spice_model_path = os.environ.get('PITCH_DETECTORS_SPICE_MODEL_PATH', '/spice_model') model = hub.load(spice_model_path) - model_output = model.signatures['serving_default'](tf.constant(a, tf.float32)) + model_output = model.signatures['serving_default'](self.tf.constant(a, self.tf.float32)) confidence = 1.0 - model_output['uncertainty'] self.f0 = self.output2hz(model_output['pitch'].numpy()) self.f0[confidence < confidence_threshold] = np.nan self.t = np.linspace(0, self.seconds, self.f0.shape[0]) + @staticmethod def output2hz( - self, pitch_output: np.ndarray, pt_offset: float = 25.58, pt_slope: float = 63.07, diff --git a/pitch_detectors/evaluation/__main__.py b/pitch_detectors/evaluation/__main__.py index 2efa2f6..a615cf4 100644 --- a/pitch_detectors/evaluation/__main__.py +++ b/pitch_detectors/evaluation/__main__.py @@ -5,7 +5,7 @@ import mir_eval import numpy as np import tqdm -from dsplib.scale import minmax_scaler +from dsplib.scale import minmax_scaler_array from redis import Redis from pitch_detectors import algorithms @@ -59,7 +59,7 @@ def evaluate_one( wav = dataset.load_wav(wav_path) seconds = len(wav.a) / wav.fs rescale = 100000 - a = minmax_scaler(wav.a, wav.a.min(), wav.a.max(), -rescale, rescale).astype(np.float32) + a = minmax_scaler_array(wav.a, wav.a.min(), wav.a.max(), -rescale, rescale).astype(np.float32) true = dataset.load_true(wav_path, seconds) pitch = algorithm(a, wav.fs) f0 = resample_f0(pitch, t_resampled=true.t) diff --git a/pitch_detectors/schemas.py b/pitch_detectors/schemas.py index c2fb9b9..d1d6cfd 100644 --- a/pitch_detectors/schemas.py +++ b/pitch_detectors/schemas.py @@ -1,13 +1,12 @@ -from typing import Any - import numpy as np from pydantic import BaseModel -from pydantic import root_validator +from pydantic import ConfigDict +from pydantic import model_validator +from typing_extensions import Self class ArbitraryBaseModel(BaseModel): - class Config: - arbitrary_types_allowed = True + model_config = ConfigDict(arbitrary_types_allowed=True) class Wav(ArbitraryBaseModel): @@ -19,11 +18,11 @@ class F0(ArbitraryBaseModel): t: np.ndarray f0: np.ndarray - @root_validator - def check_shape(cls, values: dict[str, Any]) -> dict[str, Any]: # pylint: disable=no-self-argument - if values['t'].shape != values['f0'].shape: + @model_validator(mode='after') + def check_shape(self) -> Self: + if self.t.shape != self.f0.shape: raise ValueError('t and f0 must have the same shape') - return values + return self class Record(ArbitraryBaseModel): diff --git a/pitch_detectors/util.py b/pitch_detectors/util.py index cb190be..cdaf12c 100644 --- a/pitch_detectors/util.py +++ b/pitch_detectors/util.py @@ -1,10 +1,9 @@ import hashlib import math -import sys from pathlib import Path import numpy as np -from dsplib.scale import minmax_scaler +from dsplib.scale import minmax_scaler_array from scipy.io import wavfile @@ -18,7 +17,7 @@ def none_to_nan(x: list[float | None]) -> list[float]: def load_wav(path: Path | str, rescale: float = 100000) -> tuple[int, np.ndarray]: fs, a = wavfile.read(path) - a = minmax_scaler(a, a.min(), a.max(), -rescale, rescale).astype(np.float32) + a = minmax_scaler_array(a, a.min(), a.max(), -rescale, rescale).astype(np.float32) return fs, a @@ -35,32 +34,3 @@ def source_hashes() -> dict[str, str]: h.update(p.read_bytes()) hashes[p.stem] = h.hexdigest() return hashes - - -def ld_library_path() -> str: - site_packages = f'{sys.exec_prefix}/lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages' - libs = [ - f'{site_packages}/nvidia/curand/lib', - f'{site_packages}/nvidia/cuda_runtime/lib', - f'{site_packages}/nvidia/cusparse/lib', - f'{site_packages}/nvidia/cudnn/lib', - f'{site_packages}/nvidia/cuda_nvrtc/lib', - f'{site_packages}/nvidia/cuda_cupti/lib', - f'{site_packages}/nvidia/nccl/lib', - f'{site_packages}/nvidia/cusolver/lib', - f'{site_packages}/nvidia/nvtx/lib', - f'{site_packages}/nvidia/cufft/lib', - f'{site_packages}/nvidia/cublas/lib', - f'{site_packages}/tensorrt', - ] - return ':'.join(libs) - - -if __name__ == '__main__': - supported_actions = {'ld_library_path'} - if len(sys.argv) != 2: # noqa: PLR2004 - raise ValueError('Pass action as argument. Supported_actions:', supported_actions) - if sys.argv[1] == 'ld_library_path': - print(ld_library_path()) - else: - raise ValueError(f'Action {sys.argv[1]} not supported. Supported_actions:', supported_actions) diff --git a/pyproject.toml b/pyproject.toml index 9c6ddac..efe3f34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,29 +5,26 @@ authors = [ {name = "Alexander Rodionov", email = "tandav@tandav.me"}, ] description = "collection of pitch detection algorithms with unified interface" -requires-python = ">=3.8,<3.11" +requires-python = ">=3.8,<3.13" dependencies = [ "AMFM-decompy", - "crepe", - "dsplib>=0.7.2", + "crepe @ git+https://github.com/tandav/crepe@ca79a30daa5e4de061c5a7a26ecc8065011c3697", + "dsplib>=0.9.0", "librosa", - "numpy", + "numpy<2.0", # todo: upgrade after tensorflow will support it "praat-parselmouth>=0.4.3", "pyreaper>=0.0.9", "pysptk", "pyworld>=0.3.2", "resampy", "scipy", - "tensorflow<2.12.0", - # "tf-nightly", # trying this instead tensorflow. It support tensorrt8. (regular tensorflow only supports outdated tensorrt 7 which is python3.8 only) + "tensorflow", "tensorflow-hub", "torch", "torch-yin", "torchcrepe>=0.0.18", "penn", - "nvidia-cudnn-cu11", - "tensorrt", - "pydantic", + "pydantic>=2.0", ] [project.optional-dependencies] @@ -41,9 +38,10 @@ dev = [ "mir_eval", "tqdm", "redis", - "musiclib", + "musiclib==2.1.0", "python-dotenv", "tabulate", + "s3fs", ] [project.urls] @@ -118,7 +116,7 @@ disallow_untyped_defs = false # ============================================================================== -[tool.ruff] +[tool.ruff.lint] extend-select = [ "W", "C", @@ -139,14 +137,15 @@ ignore = [ "E501", # line too long "PLR0913", "TCH003", + "S603", ] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "examples/*" = ["INP001"] "scripts/*" = ["INP001", "S101"] "tests/*" = ["S101"] -[tool.ruff.isort] +[tool.ruff.lint.isort] force-single-line = true # ============================================================================== @@ -172,6 +171,7 @@ disable = [ "too-many-locals", "invalid-name", "protected-access", + "cyclic-import", ] [tool.pylint-per-file-ignores] @@ -188,16 +188,15 @@ aggressive = 3 # ============================================================================== [tool.pyright] -venvPath = "/home/tandav/.cache/.virtualenvs" +venvPath = "/home/tandav/.cache/virtualenvs" venv = "pitch-detectors" # ============================================================================== [tool.pytest.ini_options] filterwarnings = [ - "ignore:pkg_resources is deprecated as an API", - "ignore:Deprecated call to `pkg_resources.declare_namespace", - "ignore:distutils Version classes are deprecated", + "ignore:Type google._upb._message.ScalarMapContainer uses PyType_Spec with a metaclass that has custom tp_new.:DeprecationWarning", + "ignore:Type google._upb._message.MessageMapContainer uses PyType_Spec with a metaclass that has custom tp_new.:DeprecationWarning", ] # ============================================================================== diff --git a/scripts/download_models.py b/scripts/download_models.py new file mode 100644 index 0000000..aa752df --- /dev/null +++ b/scripts/download_models.py @@ -0,0 +1,12 @@ +import os + +import s3fs + +s3 = s3fs.S3FileSystem( + endpoint_url=os.environ['AWS_ENDPOINT_URL'], + key=os.environ['AWS_ACCESS_KEY_ID'], + secret=os.environ['AWS_SECRET_ACCESS_KEY'], +) + +s3.get('pitchtrack/spice_model', os.environ['PITCH_DETECTORS_SPICE_MODEL_PATH'], recursive=True) +s3.get('pitchtrack/fcnf0++.pt', os.environ['PITCH_DETECTORS_PENN_CHECKPOINT_PATH']) diff --git a/scripts/run_algorithm.py b/scripts/run_algorithm.py index 2e46ebc..453325d 100644 --- a/scripts/run_algorithm.py +++ b/scripts/run_algorithm.py @@ -1,35 +1,19 @@ import argparse import os -import numpy as np - from pitch_detectors import algorithms from pitch_detectors import util -from pitch_detectors.algorithms.ensemble import Ensemble -from pitch_detectors.algorithms.ensemble import vote_and_median -from pitch_detectors.schemas import F0 -def main( - audio_path: str, - algorithm: str, -) -> None: +def main(audio_path: str, algorithm: str) -> None: fs, a = util.load_wav(audio_path) - if algorithm == 'ensemble': - alg = Ensemble(a, fs, algorithms=algorithms.ALGORITHMS) - algorithms_cache = {k.name(): F0(t=alg.t, f0=alg.f0) for k, alg in alg._algorithms.items()} - vm = vote_and_median(algorithms_cache, alg.seconds) - assert np.array_equal(alg.t, vm.t) - assert np.array_equal(alg.f0, vm.f0, equal_nan=True) - else: - alg = getattr(algorithms, os.environ['PITCH_DETECTORS_ALGORITHM'])(a, fs) - + alg = getattr(algorithms, algorithm)(a, fs) assert alg.f0.shape == alg.t.shape if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument('--audio-path', type=str, default=os.environ.get('PITCH_DETECTORS_AUDIO_PATH', 'data/b1a5da49d564a7341e7e1327aa3f229a.wav')) + parser.add_argument('--audio-path', type=str, default=os.environ.get('PITCH_DETECTORS_AUDIO_PATH')) parser.add_argument('--algorithm', type=str, default=os.environ.get('PITCH_DETECTORS_ALGORITHM')) args = parser.parse_args() main(**vars(args)) diff --git a/tests/algorithms_test.py b/tests/algorithms_test.py index 3990670..83e1069 100644 --- a/tests/algorithms_test.py +++ b/tests/algorithms_test.py @@ -8,14 +8,21 @@ @pytest.mark.order(3) -@pytest.mark.parametrize('algorithm', algorithms.ALGORITHMS) -@pytest.mark.parametrize('gpu', ['false'] if os.environ.get('PITCH_DETECTORS_GPU') == 'false' else ['true', 'false']) -def test_detection(algorithm, environ, gpu, subprocess_warning): - env = environ | { - 'PITCH_DETECTORS_ALGORITHM': algorithm.name(), +@pytest.mark.parametrize('gpu', ['true', 'false'], ids=['gpu', 'cpu']) +@pytest.mark.parametrize('algorithm', (*algorithms.algorithms, 'Ensemble')) +def test_detection(algorithm, gpu): + env = { + 'PITCH_DETECTORS_GPU_MEMORY_LIMIT': 'true', + 'PITCH_DETECTORS_AUDIO_PATH': 'data/b1a5da49d564a7341e7e1327aa3f229a.wav', + 'PATH': '', # for some reason this line prevents SIGSEGV for Spice algorithm + # 'PATH': '/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', # this is from docker history of base cuda image https://hub.docker.com/layers/nvidia/cuda/12.4.1-cudnn-devel-ubuntu22.04/images/sha256-0a1cb6e7bd047a1067efe14efdf0276352d5ca643dfd77963dab1a4f05a003a4?context=explore + 'PITCH_DETECTORS_ALGORITHM': algorithm, 'PITCH_DETECTORS_GPU': gpu, } - print(subprocess_warning) + if 'PITCH_DETECTORS_PENN_CHECKPOINT_PATH' in os.environ: + env['PITCH_DETECTORS_PENN_CHECKPOINT_PATH'] = os.environ['PITCH_DETECTORS_PENN_CHECKPOINT_PATH'] + if 'PITCH_DETECTORS_SPICE_MODEL_PATH' in os.environ: + env['PITCH_DETECTORS_SPICE_MODEL_PATH'] = os.environ['PITCH_DETECTORS_SPICE_MODEL_PATH'] subprocess.check_call([sys.executable, 'scripts/run_algorithm.py'], env=env) diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 4366117..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,35 +0,0 @@ -import os -from pathlib import Path - -import pytest - -from pitch_detectors import util -from pitch_detectors.schemas import Record - - -@pytest.fixture -def record(): - fs, a = util.load_wav(Path(__file__).parent.parent / 'data' / 'b1a5da49d564a7341e7e1327aa3f229a.wav') - return Record(fs=fs, a=a) - - -@pytest.fixture -def environ(): - env = { - 'PITCH_DETECTORS_GPU_MEMORY_LIMIT': 'true', - 'LD_LIBRARY_PATH': util.ld_library_path(), - } - if 'PITCH_DETECTORS_PENN_CHECKPOINT_PATH' in os.environ: - env['PITCH_DETECTORS_PENN_CHECKPOINT_PATH'] = os.environ['PITCH_DETECTORS_PENN_CHECKPOINT_PATH'] - if 'PITCH_DETECTORS_SPICE_MODEL_PATH' in os.environ: - env['PITCH_DETECTORS_SPICE_MODEL_PATH'] = os.environ['PITCH_DETECTORS_SPICE_MODEL_PATH'] - return env - - -@pytest.fixture -def subprocess_warning(): - return '''\ - Running in subprocess to avoid pytorch cuda import caching. - It's difficult to disable gpu after it has been initialized. - It is also difficult to set LD_LIBRARY_PATH without a subprocess. - ''' diff --git a/tests/ensemble_test.py b/tests/ensemble_test.py deleted file mode 100644 index 201a1a7..0000000 --- a/tests/ensemble_test.py +++ /dev/null @@ -1,10 +0,0 @@ -import os -import subprocess -import sys - - -def test_ensemble(environ, subprocess_warning): - print(subprocess_warning) - environ['PITCH_DETECTORS_ALGORITHM'] = 'ensemble' - environ['PITCH_DETECTORS_GPU'] = os.environ.get('PITCH_DETECTORS_GPU', 'true') - subprocess.check_call([sys.executable, 'scripts/run_algorithm.py'], env=environ) diff --git a/tests/gpu_test.py b/tests/gpu_test.py index 71814fe..5862334 100644 --- a/tests/gpu_test.py +++ b/tests/gpu_test.py @@ -2,23 +2,23 @@ import subprocess import pytest -import tensorflow as tf -import torch @pytest.mark.order(0) @pytest.mark.skipif(os.environ.get('PITCH_DETECTORS_GPU') == 'false', reason='gpu is not used') def test_nvidia_smi(): - subprocess.check_call('nvidia-smi') + subprocess.check_call('/usr/bin/nvidia-smi') @pytest.mark.order(1) @pytest.mark.skipif(os.environ.get('PITCH_DETECTORS_GPU') == 'false', reason='gpu is not used') def test_tensorflow(): + import tensorflow as tf assert tf.config.experimental.list_physical_devices('GPU') @pytest.mark.order(2) @pytest.mark.skipif(os.environ.get('PITCH_DETECTORS_GPU') == 'false', reason='gpu is not used') def test_pytorch(): + import torch assert torch.cuda.is_available()