├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── ci.yml │ ├── docker-build.yml │ ├── documentation.yml │ ├── lint.yml │ ├── publish.yml │ └── test-publish.yml ├── .gitignore ├── .vscode └── extensions.json ├── Dockerfile ├── LICENSE ├── README.md ├── birdnet_analyzer ├── __init__.py ├── analyze │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── core.py │ └── utils.py ├── audio.py ├── cli.py ├── config.py ├── eBird_taxonomy_codes_2024E.json ├── embeddings │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── core.py │ └── utils.py ├── evaluation │ ├── __init__.py │ ├── __main__.py │ ├── assessment │ │ ├── __init__.py │ │ ├── metrics.py │ │ ├── performance_assessor.py │ │ └── plotting.py │ └── preprocessing │ │ ├── __init__.py │ │ ├── data_processor.py │ │ └── utils.py ├── example │ ├── soundscape.wav │ └── species_list.txt ├── gui │ ├── __init__.py │ ├── __main__.py │ ├── analysis.py │ ├── assets │ │ ├── arrow_down.svg │ │ ├── arrow_left.svg │ │ ├── arrow_right.svg │ │ ├── arrow_up.svg │ │ ├── gui.css │ │ ├── gui.js │ │ └── img │ │ │ ├── birdnet-icon.ico │ │ │ ├── birdnet_logo.png │ │ │ ├── birdnet_logo_no_transparent.png │ │ │ └── clo-logo-bird.svg │ ├── embeddings.py │ ├── evaluation.py │ ├── localization.py │ ├── multi_file.py │ ├── review.py │ ├── segments.py │ ├── settings.py │ ├── single_file.py │ ├── species.py │ ├── train.py │ └── utils.py ├── labels │ └── V2.4 │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_af.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_ar.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_bg.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_ca.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_cs.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_da.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_de.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_el.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_en_uk.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_es.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_fi.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_fr.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_he.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_hr.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_hu.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_in.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_is.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_it.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_ja.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_ko.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_lt.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_ml.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_nl.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_no.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_pl.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_pt_BR.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_pt_PT.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_ro.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_ru.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_sk.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_sl.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_sr.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_sv.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_th.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_tr.txt │ │ ├── BirdNET_GLOBAL_6K_V2.4_Labels_uk.txt │ │ └── BirdNET_GLOBAL_6K_V2.4_Labels_zh.txt ├── lang │ ├── de.json │ ├── en.json │ ├── fi.json │ ├── fr.json │ ├── id.json │ ├── pt-br.json │ ├── ru.json │ ├── se.json │ ├── tlh.json │ └── zh_TW.json ├── model.py ├── network │ ├── __init__.py │ ├── client.py │ ├── server.py │ └── utils.py ├── search │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── core.py │ └── utils.py ├── segments │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── core.py │ └── utils.py ├── species │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── core.py │ └── utils.py ├── train │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── core.py │ └── utils.py ├── translate.py └── utils.py ├── docs ├── Makefile ├── _static │ ├── BirdNET-Go-logo.webp │ ├── BirdNET_Guide-Introduction-NotebookLM.mp3 │ ├── BirdNET_Guide-Segment_review-NotebookLM.mp3 │ ├── BirdNET_Guide-Training-NotebookLM.mp3 │ ├── Muuttolintujen-Kevät.png │ ├── birdnet-icon.ico │ ├── birdnet-pi.png │ ├── birdnet-tiny-forge-logo.png │ ├── birdnet_logo.png │ ├── birdnetlib.png │ ├── birdnetr-logo.png │ ├── birdweather.png │ ├── chirpity.png │ ├── css │ │ └── custom.css │ ├── dawnchorus.png │ ├── dummy_birds_image.png │ ├── dummy_frogs_image.png │ ├── dummy_project_image.png │ ├── ecopi.png │ ├── ecosound-web_logo_large_white_on_black.png │ ├── faunanet_logo.png │ ├── gui.png │ ├── haikubox.png │ ├── logo_birdnet_big.png │ ├── ribbit.png │ └── whobird.png ├── best-practices.rst ├── best-practices │ ├── embeddings.rst │ ├── segment-review.rst │ ├── species-lists.rst │ └── training.rst ├── birdnet-tiny.rst ├── birdnetr.rst ├── conf.py ├── contribute.rst ├── faq.rst ├── implementation-details.rst ├── implementation-details │ └── crop-modes.rst ├── index.rst ├── installation.rst ├── make.bat ├── models.rst ├── projects.html ├── projects_data.js ├── showroom.rst ├── usage.rst └── usage │ ├── cli.rst │ ├── docker.rst │ ├── gui.rst │ └── projects-map.rst ├── pyproject.toml └── tests ├── __init__.py ├── analyze ├── __init__.py └── test_analyze.py ├── embeddings ├── __init__.py └── test_embeddings.py ├── evaluation ├── __init__.py ├── assessment │ ├── __init__.py │ ├── test_metrics.py │ ├── test_performance_assessor.py │ └── test_plotting.py └── preprocessing │ ├── __init__.py │ ├── test_data_processor.py │ └── test_utils.py ├── gui ├── __init__.py └── test_language.py ├── segments ├── __init__.py └── test_segments.py ├── species ├── __init__.py └── test_species.py ├── test_utils.py └── train ├── __init__.py └── test_train.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | For the GUI: 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | For the CLI provide us with the arguments used. 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Desktop (please complete the following information):** 29 | - OS: [e.g. MacOS, Win10] 30 | - Version [e.g. 1.0.2] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened, edited] 6 | branches: [main] 7 | paths: [birdnet_analyzer/**, .github/workflows/ci.yml, tests/**, pyproject.toml] 8 | push: 9 | branches: [main] 10 | paths: [birdnet_analyzer/**, .github/workflows/ci.yml, tests/**, pyproject.toml] 11 | 12 | concurrency: 13 | group: "${{ github.event.pull_request.number }}-${{ github.ref_name }}-${{ github.workflow }}" 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | running-tests: 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | os: [ubuntu-latest, macos-latest, windows-latest] 22 | python-version: ["3.11"] 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | cache: 'pip' 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | python -m pip install .[embeddings,train] 34 | - name: Run tests 35 | run: | 36 | python -m pip install .[tests] 37 | python -m birdnet_analyzer.utils 38 | python -m pytest 39 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - 'Dockerfile' 8 | - '.github/workflows/docker-build.yml' 9 | pull_request: 10 | branches: [ main ] 11 | paths: 12 | - 'Dockerfile' 13 | - '.github/workflows/docker-build.yml' 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - name: Build Docker image 24 | run: docker build . --file Dockerfile --tag birdnet:local-test 25 | 26 | # Optional: Add basic test to verify the image works 27 | - name: Test Docker image 28 | run: | 29 | docker run -v $PWD/birdnet_analyzer/example:/audio birdnet:local-test -m birdnet_analyzer.analyze --i /audio --o /audio --slist /audio 30 | 31 | # Verify output file content 32 | expected_header="Selection View Channel Begin Time (s) End Time (s) Low Freq (Hz) High Freq (Hz) Common Name Species Code Confidence Begin Path File Offset (s)" 33 | expected_first_line="1 Spectrogram 1 1 0 3.0 0 15000 Black-capped Chickadee bkcchi 0.8141 /audio/soundscape.wav 0" 34 | 35 | actual_header=$(head -n 1 birdnet_analyzer/example/soundscape.BirdNET.selection.table.txt) 36 | actual_first_line=$(head -n 2 birdnet_analyzer/example/soundscape.BirdNET.selection.table.txt | tail -n 1) 37 | 38 | if [ "$actual_header" != "$expected_header" ] || [ "$actual_first_line" != "$expected_first_line" ] 39 | then 40 | echo "Output file content does not match expected content" 41 | echo "Expected header: $expected_header" 42 | echo "Actual header: $actual_header" 43 | echo "Expected first line: $expected_first_line" 44 | echo "Actual first line: $actual_first_line" 45 | exit 1 46 | else 47 | echo "test received expected output file contents" 48 | fi -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened, edited] 6 | branches: [main] 7 | paths: [docs/**, .github/workflows/documentation.yml, birdnet_analyzer/cli.py] 8 | push: 9 | branches: 10 | - main 11 | paths: [docs/**, .github/workflows/documentation.yml, birdnet_analyzer/cli.py] 12 | 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | docs: 18 | runs-on: ubuntu-latest 19 | env: 20 | IS_GITHUB_RUNNER: "true" 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-python@v5 24 | with: 25 | python-version: '3.11' 26 | - name: Install dependencies 27 | run: | 28 | pip install .[docs] 29 | - name: Sphinx build 30 | run: | 31 | sphinx-build -E docs _build 32 | - name: Deploy to GitHub Pages 33 | uses: peaceiris/actions-gh-pages@v3 34 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} 35 | with: 36 | publish_branch: gh-pages 37 | github_token: ${{ secrets.GITHUB_TOKEN }} 38 | publish_dir: _build/ 39 | force_orphan: true -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: [birdnet_analyzer/**, .github/workflows/lint.yml, tests/**, pyproject.toml] 7 | pull_request: 8 | branches: [main] 9 | types: [opened, synchronize, reopened, edited] 10 | paths: [birdnet_analyzer/**, .github/workflows/lint.yml, tests/**, pyproject.toml] 11 | 12 | jobs: 13 | ruff: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.11' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install ruff 25 | - name: Lint with Ruff 26 | run: | 27 | ruff check 28 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | environment: 10 | name: pypi 11 | url: https://pypi.org/p/birdnet_analyzer 12 | 13 | permissions: 14 | id-token: write # Needed for trusted publishing 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: "3.11" 24 | 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install build 29 | 30 | - name: Build package 31 | run: python -m build 32 | 33 | - name: Publish to PyPI (Trusted) 34 | uses: pypa/gh-action-pypi-publish@release/v1 35 | -------------------------------------------------------------------------------- /.github/workflows/test-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Test-PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | environment: 11 | name: testpypi 12 | url: https://test.pypi.org/p/birdnet_analyzer 13 | 14 | permissions: 15 | id-token: write # Needed for trusted publishing 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: "3.11" 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install build 30 | 31 | - name: Build package 32 | run: python -m build 33 | 34 | - name: Publish to PyPI (Trusted) 35 | uses: pypa/gh-action-pypi-publish@release/v1 36 | with: 37 | repository-url: https://test.pypi.org/legacy/ 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # UV 2 | uv.lock 3 | 4 | # IDE/Editor settings 5 | .vscode/* 6 | 7 | # Allow VSCode recommendations 8 | !.vscode/extensions.json 9 | 10 | # Ruff 11 | .ruff_cache 12 | 13 | # Models 14 | checkpoints 15 | 16 | # Local testing 17 | playground* 18 | foo* 19 | 20 | # Apple :/ 21 | .DS_Store 22 | 23 | # Documentation 24 | _build 25 | 26 | # Installers 27 | installers/ 28 | desktop.ini 29 | *.iss 30 | *deploy* 31 | *hook* 32 | 33 | # Custom classifier 34 | checkpoints/custom/ 35 | 36 | # Example output 37 | /birdnet_analyzer/example/**/* 38 | !/birdnet_analyzer/example/soundscape.wav 39 | !/birdnet_analyzer/example/species_list.txt 40 | 41 | # Logs 42 | error_log.txt 43 | 44 | # Byte-compiled / optimized / DLL files 45 | __pycache__/ 46 | *.py[cod] 47 | *$py.class 48 | 49 | # C extensions 50 | *.so 51 | 52 | # Distribution / packaging 53 | .Python 54 | build* 55 | develop-eggs/ 56 | dist/ 57 | downloads/ 58 | eggs/ 59 | .eggs/ 60 | lib/ 61 | lib64/ 62 | parts/ 63 | sdist/ 64 | var/ 65 | wheels/ 66 | pip-wheel-metadata/ 67 | share/python-wheels/ 68 | *.egg-info/ 69 | .installed.cfg 70 | *.egg 71 | MANIFEST 72 | 73 | # PyInstaller 74 | # Usually these files are written by a python script from a template 75 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 76 | *.manifest 77 | *.spec 78 | 79 | # Installer logs 80 | pip-log.txt 81 | pip-delete-this-directory.txt 82 | 83 | # Unit test / coverage reports 84 | htmlcov/ 85 | .tox/ 86 | .nox/ 87 | .coverage 88 | .coverage.* 89 | .cache 90 | nosetests.xml 91 | coverage.xml 92 | *.cover 93 | *.py,cover 94 | .hypothesis/ 95 | .pytest_cache/ 96 | 97 | # Translations 98 | *.mo 99 | *.pot 100 | 101 | # Django stuff: 102 | *.log 103 | local_settings.py 104 | db.sqlite3 105 | db.sqlite3-journal 106 | 107 | # Flask stuff: 108 | instance/ 109 | .webassets-cache 110 | 111 | # Scrapy stuff: 112 | .scrapy 113 | 114 | # Sphinx documentation 115 | docs/_build/ 116 | 117 | # PyBuilder 118 | target/ 119 | 120 | # Jupyter Notebook 121 | .ipynb_checkpoints 122 | 123 | # IPython 124 | profile_default/ 125 | ipython_config.py 126 | 127 | # pyenv 128 | .python-version 129 | 130 | # pipenv 131 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 132 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 133 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 134 | # install all needed dependencies. 135 | #Pipfile.lock 136 | 137 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 138 | __pypackages__/ 139 | 140 | # Celery stuff 141 | celerybeat-schedule 142 | celerybeat.pid 143 | 144 | # SageMath parsed files 145 | *.sage.py 146 | 147 | # Environments 148 | .env 149 | .venv 150 | env/ 151 | venv/ 152 | ENV/ 153 | env.bak/ 154 | venv.bak/ 155 | 156 | # Spyder project settings 157 | .spyderproject 158 | .spyproject 159 | 160 | # Rope project settings 161 | .ropeproject 162 | 163 | # mkdocs documentation 164 | /site 165 | 166 | # mypy 167 | .mypy_cache/ 168 | .dmypy.json 169 | dmypy.json 170 | 171 | # Pyre type checker 172 | .pyre/ 173 | 174 | # GUI generated 175 | train_data/ 176 | train_cache.npz 177 | autotune/ 178 | gui-settings.json 179 | state.json 180 | BirdNET_analysis_params.csv 181 | 182 | # Build files 183 | entitlements.plist -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=827846 2 | { 3 | "recommendations": [ 4 | "charliermarsh.ruff" 5 | ] 6 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build from Python slim 2 | FROM python:3.11 3 | 4 | # Install required packages while keeping the image small 5 | RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* 6 | 7 | # Import all scripts 8 | COPY . ./ 9 | 10 | # Install required Python packages 11 | RUN pip3 install --no-cache-dir -r requirements.txt 12 | 13 | # Add entry point to run the script 14 | ENTRYPOINT [ "python3" ] 15 | CMD [ "-m", "birdnet_analyzer.analyze" ] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 birdnet-team 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

BirdNET-Analyzer

3 | 4 | BirdNET-Logo 5 | 6 |
7 |
8 |
9 | 10 | ![License](https://img.shields.io/github/license/birdnet-team/BirdNET-Analyzer) 11 | ![OS](https://badgen.net/badge/OS/Linux%2C%20Windows%2C%20macOS/blue) 12 | [![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)](https://www.python.org/downloads/release/python-3110/) 13 | ![Species](https://badgen.net/badge/Species/6512/blue) 14 | ![Downloads](https://www-user.tu-chemnitz.de/~johau/birdnet_total_downloads_badge.php) 15 | 16 | [![Docker](https://github.com/birdnet-team/BirdNET-Analyzer/actions/workflows/docker-build.yml/badge.svg)](https://github.com/birdnet-team/BirdNET-Analyzer/actions/workflows/docker-build.yml) 17 | [![Reddit](https://img.shields.io/badge/Reddit-FF4500?style=flat&logo=reddit&logoColor=white)](https://www.reddit.com/r/BirdNET_Analyzer/) 18 | ![GitHub stars)](https://img.shields.io/github/stars/birdnet-team/BirdNET-Analyzer) 19 | 20 | [![GitHub release](https://img.shields.io/github/v/release/birdnet-team/BirdNET-Analyzer)](https://github.com/birdnet-team/BirdNET-Analyzer/releases/latest) 21 | [![PyPI - Version](https://img.shields.io/pypi/v/birdnet_analyzer?logo=pypi)](https://pypi.org/project/birdnet-analyzer/) 22 | 23 | [![Sponsor](https://img.shields.io/badge/Support%20our%20work-8A2BE2?logo=data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgd2lkdGg9IjE2IiBkYXRhLXZpZXctY29tcG9uZW50PSJ0cnVlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIGQ9Im04IDE0LjI1LjM0NS42NjZhLjc1Ljc1IDAgMCAxLS42OSAwbC0uMDA4LS4wMDQtLjAxOC0uMDFhNy4xNTIgNy4xNTIgMCAwIDEtLjMxLS4xNyAyMi4wNTUgMjIuMDU1IDAgMCAxLTMuNDM0LTIuNDE0QzIuMDQ1IDEwLjczMSAwIDguMzUgMCA1LjUgMCAyLjgzNiAyLjA4NiAxIDQuMjUgMSA1Ljc5NyAxIDcuMTUzIDEuODAyIDggMy4wMiA4Ljg0NyAxLjgwMiAxMC4yMDMgMSAxMS43NSAxIDEzLjkxNCAxIDE2IDIuODM2IDE2IDUuNWMwIDIuODUtMi4wNDUgNS4yMzEtMy44ODUgNi44MThhMjIuMDY2IDIyLjA2NiAwIDAgMS0zLjc0NCAyLjU4NGwtLjAxOC4wMS0uMDA2LjAwM2gtLjAwMlpNNC4yNSAyLjVjLTEuMzM2IDAtMi43NSAxLjE2NC0yLjc1IDMgMCAyLjE1IDEuNTggNC4xNDQgMy4zNjUgNS42ODJBMjAuNTggMjAuNTggMCAwIDAgOCAxMy4zOTNhMjAuNTggMjAuNTggMCAwIDAgMy4xMzUtMi4yMTFDMTIuOTIgOS42NDQgMTQuNSA3LjY1IDE0LjUgNS41YzAtMS44MzYtMS40MTQtMy0yLjc1LTMtMS4zNzMgMC0yLjYwOS45ODYtMy4wMjkgMi40NTZhLjc0OS43NDkgMCAwIDEtMS40NDIgMEM2Ljg1OSAzLjQ4NiA1LjYyMyAyLjUgNC4yNSAyLjVaIj48L3BhdGg+DQo8L3N2Zz4=)](https://give.birds.cornell.edu/page/132162/donate/1) 24 | 25 |
26 | 27 | This repo contains BirdNET scripts for processing large amounts of audio data or single audio files. 28 | This is the most advanced version of BirdNET for acoustic analyses and we will keep this repository up-to-date with new models and improved interfaces to enable scientists with no CS background to run the analysis. 29 | 30 | Feel free to use BirdNET for your acoustic analyses and research. 31 | If you do, please cite as: 32 | 33 | ```bibtex 34 | @article{kahl2021birdnet, 35 | title={BirdNET: A deep learning solution for avian diversity monitoring}, 36 | author={Kahl, Stefan and Wood, Connor M and Eibl, Maximilian and Klinck, Holger}, 37 | journal={Ecological Informatics}, 38 | volume={61}, 39 | pages={101236}, 40 | year={2021}, 41 | publisher={Elsevier} 42 | } 43 | ``` 44 | 45 | ## Documentation 46 | 47 | You can access documentation for this project [here](https://birdnet-team.github.io/BirdNET-Analyzer/). 48 | 49 | ## Download 50 | 51 | You can download installers for Windows and macOS from the [releases page](https://github.com/birdnet-team/BirdNET-Analyzer/releases/latest). 52 | Models can be found on [Zenodo](https://zenodo.org/records/15050749). 53 | 54 | ## About 55 | 56 | Developed by the [K. Lisa Yang Center for Conservation Bioacoustics](https://www.birds.cornell.edu/ccb/) at the [Cornell Lab of Ornithology](https://www.birds.cornell.edu/home) in collaboration with [Chemnitz University of Technology](https://www.tu-chemnitz.de/index.html.en). 57 | 58 | Go to https://birdnet.cornell.edu to learn more about the project. 59 | 60 | Want to use BirdNET to analyze a large dataset? Don't hesitate to contact us: ccb-birdnet@cornell.edu 61 | 62 | **Have a question, remark, or feature request? Please start a new issue thread to let us know. Feel free to submit a pull request.** 63 | 64 | ## License 65 | 66 | - **Source Code**: The source code for this project is licensed under the [MIT License](https://opensource.org/licenses/MIT). 67 | - **Models**: The models used in this project are licensed under the [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/). 68 | 69 | Please ensure you review and adhere to the specific license terms provided with each model. 70 | 71 | *Please note that all educational and research purposes are considered non-commercial use and it is therefore freely permitted to use BirdNET models in any way.* 72 | 73 | ## Funding 74 | 75 | This project is supported by Jake Holshuh (Cornell class of ´69) and The Arthur Vining Davis Foundations. 76 | Our work in the K. Lisa Yang Center for Conservation Bioacoustics is made possible by the generosity of K. Lisa Yang to advance innovative conservation technologies to inspire and inform the conservation of wildlife and habitats. 77 | 78 | The development of BirdNET is supported by the German Federal Ministry of Education and Research through the project “BirdNET+” (FKZ 01|S22072). 79 | The German Federal Ministry for the Environment, Nature Conservation and Nuclear Safety contributes through the “DeepBirdDetect” project (FKZ 67KI31040E). 80 | In addition, the Deutsche Bundesstiftung Umwelt supports BirdNET through the project “RangerSound” (project 39263/01). 81 | 82 | ## Partners 83 | 84 | BirdNET is a joint effort of partners from academia and industry. 85 | Without these partnerships, this project would not have been possible. 86 | Thank you! 87 | 88 | ![Logos of all partners](https://tuc.cloud/index.php/s/KSdWfX5CnSRpRgQ/download/box_logos.png) 89 | -------------------------------------------------------------------------------- /birdnet_analyzer/__init__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.analyze import analyze 2 | from birdnet_analyzer.embeddings import embeddings 3 | from birdnet_analyzer.search import search 4 | from birdnet_analyzer.segments import segments 5 | from birdnet_analyzer.species import species 6 | from birdnet_analyzer.train import train 7 | 8 | __version__ = "2.0.0" 9 | __all__ = ["analyze", "embeddings", "search", "segments", "species", "train"] 10 | -------------------------------------------------------------------------------- /birdnet_analyzer/analyze/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import birdnet_analyzer.config as cfg 4 | from birdnet_analyzer.analyze.core import analyze 5 | 6 | POSSIBLE_ADDITIONAL_COLUMNS_MAP = { 7 | "lat": lambda: cfg.LATITUDE, 8 | "lon": lambda: cfg.LONGITUDE, 9 | "week": lambda: cfg.WEEK, 10 | "overlap": lambda: cfg.SIG_OVERLAP, 11 | "sensitivity": lambda: cfg.SIGMOID_SENSITIVITY, 12 | "min_conf": lambda: cfg.MIN_CONFIDENCE, 13 | "species_list": lambda: cfg.SPECIES_LIST_FILE or "", 14 | "model": lambda: os.path.basename(cfg.MODEL_PATH), 15 | } 16 | 17 | __all__ = [ 18 | "analyze", 19 | ] 20 | -------------------------------------------------------------------------------- /birdnet_analyzer/analyze/__main__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.analyze.cli import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /birdnet_analyzer/analyze/cli.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer import analyze 2 | from birdnet_analyzer.utils import runtime_error_handler 3 | 4 | 5 | @runtime_error_handler 6 | def main(): 7 | import os 8 | from multiprocessing import freeze_support 9 | 10 | from birdnet_analyzer import cli 11 | 12 | # Freeze support for executable 13 | freeze_support() 14 | 15 | parser = cli.analyzer_parser() 16 | 17 | args = parser.parse_args() 18 | 19 | try: 20 | if os.get_terminal_size().columns >= 64: 21 | print(cli.ASCII_LOGO, flush=True) 22 | except Exception: 23 | pass 24 | 25 | if "additional_columns" in args and args.additional_columns and "csv" not in args.rtype: 26 | import warnings 27 | 28 | warnings.warn("The --additional_columns argument is only valid for CSV output. It will be ignored.", stacklevel=1) 29 | 30 | analyze(**vars(args)) 31 | -------------------------------------------------------------------------------- /birdnet_analyzer/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__)) 4 | 5 | ################# 6 | # Misc settings # 7 | ################# 8 | 9 | # Random seed for gaussian noise 10 | RANDOM_SEED: int = 42 11 | 12 | ########################## 13 | # Model paths and config # 14 | ########################## 15 | 16 | MODEL_VERSION: str = "V2.4" 17 | PB_MODEL: str = os.path.join(SCRIPT_DIR, "checkpoints/V2.4/BirdNET_GLOBAL_6K_V2.4_Model") 18 | # MODEL_PATH = PB_MODEL # This will load the protobuf model 19 | MODEL_PATH: str = os.path.join(SCRIPT_DIR, "checkpoints/V2.4/BirdNET_GLOBAL_6K_V2.4_Model_FP32.tflite") 20 | MDATA_MODEL_PATH: str = os.path.join(SCRIPT_DIR, "checkpoints/V2.4/BirdNET_GLOBAL_6K_V2.4_MData_Model_V2_FP16.tflite") 21 | LABELS_FILE: str = os.path.join(SCRIPT_DIR, "checkpoints/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels.txt") 22 | TRANSLATED_LABELS_PATH: str = os.path.join(SCRIPT_DIR, "labels/V2.4") 23 | 24 | ################## 25 | # Audio settings # 26 | ################## 27 | 28 | # We use a sample rate of 48kHz, so the model input size is 29 | # (batch size, 48000 kHz * 3 seconds) = (1, 144000) 30 | # Recordings will be resampled automatically. 31 | SAMPLE_RATE: int = 48000 32 | 33 | # We're using 3-second chunks 34 | SIG_LENGTH: float = 3.0 35 | 36 | # Define overlap between consecutive chunks <3.0; 0 = no overlap 37 | SIG_OVERLAP: float = 0 38 | 39 | # Define minimum length of audio chunk for prediction, 40 | # chunks shorter than 3 seconds will be padded with zeros 41 | SIG_MINLEN: float = 1.0 42 | 43 | # Frequency range. This is model specific and should not be changed. 44 | SIG_FMIN: int = 0 45 | SIG_FMAX: int = 15000 46 | 47 | # Settings for bandpass filter 48 | BANDPASS_FMIN: int = 0 49 | BANDPASS_FMAX: int = 15000 50 | 51 | # Top N species to display in selection table, ignored if set to None 52 | TOP_N = None 53 | 54 | # Audio speed 55 | AUDIO_SPEED: float = 1.0 56 | 57 | ##################### 58 | # Metadata settings # 59 | ##################### 60 | 61 | LATITUDE: float = -1 62 | LONGITUDE: float = -1 63 | WEEK: int = -1 64 | LOCATION_FILTER_THRESHOLD: float = 0.03 65 | 66 | ###################### 67 | # Inference settings # 68 | ###################### 69 | 70 | # If None or empty file, no custom species list will be used 71 | # Note: Entries in this list have to match entries from the LABELS_FILE 72 | # We use the 2024 eBird taxonomy for species names (Clements list) 73 | CODES_FILE: str = os.path.join(SCRIPT_DIR, "eBird_taxonomy_codes_2024E.json") 74 | SPECIES_LIST_FILE: str = os.path.join(SCRIPT_DIR, "example/species_list.txt") 75 | 76 | # Supported file types 77 | ALLOWED_FILETYPES: list[str] = ["wav", "flac", "mp3", "ogg", "m4a", "wma", "aiff", "aif"] 78 | 79 | # Number of threads to use for inference. 80 | # Can be as high as number of CPUs in your system 81 | CPU_THREADS: int = 8 82 | TFLITE_THREADS: int = 1 83 | 84 | # False will output logits, True will convert to sigmoid activations 85 | APPLY_SIGMOID: bool = True 86 | SIGMOID_SENSITIVITY: float = 1.0 87 | 88 | # Minimum confidence score to include in selection table 89 | # (be aware: if APPLY_SIGMOID = False, this no longer represents 90 | # probabilities and needs to be adjusted) 91 | MIN_CONFIDENCE: float = 0.25 92 | 93 | # Number of consecutive detections for one species to merge into one 94 | # If set to 1 or 0, no merging will be done 95 | # If set to None, all detections will be included 96 | MERGE_CONSECUTIVE: int = 1 97 | 98 | # Number of samples to process at the same time. Higher values can increase 99 | # processing speed, but will also increase memory usage. 100 | # Might only be useful for GPU inference. 101 | BATCH_SIZE: int = 1 102 | 103 | 104 | # Number of seconds to load from a file at a time 105 | # Files will be loaded into memory in segments that are only as long as this value 106 | # Lowering this value results in lower memory usage 107 | FILE_SPLITTING_DURATION: int = 600 108 | 109 | # Whether to use noise to pad the signal 110 | # If set to False, the signal will be padded with zeros 111 | USE_NOISE: bool = False 112 | 113 | # Specifies the output format. 'table' denotes a Raven selection table, 114 | # 'audacity' denotes a TXT file with the same format as Audacity timeline labels 115 | # 'csv' denotes a generic CSV file with start, end, species and confidence. 116 | RESULT_TYPES: set[str] | list[str] = {"table"} 117 | ADDITIONAL_COLUMNS: list[str] | None = None 118 | OUTPUT_RAVEN_FILENAME: str = "BirdNET_SelectionTable.txt" # this is for combined Raven selection tables only 119 | # OUTPUT_RTABLE_FILENAME: str = "BirdNET_RTable.csv" 120 | OUTPUT_KALEIDOSCOPE_FILENAME: str = "BirdNET_Kaleidoscope.csv" 121 | OUTPUT_CSV_FILENAME: str = "BirdNET_CombinedTable.csv" 122 | 123 | # File name of the settings csv for batch analysis 124 | ANALYSIS_PARAMS_FILENAME: str = "BirdNET_analysis_params.csv" 125 | 126 | # Whether to skip existing results in the output path 127 | # If set to False, existing files will not be overwritten 128 | SKIP_EXISTING_RESULTS: bool = False 129 | 130 | COMBINE_RESULTS: bool = False 131 | ##################### 132 | # Training settings # 133 | ##################### 134 | 135 | # Sample crop mode 136 | SAMPLE_CROP_MODE: str = "center" 137 | 138 | # List of non-event classes 139 | NON_EVENT_CLASSES: list[str] = ["noise", "other", "background", "silence"] 140 | 141 | # Upsampling settings 142 | UPSAMPLING_RATIO: float = 0.0 143 | UPSAMPLING_MODE = "repeat" 144 | 145 | # Number of epochs to train for 146 | TRAIN_EPOCHS: int = 50 147 | 148 | # Batch size for training 149 | TRAIN_BATCH_SIZE: int = 32 150 | 151 | # Validation split (percentage) 152 | TRAIN_VAL_SPLIT: float = 0.2 153 | 154 | # Learning rate for training 155 | TRAIN_LEARNING_RATE: float = 0.0001 156 | 157 | # Number of hidden units in custom classifier 158 | # If >0, a two-layer classifier will be trained 159 | TRAIN_HIDDEN_UNITS: int = 0 160 | 161 | # Dropout rate for training 162 | TRAIN_DROPOUT: float = 0.0 163 | 164 | # Whether to use mixup for training 165 | TRAIN_WITH_MIXUP: bool = False 166 | 167 | # Whether to apply label smoothing for training 168 | TRAIN_WITH_LABEL_SMOOTHING: bool = False 169 | 170 | # Whether to use focal loss for training 171 | TRAIN_WITH_FOCAL_LOSS: bool = False 172 | 173 | # Focal loss gamma parameter 174 | FOCAL_LOSS_GAMMA: float = 2.0 175 | 176 | # Focal loss alpha parameter 177 | FOCAL_LOSS_ALPHA: float = 0.25 178 | 179 | # Model output format 180 | TRAINED_MODEL_OUTPUT_FORMAT: str = "tflite" 181 | 182 | # Model save mode (replace or append new classifier) 183 | TRAINED_MODEL_SAVE_MODE: str = "replace" 184 | 185 | # Cache settings 186 | TRAIN_CACHE_MODE: str | None = None 187 | TRAIN_CACHE_FILE: str = "train_cache.npz" 188 | 189 | # Use automatic Hyperparameter tuning 190 | AUTOTUNE: bool = False 191 | 192 | # How many trials are done for the hyperparameter tuning 193 | AUTOTUNE_TRIALS: int = 50 194 | 195 | # How many executions per trial are done for the hyperparameter tuning 196 | # Mutliple executions will be averaged, so the evaluation is more consistent 197 | AUTOTUNE_EXECUTIONS_PER_TRIAL: int = 1 198 | 199 | # If a binary classification model is trained. 200 | # This value will be detected automatically in the training script, if only one class and a non-event class is used. 201 | BINARY_CLASSIFICATION: bool = False 202 | 203 | # If a model for a multi-label setting is trained. 204 | # This value will automatically be set, if subfolders in the input direcotry are named with multiple classes separated by commas. 205 | MULTI_LABEL: bool = False 206 | 207 | ################ 208 | # Runtime vars # 209 | ################ 210 | 211 | # File input path and output path for selection tables 212 | INPUT_PATH: str = "" 213 | OUTPUT_PATH: str = "" 214 | 215 | # Training data path 216 | TRAIN_DATA_PATH: str = "" 217 | TEST_DATA_PATH: str = "" 218 | 219 | CODES = {} 220 | LABELS: list[str] = [] 221 | TRANSLATED_LABELS: list[str] = [] 222 | SPECIES_LIST: list[str] = [] 223 | ERROR_LOG_FILE: str = os.path.join(SCRIPT_DIR, "error_log.txt") 224 | FILE_LIST = [] 225 | FILE_STORAGE_PATH: str = "" 226 | 227 | # Path to custom trained classifier 228 | # If None, no custom classifier will be used 229 | # Make sure to set the LABELS_FILE above accordingly 230 | CUSTOM_CLASSIFIER: str | None = None 231 | 232 | ###################### 233 | # Get and set config # 234 | ###################### 235 | 236 | 237 | def get_config(): 238 | return {k: v for k, v in globals().items() if k.isupper()} 239 | 240 | 241 | def set_config(c: dict): 242 | for k, v in c.items(): 243 | globals()[k] = v 244 | -------------------------------------------------------------------------------- /birdnet_analyzer/embeddings/__init__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.embeddings.core import embeddings 2 | 3 | __all__ = ["embeddings"] 4 | -------------------------------------------------------------------------------- /birdnet_analyzer/embeddings/__main__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.embeddings.cli import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /birdnet_analyzer/embeddings/cli.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer import embeddings 2 | from birdnet_analyzer.utils import runtime_error_handler 3 | 4 | 5 | @runtime_error_handler 6 | def main(): 7 | from birdnet_analyzer import cli 8 | 9 | parser = cli.embeddings_parser() 10 | args = parser.parse_args() 11 | 12 | embeddings(**vars(args)) 13 | -------------------------------------------------------------------------------- /birdnet_analyzer/embeddings/core.py: -------------------------------------------------------------------------------- 1 | def embeddings( 2 | audio_input: str, 3 | database: str, 4 | *, 5 | overlap: float = 0.0, 6 | audio_speed: float = 1.0, 7 | fmin: int = 0, 8 | fmax: int = 15000, 9 | threads: int = 8, 10 | batch_size: int = 1, 11 | file_output: str | None = None, 12 | ): 13 | """ 14 | Generates embeddings for audio files using the BirdNET-Analyzer. 15 | This function processes audio files to extract embeddings, which are 16 | representations of audio features. The embeddings can be used for 17 | further analysis or comparison. 18 | Args: 19 | audio_input (str): Path to the input audio file or directory containing audio files. 20 | database (str): Path to the database where embeddings will be stored. 21 | overlap (float, optional): Overlap between consecutive audio segments in seconds. Defaults to 0.0. 22 | audio_speed (float, optional): Speed factor for audio processing. Defaults to 1.0. 23 | fmin (int, optional): Minimum frequency (in Hz) for audio analysis. Defaults to 0. 24 | fmax (int, optional): Maximum frequency (in Hz) for audio analysis. Defaults to 15000. 25 | threads (int, optional): Number of threads to use for processing. Defaults to 8. 26 | batch_size (int, optional): Number of audio segments to process in a single batch. Defaults to 1. 27 | Raises: 28 | FileNotFoundError: If the input path or database path does not exist. 29 | ValueError: If any of the parameters are invalid. 30 | Note: 31 | Ensure that the required model files are downloaded and available before 32 | calling this function. The `ensure_model_exists` function is used to 33 | verify this. 34 | Example: 35 | embeddings( 36 | "path/to/audio", 37 | "path/to/database", 38 | overlap=0.5, 39 | audio_speed=1.0, 40 | fmin=500, 41 | fmax=10000, 42 | threads=4, 43 | batch_size=2 44 | ) 45 | """ 46 | from birdnet_analyzer.embeddings.utils import run 47 | from birdnet_analyzer.utils import ensure_model_exists 48 | 49 | ensure_model_exists() 50 | run(audio_input, database, overlap, audio_speed, fmin, fmax, threads, batch_size, file_output) 51 | 52 | 53 | def get_database(db_path: str): 54 | """Get the database object. Creates or opens the databse. 55 | Args: 56 | db: The path to the database. 57 | Returns: 58 | The database object. 59 | """ 60 | import os 61 | 62 | from perch_hoplite.db import sqlite_usearch_impl 63 | 64 | if not os.path.exists(db_path): 65 | os.makedirs(os.path.dirname(db_path), exist_ok=True) 66 | return sqlite_usearch_impl.SQLiteUsearchDB.create( 67 | db_path=db_path, 68 | usearch_cfg=sqlite_usearch_impl.get_default_usearch_config(embedding_dim=1024), # TODO: dont hardcode this 69 | ) 70 | return sqlite_usearch_impl.SQLiteUsearchDB.create(db_path=db_path) 71 | -------------------------------------------------------------------------------- /birdnet_analyzer/embeddings/utils.py: -------------------------------------------------------------------------------- 1 | """Module used to extract embeddings for samples.""" 2 | 3 | import datetime 4 | import os 5 | from functools import partial 6 | from multiprocessing import Pool 7 | 8 | import numpy as np 9 | from ml_collections import ConfigDict 10 | from perch_hoplite.db import interface as hoplite 11 | from perch_hoplite.db import sqlite_usearch_impl 12 | from tqdm import tqdm 13 | 14 | import birdnet_analyzer.config as cfg 15 | from birdnet_analyzer import audio, model, utils 16 | from birdnet_analyzer.analyze.utils import get_raw_audio_from_file 17 | from birdnet_analyzer.embeddings.core import get_database 18 | 19 | DATASET_NAME: str = "birdnet_analyzer_dataset" 20 | 21 | 22 | def analyze_file(item, db: sqlite_usearch_impl.SQLiteUsearchDB): 23 | """Extracts the embeddings for a file. 24 | 25 | Args: 26 | item: (filepath, config) 27 | """ 28 | 29 | # Get file path and restore cfg 30 | fpath: str = item[0] 31 | cfg.set_config(item[1]) 32 | 33 | offset = 0 34 | duration = cfg.FILE_SPLITTING_DURATION 35 | 36 | try: 37 | fileLengthSeconds = int(audio.get_audio_file_length(fpath)) 38 | except Exception as ex: 39 | # Write error log 40 | print(f"Error: Cannot analyze audio file {fpath}. File corrupt?\n", flush=True) 41 | utils.write_error_log(ex) 42 | 43 | return 44 | 45 | # Start time 46 | start_time = datetime.datetime.now() 47 | 48 | # Status 49 | print(f"Analyzing {fpath}", flush=True) 50 | 51 | source_id = fpath 52 | 53 | # Process each chunk 54 | try: 55 | while offset < fileLengthSeconds: 56 | chunks = get_raw_audio_from_file(fpath, offset, duration) 57 | start, end = offset, cfg.SIG_LENGTH + offset 58 | samples = [] 59 | timestamps = [] 60 | 61 | for c in range(len(chunks)): 62 | # Add to batch 63 | samples.append(chunks[c]) 64 | timestamps.append([start, end]) 65 | 66 | # Advance start and end 67 | start += cfg.SIG_LENGTH - cfg.SIG_OVERLAP 68 | end = start + cfg.SIG_LENGTH 69 | 70 | # Check if batch is full or last chunk 71 | if len(samples) < cfg.BATCH_SIZE and c < len(chunks) - 1: 72 | continue 73 | 74 | # Prepare sample and pass through model 75 | data = np.array(samples, dtype="float32") 76 | e = model.embeddings(data) 77 | 78 | # Add to results 79 | for i in range(len(samples)): 80 | # Get timestamp 81 | s_start, s_end = timestamps[i] 82 | 83 | # Check if embedding already exists 84 | existing_embedding = db.get_embeddings_by_source(DATASET_NAME, source_id, np.array([s_start, s_end])) 85 | 86 | if existing_embedding.size == 0: 87 | # Get prediction 88 | embeddings = e[i] 89 | 90 | # Store embeddings 91 | embeddings_source = hoplite.EmbeddingSource(DATASET_NAME, source_id, np.array([s_start, s_end])) 92 | 93 | # Insert into database 94 | db.insert_embedding(embeddings, embeddings_source) 95 | db.commit() 96 | 97 | # Reset batch 98 | samples = [] 99 | timestamps = [] 100 | 101 | offset = offset + duration 102 | 103 | except Exception as ex: 104 | # Write error log 105 | print(f"Error: Cannot analyze audio file {fpath}.", flush=True) 106 | utils.write_error_log(ex) 107 | 108 | return 109 | 110 | delta_time = (datetime.datetime.now() - start_time).total_seconds() 111 | print(f"Finished {fpath} in {delta_time:.2f} seconds", flush=True) 112 | 113 | 114 | def check_database_settings(db: sqlite_usearch_impl.SQLiteUsearchDB): 115 | try: 116 | settings = db.get_metadata("birdnet_analyzer_settings") 117 | if settings["BANDPASS_FMIN"] != cfg.BANDPASS_FMIN or settings["BANDPASS_FMAX"] != cfg.BANDPASS_FMAX or settings["AUDIO_SPEED"] != cfg.AUDIO_SPEED: 118 | raise ValueError( 119 | "Database settings do not match current configuration. DB Settings are: fmin:" 120 | + f"{settings['BANDPASS_FMIN']}, fmax: {settings['BANDPASS_FMAX']}, audio_speed: {settings['AUDIO_SPEED']}" 121 | ) 122 | except KeyError: 123 | settings = ConfigDict({"BANDPASS_FMIN": cfg.BANDPASS_FMIN, "BANDPASS_FMAX": cfg.BANDPASS_FMAX, "AUDIO_SPEED": cfg.AUDIO_SPEED}) 124 | db.insert_metadata("birdnet_analyzer_settings", settings) 125 | db.commit() 126 | 127 | 128 | def create_file_output(output_path: str, db: sqlite_usearch_impl.SQLiteUsearchDB): 129 | """Creates a file output for the database. 130 | 131 | Args: 132 | output_path: Path to the output file. 133 | db: Database object. 134 | """ 135 | # Check if output path exists 136 | if not os.path.exists(output_path): 137 | os.makedirs(output_path) 138 | # Get all embeddings 139 | embedding_ids = db.get_embedding_ids() 140 | 141 | # Write embeddings to file 142 | for embedding_id in embedding_ids: 143 | embedding = db.get_embedding(embedding_id) 144 | source = db.get_embedding_source(embedding_id) 145 | 146 | # Get start and end time 147 | start, end = source.offsets 148 | 149 | source_id = source.source_id.rsplit(".", 1)[0] 150 | 151 | filename = f"{source_id}_{start}_{end}.birdnet.embeddings.txt" 152 | 153 | # Get the common prefix between the output path and the filename 154 | common_prefix = os.path.commonpath([output_path, os.path.dirname(filename)]) 155 | relative_filename = os.path.relpath(filename, common_prefix) 156 | target_path = os.path.join(output_path, relative_filename) 157 | 158 | # Ensure the target directory exists 159 | os.makedirs(os.path.dirname(target_path), exist_ok=True) 160 | 161 | # Write embedding values to a text file 162 | with open(target_path, "w") as f: 163 | f.write(",".join(map(str, embedding.tolist()))) 164 | 165 | def run(audio_input, database, overlap, audio_speed, fmin, fmax, threads, batchsize, file_output): 166 | ### Make sure to comment out appropriately if you are not using args. ### 167 | 168 | # Set input and output path 169 | cfg.INPUT_PATH = audio_input 170 | 171 | # Parse input files 172 | if os.path.isdir(cfg.INPUT_PATH): 173 | cfg.FILE_LIST = utils.collect_audio_files(cfg.INPUT_PATH) 174 | else: 175 | cfg.FILE_LIST = [cfg.INPUT_PATH] 176 | 177 | # Set overlap 178 | cfg.SIG_OVERLAP = max(0.0, min(2.9, float(overlap))) 179 | 180 | # Set audio speed 181 | cfg.AUDIO_SPEED = max(0.01, audio_speed) 182 | 183 | # Set bandpass frequency range 184 | cfg.BANDPASS_FMIN = max(0, min(cfg.SIG_FMAX, int(fmin))) 185 | cfg.BANDPASS_FMAX = max(cfg.SIG_FMIN, min(cfg.SIG_FMAX, int(fmax))) 186 | 187 | # Set number of threads 188 | if os.path.isdir(cfg.INPUT_PATH): 189 | cfg.CPU_THREADS = max(1, int(threads)) 190 | cfg.TFLITE_THREADS = 1 191 | else: 192 | cfg.CPU_THREADS = 1 193 | cfg.TFLITE_THREADS = max(1, int(threads)) 194 | 195 | cfg.CPU_THREADS = 1 # TODO: with the current implementation, we can't use more than 1 thread 196 | 197 | # Set batch size 198 | cfg.BATCH_SIZE = max(1, int(batchsize)) 199 | 200 | # Add config items to each file list entry. 201 | # We have to do this for Windows which does not 202 | # support fork() and thus each process has to 203 | # have its own config. USE LINUX! 204 | flist = [(f, cfg.get_config()) for f in cfg.FILE_LIST] 205 | 206 | db = get_database(database) 207 | check_database_settings(db) 208 | 209 | # Analyze files 210 | if cfg.CPU_THREADS < 2: 211 | for entry in tqdm(flist): 212 | analyze_file(entry, db) 213 | else: 214 | with Pool(cfg.CPU_THREADS) as p: 215 | tqdm(p.imap(partial(analyze_file, db=db), flist)) 216 | 217 | if file_output: 218 | create_file_output(file_output, db) 219 | 220 | db.db.close() 221 | -------------------------------------------------------------------------------- /birdnet_analyzer/evaluation/__main__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.evaluation import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /birdnet_analyzer/evaluation/assessment/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/birdnet_analyzer/evaluation/assessment/__init__.py -------------------------------------------------------------------------------- /birdnet_analyzer/evaluation/preprocessing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/birdnet_analyzer/evaluation/preprocessing/__init__.py -------------------------------------------------------------------------------- /birdnet_analyzer/evaluation/preprocessing/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility Functions for Data Processing Tasks 3 | 4 | This module provides helper functions to handle common data processing tasks, such as: 5 | - Extracting recording filenames from file paths or filenames. 6 | - Reading and concatenating text files from a specified directory. 7 | 8 | It is designed to work seamlessly with pandas and file system operations. 9 | """ 10 | 11 | import os 12 | 13 | import pandas as pd 14 | 15 | 16 | def extract_recording_filename(path_column: pd.Series) -> pd.Series: 17 | """ 18 | Extract the recording filename from a path column. 19 | 20 | This function processes a pandas Series containing file paths and extracts the base filename 21 | (without the extension) for each path. 22 | 23 | Args: 24 | path_column (pd.Series): A pandas Series containing file paths. 25 | 26 | Returns: 27 | pd.Series: A pandas Series containing the extracted recording filenames. 28 | """ 29 | # Apply a lambda function to extract the base filename without extension 30 | return path_column.apply(lambda x: os.path.splitext(os.path.basename(x))[0] if isinstance(x, str) else x) 31 | 32 | 33 | def extract_recording_filename_from_filename(filename_series: pd.Series) -> pd.Series: 34 | """ 35 | Extract the recording filename from a filename Series. 36 | 37 | This function processes a pandas Series containing filenames and extracts the base filename 38 | (without the extension) for each. 39 | 40 | Args: 41 | filename_series (pd.Series): A pandas Series containing filenames. 42 | 43 | Returns: 44 | pd.Series: A pandas Series containing the extracted recording filenames. 45 | """ 46 | # Apply a lambda function to split filenames and remove the extension 47 | return filename_series.apply(lambda x: x.split(".")[0] if isinstance(x, str) else x) 48 | 49 | 50 | def read_and_concatenate_files_in_directory(directory_path: str) -> pd.DataFrame: 51 | """ 52 | Read and concatenate all .txt files in a directory into a single DataFrame. 53 | 54 | This function scans the specified directory for all .txt files, reads each file into a DataFrame, 55 | appends a 'source_file' column containing the filename, and concatenates all DataFrames into one. 56 | If the files have inconsistent columns, a ValueError is raised. 57 | 58 | Args: 59 | directory_path (str): Path to the directory containing the .txt files. 60 | 61 | Returns: 62 | pd.DataFrame: A concatenated DataFrame containing the data from all .txt files, 63 | or an empty DataFrame if no files are found. 64 | 65 | Raises: 66 | ValueError: If the columns in the files are inconsistent. 67 | """ 68 | df_list: list[pd.DataFrame] = [] # List to hold individual DataFrames 69 | columns_set = None # To ensure consistency in column names 70 | 71 | # Iterate through each file in the directory 72 | for filename in sorted(os.listdir(directory_path)): 73 | if filename.endswith(".txt"): 74 | filepath = os.path.join(directory_path, filename) # Construct the full file path 75 | 76 | try: 77 | # Attempt to read the file as a tab-separated values file with UTF-8 encoding 78 | df = pd.read_csv(filepath, sep="\t", encoding="utf-8") 79 | except UnicodeDecodeError: 80 | # Fallback to 'latin-1' encoding if UTF-8 fails 81 | df = pd.read_csv(filepath, sep="\t", encoding="latin-1") 82 | 83 | # Check for column consistency across files 84 | if columns_set is None: 85 | columns_set = set(df.columns) # Initialize with the first file's columns 86 | elif set(df.columns) != columns_set: 87 | raise ValueError(f"File {filename} has different columns than the previous files.") 88 | 89 | # Add a column to indicate the source file for traceability 90 | df["source_file"] = filename 91 | 92 | # Append the DataFrame to the list 93 | df_list.append(df) 94 | 95 | # Concatenate all DataFrames if any were processed, else return an empty DataFrame 96 | if df_list: 97 | return pd.concat(df_list, ignore_index=True) 98 | return pd.DataFrame() # Return an empty DataFrame if no .txt files were found 99 | -------------------------------------------------------------------------------- /birdnet_analyzer/example/soundscape.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/birdnet_analyzer/example/soundscape.wav -------------------------------------------------------------------------------- /birdnet_analyzer/example/species_list.txt: -------------------------------------------------------------------------------- 1 | Accipiter cooperii_Cooper's Hawk 2 | Agelaius phoeniceus_Red-winged Blackbird 3 | Anas platyrhynchos_Mallard 4 | Anas rubripes_American Black Duck 5 | Ardea herodias_Great Blue Heron 6 | Baeolophus bicolor_Tufted Titmouse 7 | Branta canadensis_Canada Goose 8 | Bucephala albeola_Bufflehead 9 | Bucephala clangula_Common Goldeneye 10 | Buteo jamaicensis_Red-tailed Hawk 11 | Cardinalis cardinalis_Northern Cardinal 12 | Certhia americana_Brown Creeper 13 | Colaptes auratus_Northern Flicker 14 | Columba livia_Rock Pigeon 15 | Corvus brachyrhynchos_American Crow 16 | Corvus corax_Common Raven 17 | Cyanocitta cristata_Blue Jay 18 | Cygnus olor_Mute Swan 19 | Dryobates pubescens_Downy Woodpecker 20 | Dryobates villosus_Hairy Woodpecker 21 | Dryocopus pileatus_Pileated Woodpecker 22 | Eremophila alpestris_Horned Lark 23 | Haemorhous mexicanus_House Finch 24 | Haemorhous purpureus_Purple Finch 25 | Haliaeetus leucocephalus_Bald Eagle 26 | Junco hyemalis_Dark-eyed Junco 27 | Larus argentatus_Herring Gull 28 | Larus delawarensis_Ring-billed Gull 29 | Lophodytes cucullatus_Hooded Merganser 30 | Melanerpes carolinus_Red-bellied Woodpecker 31 | Meleagris gallopavo_Wild Turkey 32 | Melospiza melodia_Song Sparrow 33 | Mergus merganser_Common Merganser 34 | Mergus serrator_Red-breasted Merganser 35 | Passer domesticus_House Sparrow 36 | Poecile atricapillus_Black-capped Chickadee 37 | Regulus satrapa_Golden-crowned Kinglet 38 | Sialia sialis_Eastern Bluebird 39 | Sitta canadensis_Red-breasted Nuthatch 40 | Sitta carolinensis_White-breasted Nuthatch 41 | Spinus pinus_Pine Siskin 42 | Spinus tristis_American Goldfinch 43 | Spizelloides arborea_American Tree Sparrow 44 | Sturnus vulgaris_European Starling 45 | Thryothorus ludovicianus_Carolina Wren 46 | Turdus migratorius_American Robin 47 | Zenaida macroura_Mourning Dove 48 | Zonotrichia albicollis_White-throated Sparrow 49 | -------------------------------------------------------------------------------- /birdnet_analyzer/gui/__init__.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | import birdnet_analyzer.gui.multi_file as mfa 3 | import birdnet_analyzer.gui.segments as gs 4 | import birdnet_analyzer.gui.single_file as sfa 5 | import birdnet_analyzer.gui.utils as gu 6 | from birdnet_analyzer.gui import embeddings, evaluation, review, species, train 7 | 8 | gu.open_window( 9 | [ 10 | sfa.build_single_analysis_tab, 11 | mfa.build_multi_analysis_tab, 12 | train.build_train_tab, 13 | gs.build_segments_tab, 14 | review.build_review_tab, 15 | species.build_species_tab, 16 | embeddings.build_embeddings_tab, 17 | evaluation.build_evaluation_tab, 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /birdnet_analyzer/gui/__main__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.gui import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /birdnet_analyzer/gui/analysis.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import os 3 | from pathlib import Path 4 | 5 | import gradio as gr 6 | 7 | import birdnet_analyzer.config as cfg 8 | import birdnet_analyzer.gui.utils as gu 9 | from birdnet_analyzer import model 10 | from birdnet_analyzer.analyze.utils import ( 11 | analyze_file, 12 | combine_results, 13 | save_analysis_params, 14 | ) 15 | 16 | SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__)) 17 | ORIGINAL_LABELS_FILE = str(Path(SCRIPT_DIR).parent / cfg.LABELS_FILE) 18 | 19 | 20 | def analyze_file_wrapper(entry): 21 | """ 22 | Wrapper function for analyzing a file. 23 | 24 | Args: 25 | entry (tuple): A tuple where the first element is the file path and the 26 | remaining elements are arguments to be passed to the 27 | analyze.analyzeFile function. 28 | 29 | Returns: 30 | tuple: A tuple where the first element is the file path and the second 31 | element is the result of the analyze.analyzeFile function. 32 | """ 33 | return (entry[0], analyze_file(entry)) 34 | 35 | 36 | def run_analysis( 37 | input_path: str, 38 | output_path: str | None, 39 | use_top_n: bool, 40 | top_n: int, 41 | confidence: float, 42 | sensitivity: float, 43 | overlap: float, 44 | merge_consecutive: int, 45 | audio_speed: float, 46 | fmin: int, 47 | fmax: int, 48 | species_list_choice: str, 49 | species_list_file, 50 | lat: float, 51 | lon: float, 52 | week: int, 53 | use_yearlong: bool, 54 | sf_thresh: float, 55 | custom_classifier_file, 56 | output_types: str, 57 | additional_columns: list[str] | None, 58 | combine_tables: bool, 59 | locale: str, 60 | batch_size: int, 61 | threads: int, 62 | input_dir: str, 63 | skip_existing: bool, 64 | save_params: bool, 65 | progress: gr.Progress | None, 66 | ): 67 | """Starts the analysis. 68 | 69 | Args: 70 | input_path: Either a file or directory. 71 | output_path: The output path for the result, if None the input_path is used 72 | confidence: The selected minimum confidence. 73 | sensitivity: The selected sensitivity. 74 | overlap: The selected segment overlap. 75 | merge_consecutive: The number of consecutive segments to merge into one. 76 | audio_speed: The selected audio speed. 77 | fmin: The selected minimum bandpass frequency. 78 | fmax: The selected maximum bandpass frequency. 79 | species_list_choice: The choice for the species list. 80 | species_list_file: The selected custom species list file. 81 | lat: The selected latitude. 82 | lon: The selected longitude. 83 | week: The selected week of the year. 84 | use_yearlong: Use yearlong instead of week. 85 | sf_thresh: The threshold for the predicted species list. 86 | custom_classifier_file: Custom classifier to be used. 87 | output_type: The type of result to be generated. 88 | additional_columns: Additional columns to be added to the result. 89 | output_filename: The filename for the combined output. 90 | locale: The translation to be used. 91 | batch_size: The number of samples in a batch. 92 | threads: The number of threads to be used. 93 | input_dir: The input directory. 94 | progress: The gradio progress bar. 95 | """ 96 | import birdnet_analyzer.gui.localization as loc 97 | 98 | if progress is not None: 99 | progress(0, desc=f"{loc.localize('progress-preparing')} ...") 100 | 101 | from birdnet_analyzer.analyze.core import _set_params 102 | 103 | locale = locale.lower() 104 | custom_classifier = custom_classifier_file if species_list_choice == gu._CUSTOM_CLASSIFIER else None 105 | slist = species_list_file if species_list_choice == gu._CUSTOM_SPECIES else None 106 | lat = lat if species_list_choice == gu._PREDICT_SPECIES else -1 107 | lon = lon if species_list_choice == gu._PREDICT_SPECIES else -1 108 | week = -1 if use_yearlong else week 109 | 110 | flist = _set_params( 111 | audio_input=input_dir if input_dir else input_path, 112 | min_conf=confidence, 113 | custom_classifier=custom_classifier, 114 | sensitivity=min(1.25, max(0.75, float(sensitivity))), 115 | locale=locale, 116 | overlap=max(0.0, min(2.9, float(overlap))), 117 | merge_consecutive=max(1, int(merge_consecutive)), 118 | audio_speed=max(0.1, 1.0 / (audio_speed * -1)) if audio_speed < 0 else max(1.0, float(audio_speed)), 119 | fmin=max(0, min(cfg.SIG_FMAX, int(fmin))), 120 | fmax=max(cfg.SIG_FMIN, min(cfg.SIG_FMAX, int(fmax))), 121 | bs=max(1, int(batch_size)), 122 | combine_results=combine_tables, 123 | rtype=output_types, 124 | skip_existing_results=skip_existing, 125 | threads=max(1, int(threads)), 126 | labels_file=ORIGINAL_LABELS_FILE, 127 | sf_thresh=sf_thresh, 128 | lat=lat, 129 | lon=lon, 130 | week=week, 131 | slist=slist, 132 | top_n=top_n if use_top_n else None, 133 | output=output_path, 134 | additional_columns=additional_columns, 135 | ) 136 | 137 | if species_list_choice == gu._CUSTOM_CLASSIFIER: 138 | if custom_classifier_file is None: 139 | raise gr.Error(loc.localize("validation-no-custom-classifier-selected")) 140 | 141 | model.reset_custom_classifier() 142 | 143 | gu.validate(cfg.FILE_LIST, loc.localize("validation-no-audio-files-found")) 144 | 145 | result_list = [] 146 | 147 | if progress is not None: 148 | progress(0, desc=f"{loc.localize('progress-starting')} ...") 149 | 150 | # Analyze files 151 | if cfg.CPU_THREADS < 2: 152 | result_list.extend(analyze_file_wrapper(entry) for entry in flist) 153 | else: 154 | with concurrent.futures.ProcessPoolExecutor(max_workers=cfg.CPU_THREADS) as executor: 155 | futures = (executor.submit(analyze_file_wrapper, arg) for arg in flist) 156 | for i, f in enumerate(concurrent.futures.as_completed(futures), start=1): 157 | if progress is not None: 158 | progress((i, len(flist)), total=len(flist), unit="files") 159 | result = f.result() 160 | 161 | result_list.append(result) 162 | 163 | # Combine results? 164 | if cfg.COMBINE_RESULTS: 165 | combine_list = [[r[1] for r in result_list if r[0] == i[0]][0] for i in flist] 166 | print(f"Combining results, writing to {cfg.OUTPUT_PATH}...", end="", flush=True) 167 | combine_results(combine_list) 168 | print("done!", flush=True) 169 | 170 | if save_params: 171 | save_analysis_params(os.path.join(cfg.OUTPUT_PATH, cfg.ANALYSIS_PARAMS_FILENAME)) 172 | 173 | return ( 174 | [[os.path.relpath(r[0], input_dir), bool(r[1])] for r in result_list] 175 | if input_dir 176 | else result_list[0][1]["csv"] 177 | if result_list[0][1] 178 | else None 179 | ) 180 | -------------------------------------------------------------------------------- /birdnet_analyzer/gui/assets/arrow_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /birdnet_analyzer/gui/assets/arrow_left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /birdnet_analyzer/gui/assets/arrow_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /birdnet_analyzer/gui/assets/arrow_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /birdnet_analyzer/gui/assets/gui.css: -------------------------------------------------------------------------------- 1 | footer { 2 | display: none !important; 3 | } 4 | 5 | #single_file_audio, 6 | #single_file_audio * { 7 | max-height: 81.6px; 8 | min-height: 0; 9 | } 10 | 11 | #update-available a { 12 | text-decoration: none; 13 | } 14 | 15 | :root { 16 | --block-title-text-color: var(--neutral-800); 17 | --block-info-text-color: var(--neutral-500); 18 | } 19 | 20 | #single-file-output td:first-of-type span { 21 | text-align: center; 22 | } 23 | 24 | #embeddings-search-results { 25 | max-height: 1107px; 26 | overflow: auto; 27 | flex-wrap: nowrap; 28 | padding-right: 5px; 29 | } 30 | 31 | #heart { 32 | display: inline; 33 | background-color: white; 34 | border-radius: 3px; 35 | margin-right: 3px; 36 | padding: 1px; 37 | } -------------------------------------------------------------------------------- /birdnet_analyzer/gui/assets/gui.js: -------------------------------------------------------------------------------- 1 | function init() { 2 | function checkForNewerVersion() { 3 | let gui_version_element = document.getElementById("current-version") 4 | 5 | if (gui_version_element && gui_version_element.textContent != "main") { 6 | console.log("Checking for newer version..."); 7 | 8 | function sendGetRequest(url) { 9 | return new Promise((resolve, reject) => { 10 | const xhr = new XMLHttpRequest(); 11 | xhr.open("GET", url); 12 | xhr.onload = () => { 13 | if (xhr.status === 200) { 14 | resolve(xhr.responseText); 15 | } else { 16 | reject(new Error(`Request failed with status ${xhr.status}`)); 17 | } 18 | }; 19 | xhr.onerror = () => { 20 | reject(new Error("Request failed")); 21 | }; 22 | xhr.send(); 23 | }); 24 | } 25 | 26 | const apiUrl = "https://api.github.com/repos/birdnet-team/BirdNET-Analyzer/releases/latest"; 27 | 28 | sendGetRequest(apiUrl) 29 | .then(response => { 30 | const current_version = document.getElementById("current-version").textContent; 31 | const response_object = JSON.parse(response); 32 | const latest_version = response_object.tag_name; 33 | 34 | if (latest_version.startsWith("v")) { 35 | latest_version = latest_version.slice(1); 36 | } 37 | 38 | if (current_version !== latest_version) { 39 | const updateNotification = document.getElementById("update-available"); 40 | 41 | updateNotification.style.display = "block"; 42 | const linkElement = updateNotification.getElementsByTagName("a")[0] 43 | linkElement.href = response_object.html_url; 44 | linkElement.target = "_blank"; 45 | } 46 | }) 47 | .catch(error => { 48 | console.error(error); 49 | }); 50 | } 51 | } 52 | 53 | function overwriteStyles() { 54 | console.log("Overwriting styles..."); 55 | const styles = document.createElement("style"); 56 | styles.innerHTML = "@media (width <= 1024px) { .app {max-width: initial !important;}}"; 57 | document.head.appendChild(styles); 58 | } 59 | 60 | function bindReviewKeyShortcuts() { 61 | const posBtn = document.getElementById("positive-button"); 62 | const negBtn = document.getElementById("negative-button"); 63 | const skipBtn = document.getElementById("skip-button"); 64 | const undoBtn = document.getElementById("undo-button"); 65 | 66 | if (!posBtn || !negBtn) return; 67 | 68 | console.log("Binding review key shortcuts..."); 69 | 70 | document.addEventListener("keydown", function (event) { 71 | const reviewTabBtn = document.getElementById("review-tab-button"); 72 | 73 | if (reviewTabBtn.ariaSelected === "false") return; 74 | 75 | if (event.key === "ArrowUp") { 76 | event.preventDefault(); 77 | posBtn.click(); 78 | } else if (event.key === "ArrowDown") { 79 | event.preventDefault(); 80 | negBtn.click(); 81 | } else if (event.key === "ArrowLeft") { 82 | event.preventDefault(); 83 | undoBtn.click(); 84 | } else if (event.key === "ArrowRight") { 85 | event.preventDefault(); 86 | skipBtn.click(); 87 | } 88 | }); 89 | } 90 | 91 | checkForNewerVersion(); 92 | overwriteStyles(); 93 | bindReviewKeyShortcuts(); 94 | } -------------------------------------------------------------------------------- /birdnet_analyzer/gui/assets/img/birdnet-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/birdnet_analyzer/gui/assets/img/birdnet-icon.ico -------------------------------------------------------------------------------- /birdnet_analyzer/gui/assets/img/birdnet_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/birdnet_analyzer/gui/assets/img/birdnet_logo.png -------------------------------------------------------------------------------- /birdnet_analyzer/gui/assets/img/birdnet_logo_no_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/birdnet_analyzer/gui/assets/img/birdnet_logo_no_transparent.png -------------------------------------------------------------------------------- /birdnet_analyzer/gui/assets/img/clo-logo-bird.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /birdnet_analyzer/gui/localization.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: PLW0603 2 | import json 3 | import os 4 | 5 | from birdnet_analyzer.gui import settings 6 | 7 | SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__)) 8 | LANGUAGE_DIR = os.path.join(os.path.dirname(SCRIPT_DIR), "lang") 9 | LANGUAGE_LOOKUP = {} 10 | TARGET_LANGUAGE = settings.FALLBACK_LANGUAGE 11 | 12 | 13 | def load_local_state(): 14 | """ 15 | Loads the local language settings and populates the LANGUAGE_LOOKUP dictionary with the appropriate translations. 16 | This function performs the following steps: 17 | """ 18 | global LANGUAGE_LOOKUP 19 | global TARGET_LANGUAGE 20 | 21 | settings.ensure_settings_file() 22 | 23 | try: 24 | with open(settings.GUI_SETTINGS_PATH, encoding="utf-8") as f: 25 | settings_data = json.load(f) 26 | 27 | if "language-id" in settings_data: 28 | TARGET_LANGUAGE = settings_data["language-id"] 29 | except FileNotFoundError: 30 | print(f"gui-settings.json not found. Using fallback language {settings.FALLBACK_LANGUAGE}.") 31 | 32 | try: 33 | with open(f"{LANGUAGE_DIR}/{TARGET_LANGUAGE}.json", encoding="utf-8") as f: 34 | LANGUAGE_LOOKUP = json.load(f) 35 | except FileNotFoundError: 36 | print( 37 | f"Language file for {TARGET_LANGUAGE} not found in {LANGUAGE_DIR}." 38 | + "Using fallback language {settings.FALLBACK_LANGUAGE}." 39 | ) 40 | 41 | if TARGET_LANGUAGE != settings.FALLBACK_LANGUAGE: 42 | with open(f"{LANGUAGE_DIR}/{settings.FALLBACK_LANGUAGE}.json") as f: 43 | fallback: dict = json.load(f) 44 | 45 | for key, value in fallback.items(): 46 | if key not in LANGUAGE_LOOKUP: 47 | LANGUAGE_LOOKUP[key] = value 48 | 49 | 50 | def localize(key: str) -> str: 51 | """ 52 | Translates a given key into its corresponding localized string. 53 | 54 | Args: 55 | key (str): The key to be localized. 56 | 57 | Returns: 58 | str: The localized string corresponding to the given key. 59 | If the key is not found in the localization lookup, the original key is returned. 60 | """ 61 | return LANGUAGE_LOOKUP.get(key, key) 62 | 63 | 64 | def set_language(language: str): 65 | """ 66 | Sets the language for the application by updating the GUI settings file. 67 | This function ensures that the settings file exists, reads the current settings, 68 | updates the "language-id" field with the provided language, and writes the updated 69 | settings back to the file. 70 | 71 | Args: 72 | language (str): The language identifier to set in the settings file. 73 | """ 74 | if language: 75 | settings.set_setting("language-id", language) 76 | -------------------------------------------------------------------------------- /birdnet_analyzer/gui/segments.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import os 3 | from functools import partial 4 | 5 | import gradio as gr 6 | 7 | import birdnet_analyzer.config as cfg 8 | import birdnet_analyzer.gui.localization as loc 9 | import birdnet_analyzer.gui.utils as gu 10 | from birdnet_analyzer.segments.utils import extract_segments 11 | 12 | 13 | def extract_segments_wrapper(entry): 14 | return (entry[0][0], extract_segments(entry)) 15 | 16 | 17 | @gu.gui_runtime_error_handler 18 | def _extract_segments( 19 | audio_dir, result_dir, output_dir, min_conf, num_seq, audio_speed, seq_length, threads, progress=gr.Progress() 20 | ): 21 | from birdnet_analyzer.segments.utils import parse_files, parse_folders 22 | 23 | gu.validate(audio_dir, loc.localize("validation-no-audio-directory-selected")) 24 | 25 | if not result_dir: 26 | result_dir = audio_dir 27 | 28 | if not output_dir: 29 | output_dir = audio_dir 30 | 31 | if progress is not None: 32 | progress(0, desc=f"{loc.localize('progress-search')} ...") 33 | 34 | # Parse audio and result folders 35 | cfg.FILE_LIST = parse_folders(audio_dir, result_dir) 36 | 37 | # Set output folder 38 | cfg.OUTPUT_PATH = output_dir 39 | 40 | # Set number of threads 41 | cfg.CPU_THREADS = int(threads) 42 | 43 | # Set confidence threshold 44 | cfg.MIN_CONFIDENCE = max(0.01, min(0.99, min_conf)) 45 | 46 | # Parse file list and make list of segments 47 | cfg.FILE_LIST = parse_files(cfg.FILE_LIST, max(1, int(num_seq))) 48 | 49 | # Audio speed 50 | cfg.AUDIO_SPEED = max(0.1, 1.0 / (audio_speed * -1)) if audio_speed < 0 else max(1.0, float(audio_speed)) 51 | 52 | # Add config items to each file list entry. 53 | # We have to do this for Windows which does not 54 | # support fork() and thus each process has to 55 | # have its own config. USE LINUX! 56 | # flist = [(entry, max(cfg.SIG_LENGTH, float(seq_length)), cfg.getConfig()) for entry in cfg.FILE_LIST] 57 | flist = [(entry, float(seq_length), cfg.get_config()) for entry in cfg.FILE_LIST] 58 | 59 | result_list = [] 60 | 61 | # Extract segments 62 | if cfg.CPU_THREADS < 2: 63 | for i, entry in enumerate(flist): 64 | result = extract_segments_wrapper(entry) 65 | result_list.append(result) 66 | 67 | if progress is not None: 68 | progress((i, len(flist)), total=len(flist), unit="files") 69 | else: 70 | with concurrent.futures.ProcessPoolExecutor(max_workers=cfg.CPU_THREADS) as executor: 71 | futures = (executor.submit(extract_segments_wrapper, arg) for arg in flist) 72 | for i, f in enumerate(concurrent.futures.as_completed(futures), start=1): 73 | if progress is not None: 74 | progress((i, len(flist)), total=len(flist), unit="files") 75 | result = f.result() 76 | 77 | result_list.append(result) 78 | 79 | return [[os.path.relpath(r[0], audio_dir), r[1]] for r in result_list] 80 | 81 | 82 | def build_segments_tab(): 83 | with gr.Tab(loc.localize("segments-tab-title")): 84 | audio_directory_state = gr.State() 85 | result_directory_state = gr.State() 86 | output_directory_state = gr.State() 87 | 88 | def select_directory_to_state_and_tb(state_key): 89 | return (gu.select_directory(collect_files=False, state_key=state_key),) * 2 90 | 91 | with gr.Row(): 92 | select_audio_directory_btn = gr.Button( 93 | loc.localize("segments-tab-select-audio-input-directory-button-label") 94 | ) 95 | selected_audio_directory_tb = gr.Textbox(show_label=False, interactive=False) 96 | select_audio_directory_btn.click( 97 | partial(select_directory_to_state_and_tb, state_key="segments-audio-dir"), 98 | outputs=[selected_audio_directory_tb, audio_directory_state], 99 | show_progress=False, 100 | ) 101 | 102 | with gr.Row(): 103 | select_result_directory_btn = gr.Button( 104 | loc.localize("segments-tab-select-results-input-directory-button-label") 105 | ) 106 | selected_result_directory_tb = gr.Textbox( 107 | show_label=False, 108 | interactive=False, 109 | placeholder=loc.localize("segments-tab-results-input-textbox-placeholder"), 110 | ) 111 | select_result_directory_btn.click( 112 | partial(select_directory_to_state_and_tb, state_key="segments-result-dir"), 113 | outputs=[result_directory_state, selected_result_directory_tb], 114 | show_progress=False, 115 | ) 116 | 117 | with gr.Row(): 118 | select_output_directory_btn = gr.Button(loc.localize("segments-tab-output-selection-button-label")) 119 | selected_output_directory_tb = gr.Textbox( 120 | show_label=False, 121 | interactive=False, 122 | placeholder=loc.localize("segments-tab-output-selection-textbox-placeholder"), 123 | ) 124 | select_output_directory_btn.click( 125 | partial(select_directory_to_state_and_tb, state_key="segments-output-dir"), 126 | outputs=[selected_output_directory_tb, output_directory_state], 127 | show_progress=False, 128 | ) 129 | 130 | min_conf_slider = gr.Slider( 131 | minimum=0.1, 132 | maximum=0.99, 133 | step=0.01, 134 | value=cfg.MIN_CONFIDENCE, 135 | label=loc.localize("segments-tab-min-confidence-slider-label"), 136 | info=loc.localize("segments-tab-min-confidence-slider-info"), 137 | ) 138 | num_seq_number = gr.Number( 139 | 100, 140 | label=loc.localize("segments-tab-max-seq-number-label"), 141 | info=loc.localize("segments-tab-max-seq-number-info"), 142 | minimum=1, 143 | ) 144 | audio_speed_slider = gr.Slider( 145 | minimum=-10, 146 | maximum=10, 147 | value=cfg.AUDIO_SPEED, 148 | step=1, 149 | label=loc.localize("inference-settings-audio-speed-slider-label"), 150 | info=loc.localize("inference-settings-audio-speed-slider-info"), 151 | ) 152 | seq_length_number = gr.Number( 153 | cfg.SIG_LENGTH, 154 | label=loc.localize("segments-tab-seq-length-number-label"), 155 | info=loc.localize("segments-tab-seq-length-number-info"), 156 | minimum=0.1, 157 | ) 158 | threads_number = gr.Number( 159 | 4, 160 | label=loc.localize("segments-tab-threads-number-label"), 161 | info=loc.localize("segments-tab-threads-number-info"), 162 | minimum=1, 163 | ) 164 | 165 | extract_segments_btn = gr.Button(loc.localize("segments-tab-extract-button-label"), variant="huggingface") 166 | 167 | result_grid = gr.Matrix( 168 | headers=[ 169 | loc.localize("segments-tab-result-dataframe-column-file-header"), 170 | loc.localize("segments-tab-result-dataframe-column-execution-header"), 171 | ], 172 | ) 173 | 174 | extract_segments_btn.click( 175 | _extract_segments, 176 | inputs=[ 177 | audio_directory_state, 178 | result_directory_state, 179 | output_directory_state, 180 | min_conf_slider, 181 | num_seq_number, 182 | audio_speed_slider, 183 | seq_length_number, 184 | threads_number, 185 | ], 186 | outputs=result_grid, 187 | ) 188 | 189 | 190 | if __name__ == "__main__": 191 | gu.open_window(build_segments_tab) 192 | -------------------------------------------------------------------------------- /birdnet_analyzer/gui/settings.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | import birdnet_analyzer.config as cfg 7 | from birdnet_analyzer import utils 8 | 9 | if utils.FROZEN: 10 | # divert stdout & stderr to logs.txt file since we have no console when deployed 11 | userdir = Path.home() 12 | 13 | if sys.platform == "win32": 14 | userdir /= "AppData/Roaming" 15 | elif sys.platform == "linux": 16 | userdir /= ".local/share" 17 | elif sys.platform == "darwin": 18 | userdir /= "Library/Application Support" 19 | 20 | APPDIR = userdir / "BirdNET-Analyzer-GUI" 21 | 22 | APPDIR.mkdir(parents=True, exist_ok=True) 23 | 24 | sys.stderr = sys.stdout = open(str(APPDIR / "logs.txt"), "a") # noqa: SIM115 25 | cfg.ERROR_LOG_FILE = str(APPDIR / os.path.basename(cfg.ERROR_LOG_FILE)) 26 | else: 27 | APPDIR = "" 28 | 29 | FALLBACK_LANGUAGE = "en" 30 | SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__)) 31 | GUI_SETTINGS_PATH = os.path.join(APPDIR if utils.FROZEN else os.path.dirname(SCRIPT_DIR), "gui-settings.json") 32 | LANG_DIR = str(Path(SCRIPT_DIR).parent / "lang") 33 | STATE_SETTINGS_PATH = os.path.join(APPDIR if utils.FROZEN else os.path.dirname(SCRIPT_DIR), "state.json") 34 | 35 | 36 | def get_state_dict() -> dict: 37 | """ 38 | Retrieves the state dictionary from a JSON file specified by STATE_SETTINGS_PATH. 39 | 40 | If the file does not exist, it creates an empty JSON file and returns an empty dictionary. 41 | If any other exception occurs during file operations, it logs the error and returns an empty dictionary. 42 | 43 | Returns: 44 | dict: The state dictionary loaded from the JSON file, or an empty dictionary if the file does not exist or an error occurs. 45 | """ 46 | try: 47 | with open(STATE_SETTINGS_PATH, encoding="utf-8") as f: 48 | return json.load(f) 49 | except FileNotFoundError: 50 | try: 51 | with open(STATE_SETTINGS_PATH, "w", encoding="utf-8") as f: 52 | json.dump({}, f) 53 | return {} 54 | except Exception as e: 55 | utils.write_error_log(e) 56 | return {} 57 | 58 | 59 | def get_state(key: str, default=None): 60 | """ 61 | Retrieves the value associated with the given key from the state dictionary. 62 | 63 | Args: 64 | key (str): The key to look up in the state dictionary. 65 | default: The value to return if the key is not found. Defaults to None. 66 | 67 | Returns: 68 | str: The value associated with the key if found, otherwise the default value. 69 | """ 70 | return get_state_dict().get(key, default) 71 | 72 | 73 | def set_state(key: str, value: str): 74 | """ 75 | Updates the state dictionary with the given key-value pair and writes it to a JSON file. 76 | 77 | Args: 78 | key (str): The key to update in the state dictionary. 79 | value (str): The value to associate with the key in the state dictionary. 80 | """ 81 | try: 82 | state = get_state_dict() 83 | state[key] = value 84 | 85 | with open(STATE_SETTINGS_PATH, "w") as f: 86 | json.dump(state, f, indent=4) 87 | except Exception as e: 88 | utils.write_error_log(e) 89 | 90 | 91 | def ensure_settings_file(): 92 | """ 93 | Ensures that the settings file exists at the specified path. If the file does not exist, 94 | it creates a new settings file with default settings. 95 | 96 | If the file creation fails, the error is logged. 97 | """ 98 | if not os.path.exists(GUI_SETTINGS_PATH): 99 | try: 100 | with open(GUI_SETTINGS_PATH, "w") as f: 101 | settings = {"language-id": FALLBACK_LANGUAGE, "theme": "light"} 102 | f.write(json.dumps(settings, indent=4)) 103 | except Exception as e: 104 | utils.write_error_log(e) 105 | 106 | 107 | def get_setting(key, default=None): 108 | """ 109 | Retrieves the value associated with the given key from the settings file. 110 | 111 | Args: 112 | key (str): The key to look up in the settings file. 113 | default: The value to return if the key is not found. Defaults to None. 114 | 115 | Returns: 116 | str: The value associated with the key if found, otherwise the default value. 117 | """ 118 | ensure_settings_file() 119 | 120 | try: 121 | with open(GUI_SETTINGS_PATH, encoding="utf-8") as f: 122 | settings_dict: dict = json.load(f) 123 | 124 | return settings_dict.get(key, default) 125 | except FileNotFoundError: 126 | return default 127 | 128 | 129 | def set_setting(key, value): 130 | ensure_settings_file() 131 | settings_dict = {} 132 | 133 | try: 134 | with open(GUI_SETTINGS_PATH, "r+", encoding="utf-8") as f: 135 | settings_dict = json.load(f) 136 | settings_dict[key] = value 137 | f.seek(0) 138 | json.dump(settings_dict, f, indent=4) 139 | f.truncate() 140 | 141 | except FileNotFoundError: 142 | pass 143 | 144 | 145 | def theme(): 146 | options = ("light", "dark") 147 | current_time = get_setting("theme", "light") 148 | 149 | return current_time if current_time in options else "light" 150 | -------------------------------------------------------------------------------- /birdnet_analyzer/gui/species.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import gradio as gr 4 | 5 | import birdnet_analyzer.config as cfg 6 | import birdnet_analyzer.gui.localization as loc 7 | import birdnet_analyzer.gui.utils as gu 8 | from birdnet_analyzer.gui import settings 9 | 10 | 11 | @gu.gui_runtime_error_handler 12 | def run_species_list(out_path, filename, lat, lon, week, use_yearlong, sf_thresh, sortby): 13 | from birdnet_analyzer.species.utils import run 14 | 15 | gu.validate(out_path, loc.localize("validation-no-directory-selected")) 16 | 17 | run( 18 | os.path.join(out_path, filename if filename else "species_list.txt"), 19 | lat, 20 | lon, 21 | -1 if use_yearlong else week, 22 | sf_thresh, 23 | sortby, 24 | ) 25 | 26 | gr.Info(f"{loc.localize('species-tab-finish-info')} {cfg.OUTPUT_PATH}") 27 | 28 | 29 | def build_species_tab(): 30 | with gr.Tab(loc.localize("species-tab-title")) as species_tab: 31 | output_directory_state = gr.State() 32 | select_directory_btn = gr.Button(loc.localize("species-tab-select-output-directory-button-label")) 33 | classifier_name = gr.Textbox( 34 | "species_list.txt", 35 | visible=False, 36 | info=loc.localize("species-tab-filename-textbox-label"), 37 | ) 38 | 39 | def select_directory_and_update_tb(name_tb): 40 | dir_name = gu.select_folder(state_key="species-output-dir") 41 | 42 | if dir_name: 43 | settings.set_state("species-output-dir", dir_name) 44 | return ( 45 | dir_name, 46 | gr.Textbox(label=dir_name, visible=True, value=name_tb), 47 | ) 48 | 49 | return None, name_tb 50 | 51 | select_directory_btn.click( 52 | select_directory_and_update_tb, 53 | inputs=classifier_name, 54 | outputs=[output_directory_state, classifier_name], 55 | show_progress=False, 56 | ) 57 | 58 | lat_number, lon_number, week_number, sf_thresh_number, yearlong_checkbox, map_plot = ( 59 | gu.species_list_coordinates(show_map=True) 60 | ) 61 | 62 | sortby = gr.Radio( 63 | [ 64 | (loc.localize("species-tab-sort-radio-option-frequency"), "freq"), 65 | (loc.localize("species-tab-sort-radio-option-alphabetically"), "alpha"), 66 | ], 67 | value="freq", 68 | label=loc.localize("species-tab-sort-radio-label"), 69 | info=loc.localize("species-tab-sort-radio-info"), 70 | ) 71 | 72 | start_btn = gr.Button(loc.localize("species-tab-start-button-label"), variant="huggingface") 73 | start_btn.click( 74 | run_species_list, 75 | inputs=[ 76 | output_directory_state, 77 | classifier_name, 78 | lat_number, 79 | lon_number, 80 | week_number, 81 | yearlong_checkbox, 82 | sf_thresh_number, 83 | sortby, 84 | ], 85 | ) 86 | 87 | species_tab.select( 88 | lambda lat, lon: gu.plot_map_scatter_mapbox(lat, lon, zoom=3), inputs=[lat_number, lon_number], outputs=map_plot 89 | ) 90 | 91 | return lat_number, lon_number, map_plot 92 | 93 | 94 | if __name__ == "__main__": 95 | gu.open_window(build_species_tab) 96 | -------------------------------------------------------------------------------- /birdnet_analyzer/network/__init__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.network.client import send_request 2 | from birdnet_analyzer.network.server import start_server 3 | 4 | __all__ = ["send_request", "start_server"] 5 | -------------------------------------------------------------------------------- /birdnet_analyzer/network/client.py: -------------------------------------------------------------------------------- 1 | """Client to send requests to the server.""" 2 | 3 | import json 4 | import os 5 | import time 6 | from multiprocessing import freeze_support 7 | 8 | import requests 9 | 10 | 11 | def send_request(host: str, port: int, fpath: str, mdata: str) -> dict: 12 | """ 13 | Sends a classification request to the server. 14 | This function sends an HTTP POST request to a server for analyzing an audio file. 15 | It includes the audio file and additional metadata in the request payload. 16 | Args: 17 | host (str): The host address of the server. 18 | port (int): The port number to connect to on the server. 19 | fpath (str): The file path of the audio file to be analyzed. 20 | mdata (str): A JSON string containing additional metadata for the analysis. 21 | dict: The JSON-decoded response from the server. 22 | Returns: 23 | dict: The JSON-decoded response from the server. 24 | Raises: 25 | FileNotFoundError: If the specified file path does not exist. 26 | requests.exceptions.RequestException: If the HTTP request fails. 27 | """ 28 | url = f"http://{host}:{port}/analyze" 29 | 30 | print(f"Requesting analysis for {fpath}") 31 | 32 | with open(fpath, "rb") as f: 33 | # Make payload 34 | multipart_form_data = {"audio": (fpath.rsplit(os.sep, 1)[-1], f), "meta": (None, mdata)} 35 | 36 | # Send request 37 | start_time = time.time() 38 | response = requests.post(url, files=multipart_form_data) 39 | end_time = time.time() 40 | 41 | print(f"Response: {response.text}, Time: {end_time - start_time:.4f}s", flush=True) 42 | 43 | # Convert to dict 44 | return json.loads(response.text) 45 | 46 | 47 | def _save_result(data, fpath): 48 | """Saves the server response. 49 | 50 | Args: 51 | data: The response data. 52 | fpath: The path to save the data at. 53 | """ 54 | # Make directory 55 | dir_path = os.path.dirname(fpath) 56 | os.makedirs(dir_path, exist_ok=True) 57 | 58 | # Save result 59 | with open(fpath, "w") as f: 60 | json.dump(data, f, indent=4) 61 | 62 | 63 | if __name__ == "__main__": 64 | from birdnet_analyzer import cli 65 | 66 | # Freeze support for executable 67 | freeze_support() 68 | 69 | # Parse arguments 70 | parser = cli.client_parser() 71 | 72 | args = parser.parse_args() 73 | 74 | # TODO: If specified, read and send species list 75 | 76 | # Make metadata 77 | mdata = { 78 | "lat": args.lat, 79 | "lon": args.lon, 80 | "week": args.week, 81 | "overlap": args.overlap, 82 | "sensitivity": args.sensitivity, 83 | "sf_thresh": args.sf_thresh, 84 | "pmode": args.pmode, 85 | "num_results": args.num_results, 86 | "save": args.save, 87 | } 88 | 89 | # Send request 90 | data = send_request(args.host, args.port, args.input, json.dumps(mdata)) 91 | 92 | # Save result 93 | fpath = args.output if args.output else args.i.rsplit(".", 1)[0] + ".BirdNET.results.json" 94 | 95 | _save_result(data, fpath) 96 | -------------------------------------------------------------------------------- /birdnet_analyzer/network/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | from multiprocessing import freeze_support 5 | 6 | import birdnet_analyzer.config as cfg 7 | from birdnet_analyzer import cli, utils 8 | 9 | 10 | def start_server(host="0.0.0.0", port=8080, spath="uploads/", threads=1, locale="en"): 11 | """ 12 | Starts a web server for the BirdNET Analyzer. 13 | Args: 14 | host (str): The hostname or IP address to bind the server to. Defaults to "0.0.0.0". 15 | port (int): The port number to listen on. Defaults to 8080. 16 | spath (str): The file storage path for uploads. Defaults to "uploads/". 17 | threads (int): The number of threads to use for TensorFlow Lite inference. Defaults to 1. 18 | locale (str): The locale for translated labels. Defaults to "en". 19 | Behavior: 20 | - Ensures the required model files exist. 21 | - Loads eBird codes and labels, including translated labels if available for the specified locale. 22 | - Configures various settings such as file storage path, minimum confidence, result types, and temporary output path. 23 | - Starts a Bottle web server to handle requests. 24 | - Cleans up temporary files upon server shutdown. 25 | Note: 26 | This function blocks execution while the server is running. 27 | """ 28 | import bottle 29 | 30 | import birdnet_analyzer.analyze.utils as analyze 31 | 32 | utils.ensure_model_exists() 33 | 34 | # Load eBird codes, labels 35 | cfg.CODES = analyze.load_codes() 36 | cfg.LABELS = utils.read_lines(cfg.LABELS_FILE) 37 | 38 | # Load translated labels 39 | lfile = os.path.join( 40 | cfg.TRANSLATED_LABELS_PATH, os.path.basename(cfg.LABELS_FILE).replace(".txt", f"_{locale}.txt") 41 | ) 42 | 43 | if locale not in ["en"] and os.path.isfile(lfile): 44 | cfg.TRANSLATED_LABELS = utils.read_lines(lfile) 45 | else: 46 | cfg.TRANSLATED_LABELS = cfg.LABELS 47 | 48 | # Set storage file path 49 | cfg.FILE_STORAGE_PATH = spath 50 | 51 | # Set min_conf to 0.0, because we want all results 52 | cfg.MIN_CONFIDENCE = 0.0 53 | 54 | # Set path for temporary result file 55 | cfg.OUTPUT_PATH = tempfile.mkdtemp() 56 | 57 | # Set result types 58 | cfg.RESULT_TYPES = ["audacity"] 59 | 60 | # Set number of TFLite threads 61 | cfg.TFLITE_THREADS = threads 62 | 63 | # Run server 64 | print(f"UP AND RUNNING! LISTENING ON {host}:{port}", flush=True) 65 | 66 | try: 67 | bottle.run(host=host, port=port, quiet=True) 68 | finally: 69 | shutil.rmtree(cfg.OUTPUT_PATH) 70 | 71 | 72 | if __name__ == "__main__": 73 | # Freeze support for executable 74 | freeze_support() 75 | 76 | # Parse arguments 77 | parser = cli.server_parser() 78 | 79 | args = parser.parse_args() 80 | 81 | start_server(**vars(args)) 82 | -------------------------------------------------------------------------------- /birdnet_analyzer/network/utils.py: -------------------------------------------------------------------------------- 1 | """Module to create a remote endpoint for classification. 2 | 3 | Can be used to start up a server and feed it classification requests. 4 | """ 5 | 6 | import json 7 | import os 8 | import tempfile 9 | from datetime import date, datetime 10 | 11 | import bottle 12 | 13 | import birdnet_analyzer.config as cfg 14 | from birdnet_analyzer import analyze, species, utils 15 | 16 | 17 | def result_pooling(lines: list[str], num_results=5, pmode="avg"): 18 | """Parses the results into list of (species, score). 19 | 20 | Args: 21 | lines: List of result scores. 22 | num_results: The number of entries to be returned. 23 | pmode: Decides how the score for each species is computed. 24 | If "max" used the maximum score for the species, 25 | if "avg" computes the average score per species. 26 | 27 | Returns: 28 | A List of (species, score). 29 | """ 30 | # Parse results 31 | results = {} 32 | 33 | for line in lines: 34 | d = line.split("\t") 35 | species = d[2].replace(", ", "_") 36 | score = float(d[-1]) 37 | 38 | if species not in results: 39 | results[species] = [] 40 | 41 | results[species].append(score) 42 | 43 | # Compute score for each species 44 | for species in results: 45 | if pmode == "max": 46 | results[species] = max(results[species]) 47 | else: 48 | results[species] = sum(results[species]) / len(results[species]) 49 | 50 | # Sort results 51 | results = sorted(results.items(), key=lambda x: x[1], reverse=True) 52 | 53 | return results[:num_results] 54 | 55 | 56 | @bottle.route("/healthcheck", method="GET") 57 | def healthcheck(): 58 | """Checks the health of the running server. 59 | Returns: 60 | A json message. 61 | """ 62 | return json.dumps({"msg": "Server is healthy."}) 63 | 64 | 65 | @bottle.route("/analyze", method="POST") 66 | def handle_request(): 67 | """Handles a classification request. 68 | 69 | Takes a POST request and tries to analyze it. 70 | 71 | The response contains the result or error message. 72 | 73 | Returns: 74 | A json response with the result. 75 | """ 76 | # Print divider 77 | print(f"{'#' * 20} {datetime.now()} {'#' * 20}") 78 | 79 | # Get request payload 80 | upload = bottle.request.files.get("audio") 81 | mdata = json.loads(bottle.request.forms.get("meta", {})) 82 | 83 | if not upload: 84 | return json.dumps({"msg": "No audio file."}) 85 | 86 | print(mdata) 87 | 88 | # Get filename 89 | name, ext = os.path.splitext(upload.filename.lower()) 90 | file_path = upload.filename 91 | file_path_tmp = None 92 | 93 | # Save file 94 | try: 95 | if ext[1:].lower() in cfg.ALLOWED_FILETYPES: 96 | if mdata.get("save", False): 97 | save_path = os.path.join(cfg.FILE_STORAGE_PATH, str(date.today())) 98 | 99 | os.makedirs(save_path, exist_ok=True) 100 | 101 | file_path = os.path.join(save_path, name + ext) 102 | else: 103 | save_path = "" 104 | file_path_tmp = tempfile.mkstemp(suffix=ext.lower(), dir=cfg.OUTPUT_PATH) 105 | file_path = file_path_tmp.name 106 | 107 | upload.save(file_path, overwrite=True) 108 | else: 109 | return json.dumps({"msg": "Filetype not supported."}) 110 | 111 | except Exception as ex: 112 | if file_path_tmp: 113 | os.unlink(file_path_tmp.name) 114 | 115 | # Write error log 116 | print(f"Error: Cannot save file {file_path}.", flush=True) 117 | utils.write_error_log(ex) 118 | 119 | # Return error 120 | return json.dumps({"msg": "Error while saving file."}) 121 | 122 | # Analyze file 123 | try: 124 | # Set config based on mdata 125 | if "lat" in mdata and "lon" in mdata: 126 | cfg.LATITUDE = float(mdata["lat"]) 127 | cfg.LONGITUDE = float(mdata["lon"]) 128 | else: 129 | cfg.LATITUDE = -1 130 | cfg.LONGITUDE = -1 131 | 132 | cfg.WEEK = int(mdata.get("week", -1)) 133 | cfg.SIG_OVERLAP = max(0.0, min(2.9, float(mdata.get("overlap", 0.0)))) 134 | cfg.SIGMOID_SENSITIVITY = max(0.5, min(1.0 - (float(mdata.get("sensitivity", 1.0)) - 1.0), 1.5)) 135 | cfg.LOCATION_FILTER_THRESHOLD = max(0.01, min(0.99, float(mdata.get("sf_thresh", 0.03)))) 136 | 137 | # Set species list 138 | if cfg.LATITUDE != -1 and cfg.LONGITUDE != -1: 139 | cfg.SPECIES_LIST_FILE = None 140 | cfg.SPECIES_LIST = species.get_species_list( 141 | cfg.LATITUDE, cfg.LONGITUDE, cfg.WEEK, cfg.LOCATION_FILTER_THRESHOLD 142 | ) 143 | else: 144 | cfg.SPECIES_LIST_FILE = None 145 | cfg.SPECIES_LIST = [] 146 | 147 | # Analyze file 148 | success = analyze.analyze_file((file_path, cfg.get_config())) 149 | 150 | # Parse results 151 | if success: 152 | # Open result file 153 | output_path = success["audacity"] 154 | lines = utils.read_lines(output_path) 155 | pmode = mdata.get("pmode", "avg").lower() 156 | 157 | # Pool results 158 | if pmode not in ["avg", "max"]: 159 | pmode = "avg" 160 | 161 | num_results = min(99, max(1, int(mdata.get("num_results", 5)))) 162 | 163 | results = result_pooling(lines, num_results, pmode) 164 | 165 | # Prepare response 166 | data = {"msg": "success", "results": results, "meta": mdata} 167 | 168 | # Save response as metadata file 169 | if mdata.get("save", False): 170 | with open(file_path.rsplit(".", 1)[0] + ".json", "w") as f: 171 | json.dump(data, f, indent=2) 172 | 173 | # Return response 174 | del data["meta"] 175 | 176 | return json.dumps(data) 177 | 178 | return json.dumps({"msg": "Error during analysis."}) 179 | 180 | except Exception as e: 181 | # Write error log 182 | print(f"Error: Cannot analyze file {file_path}.", flush=True) 183 | utils.write_error_log(e) 184 | 185 | data = {"msg": f"Error during analysis: {e}"} 186 | 187 | return json.dumps(data) 188 | finally: 189 | if file_path_tmp: 190 | os.unlink(file_path_tmp.name) 191 | -------------------------------------------------------------------------------- /birdnet_analyzer/search/__init__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.search.core import search 2 | 3 | __all__ = ["search"] 4 | -------------------------------------------------------------------------------- /birdnet_analyzer/search/__main__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.search.cli import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /birdnet_analyzer/search/cli.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer import utils 2 | 3 | 4 | @utils.runtime_error_handler 5 | def main(): 6 | from birdnet_analyzer import cli, search 7 | 8 | parser = cli.search_parser() 9 | args = parser.parse_args() 10 | 11 | search(**vars(args)) 12 | -------------------------------------------------------------------------------- /birdnet_analyzer/search/core.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | 4 | def search( 5 | output: str, 6 | database: str, 7 | queryfile: str, 8 | *, 9 | n_results: int = 10, 10 | score_function: Literal["cosine", "euclidean", "dot"] = "cosine", 11 | crop_mode: Literal["center", "first", "segments"] = "center", 12 | overlap: float = 0.0, 13 | ): 14 | """ 15 | Executes a search query on a given database and saves the results as audio files. 16 | Args: 17 | output (str): Path to the output directory where the results will be saved. 18 | database (str): Path to the database file to search in. 19 | queryfile (str): Path to the query file containing the search input. 20 | n_results (int, optional): Number of top results to return. Defaults to 10. 21 | score_function (Literal["cosine", "euclidean", "dot"], optional): 22 | Scoring function to use for similarity calculation. Defaults to "cosine". 23 | crop_mode (Literal["center", "first", "segments"], optional): 24 | Mode for cropping audio segments. Defaults to "center". 25 | overlap (float, optional): Overlap ratio for audio segments. Defaults to 0.0. 26 | Raises: 27 | ValueError: If the database does not contain the required settings metadata. 28 | Notes: 29 | - The function creates the output directory if it does not exist. 30 | - It retrieves metadata from the database to configure the search, including 31 | bandpass filter settings and audio speed. 32 | - The results are saved as audio files in the specified output directory, with 33 | filenames containing the score, source file name, and time offsets. 34 | Returns: 35 | None 36 | """ 37 | import os 38 | 39 | import birdnet_analyzer.config as cfg 40 | from birdnet_analyzer import audio 41 | from birdnet_analyzer.search.utils import get_search_results 42 | 43 | # Create output folder 44 | if not os.path.exists(output): 45 | os.makedirs(output) 46 | 47 | # Load the database 48 | db = get_database(database) 49 | 50 | try: 51 | settings = db.get_metadata("birdnet_analyzer_settings") 52 | except KeyError as e: 53 | raise ValueError("No settings present in database.") from e 54 | 55 | fmin = settings["BANDPASS_FMIN"] 56 | fmax = settings["BANDPASS_FMAX"] 57 | audio_speed = settings["AUDIO_SPEED"] 58 | 59 | # Execute the search 60 | results = get_search_results(queryfile, db, n_results, audio_speed, fmin, fmax, score_function, crop_mode, overlap) 61 | 62 | # Save the results 63 | for r in results: 64 | embedding_source = db.get_embedding_source(r.embedding_id) 65 | file = embedding_source.source_id 66 | filebasename = os.path.basename(file) 67 | filebasename = os.path.splitext(filebasename)[0] 68 | offset = embedding_source.offsets[0] * audio_speed 69 | duration = cfg.SIG_LENGTH * audio_speed 70 | sig, rate = audio.open_audio_file(file, offset=offset, duration=duration, sample_rate=None) 71 | result_path = os.path.join(output, f"{r.sort_score:.5f}_{filebasename}_{offset}_{offset + duration}.wav") 72 | audio.save_signal(sig, result_path, rate) 73 | 74 | 75 | def get_database(database_path): 76 | from perch_hoplite.db import sqlite_usearch_impl 77 | 78 | return sqlite_usearch_impl.SQLiteUsearchDB.create(database_path).thread_split() 79 | -------------------------------------------------------------------------------- /birdnet_analyzer/search/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from perch_hoplite.db import brutalism 3 | from perch_hoplite.db.search_results import SearchResult 4 | from scipy.spatial.distance import euclidean 5 | 6 | import birdnet_analyzer.config as cfg 7 | from birdnet_analyzer import audio, model 8 | 9 | 10 | def cosine_sim(a, b): 11 | if a.ndim == 2: 12 | return np.array([cosine_sim(a[i], b) for i in range(a.shape[0])]) 13 | return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) 14 | 15 | 16 | def euclidean_scoring(a, b): 17 | if a.ndim == 2: 18 | return np.array([euclidean_scoring(a[i], b) for i in range(a.shape[0])]) 19 | return euclidean(a, b) 20 | 21 | 22 | def euclidean_scoring_inverse(a, b): 23 | return -euclidean_scoring(a, b) 24 | 25 | 26 | def get_query_embedding(queryfile_path): 27 | """ 28 | Extracts the embedding for a query file. Reads only the first 3 seconds 29 | Args: 30 | queryfile_path: The path to the query file. 31 | Returns: 32 | The query embedding. 33 | """ 34 | 35 | # Load audio 36 | sig, rate = audio.open_audio_file( 37 | queryfile_path, 38 | duration=cfg.SIG_LENGTH * cfg.AUDIO_SPEED if cfg.SAMPLE_CROP_MODE == "first" else None, 39 | fmin=cfg.BANDPASS_FMIN, 40 | fmax=cfg.BANDPASS_FMAX, 41 | speed=cfg.AUDIO_SPEED, 42 | ) 43 | 44 | # Crop query audio 45 | if cfg.SAMPLE_CROP_MODE == "center": 46 | sig_splits = [audio.crop_center(sig, rate, cfg.SIG_LENGTH)] 47 | elif cfg.SAMPLE_CROP_MODE == "first": 48 | sig_splits = [audio.split_signal(sig, rate, cfg.SIG_LENGTH, cfg.SIG_OVERLAP, cfg.SIG_MINLEN)[0]] 49 | else: 50 | sig_splits = audio.split_signal(sig, rate, cfg.SIG_LENGTH, cfg.SIG_OVERLAP, cfg.SIG_MINLEN) 51 | 52 | samples = sig_splits 53 | data = np.array(samples, dtype="float32") 54 | 55 | return model.embeddings(data) 56 | 57 | 58 | def get_search_results( 59 | queryfile_path, db, n_results, audio_speed, fmin, fmax, score_function: str, crop_mode, crop_overlap 60 | ): 61 | # Set bandpass frequency range 62 | cfg.BANDPASS_FMIN = max(0, min(cfg.SIG_FMAX, int(fmin))) 63 | cfg.BANDPASS_FMAX = max(cfg.SIG_FMIN, min(cfg.SIG_FMAX, int(fmax))) 64 | cfg.AUDIO_SPEED = max(0.01, audio_speed) 65 | cfg.SAMPLE_CROP_MODE = crop_mode 66 | cfg.SIG_OVERLAP = max(0.0, min(2.9, float(crop_overlap))) 67 | 68 | # Get query embedding 69 | query_embeddings = get_query_embedding(queryfile_path) 70 | 71 | # Set score function 72 | if score_function == "cosine": 73 | score_fn = cosine_sim 74 | elif score_function == "dot": 75 | score_fn = np.dot 76 | elif score_function == "euclidean": 77 | score_fn = euclidean_scoring_inverse # TODO: this is a bit hacky since the search function expects the score to be high for similar embeddings 78 | else: 79 | raise ValueError("Invalid score function. Choose 'cosine', 'euclidean' or 'dot'.") 80 | 81 | db_embeddings_count = db.count_embeddings() 82 | n_results = min(n_results, db_embeddings_count - 1) 83 | scores_by_embedding_id = {} 84 | 85 | for embedding in query_embeddings: 86 | results, scores = brutalism.threaded_brute_search(db, embedding, n_results, score_fn) 87 | sorted_results = results.search_results 88 | 89 | if score_function == "euclidean": 90 | for result in sorted_results: 91 | result.sort_score *= -1 92 | 93 | for result in sorted_results: 94 | if result.embedding_id not in scores_by_embedding_id: 95 | scores_by_embedding_id[result.embedding_id] = [] 96 | scores_by_embedding_id[result.embedding_id].append(result.sort_score) 97 | 98 | results = [] 99 | 100 | for embedding_id, scores in scores_by_embedding_id.items(): 101 | results.append(SearchResult(embedding_id, np.sum(scores) / len(query_embeddings))) 102 | 103 | reverse = score_function != "euclidean" 104 | 105 | results.sort(key=lambda x: x.sort_score, reverse=reverse) 106 | 107 | return results[0:n_results] 108 | -------------------------------------------------------------------------------- /birdnet_analyzer/segments/__init__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.segments.core import segments 2 | 3 | __all__ = ["segments"] 4 | -------------------------------------------------------------------------------- /birdnet_analyzer/segments/__main__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.segments.cli import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /birdnet_analyzer/segments/cli.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.utils import runtime_error_handler 2 | 3 | 4 | @runtime_error_handler 5 | def main(): 6 | from birdnet_analyzer import cli, segments 7 | 8 | # Parse arguments 9 | parser = cli.segments_parser() 10 | 11 | args = parser.parse_args() 12 | 13 | segments(**vars(args)) 14 | -------------------------------------------------------------------------------- /birdnet_analyzer/segments/core.py: -------------------------------------------------------------------------------- 1 | def segments( 2 | audio_input: str, 3 | output: str | None = None, 4 | results: str | None = None, 5 | *, 6 | min_conf: float = 0.25, 7 | max_segments: int = 100, 8 | audio_speed: float = 1.0, 9 | seg_length: float = 3.0, 10 | threads: int = 1, 11 | ): 12 | """ 13 | Processes audio files to extract segments based on detection results. 14 | Args: 15 | audio_input (str): Path to the input folder containing audio files. 16 | output (str | None, optional): Path to the output folder where segments will be saved. 17 | If not provided, the input folder will be used as the output folder. Defaults to None. 18 | results (str | None, optional): Path to the folder containing detection result files. 19 | If not provided, the input folder will be used. Defaults to None. 20 | min_conf (float, optional): Minimum confidence threshold for detections to be considered. 21 | Defaults to 0.25. 22 | max_segments (int, optional): Maximum number of segments to extract per audio file. 23 | Defaults to 100. 24 | audio_speed (float, optional): Speed factor for audio processing. Defaults to 1.0. 25 | seg_length (float, optional): Length of each audio segment in seconds. Defaults to 3.0. 26 | threads (int, optional): Number of CPU threads to use for parallel processing. 27 | Defaults to 1. 28 | Returns: 29 | None 30 | Notes: 31 | - The function uses multiprocessing for parallel processing if `threads` is greater than 1. 32 | - On Windows, due to the lack of `fork()` support, configuration items are passed to each 33 | process explicitly. 34 | - It is recommended to use this function on Linux for better performance. 35 | """ 36 | from multiprocessing import Pool 37 | 38 | import birdnet_analyzer.config as cfg 39 | from birdnet_analyzer.segments.utils import ( 40 | extract_segments, 41 | parse_files, 42 | parse_folders, 43 | ) 44 | 45 | cfg.INPUT_PATH = audio_input 46 | 47 | if not output: 48 | cfg.OUTPUT_PATH = cfg.INPUT_PATH 49 | else: 50 | cfg.OUTPUT_PATH = output 51 | 52 | results = results if results else cfg.INPUT_PATH 53 | 54 | # Parse audio and result folders 55 | cfg.FILE_LIST = parse_folders(audio_input, results) 56 | 57 | # Set number of threads 58 | cfg.CPU_THREADS = threads 59 | 60 | # Set confidence threshold 61 | cfg.MIN_CONFIDENCE = min_conf 62 | 63 | # Parse file list and make list of segments 64 | cfg.FILE_LIST = parse_files(cfg.FILE_LIST, max_segments) 65 | 66 | # Set audio speed 67 | cfg.AUDIO_SPEED = audio_speed 68 | 69 | # Add config items to each file list entry. 70 | # We have to do this for Windows which does not 71 | # support fork() and thus each process has to 72 | # have its own config. USE LINUX! 73 | flist = [(entry, seg_length, cfg.get_config()) for entry in cfg.FILE_LIST] 74 | 75 | # Extract segments 76 | if cfg.CPU_THREADS < 2: 77 | for entry in flist: 78 | extract_segments(entry) 79 | else: 80 | with Pool(cfg.CPU_THREADS) as p: 81 | p.map(extract_segments, flist) 82 | -------------------------------------------------------------------------------- /birdnet_analyzer/species/__init__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.species.core import species 2 | 3 | __all__ = ["species"] 4 | -------------------------------------------------------------------------------- /birdnet_analyzer/species/__main__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.species.cli import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /birdnet_analyzer/species/cli.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.utils import runtime_error_handler 2 | 3 | 4 | @runtime_error_handler 5 | def main(): 6 | from birdnet_analyzer import cli, species 7 | 8 | # Parse arguments 9 | parser = cli.species_parser() 10 | 11 | args = parser.parse_args() 12 | 13 | species(**vars(args)) 14 | -------------------------------------------------------------------------------- /birdnet_analyzer/species/core.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | 4 | def species( 5 | output: str, 6 | *, 7 | lat: float = -1, 8 | lon: float = -1, 9 | week: int = -1, 10 | sf_thresh: float = 0.03, 11 | sortby: Literal["freq", "alpha"] = "freq", 12 | ): 13 | """ 14 | Retrieves and processes species data based on the provided parameters. 15 | Args: 16 | output (str): The output directory or file path where the results will be stored. 17 | lat (float, optional): Latitude of the location for species filtering. Defaults to -1 (no filtering by location). 18 | lon (float, optional): Longitude of the location for species filtering. Defaults to -1 (no filtering by location). 19 | week (int, optional): Week of the year for species filtering. Defaults to -1 (no filtering by time). 20 | sf_thresh (float, optional): Species frequency threshold for filtering. Defaults to 0.03. 21 | sortby (Literal["freq", "alpha"], optional): Sorting method for the species list. 22 | "freq" sorts by frequency, and "alpha" sorts alphabetically. Defaults to "freq". 23 | Raises: 24 | FileNotFoundError: If the required model files are not found. 25 | ValueError: If invalid parameters are provided. 26 | Notes: 27 | This function ensures that the required model files exist before processing. 28 | It delegates the main processing to the `run` function from `birdnet_analyzer.species.utils`. 29 | """ 30 | from birdnet_analyzer.species.utils import run 31 | from birdnet_analyzer.utils import ensure_model_exists 32 | 33 | ensure_model_exists() 34 | 35 | run(output, lat, lon, week, sf_thresh, sortby) 36 | -------------------------------------------------------------------------------- /birdnet_analyzer/species/utils.py: -------------------------------------------------------------------------------- 1 | """Module for predicting a species list. 2 | 3 | Can be used to predict a species list using coordinates and weeks. 4 | """ 5 | 6 | import os 7 | 8 | import birdnet_analyzer.config as cfg 9 | from birdnet_analyzer import model, utils 10 | 11 | 12 | def get_species_list(lat: float, lon: float, week: int, threshold=0.05, sort=False) -> list[str]: 13 | """Predict a species list. 14 | 15 | Uses the model to predict the species list for the given coordinates and filters by threshold. 16 | 17 | Args: 18 | lat: The latitude. 19 | lon: The longitude. 20 | week: The week of the year [1-48]. Use -1 for year-round. 21 | threshold: Only values above or equal to threshold will be shown. 22 | sort: If the species list should be sorted. 23 | 24 | Returns: 25 | A list of all eligible species. 26 | """ 27 | # Extract species from model 28 | pred = model.explore(lat, lon, week) 29 | 30 | # Make species list 31 | slist = [p[1] for p in pred if p[0] >= threshold] 32 | 33 | return sorted(slist) if sort else slist 34 | 35 | 36 | def run(output_path, lat, lon, week, threshold, sortby): 37 | """ 38 | Generates a species list for a given location and time, and saves it to the specified output path. 39 | Args: 40 | output_path (str): The path where the species list will be saved. If it's a directory, the list will be saved as "species_list.txt" inside it. 41 | lat (float): Latitude of the location. 42 | lon (float): Longitude of the location. 43 | week (int): Week of the year (1-52) for which the species list is generated. 44 | threshold (float): Threshold for location filtering. 45 | sortby (str): Sorting criteria for the species list. Can be "freq" for frequency or any other value for alphabetical sorting. 46 | Returns: 47 | None 48 | """ 49 | # Load eBird codes, labels 50 | cfg.LABELS = utils.read_lines(cfg.LABELS_FILE) 51 | 52 | # Set output path 53 | cfg.OUTPUT_PATH = output_path 54 | 55 | if os.path.isdir(cfg.OUTPUT_PATH): 56 | cfg.OUTPUT_PATH = os.path.join(cfg.OUTPUT_PATH, "species_list.txt") 57 | 58 | # Set config 59 | cfg.LATITUDE, cfg.LONGITUDE, cfg.WEEK = lat, lon, week 60 | cfg.LOCATION_FILTER_THRESHOLD = threshold 61 | 62 | print(f"Getting species list for {cfg.LATITUDE}/{cfg.LONGITUDE}, Week {cfg.WEEK}...", end="", flush=True) 63 | 64 | # Get species list 65 | species_list = get_species_list( 66 | cfg.LATITUDE, cfg.LONGITUDE, cfg.WEEK, cfg.LOCATION_FILTER_THRESHOLD, sortby != "freq" 67 | ) 68 | 69 | print(f"Done. {len(species_list)} species on list.", flush=True) 70 | 71 | # Save species list 72 | with open(cfg.OUTPUT_PATH, "w") as f: 73 | for s in species_list: 74 | f.write(s + "\n") 75 | -------------------------------------------------------------------------------- /birdnet_analyzer/train/__init__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.train.core import train 2 | 3 | __all__ = ["train"] 4 | -------------------------------------------------------------------------------- /birdnet_analyzer/train/__main__.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.train.cli import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /birdnet_analyzer/train/cli.py: -------------------------------------------------------------------------------- 1 | from birdnet_analyzer.utils import runtime_error_handler 2 | 3 | 4 | @runtime_error_handler 5 | def main(): 6 | from birdnet_analyzer import cli, train 7 | 8 | # Parse arguments 9 | parser = cli.train_parser() 10 | 11 | args = parser.parse_args() 12 | 13 | train(**vars(args)) 14 | -------------------------------------------------------------------------------- /birdnet_analyzer/train/core.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | 4 | def train( 5 | audio_input: str, 6 | output: str = "checkpoints/custom/Custom_Classifier", 7 | test_data: str | None = None, 8 | *, 9 | crop_mode: Literal["center", "first", "segments"] = "center", 10 | overlap: float = 0.0, 11 | epochs: int = 50, 12 | batch_size: int = 32, 13 | val_split: float = 0.2, 14 | learning_rate: float = 0.0001, 15 | use_focal_loss: bool = False, 16 | focal_loss_gamma: float = 2.0, 17 | focal_loss_alpha: float = 0.25, 18 | hidden_units: int = 0, 19 | dropout: float = 0.0, 20 | label_smoothing: bool = False, 21 | mixup: bool = False, 22 | upsampling_ratio: float = 0.0, 23 | upsampling_mode: Literal["repeat", "mean", "smote"] = "repeat", 24 | model_format: Literal["tflite", "raven", "both"] = "tflite", 25 | model_save_mode: Literal["replace", "append"] = "replace", 26 | cache_mode: Literal["load", "save"] | None = None, 27 | cache_file: str = "train_cache.npz", 28 | threads: int = 1, 29 | fmin: float = 0.0, 30 | fmax: float = 15000.0, 31 | audio_speed: float = 1.0, 32 | autotune: bool = False, 33 | autotune_trials: int = 50, 34 | autotune_executions_per_trial: int = 1, 35 | ): 36 | """ 37 | Trains a custom classifier model using the BirdNET-Analyzer framework. 38 | Args: 39 | audio_input (str): Path to the training data directory. 40 | test_data (str, optional): Path to the test data directory. Defaults to None. If not specified, a validation split will be used. 41 | output (str, optional): Path to save the trained model. Defaults to "checkpoints/custom/Custom_Classifier". 42 | crop_mode (Literal["center", "first", "segments", "smart"], optional): Mode for cropping audio samples. Defaults to "center". 43 | overlap (float, optional): Overlap ratio for audio segments. Defaults to 0.0. 44 | epochs (int, optional): Number of training epochs. Defaults to 50. 45 | batch_size (int, optional): Batch size for training. Defaults to 32. 46 | val_split (float, optional): Fraction of data to use for validation. Defaults to 0.2. 47 | learning_rate (float, optional): Learning rate for the optimizer. Defaults to 0.0001. 48 | use_focal_loss (bool, optional): Whether to use focal loss for training. Defaults to False. 49 | focal_loss_gamma (float, optional): Gamma parameter for focal loss. Defaults to 2.0. 50 | focal_loss_alpha (float, optional): Alpha parameter for focal loss. Defaults to 0.25. 51 | hidden_units (int, optional): Number of hidden units in the model. Defaults to 0. 52 | dropout (float, optional): Dropout rate for regularization. Defaults to 0.0. 53 | label_smoothing (bool, optional): Whether to use label smoothing. Defaults to False. 54 | mixup (bool, optional): Whether to use mixup data augmentation. Defaults to False. 55 | upsampling_ratio (float, optional): Ratio for upsampling underrepresented classes. Defaults to 0.0. 56 | upsampling_mode (Literal["repeat", "mean", "smote"], optional): Mode for upsampling. Defaults to "repeat". 57 | model_format (Literal["tflite", "raven", "both"], optional): Format to save the trained model. Defaults to "tflite". 58 | model_save_mode (Literal["replace", "append"], optional): Save mode for the model. Defaults to "replace". 59 | cache_mode (Literal["load", "save"] | None, optional): Cache mode for training data. Defaults to None. 60 | cache_file (str, optional): Path to the cache file. Defaults to "train_cache.npz". 61 | threads (int, optional): Number of CPU threads to use. Defaults to 1. 62 | fmin (float, optional): Minimum frequency for bandpass filtering. Defaults to 0.0. 63 | fmax (float, optional): Maximum frequency for bandpass filtering. Defaults to 15000.0. 64 | audio_speed (float, optional): Speed factor for audio playback. Defaults to 1.0. 65 | autotune (bool, optional): Whether to use hyperparameter autotuning. Defaults to False. 66 | autotune_trials (int, optional): Number of trials for autotuning. Defaults to 50. 67 | autotune_executions_per_trial (int, optional): Number of executions per autotuning trial. Defaults to 1. 68 | Returns: 69 | None 70 | """ 71 | import birdnet_analyzer.config as cfg 72 | from birdnet_analyzer.train.utils import train_model 73 | from birdnet_analyzer.utils import ensure_model_exists 74 | 75 | ensure_model_exists() 76 | 77 | # Config 78 | cfg.TRAIN_DATA_PATH = audio_input 79 | cfg.TEST_DATA_PATH = test_data 80 | cfg.SAMPLE_CROP_MODE = crop_mode 81 | cfg.SIG_OVERLAP = overlap 82 | cfg.CUSTOM_CLASSIFIER = output 83 | cfg.TRAIN_EPOCHS = epochs 84 | cfg.TRAIN_BATCH_SIZE = batch_size 85 | cfg.TRAIN_VAL_SPLIT = val_split 86 | cfg.TRAIN_LEARNING_RATE = learning_rate 87 | cfg.TRAIN_WITH_FOCAL_LOSS = use_focal_loss if use_focal_loss is not None else cfg.TRAIN_WITH_FOCAL_LOSS 88 | cfg.FOCAL_LOSS_GAMMA = focal_loss_gamma 89 | cfg.FOCAL_LOSS_ALPHA = focal_loss_alpha 90 | cfg.TRAIN_HIDDEN_UNITS = hidden_units 91 | cfg.TRAIN_DROPOUT = dropout 92 | cfg.TRAIN_WITH_LABEL_SMOOTHING = label_smoothing if label_smoothing is not None else cfg.TRAIN_WITH_LABEL_SMOOTHING 93 | cfg.TRAIN_WITH_MIXUP = mixup if mixup is not None else cfg.TRAIN_WITH_MIXUP 94 | cfg.UPSAMPLING_RATIO = upsampling_ratio 95 | cfg.UPSAMPLING_MODE = upsampling_mode 96 | cfg.TRAINED_MODEL_OUTPUT_FORMAT = model_format 97 | cfg.TRAINED_MODEL_SAVE_MODE = model_save_mode 98 | cfg.TRAIN_CACHE_MODE = cache_mode 99 | cfg.TRAIN_CACHE_FILE = cache_file 100 | cfg.TFLITE_THREADS = 1 101 | cfg.CPU_THREADS = threads 102 | 103 | cfg.BANDPASS_FMIN = fmin 104 | cfg.BANDPASS_FMAX = fmax 105 | 106 | cfg.AUDIO_SPEED = audio_speed 107 | 108 | cfg.AUTOTUNE = autotune 109 | cfg.AUTOTUNE_TRIALS = autotune_trials 110 | cfg.AUTOTUNE_EXECUTIONS_PER_TRIAL = autotune_executions_per_trial 111 | 112 | # Train model 113 | train_model() 114 | -------------------------------------------------------------------------------- /birdnet_analyzer/translate.py: -------------------------------------------------------------------------------- 1 | """Module for translating species labels. 2 | 3 | Can be used to translate species names into other languages. 4 | 5 | Uses the requests to the eBird-API. 6 | """ 7 | 8 | import json 9 | import os 10 | import urllib.request 11 | 12 | import birdnet_analyzer.config as cfg 13 | from birdnet_analyzer import utils 14 | 15 | LOCALES = [ 16 | "af", 17 | "ar", 18 | "cs", 19 | "da", 20 | "de", 21 | "en_uk", 22 | "es", 23 | "fi", 24 | "fr", 25 | "hu", 26 | "it", 27 | "ja", 28 | "ko", 29 | "nl", 30 | "no", 31 | "pl", 32 | "pt_BR", 33 | "pt_PT", 34 | "ro", 35 | "ru", 36 | "sk", 37 | "sl", 38 | "sv", 39 | "th", 40 | "tr", 41 | "uk", 42 | "zh", 43 | ] 44 | """ Locales for 26 common languages (according to GitHub Copilot) """ 45 | 46 | API_TOKEN = "yourAPIToken" 47 | """ Sign up for your personal access token here: https://ebird.org/api/keygen """ 48 | 49 | 50 | def get_locale_data(locale: str): 51 | """Download eBird locale species data. 52 | 53 | Requests the locale data through the eBird API. 54 | 55 | Args: 56 | locale: Two character string of a language. 57 | 58 | Returns: 59 | A data object containing the response from eBird. 60 | """ 61 | url = "https://api.ebird.org/v2/ref/taxonomy/ebird?cat=species&fmt=json&locale=" + locale 62 | header = {"X-eBirdAPIToken": API_TOKEN} 63 | 64 | req = urllib.request.Request(url, headers=header) 65 | response = urllib.request.urlopen(req) 66 | 67 | return json.loads(response.read()) 68 | 69 | 70 | def translate(locale: str): 71 | """Translates species names for a locale. 72 | 73 | Translates species names for the given language with the eBird API. 74 | 75 | Args: 76 | locale: Two character string of a language. 77 | 78 | Returns: 79 | The translated list of labels. 80 | """ 81 | print(f"Translating species names for {locale}...", end="", flush=True) 82 | 83 | # Get locale data 84 | data = get_locale_data(locale) 85 | 86 | # Create list of translated labels 87 | labels: list[str] = [] 88 | 89 | for label in cfg.LABELS: 90 | has_translation = False 91 | for entry in data: 92 | if label.split("_", 1)[0] == entry["sciName"]: 93 | labels.append("{}_{}".format(label.split("_", 1)[0], entry["comName"])) 94 | has_translation = True 95 | break 96 | if not has_translation: 97 | labels.append(label) 98 | 99 | print("Done.", flush=True) 100 | 101 | return labels 102 | 103 | 104 | def save_labels_file(labels: list[str], locale: str): 105 | """Saves localized labels to a file. 106 | 107 | Saves the given labels into a file with the format: 108 | "{config.LABELSFILE}_{locale}.txt" 109 | 110 | Args: 111 | labels: List of labels. 112 | locale: Two character string of a language. 113 | """ 114 | # Create folder 115 | os.makedirs(cfg.TRANSLATED_LABELS_PATH, exist_ok=True) 116 | 117 | # Save labels file 118 | fpath = os.path.join( 119 | cfg.TRANSLATED_LABELS_PATH, "{}_{}.txt".format(os.path.basename(cfg.LABELS_FILE).rsplit(".", 1)[0], locale) 120 | ) 121 | with open(fpath, "w", encoding="utf-8") as f: 122 | for label in labels: 123 | f.write(label + "\n") 124 | 125 | 126 | if __name__ == "__main__": 127 | # Load labels 128 | cfg.LABELS = utils.read_lines(cfg.LABELS_FILE) 129 | 130 | # Translate labels 131 | for locale in LOCALES: 132 | labels = translate(locale) 133 | save_labels_file(labels, locale) 134 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/BirdNET-Go-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/BirdNET-Go-logo.webp -------------------------------------------------------------------------------- /docs/_static/BirdNET_Guide-Introduction-NotebookLM.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/BirdNET_Guide-Introduction-NotebookLM.mp3 -------------------------------------------------------------------------------- /docs/_static/BirdNET_Guide-Segment_review-NotebookLM.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/BirdNET_Guide-Segment_review-NotebookLM.mp3 -------------------------------------------------------------------------------- /docs/_static/BirdNET_Guide-Training-NotebookLM.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/BirdNET_Guide-Training-NotebookLM.mp3 -------------------------------------------------------------------------------- /docs/_static/Muuttolintujen-Kevät.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/Muuttolintujen-Kevät.png -------------------------------------------------------------------------------- /docs/_static/birdnet-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/birdnet-icon.ico -------------------------------------------------------------------------------- /docs/_static/birdnet-pi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/birdnet-pi.png -------------------------------------------------------------------------------- /docs/_static/birdnet-tiny-forge-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/birdnet-tiny-forge-logo.png -------------------------------------------------------------------------------- /docs/_static/birdnet_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/birdnet_logo.png -------------------------------------------------------------------------------- /docs/_static/birdnetlib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/birdnetlib.png -------------------------------------------------------------------------------- /docs/_static/birdnetr-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/birdnetr-logo.png -------------------------------------------------------------------------------- /docs/_static/birdweather.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/birdweather.png -------------------------------------------------------------------------------- /docs/_static/chirpity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/chirpity.png -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .wy-table-responsive table td, 2 | .wy-table-responsive table th { 3 | white-space: normal; 4 | } -------------------------------------------------------------------------------- /docs/_static/dawnchorus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/dawnchorus.png -------------------------------------------------------------------------------- /docs/_static/dummy_birds_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/dummy_birds_image.png -------------------------------------------------------------------------------- /docs/_static/dummy_frogs_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/dummy_frogs_image.png -------------------------------------------------------------------------------- /docs/_static/dummy_project_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/dummy_project_image.png -------------------------------------------------------------------------------- /docs/_static/ecopi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/ecopi.png -------------------------------------------------------------------------------- /docs/_static/ecosound-web_logo_large_white_on_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/ecosound-web_logo_large_white_on_black.png -------------------------------------------------------------------------------- /docs/_static/faunanet_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/faunanet_logo.png -------------------------------------------------------------------------------- /docs/_static/gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/gui.png -------------------------------------------------------------------------------- /docs/_static/haikubox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/haikubox.png -------------------------------------------------------------------------------- /docs/_static/logo_birdnet_big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/logo_birdnet_big.png -------------------------------------------------------------------------------- /docs/_static/ribbit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/ribbit.png -------------------------------------------------------------------------------- /docs/_static/whobird.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/whobird.png -------------------------------------------------------------------------------- /docs/best-practices.rst: -------------------------------------------------------------------------------- 1 | Best practices 2 | ============== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | best-practices/species-lists 8 | best-practices/segment-review 9 | best-practices/training 10 | best-practices/embeddings -------------------------------------------------------------------------------- /docs/best-practices/embeddings.rst: -------------------------------------------------------------------------------- 1 | Embedding Extraction and Search 2 | =============================== 3 | 4 | 1. Introduction 5 | ---------------- 6 | 7 | The embeddings extraction and search feature allows you to quickly search for similar audio files in large datasets. 8 | This can be used to explore your data and help you to collect training data for building a custom classifier. 9 | 10 | 11 | 2. Extracting Embeddings and creating a Database 12 | ------------------------------------------------- 13 | 14 | The first step is to create a database of embeddings from your audio files. 15 | In the GUI go to the Embeddings-Tab and then to the Extract-Section. There you can select the directory containing your audio files. 16 | As with the analysis feature this will go to your directory recursively and include all audio files from the selected folder and any subdirectories. 17 | After that choose the directory where your embeddings database should be created and specify a name for your database. 18 | 19 | You can further specify the following parameters for the extraction: 20 | 21 | - | **Overlap**: Audio is still processed in 3-second snippets. This parameter specifies the overlap between these snippets. 22 | - | **Batch size**: This can be adjusted to increase the performance of the extraction process depending on your hardware. 23 | - | **Audio speed modifier**: This can be used to speed up or slow down the audio during the extraction process to enable working with ultra- and infrasonic recordings. 24 | - | **Bandpass filter frequencies**: This sets the bandpass filter which is applied after the speed modifier, to further filter out unwanted frequencies. 25 | 26 | .. note:: 27 | The audio speed and bandpass parameters will be stored in the database and will also applied during the search process. 28 | 29 | .. note:: 30 | Due to limitations of the underlying hoplite database, multithreading is not supported for the extraction process. 31 | 32 | The database will be created as a folder with the specified name containing two files: 33 | - 'hoplite.sqlite' 34 | - 'usearch.index' 35 | 36 | 2.1. File output 37 | ^^^^^^^^^^^^^^^^^^^ 38 | 39 | If you want to process the embeddings as files, you can also specify a folder for the file output. If no folder is specified the file output will be omitted. 40 | The file output will create an individual file for each 3 second audio snippet that is processed, containing the extracted embedding. 41 | 42 | The files will be named according to the following pattern: "{original_file_name}_{start}_{end}.birdnet.embeddings.txt". 43 | 44 | 3. Searching your database 45 | ------------------------------------------------- 46 | 47 | The database can be searched in the GUI in the Search-Section of the Embeddings-Tab. 48 | To start the search first select the database you want to search in. As soon as the database is loaded the extraction settings and the number of embeddings in the database will be displayed. 49 | 50 | After that select a file as a query example to find similar sounds in the database. 51 | You can select the :doc:`crop mode <../implementation-details/crop-modes>` to specify how the example file will be cropped if it is longer than 3 seconds. For the segments crop mode the simliarity measure will be averaged over all segments of the query example. 52 | 53 | Further specify the maximum number of results you want to retrieve and the score-function. 54 | 55 | The following score functions are available: 56 | 57 | - | **Cosine**: Uses the cosine of the angle between the two embedding vectors. More similar vectors will result in a higher value. 58 | - | **Dot**: Uses the dot product of the two embedding vectors. As with the cosine measure, more similar vectors will result in a higher value, but longer vectors will also increase the score. 59 | - | **Euclidean**: Uses the euclidean distance between the two embedding vectors. More similar vectors will result in a lower value. 60 | 61 | Click the start search button to show the results. The results will be displayed over multiple pages. 62 | For each result a spectrogram of the corresponding audio snippet will be shown and the audio can be played back for reference. 63 | 64 | While inspecting the results you can mark them for export. After finishing the selection click the export button and choose a folder to save the results in. 65 | You can now use the data for training custom classifiers or for further analysis. 66 | 67 | 68 | 3.1 About audio speed and bandpass filter in search 69 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 70 | 71 | The audio speed and bandpass filter settings that were used for the extraction process will also be applied to the query example and the result-snippets. 72 | This will also be reflected in the spectrograms and the audio playback. 73 | 74 | However, the exported audio will always be in the original speed and without any bandpass filter applied. 75 | This is to ensure that the exported audio retains its original sample rate and also to make using it in the training process more intuitive. 76 | 77 | As the processed audio snippets are always 3 seconds long after the audio speed modifier, the exported audio files may be shorter or longer. 78 | For example with an audio speed modifier of 2, e.g. double speed, the exported audio will only be 1.5 seconds long. 79 | 80 | 3.2. Search via CLI 81 | ^^^^^^^^^^^^^^^^^^^ 82 | 83 | Searching the database can also be done via the command line interface, although due to the missing interface a manual inspection and selection of the results is not possible. 84 | Visit the command line interface documentation for more information on how to use the CLI. 85 | 86 | -------------------------------------------------------------------------------- /docs/best-practices/segment-review.rst: -------------------------------------------------------------------------------- 1 | Segment Review 2 | ================================= 3 | 4 | Get started by listening to this AI-generated summary of segments review: 5 | 6 | .. raw:: html 7 | 8 | 12 | 13 | | 14 | | `Source: Google NotebookLM` 15 | 16 | 1. Prepare Audio and Result Files 17 | --------------------------------- 18 | 19 | - | **Collect Audio Recordings and Corresponding BirdNET Result Files**: Organize them into separate folders. 20 | - | **Result File Formats**: BirdNET-Analyzer typically produces result files with extensions ".BirdNET.txt" or ".BirdNET.csv". It can process various result file formats, including "table", "kaleidoscope", "csv", and "audacity". 21 | - | **Understanding Confidence Values**: Note that BirdNET confidence values are not probabilities and are not directly transferable between different species or recording conditions. 22 | 23 | 2. Using the "Segments" Function in the GUI or Command Line 24 | ----------------------------------------------------------- 25 | 26 | - | **Segments Function**: BirdNET provides the "segments" function to create a collection of species-specific predictions that exceed a user-defined confidence value. This function is available in the graphical user interface (GUI) under the "segments" tab or via the "segments.py" script in the command line. 27 | - | **GUI Usage**: In the GUI, you can select audio, result, and output directories. You can also set additional parameters such as the minimum confidence value, the maximum number of segments per species, the audio speed, and the segment length. 28 | 29 | 3. Setting Parameters 30 | --------------------- 31 | 32 | - | **Minimum Confidence (min_conf)**: Set a minimum confidence value for predictions to be considered. Note that this value may vary by species. It is recommended to determine the threshold by reviewing precision and recall. 33 | - | **Maximum Number of Segments (num_seq)**: Specify how many segments per species should be extracted. 34 | - | **Audio Speed (audio_speed)**: Adjust the playback speed. Extracted segments will be saved with the adjusted speed (e.g., to listen to ultrsonic calls). 35 | - | **Segment Length (seq_length)**: Define how long the extracted audio segments should be. If you set to more than 3 seconds, each segment will be padded with audio from the source recording. For example, for 5-second segment length, 1 second of audio before and after each extracted segment will be included. For 7 seconds, 2 seconds will be included, and so on. The first and last segment of each audio file might be shorter than the specified length. 36 | 37 | 4. Extracting Segments 38 | ---------------------- 39 | 40 | - | **Start the Extraction Process**: After setting all parameters, start the extraction process. BirdNET will create subfolders for each identified species and save audio clips of the corresponding recordings. 41 | - | **Progress Display**: The progress of the process will be displayed. 42 | 43 | 5. Reviewing Results 44 | -------------------- 45 | 46 | - | **Manual Review of Audio Segments**: The resulting audio segments can be manually reviewed to assess the accuracy of the predictions. It is important to note that BirdNET confidence values are not probabilities but a measure of the algorithm's prediction reliability. 47 | - | **Systematic Review**: It is recommended to start with the highest confidence scores and work down to the lower scores. 48 | - | **File Naming**: Files are named with confidence values, allowing for sorting by values. 49 | 50 | 6. Using the Review Tab in the GUI 51 | ---------------------------------- 52 | 53 | - | **Review Tab Overview**: The review tab in the GUI allows you to systematically review and label the extracted segments. It provides tools for visualizing spectrograms, listening to audio segments, and categorizing them as positive or negative detections. 54 | - | **Collect Segments**: Use the review tab to collect segments from the specified directory. You can shuffle the segments for a randomized review process. 55 | - | **Create Log Plot**: The review tab can generate a logistic regression plot to visualize the relationship between confidence values and the likelihood of correct detections. 56 | - **Review Process**: 57 | 58 | - | **Select Directory**: Choose the directory containing the segments to be reviewed. 59 | - | **Species Dropdown**: Select the species to review from the dropdown menu. 60 | - | **File Count Matrix**: View the count of files to be reviewed, positive detections, and negative detections. 61 | - | **Spectrogram and Audio**: Visualize the spectrogram and listen to the audio segment. 62 | - | **Label Segments**: Use the buttons to label segments as positive or negative detections. You can also use the left and right arrow keys to assign labels. 63 | - | **Undo**: Undo the last action if needed. 64 | - | **Download Plots**: Download the spectrogram and regression plots for further analysis. 65 | 66 | 7. Alternative Approaches 67 | ------------------------- 68 | 69 | - | **Raven Pro**: BirdNET result tables can be imported into Raven Pro and reviewed using the selection review function. 70 | - | **Converting Confidence Values to Probabilities**: Another approach is converting confidence values to probabilities using logistic regression in R. However, this still requires manual evaluation of predictions. 71 | 72 | 8. Important Notes 73 | ------------------ 74 | 75 | - | **Non-Transferability of Confidence Values**: BirdNET confidence values are not easily transferable between species. 76 | - | **Audio Quality**: The accuracy of results heavily depends on the quality of audio recordings, such as sample rate and microphone quality. 77 | - | **Environmental Factors**: Results can be influenced by the recording environment, such as wind or rain. 78 | - | **Standardized Test Data**: Using standardized test data for evaluation is important to make results comparable. 79 | 80 | This guide summarizes the best practices for using the "segments" function of BirdNET-Analyzer and emphasizes the need for careful interpretation of the results. -------------------------------------------------------------------------------- /docs/best-practices/species-lists.rst: -------------------------------------------------------------------------------- 1 | Creating Your Own Species List 2 | ============================== 3 | 4 | When editing your own `species_list.txt` file, make sure to copy species names from the labels file of each model. 5 | 6 | You can find label files in the checkpoints folder, e.g., `checkpoints/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels.txt`. 7 | 8 | Species names need to consist of `scientific name_common name` to be valid. 9 | 10 | You can generate a species list for a given location using :ref:`species.py `. 11 | 12 | Practical Information and Considerations 13 | ---------------------------------------- 14 | 15 | **Understanding the GeoModel** 16 | 17 | The BirdNET Species Range Model V2.4 - V2 uses eBird checklist frequency data to estimate the range of bird species and the probability of their occurrence given latitude, longitude, and week of the year. eBird relies on citizen scientists to collect bird species observations around the world. Due to biases in these data, some regions such as North and South America, Europe, India, and Australia are well represented in the data, while large parts of Africa or Asia are underrepresented. 18 | 19 | In cases where eBird does not have enough observations (i.e., checklists), the data "only" contain binary filter data of likely species that could occur in a given location. Therefore, the training data for our biodiversity model is a mixture of actual observations and filter data curated by experts. We included all locations for which at least 10 checklists are available for each week of the year, and randomly added other locations with a 3% probability. 20 | 21 | **Limitations of the GeoModel** 22 | 23 | - **Data Coverage**: The model works well in regions with good eBird data coverage, such as North and South America, Europe, India, and Australia. In other regions, the lack of eBird observations means the resulting species lists may not reflect actual probabilities of occurrence. 24 | - **Binary Filter Data**: In areas with insufficient eBird data, the model relies on binary filter data, which may not be as accurate as actual observations. 25 | - **Seasonal Variations**: The model accounts for seasonal variations in bird presence, but the accuracy depends on the availability of data for each week of the year. 26 | 27 | **Creating Custom Species Lists** 28 | 29 | If you know which species to expect in your area, it is recommended to compile your own species list. This can help improve the accuracy of BirdNET-Analyzer for your specific use case. 30 | 31 | 1. **Collect Species Names**: Use the labels file from the model checkpoints to get the correct species names. Ensure the names are in the format `scientific name_common name`. 32 | 2. **Generate Species List**: Use the `species.py` script to generate a species list for a given location and time. This script uses the GeoModel to predict species occurrence based on latitude, longitude, and week of the year. 33 | 34 | **Example of Training Data** 35 | 36 | Here is an example of what the training data for a given location (Chemnitz) looks like: 37 | 38 | .. code:: python 39 | 40 | 'gretit1': [72, 90, 98, 93, 96, 88, 95, 94, 99, 99, 93, 92, 90, 96, 85, 97, 89, 78, 67, 68, 48, 39, 35, 40, 49, 49, 49, 51, 48, 55, 55, 73, 60, 64, 62, 63, 72, 72, 72, 67, 66, 80, 63, 74, 67, 76, 88, 70], 41 | 'carcro1': [62, 81, 83, 82, 85, 75, 90, 75, 83, 80, 76, 80, 84, 90, 72, 73, 83, 67, 70, 75, 54, 48, 42, 55, 51, 53, 55, 49, 55, 53, 55, 62, 57, 55, 66, 69, 63, 65, 69, 63, 59, 74, 61, 63, 76, 79, 69, 60], 42 | 'eurbla': [55, 80, 84, 92, 71, 70, 72, 84, 85, 86, 82, 95, 88, 92, 86, 91, 90, 75, 87, 81, 84, 72, 69, 62, 67, 70, 57, 66, 55, 56, 49, 32, 36, 37, 41, 49, 55, 62, 57, 58, 41, 37, 58, 67, 69, 64, 69, 49], 43 | 'blutit': [67, 83, 92, 93, 96, 83, 87, 93, 96, 90, 82, 80, 84, 88, 58, 79, 74, 52, 46, 36, 34, 29, 25, 26, 39, 43, 36, 43, 47, 42, 49, 48, 49, 51, 45, 52, 61, 64, 55, 55, 65, 72, 62, 71, 66, 67, 69, 64], 44 | 'grswoo': [61, 84, 80, 80, 90, 83, 85, 77, 76, 82, 72, 77, 77, 78, 64, 76, 81, 69, 73, 75, 66, 44, 46, 41, 47, 41, 38, 44, 42, 42, 52, 68, 37, 35, 38, 43, 44, 41, 43, 41, 49, 61, 41, 49, 48, 47, 67, 47], 45 | 'cowpig1': [9, 10, 3, 3, 16, 16, 30, 54, 65, 61, 69, 76, 83, 81, 80, 86, 80, 71, 68, 78, 68, 69, 79, 68, 76, 69, 69, 79, 70, 70, 68, 73, 64, 63, 58, 54, 53, 49, 53, 56, 44, 21, 33, 38, 45, 43, 5, 11], 46 | 'eurnut2': [43, 76, 88, 82, 79, 78, 91, 84, 92, 86, 76, 77, 75, 85, 69, 75, 60, 34, 47, 58, 34, 24, 33, 33, 31, 23, 28, 25, 23, 21, 23, 52, 26, 26, 31, 28, 25, 29, 32, 23, 47, 46, 24, 31, 30, 36, 61, 53], 47 | 'comcha': [26, 33, 30, 33, 34, 34, 39, 48, 70, 75, 80, 83, 80, 90, 76, 85, 80, 74, 77, 74, 59, 52, 51, 40, 34, 44, 33, 31, 22, 15, 17, 21, 17, 18, 26, 34, 44, 48, 53, 49, 31, 27, 33, 39, 44, 39, 30, 28] 48 | 49 | **Example of Model Predictions** 50 | 51 | If we query the trained model for the same location as above, we get these values for great tits: 52 | 53 | .. code:: python 54 | 55 | 'gretit': [99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 98, 98, 98, 98, 98, 97, 97, 97, 97, 97, 97, 98, 98, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99] 56 | 57 | **Conclusion** 58 | 59 | Overall, the model works well in regions with good data coverage. In other regions, the lack of eBird observations means the resulting species lists may not reflect actual probabilities of occurrence. Nevertheless, these lists can be used to filter for species that may or may not occur in these locations. 60 | 61 | By understanding the limitations and capabilities of the GeoModel, you can make informed decisions when creating and using custom species lists for BirdNET-Analyzer. 62 | 63 | See this post in the discussion forum for more details: `Species range model details `_ -------------------------------------------------------------------------------- /docs/birdnet-tiny.rst: -------------------------------------------------------------------------------- 1 | BirdNET-Tiny 2 | ============ 3 | 4 | We also provide training and deployment tools for microcontrollers. **BirdNET-Tiny Forge** simplifies the training of BirdNET-Tiny models and their deployment on embedded devices. 5 | 6 | .. image:: _static/birdnet-tiny-forge-logo.png 7 | :alt: BirdNET-Tiny Forge 8 | :align: center 9 | :width: 150px 10 | 11 | | 12 | 13 | **BirdNET-Tiny Forge** is a collaboration between the BirdNET-team and fold ecosystemics. Start building your own tiny models for bioacoustics here: `https://github.com/birdnet-team/BirdNET-Tiny-Forge `_. 14 | 15 | .. note:: 16 | 17 | BirdNET-Tiny Forge is under active development, so you might encounter changes that could affect your current workflow. 18 | We recommend checking for updates regularly. -------------------------------------------------------------------------------- /docs/birdnetr.rst: -------------------------------------------------------------------------------- 1 | BirdNET in R 2 | ============ 3 | 4 | We do also provide a BirdNET package for R, which allows you to analyze audio recordings directly in R. 5 | 6 | .. image:: _static/birdnetr-logo.png 7 | :alt: BirdNET-Tiny Forge 8 | :align: center 9 | :width: 150px 10 | 11 | | 12 | 13 | **birdnetR** is geared towards providing a robust workflow for ecological data analysis in bioacoustic projects. 14 | While it covers essential functionalities, it doesn’t include all the features found in BirdNET-Analyzer. 15 | Some features might only be available in the BirdNET Analyzer and not in this package. 16 | 17 | .. note:: 18 | Please note that birdnetR is under active development, so you might encounter changes that could affect your current workflow. 19 | We recommend checking for updates regularly. 20 | 21 | See our website for more information: 22 | 23 | `https://birdnet-team.github.io/birdnetR/index.html `_ -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | import os 10 | import sys 11 | 12 | sys.path.insert(0, os.path.abspath(".")) 13 | sys.path.insert(1, os.path.abspath("..")) 14 | 15 | project = "BirdNET-Analyzer" 16 | copyright = "%Y, BirdNET-Team" 17 | author = "Stefan Kahl" 18 | version = "1.5.1" 19 | 20 | # -- General configuration --------------------------------------------------- 21 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 22 | 23 | extensions = [ 24 | "sphinx.ext.intersphinx", 25 | "sphinxarg.ext", 26 | ] 27 | 28 | intersphinx_mapping = { 29 | "python": ("https://docs.python.org/3", None), 30 | "matplotlib": ("https://matplotlib.org/stable/", None), 31 | "numpy": ("https://numpy.org/doc/stable/", None), 32 | } 33 | 34 | templates_path = ["_templates"] 35 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 36 | 37 | # -- Options for HTML output ------------------------------------------------- 38 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 39 | 40 | # :github_url: is meta data used to force the "Edit on GitHub" link to point to the exact url. 41 | # https://sphinx-rtd-theme.readthedocs.io/en/stable/configuring.html#file-wide-metadata 42 | rst_prolog = ":github_url: https://github.com/birdnet-team/BirdNET-Analyzer\n" 43 | html_theme = "sphinx_rtd_theme" 44 | html_favicon = "_static/birdnet-icon.ico" 45 | html_logo = "_static/birdnet_logo.png" 46 | html_static_path = ["_static"] 47 | html_css_files = ["css/custom.css"] 48 | html_theme_options = {"style_external_links": True} 49 | html_show_sourcelink = False 50 | html_show_sphinx = False 51 | html_extra_path = ["projects.html", "projects_data.js"] 52 | -------------------------------------------------------------------------------- /docs/contribute.rst: -------------------------------------------------------------------------------- 1 | How To Contibute 2 | ================ 3 | 4 | Feel free to clone the `repository `_ and contribute to the project. We are always looking for new ideas and improvements. If you have any questions, please don't hesitate to ask. 5 | 6 | Let us know if you have any ideas for new features or improvements or submit a pull request. 7 | 8 | **Help us to improve the documentation!** 9 | 10 | Install `sphinx` and all required themes + plugins with `pip install sphinx sphinx_rtd_theme sphinx-argparse`. 11 | 12 | Run `sphinx-build docs docs/_build`. 13 | 14 | Navigate to `BirdNET-Analyzer/docs/_build` and open `index.html` with a browser of your choice. 15 | 16 | Make your changes to the `.rst` files in the `docs` directory and submit a pull request. -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | === 3 | 4 | We will answer frequently asked questions here. If you have a question that is not answered here, please let us know at `ccb-birdnet@cornell.edu `_. 5 | 6 | What is BirdNET-Analyzer? 7 | ------------------------- 8 | 9 | BirdNET-Analyzer is a tool for analyzing bird sounds using machine learning models. It can identify bird species from audio recordings and provides various functionalities for training custom classifiers, extracting segments, and reviewing results. 10 | 11 | How do I install BirdNET-Analyzer? 12 | ---------------------------------- 13 | 14 | BirdNET-Analyzer can be installed using different methods, including: 15 | 16 | - | **Raven Pro**: Follow the instructions provided in the Raven Pro documentation. 17 | - | **Python Package**: Install via pip using `pip install birdnet`. 18 | - | **Command Line**: Download the repository and run the scripts from the command line. 19 | - | **GUI**: Download the GUI version from the `releases page `_ and follow the installation instructions. 20 | 21 | What licenses are used in BirdNET-Analyzer? 22 | ------------------------------------------- 23 | 24 | BirdNET-Analyzer source code is released under the **MIT License**. The models used in the project are licensed under the **Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0)**. Please review and adhere to the specific license terms provided with each model. 25 | Custom models trained with BirdNET-Analyzer are also subject to the same licensing terms. 26 | 27 | .. note:: Please note that all educational and research purposes are considered non-commercial use and it is therefore freely permitted to use BirdNET models in any way. 28 | 29 | Please get in touch if you have any questions or need further assistance. 30 | 31 | How can I contribute training data to BirdNET? 32 | ---------------------------------------------- 33 | 34 | The prefered way to contribute labeled audio recordings is through the `Xeno-Canto `_ platform. We regularly download new recordings from Xeno-Canto to improve the models. 35 | 36 | Fully annotated soundscape recordings should be shared on Zenodo or other data repositories - this way, they can be used for training and validation by the BirdNET team and other researchers. 37 | 38 | If you have large amounts of validated detections, please get in touch with us at `ccb-birdnet@cornell.edu `_. 39 | 40 | What are the non-event classes in BirdNET? 41 | ------------------------------------------ 42 | 43 | There are currently 11 non-event classes in BirdNET: 44 | 45 | * Human non-vocal_Human non-vocal 46 | * Human vocal_Human vocal 47 | * Human whistle_Human whistle 48 | * Noise_Noise 49 | * Dog_Dog 50 | * Engine_Engine 51 | * Environmental_Environmental 52 | * Fireworks_Fireworks 53 | * Gun_Gun 54 | * Power tools_Power tools 55 | * Siren_Siren 56 | 57 | `Noise_Noise` and `Environmental_Environmental` are auxiliary classes used for training and will never be predicted by the model. -------------------------------------------------------------------------------- /docs/implementation-details.rst: -------------------------------------------------------------------------------- 1 | Implementation details 2 | ============== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | implementation-details/crop-modes -------------------------------------------------------------------------------- /docs/implementation-details/crop-modes.rst: -------------------------------------------------------------------------------- 1 | Crop Modes 2 | =============================== 3 | 4 | This page describes the different crop modes available for the training and embeddings-search feature the BirdNET-Analyzer. 5 | In general a crop mode selection will be available in cases where audio files longer than 3 seconds are processed. 6 | With the the crop mode you can specify how the audio files should be cropped into 3 second snippets. 7 | 8 | 1. Center 9 | ---------------- 10 | 11 | This crop mode will take the center 3 seconds of the audio file. 12 | 13 | 2. First 14 | ---------------- 15 | 16 | This crop mode will take the first 3 seconds of the audio file. 17 | 18 | 3. Segments 19 | ---------------- 20 | 21 | With this crop mode you can also specify an overlap. The crop mode will then split the audio file into 3 second segments with the specified overlap. 22 | In the training feature this will result in multiple training examples that are generated from the same audio file. 23 | In the search feature the similarity measure will be averaged over all segments of the query example. 24 | 25 | 26 | 4. Smart 27 | ---------------- 28 | 29 | # TODO -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | BirdNET-Analyzer Documentation 2 | ============================== 3 | 4 | Welcome to the BirdNET-Analyzer documentation! This guide provides detailed information on installing, configuring, and using BirdNET-Analyzer. 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | :caption: Contents: 9 | 10 | installation 11 | usage 12 | models 13 | best-practices 14 | implementation-details 15 | faq 16 | showroom 17 | birdnetr 18 | birdnet-tiny 19 | contribute 20 | 21 | Introduction 22 | ------------ 23 | 24 | BirdNET-Analyzer is an open source tool for analyzing bird calls using machine learning models. It can process large amounts of audio recordings and identify (bird) species based on their calls. 25 | 26 | Get started by listening to this AI-generated introduction of the BirdNET-Analyzer: 27 | 28 | .. raw:: html 29 | 30 | 34 | 35 | | 36 | | `Source: Google NotebookLM` 37 | 38 | Citing BirdNET-Analyzer 39 | ----------------------- 40 | 41 | Feel free to use BirdNET for your acoustic analyses and research. If you do, please cite as: 42 | 43 | .. code-block:: bibtex 44 | 45 | @article{kahl2021birdnet, 46 | title={BirdNET: A deep learning solution for avian diversity monitoring}, 47 | author={Kahl, Stefan and Wood, Connor M and Eibl, Maximilian and Klinck, Holger}, 48 | journal={Ecological Informatics}, 49 | volume={61}, 50 | pages={101236}, 51 | year={2021}, 52 | publisher={Elsevier} 53 | } 54 | 55 | About 56 | ----- 57 | 58 | Developed by the `K. Lisa Yang Center for Conservation Bioacoustics `_ at the `Cornell Lab of Ornithology `_ in collaboration with `Chemnitz University of Technology `_. 59 | 60 | Go to https://birdnet.cornell.edu to learn more about the project. 61 | 62 | Want to use BirdNET to analyze a large dataset? Don't hesitate to contact us: ccb-birdnet@cornell.edu 63 | 64 | We also have a discussion forum on `Reddit `_ if you have a general question or just want to chat. 65 | 66 | Have a question, remark, or feature request? Please start a new issue thread to let us know. Feel free to submit a pull request. 67 | 68 | More tools and resources 69 | ------------------------ 70 | 71 | We also provide Python and R packages to interact with BirdNET models, as well as training and deployment tools for microcontrollers. Make sure to check out our other repositories at `https://github.com/birdnet-team `_. 72 | 73 | 74 | Projects map 75 | ------------ 76 | 77 | We have created an interactive map of projects that use BirdNET. If you are working on a project that uses BirdNET, please let us know and we can add your project to the map. 78 | 79 | You can access the map here: `Open projects map `_ 80 | 81 | Please refer to the `projects map documentation `_ for more information on how to contribute. 82 | 83 | License 84 | ------- 85 | 86 | **Source Code**: The source code for this project is licensed under the `MIT License `_ 87 | 88 | **Models**: The models used in this project are licensed under the `Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0) `_ 89 | 90 | Please ensure you review and adhere to the specific license terms provided with each model. 91 | 92 | *Please note that all educational and research purposes are considered non-commercial use and it is therefore freely permitted to use BirdNET models in any way.* 93 | 94 | Funding 95 | ------- 96 | 97 | This project is supported by Jake Holshuh (Cornell class of ´69) and The Arthur Vining Davis Foundations. 98 | Our work in the K. Lisa Yang Center for Conservation Bioacoustics is made possible by the generosity of K. Lisa Yang to advance innovative conservation technologies to inspire and inform the conservation of wildlife and habitats. 99 | 100 | The development of BirdNET is supported by the German Federal Ministry of Education and Research through the project “BirdNET+” (FKZ 01|S22072). 101 | The German Federal Ministry for the Environment, Nature Conservation and Nuclear Safety contributes through the “DeepBirdDetect” project (FKZ 67KI31040E). 102 | In addition, the Deutsche Bundesstiftung Umwelt supports BirdNET through the project “RangerSound” (project 39263/01). -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Download & Setup 4 | ================ 5 | 6 | GUI installer 7 | ------------- 8 | 9 | You can download the latest BirdNET-Analyzer installer for Windows and MacOS from our `Releases page `_. This installer provides an easy setup process for running BirdNET-Analyzer on your system. Make sure to check to select the correct installer for your system. 10 | 11 | .. note:: 12 | | Installation was only tested on M1 and M2 chips. 13 | | Feedback on older Intel CPUs or newer M3 chips is welcome! 14 | 15 | Raven Pro 16 | --------- 17 | 18 | If you want to analyze audio files without any additional coding or package install, you can now use `Raven Pro software `_ to run BirdNET models. 19 | After download, BirdNET is available through the new "Learning detector" feature in Raven Pro. 20 | 21 | For more information on how to use this feature, please visit the `Raven Pro Knowledge Base `_. 22 | 23 | `Download the newest model version here `_, extract the zip-file and move the extracted folder to the Raven models folder. 24 | On Windows, the models folder is ``C:\Users\\Raven Pro 1.6\Models``. Start Raven Pro and select *BirdNET_GLOBAL_6K_V2.4_Model_Raven* as learning detector. 25 | 26 | Python Package 27 | -------------- 28 | 29 | The easiest way to setup BirdNET on your machine is to install `birdnetlib `_ or `birdnet `_ through pip with: 30 | 31 | .. code-block:: bash 32 | 33 | pip install birdnetlib 34 | 35 | or 36 | 37 | .. code-block:: bash 38 | 39 | pip install birdnet 40 | 41 | Please take a look at the `birdnetlib user guide `_ on how to analyze audio with `birdnetlib`. 42 | 43 | When using the `birdnet`-package, you can run BirdNET with: 44 | 45 | .. code-block:: python 46 | 47 | from pathlib import Path 48 | from birdnet.models import ModelV2M4 49 | 50 | # create model instance for v2.4 51 | model = ModelV2M4() 52 | 53 | # predict species within the whole audio file 54 | species_in_area = model.predict_species_at_location_and_time(42.5, -76.45, week=4) 55 | predictions = model.predict_species_within_audio_file( 56 | Path("soundscape.wav"), 57 | filter_species=set(species_in_area.keys()) 58 | ) 59 | 60 | # get most probable prediction at time interval 0s-3s 61 | prediction, confidence = list(predictions[(0.0, 3.0)].items())[0] 62 | print(f"predicted '{prediction}' with a confidence of {confidence:.6f}") 63 | # predicted 'Poecile atricapillus_Black-capped Chickadee' with a confidence of 0.814056 64 | 65 | For more examples and documentation, make sure to visit `pypi.org/project/birdnet/ `_. 66 | 67 | For any feature request or questions regarding `birdnet`, please add an issue or PR at `github.com/birdnet-team/birdnet `_. 68 | 69 | Command line installation 70 | ------------------------- 71 | 72 | Requires Python 3.10. or 3.11. 73 | 74 | Clone the repository 75 | 76 | .. code-block:: bash 77 | 78 | git clone https://github.com/birdnet-team/BirdNET-Analyzer.git 79 | cd BirdNET-Analyzer 80 | 81 | Install the packages 82 | 83 | .. code-block:: bash 84 | 85 | pip install . 86 | 87 | .. note:: 88 | 89 | If you also want to use the GUI, you need to install the additional packages with: ``pip install .[gui]``. 90 | Same goes for server and training tools: ``pip install .[server]`` and ``pip install .[train]``. 91 | 92 | Use ``pip install .[all]`` to install all packages. 93 | 94 | When building a GUI for systems using GTK with pywebview, you may need to install additional packages: qtpy and PyGObject. 95 | Use the following command: ``pip install qtpy PyGObject``. 96 | 97 | Verify the installation 98 | 99 | .. code-block:: bash 100 | 101 | python -m birdnet_analyzer.analyze -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/models.rst: -------------------------------------------------------------------------------- 1 | Models 2 | ====== 3 | 4 | 5 | V2.4, June 2023 6 | --------------- 7 | 8 | * more than 6,000 species worldwide 9 | * covers frequencies from 0 Hz to 15 kHz with two-channel spectrogram (one for low and one for high frequencies) 10 | * 0.826 GFLOPs, 50.5 MB as FP32 11 | * enhanced and optimized metadata model 12 | * global selection of species (birds and non-birds) with 6,522 classes (incl. 11 non-event classes) 13 | 14 | Technical Details 15 | ^^^^^^^^^^^^^^^^^ 16 | 17 | * 48 kHz sampling rate (we up- and downsample automatically and can deal with artifacts from lower sampling rates) 18 | * we compute 2 mel spectrograms as input for the convolutional neural network: 19 | 20 | * first one has fmin = 0 Hz and fmax = 3000; nfft = 2048; hop size = 278; 96 mel bins 21 | * second one has fmin = 500 Hz and fmax = 15 kHz; nfft = 1024; hop size = 280; 96 mel bins 22 | 23 | * both spectrograms have a final resolution of 96x511 pixels 24 | * raw audio will be normalized between -1 and 1 before spectrogram conversion 25 | * we use non-linear magnitude scaling as mentioned in `Schlüter 2018 `_ 26 | * V2.4 uses an EfficienNetB0-like backbone with a final embedding size of 1024 27 | * See `this comment `_ for more details 28 | 29 | Species range model V2.4 - V2, Jan 2024 30 | --------------------------------------- 31 | 32 | * updated species range model based on eBird data 33 | * more accurate (spatial) species range prediction 34 | * slightly increased long-tail distribution in the temporal resolution 35 | * see `this discussion post `_ for more details 36 | 37 | 38 | Using older models 39 | ------------------ 40 | 41 | Older models can also be used as custom classifiers in the GUI or using the `--classifier` argument in the `birdnet_analyzer.analyze` command line interface. 42 | 43 | Just download your desired model version and unzip. 44 | 45 | * GUI: Select the \*_Model_FP32.tflite file under **Species selection / Custom classifier** 46 | * CLI: ``python -m birdnet_analyzer ... --classifier 'path_to_Model_FP32.tflite'`` 47 | 48 | Model Version History 49 | --------------------- 50 | 51 | .. note:: All models listed here are licensed under the `Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0) `_. 52 | 53 | V2.4 54 | ^^^^ 55 | 56 | - more than 6,000 species worldwide 57 | - covers frequencies from 0 Hz to 15 kHz with two-channel spectrogram (one for low and one for high frequencies) 58 | - 0.826 GFLOPs, 50.5 MB as FP32 59 | - enhanced and optimized metadata model 60 | - global selection of species (birds and non-birds) with 6,522 classes (incl. 11 non-event classes) 61 | - Download here: `BirdNET-Analyzer-V2.4.zip `_ 62 | 63 | V2.3 64 | ^^^^ 65 | 66 | - slightly larger (36.4 MB vs. 21.3 MB as FP32) but smaller computational footprint (0.698 vs. 1.31 GFLOPs) than V2.2 67 | - larger embedding size (1024 vs 320) than V2.2 (hence the bigger model) 68 | - enhanced and optimized metadata model 69 | - global selection of species (birds and non-birds) with 3,337 classes (incl. 11 non-event classes) 70 | - Download here: `BirdNET-Analyzer-V2.3.zip `_ 71 | 72 | V2.2 73 | ^^^^ 74 | 75 | - smaller (21.3 MB vs. 29.5 MB as FP32) and faster (1.31 vs 2.03 GFLOPs) than V2.1 76 | - maintains same accuracy as V2.1 despite more classes 77 | - global selection of species (birds and non-birds) with 3,337 classes (incl. 11 non-event classes) 78 | - Download here: `BirdNET-Analyzer-V2.2.zip `_ 79 | 80 | V2.1 81 | ^^^^ 82 | 83 | - same model architecture as V2.0 84 | - extended 2022 training data 85 | - global selection of species (birds and non-birds) with 2,434 classes (incl. 11 non-event classes) 86 | - Download here: `BirdNET-Analyzer-V2.1.zip `_ 87 | 88 | V2.0 89 | ^^^^ 90 | 91 | - same model design as 1.4 but a bit wider 92 | - extended 2022 training data 93 | - global selection of species (birds and non-birds) with 1,328 classes (incl. 11 non-event classes) 94 | - Download here: `BirdNET-Analyzer-V2.0.zip `_ 95 | 96 | V1.4 97 | ^^^^ 98 | 99 | - smaller, deeper, faster 100 | - only 30% of the size of V1.3 101 | - still linear spectrogram and EfficientNet blocks 102 | - extended 2021 training data 103 | - 1,133 birds and non-birds for North America and Europe 104 | - Download here: `BirdNET-Analyzer-V1.4.zip `_ 105 | 106 | V1.3 107 | ^^^^ 108 | 109 | - Model uses linear frequency scale for spectrograms 110 | - uses V2 fusion blocks and V1 efficient blocks 111 | - extended 2021 training data 112 | - 1,133 birds and non-birds for North America and Europe 113 | - Download here: `BirdNET-Analyzer-V1.3.zip `_ 114 | 115 | V1.2 116 | ^^^^ 117 | 118 | - Model based on EfficientNet V2 blocks 119 | - uses V2 fusion blocks and V1 efficient blocks 120 | - extended 2021 training data 121 | - 1,133 birds and non-birds for North America and Europe 122 | - Download here: `BirdNET-Analyzer-V1.2.zip `_ 123 | 124 | V1.1 125 | ^^^^ 126 | 127 | - Model based on Wide-ResNet (aka "App model") 128 | - extended 2021 training data 129 | - 1,133 birds and non-birds for North America and Europe 130 | - Download here: `BirdNET-Analyzer-V1.1.zip `_ 131 | 132 | App Model 133 | ^^^^^^^^^ 134 | 135 | - Model based on Wide-ResNet 136 | - ~3,000 species worldwide 137 | - currently deployed as BirdNET app model 138 | - Download here: `BirdNET-Analyzer-App-Model.zip `_ 139 | -------------------------------------------------------------------------------- /docs/projects.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BirdNET Projects Map 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 33 | 34 | 35 |
36 |
37 | 38 |
39 | 40 | 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /docs/showroom.rst: -------------------------------------------------------------------------------- 1 | Showroom 2 | ======== 3 | 4 | BirdNET powers a number of fantastic community projects dedicated to bird song identification, all of which use models from this repository. 5 | These are some highlights, make sure to check them out! 6 | 7 | .. list-table:: 8 | :widths: 25 75 9 | 10 | * - .. image:: _static/haikubox.png 11 | :alt: HaikuBox 12 | :align: center 13 | :target: https://haikubox.com/ 14 | 15 | - | **HaikuBox** 16 | 17 | Once connected to your WiFi, Haikubox will listen for birds 24/7. 18 | When BirdNET finds a match between its thousands of labeled sounds and the birdsong in your yard, it identifies the bird species and shares a three-second audio clip to the Haikubox website and smartphone app. 19 | 20 | | Learn more at: `HaikuBox.com `_ 21 | 22 | * - .. image:: _static/birdnet-pi.png 23 | :alt: BirdNET-Pi 24 | :align: center 25 | :target: https://birdnetpi.com/ 26 | 27 | - | **BirdNET-Pi** 28 | 29 | Built on the TFLite version of BirdNET, this project uses pre-built TFLite binaries for Raspberry Pi to run on-device sound analyses. 30 | It is able to recognize bird sounds from a USB sound card in realtime and share its data with the rest of the world. 31 | 32 | | Learn more at: `BirdNETPi.com `_ 33 | 34 | .. note:: You can find the most up-to-date version of BirdNET-PI at `github.com/Nachtzuster/BirdNET-Pi `_ 35 | 36 | 37 | * - .. image:: _static/birdweather.png 38 | :alt: BirdWeather 39 | :align: center 40 | :target: https://app.birdweather.com/ 41 | 42 | - | **BirdWeather** 43 | 44 | This site was built to be a living library of bird vocalizations. 45 | Using the BirdNET artificial neural network, BirdWeather is continuously listening to over 1,000 active stations around the world in real-time. 46 | 47 | | Learn more at: `BirdWeather.com `_ 48 | 49 | * - .. image:: _static/birdnetlib.png 50 | :alt: birdnetlib 51 | :align: center 52 | :target: https://joeweiss.github.io/birdnetlib/ 53 | 54 | - | **birdnetlib** 55 | 56 | A python api for BirdNET-Analyzer and BirdNET-Lite. ``birdnetlib`` provides a common interface for BirdNET-Analyzer and BirdNET-Lite. 57 | 58 | | Learn more at: `github.io/birdnetlib `_ 59 | 60 | * - .. image:: _static/ecopi.png 61 | :alt: ecoPi:Bird 62 | :align: center 63 | :target: https://oekofor.netlify.app/en/portfolio/ecopi-bird_en/ 64 | 65 | - | **ecoPi:Bird** 66 | 67 | The ecoPi:Bird is a device for automated acoustic recordings of bird songs and calls, with a self-sufficient power supply. 68 | It facilitates economical long-term monitoring, implemented with minimal personal requirements. 69 | 70 | | Learn more at: `oekofor.netlify.app `_ 71 | 72 | * - .. image:: _static/dawnchorus.png 73 | :alt: Dawn Chorus 74 | :align: center 75 | :target: https://dawn-chorus.org/en/ 76 | 77 | - | **Dawn Chorus** 78 | 79 | Dawn Chorus invites global participation to record bird sounds for biodiversity research, art, and raising awareness. 80 | This project aims to sharpen our senses and creativity by connecting us more deeply with the wonders of nature. 81 | 82 | | Learn more at: `dawn-chorus.org `_ 83 | 84 | * - .. image:: _static/chirpity.png 85 | :alt: Chirpity 86 | :align: center 87 | :target: https://chirpity.mattkirkland.co.uk/ 88 | 89 | - | **Chirpity** 90 | 91 | Chirpity is a desktop application available for Windows, Mac and Linux platforms. 92 | Optimized for speed and ease of use, it can analyze anything from short clips to hundreds of hours of audio with unparalleled speed. 93 | Detections can be validated against reference calls, edited and saved to a call library. 94 | Results can also be exported to a variety of formats including CSV, Raven and eBird. 95 | 96 | | Learn more at: `chirpity.mattkirkland.co.uk `_ 97 | 98 | * - .. image:: _static/BirdNET-Go-logo.webp 99 | :alt: BirdNET-Go 100 | :align: center 101 | :target: https://github.com/tphakala/go-birdnet 102 | 103 | - | **BirdNET-Go** 104 | 105 | Go-BirdNET is an application inspired by BirdNET-Analyzer. 106 | While the original BirdNET is based on Python, Go-BirdNET is built using Golang, aiming for simplified deployment across multiple platforms, from Windows PCs to single board computers like Raspberry Pi. 107 | 108 | | Learn more at: `github.com/tphakala/go-birdnet `_ 109 | 110 | * - .. image:: _static/whobird.png 111 | :alt: whoBIRD 112 | :align: center 113 | :target: https://github.com/woheller69/whoBIRD 114 | 115 | - | **whoBIRD** 116 | 117 | whoBIRD empowers you to identify birds anywhere, anytime, without an internet connection. 118 | Built upon the TFLite version of BirdNET, this Android application harnesses the power of machine learning to recognize birds directly on your device. 119 | 120 | | Learn more at: `whoBIRD `_ 121 | 122 | * - .. image:: _static/Muuttolintujen-Kevät.png 123 | :alt: Muuttolintujen Kevät 124 | :align: center 125 | :target: https://www.jyu.fi/en/research/muuttolintujen-kevat 126 | 127 | - | **Muuttolintujen Kevät** 128 | 129 | Muuttolintujen Kevät (Migration Birds Spring) is a mobile application developed at the University of Jyväskylä, enabling users to record bird songs and make bird observations using a re-trained version of BirdNET. 130 | 131 | | Learn more at: `jyu.fi `_ 132 | 133 | * - .. image:: _static/faunanet_logo.png 134 | :alt: faunanet 135 | :align: center 136 | :target: https://github.com/ssciwr/faunanet 137 | 138 | - | **FaunaNet** 139 | 140 | faunanet provides a platform for bioacoustics research projects and is an extension of Birdnet-Analyzer based on birdnetlib. 141 | faunanet is written in pure Python and is developed by the Scientific Software Center at the University of Heidelberg, Germany. 142 | 143 | | Learn more at: `faunanet `_ 144 | 145 | * - .. image:: _static/ecosound-web_logo_large_white_on_black.png 146 | :alt: ecoSound-web 147 | :align: center 148 | :target: https://ecosound-web.de/ecosound_web/ 149 | 150 | - | **ecoSound-web** 151 | 152 | ecoSound-web is a web application for ecoacoustics to manage, re-sample, navigate, visualize, annotate, and analyze soundscape recordings. 153 | It can execute BirdNET on recording batches and is currently being developed at INRAE, France. 154 | 155 | | Learn more at: `F1000Research `_ and `GitHub `_ 156 | 157 | * - .. image:: _static/ribbit.png 158 | :alt: Ribbit 159 | :align: center 160 | :target: https://ribbit.edi.eco/ 161 | 162 | - | **Ribbit** 163 | 164 | Record frog calls and the web app will tell you the species. Clip it, Ribbit! 165 | The app uses a custom classifier built with BirdNET embeddings to identify frog species and gives nature enthusiasts the possibility to learn more about amphibians. 166 | 167 | | Learn more at: `ribbit.edi.eco `_ 168 | 169 | **Other cool projects:** 170 | 171 | * BirdCAGE is an application for monitoring the bird songs in audio streams: `BirdCAGE at GitHub `_ 172 | * BattyBirdNET-Analyzer is a tool to assist in the automated classification of bat calls: `BattyBirdNET-Analyzer at GitHub `_ 173 | 174 | Working on a cool project that uses BirdNET? Let us know and we can feature your project here. 175 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | usage/cli 8 | usage/gui 9 | usage/docker 10 | usage/projects-map 11 | -------------------------------------------------------------------------------- /docs/usage/docker.rst: -------------------------------------------------------------------------------- 1 | Docker 2 | ====== 3 | 4 | We are currently re-working our Docker setup. Please check back later for updates. -------------------------------------------------------------------------------- /docs/usage/gui.rst: -------------------------------------------------------------------------------- 1 | GUI 2 | === 3 | 4 | We provide a stand-alone GUI which lets you launch the analysis through a web interface. 5 | 6 | .. image:: ../_static/gui.png 7 | :alt: BirdNET-Analyzer GUI 8 | :align: center 9 | 10 | 11 | You need to install two additional packages in order to use the GUI with ``pip install pywebview gradio`` 12 | 13 | Launch the GUI with ``python -m birdnet_analyzer.gui``. 14 | 15 | Set all folders and parameters, after that, click 'Analyze'. GUI items represent the command line arguments. 16 | For more information about the command line arguments, please refer to the :ref:`Command line interface documentation `. 17 | 18 | `Alternatively download the installer to run the GUI on your system`. 19 | 20 | Segment review 21 | -------------- 22 | 23 | Please read the paper from `Connor M. Wood and Stefan Kahl: Guidelines for appropriate use of BirdNET scores and other detector outputs `_. 24 | 25 | The **Review** tab in the GUI is an implementation of the workflow described in the paper. 26 | It allows you to review the segments that were detected by BirdNET and to verify the segments manually. 27 | This can help you to choose an appropriate cut-off threshold for your specific use case. 28 | 29 | General workflow: 30 | 31 | 1. Use the **Segments** tab in the GUI or the :ref:`segments.py ` script to extract short audio segments for species detections. 32 | 2. Open the **Review** tab in the GUI and select the parent directory containing the directories for all the species you want to review. 33 | 3. Review the segments and manually check "positive" if the segment does contain target species or "negative" if it does not. 34 | 35 | For each selected sample the logistic regression curve is fitted and the threshold is calculated. 36 | 37 | GUI Language 38 | ------------ 39 | 40 | The default language of the GUI is English, but you can change it to German, French, Chinese or Portuguese in the Settings tab of the GUI. 41 | If you want to contribute a translation to another language you, use the files inside the lang folder as a template. 42 | You can then send us the translated files or create a pull request. 43 | To check your translation, place your file inside the ``lang`` folder and start the GUI, your language should now be available in the **Settings** tab. 44 | After selecting your language, you should restart the GUI to apply the changes. 45 | 46 | We thank our collaborators for contributing translations: 47 | 48 | Chinese: Sunny Tseng (`@Sunny Tseng `_) 49 | 50 | French: `@FranciumSoftware `_ 51 | 52 | Portuguese: Larissa Sugai (`@LSMSugai `_) 53 | 54 | Russian: Александр Цветков (cau@yandex.ru, radio call sign: R1BAF) 55 | -------------------------------------------------------------------------------- /docs/usage/projects-map.rst: -------------------------------------------------------------------------------- 1 | Projects Map 2 | ============ 3 | 4 | We want to highlight the many use cases and great projects in bioacoustics and conservation that use BirdNET in their workflow. Therefore, we have created an interactive map showing the approximate locations of these projects and some additional information. 5 | 6 | You can access the map here: `Open projects map <../projects.html>`_ 7 | 8 | We will update the map regularly and also work on the visual representation. **However, we need your help to add more projects to the map and keep the information accurate**. 9 | 10 | There are three ways to contribute: you can submit a pull request with an additional entry in the `projects data `_ file, you can submit `this Google form `_, or you can simply reply in this thread and provide the following information: 11 | 12 | - Project name* 13 | - Organization/project lead* 14 | - Target species* 15 | - Country 16 | - Region/Location* 17 | - Latitude* 18 | - Longitude* 19 | - Contact 20 | - Website 21 | - Paper 22 | - Species Image URL 23 | - Species Image Credit 24 | 25 | The fields marked with an asterisk are required to place a valid mark on the map. 26 | 27 | If you would like to update a project or find that some information is incorrect, please reply to this thread as well. -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "birdnet_analyzer" 7 | version = "2.0.1" 8 | license = { text = "MIT" } 9 | description = "BirdNET analyzer for scientific audio data processing and bird classification." 10 | authors = [{ name = "Stefan Kahl" }] 11 | maintainers = [{ name = "Josef Haupt" }, { name = "Max Mauermann" }] 12 | keywords = ["birdnet", "birdnet-analyzer"] 13 | readme = "README.md" 14 | requires-python = ">=3.11" 15 | classifiers = [ 16 | "Programming Language :: Python :: 3.11", 17 | "Operating System :: OS Independent", 18 | "Topic :: Multimedia :: Sound/Audio :: Analysis", 19 | "Topic :: Scientific/Engineering", 20 | "Topic :: Scientific/Engineering :: Information Analysis", 21 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 22 | ] 23 | dependencies = [ 24 | "librosa", 25 | "resampy", 26 | "tensorflow==2.15.1", 27 | "scikit-learn==1.6.1", 28 | "tqdm", 29 | "pandas", 30 | "matplotlib", 31 | ] 32 | 33 | [project.optional-dependencies] 34 | train = ["keras-tuner"] 35 | server = ["bottle", "requests"] 36 | gui = [ 37 | "birdnet-analyzer[train,embeddings]", 38 | "gradio==5.32.1", 39 | "pywebview", 40 | "plotly[express]", 41 | "pywin32;platform_system=='Windows'", 42 | "qtpy;platform_system=='Linux'", 43 | "PyGObject;platform_system=='Linux'", 44 | ] 45 | embeddings = ["perch-hoplite"] 46 | all = ["birdnet-analyzer[server,gui]"] 47 | docs = ["sphinx", "sphinx-rtd-theme", "sphinx-argparse"] 48 | tests = ["pytest"] 49 | dev = ["birdnet_analyzer[tests]", "birdnet_analyzer[docs]", "ruff"] 50 | 51 | [project.scripts] 52 | birdnet-analyze = "birdnet_analyzer.analyze.cli:main" 53 | birdnet-embeddings = "birdnet_analyzer.embeddings.cli:main" 54 | birdnet-evaluate = "birdnet_analyzer.evaluation.__init__:main" 55 | birdnet-search = "birdnet_analyzer.search.cli:main" 56 | birdnet-train = "birdnet_analyzer.train.cli:main" 57 | birdnet-segments = "birdnet_analyzer.segments.cli:main" 58 | birdnet-species = "birdnet_analyzer.species.cli:main" 59 | 60 | [project.gui-scripts] 61 | birdnet-gui = "birdnet_analyzer.gui.__init__:main" 62 | 63 | [project.urls] 64 | Homepage = "https://birdnet.cornell.edu/birdnet" 65 | Documentation = "https://birdnet-team.github.io/BirdNET-Analyzer/" 66 | Repository = "https://github.com/birdnet-team/BirdNET-Analyzer" 67 | Issues = "https://github.com/birdnet-team/BirdNET-Analyzer/issues" 68 | Download = "https://github.com/birdnet-team/BirdNET-Analyzer/releases/latest" 69 | 70 | [tool.setuptools] 71 | packages = [ 72 | "birdnet_analyzer", 73 | "birdnet_analyzer.analyze", 74 | "birdnet_analyzer.gui", 75 | "birdnet_analyzer.embeddings", 76 | "birdnet_analyzer.search", 77 | "birdnet_analyzer.species", 78 | "birdnet_analyzer.segments", 79 | "birdnet_analyzer.train", 80 | "birdnet_analyzer.evaluation", 81 | "birdnet_analyzer.evaluation.preprocessing", 82 | "birdnet_analyzer.evaluation.assessment", 83 | ] 84 | 85 | [tool.setuptools.package-data] 86 | birdnet_analyzer = [ 87 | "eBird_taxonomy_codes_2024E.json", 88 | "lang/*", 89 | "labels/**/*", 90 | "gui/assets/**/*", 91 | ] 92 | 93 | [tool.pytest.ini_options] 94 | testpaths = ["tests"] 95 | pythonpath = ["birdnet_analyzer"] 96 | 97 | [tool.ruff] 98 | exclude = ["conf.py"] 99 | line-length = 165 100 | 101 | [tool.ruff.lint] 102 | select = [ 103 | "F", 104 | "B", 105 | "A", 106 | "C4", 107 | "T10", 108 | "EXE", 109 | "PIE", 110 | "PYI", 111 | "PT", 112 | "Q", 113 | "RSE", 114 | "RET", 115 | "SIM", 116 | "TID", 117 | "TD", 118 | "TC", 119 | #"PTH", 120 | "FLY", 121 | "I", 122 | "NPY", 123 | "PD", 124 | #"N", 125 | "PERF", 126 | "E", 127 | "W", 128 | #"D", 129 | "PL", 130 | "UP", 131 | "FURB", 132 | "RUF", 133 | ] 134 | ignore = [ 135 | "B008", 136 | "TD003", 137 | "TD002", 138 | "PD901", 139 | "SIM108", 140 | "E722", 141 | "PLR2004", 142 | "PLR0913", 143 | "PLR0915", 144 | "PLR0912", 145 | "PLC0206", 146 | "RUF015", 147 | ] 148 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/__init__.py -------------------------------------------------------------------------------- /tests/analyze/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/analyze/__init__.py -------------------------------------------------------------------------------- /tests/embeddings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/embeddings/__init__.py -------------------------------------------------------------------------------- /tests/embeddings/test_embeddings.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import os 3 | import shutil 4 | import tempfile 5 | from unittest.mock import MagicMock, patch 6 | 7 | import pytest 8 | 9 | import birdnet_analyzer.config as cfg 10 | from birdnet_analyzer.cli import embeddings_parser 11 | from birdnet_analyzer.embeddings.core import embeddings 12 | 13 | 14 | @pytest.fixture 15 | def setup_test_environment(): 16 | # Create a temporary directory for testing 17 | test_dir = tempfile.mkdtemp() 18 | input_dir = os.path.join(test_dir, "input") 19 | output_dir = os.path.join(test_dir, "output") 20 | 21 | os.makedirs(input_dir, exist_ok=True) 22 | os.makedirs(output_dir, exist_ok=True) 23 | 24 | # Store original config values 25 | original_config = {attr: getattr(cfg, attr) for attr in dir(cfg) if not attr.startswith("_") and not callable(getattr(cfg, attr))} 26 | 27 | yield { 28 | "test_dir": test_dir, 29 | "input_dir": input_dir, 30 | "output_dir": output_dir, 31 | } 32 | 33 | # Clean up 34 | shutil.rmtree(test_dir) 35 | 36 | # Restore original config 37 | for attr, value in original_config.items(): 38 | setattr(cfg, attr, value) 39 | 40 | 41 | @patch("birdnet_analyzer.utils.ensure_model_exists") 42 | @patch("birdnet_analyzer.embeddings.utils.run") 43 | def test_embeddings_cli(mock_run_embeddings: MagicMock, mock_ensure_model: MagicMock, setup_test_environment): 44 | env = setup_test_environment 45 | 46 | mock_ensure_model.return_value = True 47 | 48 | parser = embeddings_parser() 49 | args = parser.parse_args(["--input", env["input_dir"], "-db", env["output_dir"]]) 50 | 51 | embeddings(**vars(args)) 52 | 53 | mock_ensure_model.assert_called_once() 54 | threads = min(8, max(1, multiprocessing.cpu_count() // 2)) 55 | mock_run_embeddings.assert_called_once_with(env["input_dir"], env["output_dir"], 0, 1.0, 0, 15000, threads, 1, None) 56 | -------------------------------------------------------------------------------- /tests/evaluation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/evaluation/__init__.py -------------------------------------------------------------------------------- /tests/evaluation/assessment/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/evaluation/assessment/__init__.py -------------------------------------------------------------------------------- /tests/evaluation/preprocessing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/evaluation/preprocessing/__init__.py -------------------------------------------------------------------------------- /tests/gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/gui/__init__.py -------------------------------------------------------------------------------- /tests/gui/test_language.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections import defaultdict 3 | from pathlib import Path 4 | 5 | from birdnet_analyzer.gui.settings import LANG_DIR 6 | 7 | 8 | def test_language_keys(): 9 | language_files = list(Path(LANG_DIR).glob("*.json")) 10 | key_collection = defaultdict(list) 11 | 12 | for language_file in language_files: 13 | with open(language_file, encoding="utf-8") as f: 14 | language_data = f.read() 15 | assert language_data, f"Language file {language_file} is empty." 16 | 17 | language_keys: dict = json.loads(language_data) 18 | 19 | for k, v in language_keys.items(): 20 | assert isinstance(k, str), f"Key {k} in {language_file} is not a string." 21 | assert isinstance(v, str), f"Value for key {k} in {language_file} is not a string." 22 | assert k, f"Key in {language_file} is empty." 23 | assert v, f"Value for key {k} in {language_file} is empty." 24 | key_collection[k].append(language_file.stem) 25 | 26 | missing_keys = [] 27 | for key, files in key_collection.items(): 28 | if len(files) != len(language_files): 29 | missing_in = [f.stem for f in language_files if f.stem not in files] 30 | missing_keys.append((key, missing_in)) 31 | assert not missing_keys, ( 32 | "Not all keys are present in all language files.\n" + 33 | "\n".join(f"Key '{key}' missing in: {', '.join(missing_in)}" for key, missing_in in missing_keys) 34 | ) 35 | -------------------------------------------------------------------------------- /tests/segments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/segments/__init__.py -------------------------------------------------------------------------------- /tests/segments/test_segments.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | from unittest.mock import MagicMock, patch 5 | 6 | import pytest 7 | 8 | import birdnet_analyzer.config as cfg 9 | from birdnet_analyzer.cli import segments_parser 10 | from birdnet_analyzer.segments.core import segments 11 | 12 | 13 | @pytest.fixture 14 | def setup_test_environment(): 15 | # Create a temporary directory for testing 16 | test_dir = tempfile.mkdtemp() 17 | input_dir = os.path.join(test_dir, "input") 18 | output_dir = os.path.join(test_dir, "output") 19 | results_dir = os.path.join(test_dir, "results") 20 | 21 | os.makedirs(input_dir, exist_ok=True) 22 | os.makedirs(output_dir, exist_ok=True) 23 | os.makedirs(results_dir, exist_ok=True) 24 | 25 | file_list = [ 26 | {"audio": os.path.join(input_dir, "audio1.wav"), "result": os.path.join(results_dir, "result1.csv")}, 27 | {"audio": os.path.join(input_dir, "audio2.wav"), "result": os.path.join(results_dir, "result2.csv")} 28 | ] 29 | 30 | # Store original config values 31 | original_config = { 32 | attr: getattr(cfg, attr) for attr in dir(cfg) if not attr.startswith("_") and not callable(getattr(cfg, attr)) 33 | } 34 | 35 | yield { 36 | "test_dir": test_dir, 37 | "input_dir": input_dir, 38 | "output_dir": output_dir, 39 | "results_dir": results_dir, 40 | "file_list": file_list, 41 | } 42 | 43 | # Clean up 44 | shutil.rmtree(test_dir) 45 | 46 | # Restore original config 47 | for attr, value in original_config.items(): 48 | setattr(cfg, attr, value) 49 | 50 | @patch("birdnet_analyzer.segments.utils.extract_segments") 51 | @patch("birdnet_analyzer.segments.utils.parse_files") 52 | @patch("birdnet_analyzer.segments.utils.parse_folders") 53 | def test_segments_cli(mock_parse_folders: MagicMock, mock_parse_files: MagicMock, mock_extract_segments: MagicMock, setup_test_environment): 54 | env = setup_test_environment 55 | 56 | parser = segments_parser() 57 | args = parser.parse_args([env["input_dir"],"--results", env["results_dir"] ,"--output", env["output_dir"], "--threads", "1"]) 58 | 59 | mock_parse_files.return_value = env["file_list"] 60 | 61 | segments(**vars(args)) 62 | 63 | mock_parse_folders.assert_called_once() 64 | mock_parse_files.assert_called_once() 65 | mock_extract_segments.assert_called() 66 | -------------------------------------------------------------------------------- /tests/species/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/species/__init__.py -------------------------------------------------------------------------------- /tests/species/test_species.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | import birdnet_analyzer.config as cfg 9 | from birdnet_analyzer.cli import species_parser 10 | from birdnet_analyzer.species.core import species 11 | 12 | 13 | @pytest.fixture 14 | def setup_test_environment(): 15 | # Create a temporary directory for testing 16 | test_dir = tempfile.mkdtemp() 17 | output_dir = os.path.join(test_dir, "output") 18 | 19 | os.makedirs(output_dir, exist_ok=True) 20 | 21 | # Store original config values 22 | original_config = { 23 | attr: getattr(cfg, attr) for attr in dir(cfg) if not attr.startswith("_") and not callable(getattr(cfg, attr)) 24 | } 25 | 26 | yield { 27 | "test_dir": test_dir, 28 | "output_dir": output_dir, 29 | } 30 | 31 | # Clean up 32 | shutil.rmtree(test_dir) 33 | 34 | # Restore original config 35 | for attr, value in original_config.items(): 36 | setattr(cfg, attr, value) 37 | 38 | @patch("birdnet_analyzer.utils.ensure_model_exists") 39 | @patch("birdnet_analyzer.species.utils.run") 40 | def test_embeddings_cli(mock_run_species, mock_ensure_model, setup_test_environment): 41 | env = setup_test_environment 42 | 43 | mock_ensure_model.return_value = True 44 | 45 | parser = species_parser() 46 | args = parser.parse_args([env["output_dir"]]) 47 | 48 | species(**vars(args)) 49 | 50 | mock_ensure_model.assert_called_once() 51 | mock_run_species.assert_called_once_with(env["output_dir"], -1, -1, -1, 0.03, "freq") 52 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import birdnet_analyzer.config as cfg 4 | from birdnet_analyzer import utils 5 | 6 | 7 | def test_read_lines_label_files(): 8 | labels = Path(cfg.TRANSLATED_LABELS_PATH).glob("*.txt") 9 | expected_lines = 6522 10 | 11 | original_lines = utils.read_lines(cfg.LABELS_FILE) 12 | 13 | assert len(original_lines) == expected_lines, f"Expected {expected_lines} lines in {cfg.LABELS_FILE}, but got {len(original_lines)}" 14 | 15 | original_labels = [] 16 | 17 | for line in original_lines: 18 | names = line.split("_") 19 | assert len(names) == 2, f"Expected two names in {line}, but got {len(names)} in {cfg.LABELS_FILE}" 20 | original_labels.append(names) 21 | 22 | for label in labels: 23 | lines = utils.read_lines(label) 24 | 25 | for i, line in enumerate(lines): 26 | names = line.split("_") 27 | assert len(names) == 2, f"Expected two names in {line}, but got {len(names)} in {label}" 28 | assert original_labels[i][0] == names[0], f"Expected {original_labels[i][0]} but got {names[0]} in {label}" 29 | -------------------------------------------------------------------------------- /tests/train/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/train/__init__.py -------------------------------------------------------------------------------- /tests/train/test_train.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | import birdnet_analyzer.config as cfg 9 | from birdnet_analyzer.cli import train_parser 10 | from birdnet_analyzer.train.core import train 11 | 12 | 13 | @pytest.fixture 14 | def setup_test_environment(): 15 | # Create a temporary directory for testing 16 | test_dir = tempfile.mkdtemp() 17 | input_dir = os.path.join(test_dir, "input") 18 | output_dir = os.path.join(test_dir, "output") 19 | 20 | os.makedirs(input_dir, exist_ok=True) 21 | os.makedirs(output_dir, exist_ok=True) 22 | 23 | classifier_output = os.path.join(output_dir, "classifier_output") 24 | 25 | # Store original config values 26 | original_config = { 27 | attr: getattr(cfg, attr) for attr in dir(cfg) if not attr.startswith("_") and not callable(getattr(cfg, attr)) 28 | } 29 | 30 | yield { 31 | "test_dir": test_dir, 32 | "input_dir": input_dir, 33 | "output_dir": output_dir, 34 | "classifier_output": classifier_output, 35 | } 36 | 37 | # Clean up 38 | shutil.rmtree(test_dir) 39 | 40 | # Restore original config 41 | for attr, value in original_config.items(): 42 | setattr(cfg, attr, value) 43 | 44 | @patch("birdnet_analyzer.utils.ensure_model_exists") 45 | @patch("birdnet_analyzer.train.utils.train_model") 46 | def test_train_cli(mock_train_model, mock_ensure_model, setup_test_environment): 47 | env = setup_test_environment 48 | 49 | mock_ensure_model.return_value = True 50 | 51 | parser = train_parser() 52 | args = parser.parse_args([env["input_dir"], "--output", env["classifier_output"]]) 53 | 54 | train(**vars(args)) 55 | 56 | mock_ensure_model.assert_called_once() 57 | mock_train_model.assert_called_once_with() 58 | --------------------------------------------------------------------------------