Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ jobs:
macos-15-intel,
windows-2025,
]
python-version: ["3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13", "3.14"]
exclude:
- os: macos-15-intel
python-version: "3.13" # no TF support
- os: macos-15-intel
python-version: "3.14" # no TF / torch x86 macOS wheels

steps:
- name: Checkout
Expand Down Expand Up @@ -91,6 +93,14 @@ jobs:
run: |
python -m tox -e py313

# TensorFlow has no Python 3.14 wheels yet (issue #55), so 3.14 installs
# without it and only the TensorFlow-free surface (acoustic 3.0 via onnx)
# is exercised here.
- name: Run tests py314
if: matrix.python-version == '3.14'
run: |
python -m tox -e py314

- name: Run repro tests
if: matrix.python-version == '3.12' && matrix.os != 'macos-15-intel'
run: |
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added an `on_file_complete` callback to acoustic `predict(..)`, `predict_session(..)`, `encode(..)` and `encode_session(..)` (all models: 2.4, 3.0, Perch V2). It fires once per input file the moment that file is fully processed, receiving a single-file result (`AcousticFilePredictionResult` / `AcousticFileEncodingResult`); invalid files are reported with their input marked unprocessable. This enables streaming per-file persistence (e.g. resumable multi-file analysis) and live output. The callback runs on a background thread with a copy of the caller's context, off the inference hot path, so it does not regress throughput. File inputs only (not `run_arrays`); a callback that raises cancels the run.
- Added support for the BirdNET V3.0 (preview) acoustic model with four backends: TFLite/LiteRT (`tf`), ProtoBuf (`pb`), PyTorch (`pt`) and ONNX (`onnx`). Both `predict(..)` and `encode(..)` are supported on all backends. Load via `birdnet.load("acoustic", "3.0", <backend>)`. The `pt` and `onnx` backends require the new `birdnet[pt]` and `birdnet[onnx]` install extras (#41).
- Added support for the BirdNET-Geomodel V3.0 with TFLite/LiteRT (`tf`) and ProtoBuf (`pb`) backends. Load via `birdnet.load("geo", "3.0", <backend>)` (#41).
- Added an `apply_softmax` option to acoustic `predict(..)` (all models: 2.4, 3.0, Perch V2), mirroring `apply_sigmoid`. When enabled, output scores are the softmax over the model's logits, which is useful for obtaining confidence scores (e.g. for Perch V2). Defaults to `False` (#54).
- Added (partial) support for Python 3.14. TensorFlow does not yet ship Python 3.14 wheels, so on 3.14 `birdnet` installs without TensorFlow and supports the acoustic 3.0 model via the `onnx` and `pt` backends. The version cap `<3.14` was lifted and `tensorflow` is now only a dependency on Python ≤3.13. TensorFlow-only paths (`tf`/`pb` backends, acoustic 2.4, Perch, and all geo models — which are not distributed in ONNX/PyTorch form) raise a clear, actionable error on 3.14 instead of an `ImportError`. Full support will follow once TensorFlow provides 3.14 wheels (#55).

### Changed

- The progress callback now runs on a background worker thread with a copy of the caller's context (contextvars) as captured when the call starts, matching the behavior of the new `on_file_complete` callback (#53).

### Bugfixes

- Fixed corrupt rows in acoustic prediction and encoding output: growing the internal result buffer via `numpy.ndarray.resize` (together with an off-by-one in the initial segment count) could leave stale or uninitialized values in some segments. Buffers are now reallocated and copied, so all output rows are correct (#50).

## [0.2.16] - 2026-05-09

Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ For more detailed benchmarks, please refer to the [documentation](https://birdne

For details see the official [TensorFlow](https://www.tensorflow.org/install/pip#package_location) documentation.

#### Python 3.14

TensorFlow does not yet publish wheels for Python 3.14, so the table above (all TensorFlow-based backends: ProtoBuf, TFLite/LiteRT) is limited to Python 3.11–3.13. On Python 3.14, `birdnet` installs *without* TensorFlow and supports the **acoustic 3.0 model via the `onnx` and `pt` backends** only:

```sh
pip install birdnet[onnx] --user # or birdnet[pt]
```

```py
import birdnet

model = birdnet.load("acoustic", "3.0", "onnx") # 'pt' also works
predictions = model.predict("example/soundscape.wav")
```

The TensorFlow-only paths — the `tf`/`pb` backends, the acoustic 2.4 and Perch models, and **all geo models** — raise a clear error on Python 3.14 (the geo models are not currently distributed in ONNX/PyTorch form). To use them, install `birdnet` on Python 3.11–3.13. Full 3.14 support will follow once TensorFlow ships Python 3.14 wheels.

### Instructions

```sh
Expand Down
26 changes: 21 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "birdnet"
version = "0.2.16"
description = "A Python library for identifying bird species by their sounds."
readme = "README.md"
requires-python = ">=3.11, <3.14"
requires-python = ">=3.11"
license = "MIT"
license-files = ["LICENSE.md"]
authors = [{ name = "Stefan Taubert" }]
Expand All @@ -29,6 +29,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = [
"soundfile >= 0.11.0", # 2022/09/27
Expand All @@ -39,14 +40,18 @@ dependencies = [
"pandas >= 1.4.0", # 2022/02/22
"psutil >= 5.9.0", # 2021/12/29
"pyarrow >= 7.0.0", # 2022/02/03
"tensorflow >= 2.8.0 ; sys_platform != 'darwin' or platform_machine != 'x86_64'", # 2022/02/02
"tensorflow >= 2.8.0, <2.17 ; sys_platform == 'darwin' and platform_machine == 'x86_64'",
# TensorFlow has no wheels for Python 3.14+ yet, so it is only required on
# <=3.13. On Python 3.14 birdnet installs without TensorFlow; the acoustic 3.0
# model then runs via the optional 'onnx'/'pt' backends. TF/PB backends and all
# geo models raise a clear error there until TF ships cp314 wheels (issue #55).
"tensorflow >= 2.8.0 ; (sys_platform != 'darwin' or platform_machine != 'x86_64') and python_version < '3.14'", # 2022/02/02
"tensorflow >= 2.8.0, <2.17 ; sys_platform == 'darwin' and platform_machine == 'x86_64' and python_version < '3.14'",
"ai-edge-litert >= 2.0.2 ; python_version <= '3.13' and (sys_platform == 'linux' or (sys_platform == 'darwin' and platform_machine == 'arm64') or (sys_platform == 'win32' and platform_machine == 'AMD64'))", # 2026/05/12
]

[project.optional-dependencies]
and-cuda = [
"tensorflow[and-cuda] >= 2.8.0 ; sys_platform == 'linux'", # 2022/02/02
"tensorflow[and-cuda] >= 2.8.0 ; sys_platform == 'linux' and python_version < '3.14'", # 2022/02/02
]
pt = ["torch >= 2.0.0"]
onnx = ["onnxruntime >= 1.16.0"]
Expand Down Expand Up @@ -187,6 +192,7 @@ markers = [
"litert: tests requiring ai_edge_litert backend which can not be loaded after tf is imported (raises ImportError) which happens on parallel test runs",
"gpu: tests requiring a GPU to run and to be run sequentially",
"repro: tests requiring exact package versions to reproduce results",
"no_tf: tests for the TensorFlow-free surface, run when TF is absent (e.g. Python 3.14)",
]

[tool.ruff]
Expand Down Expand Up @@ -230,7 +236,7 @@ exclude_lines = ["if __name__ == .__main__.:"]
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py{311,312,313}, py312-repro
envlist = py{311,312,313,314}, py312-repro
isolated_build = True

[testenv]
Expand Down Expand Up @@ -268,6 +274,16 @@ commands =
pyproject-build -o dist/
python -m twine check dist/*

[testenv:py314]
# Python 3.14 has no TensorFlow wheels yet (issue #55). Install the TF-free
# surface (onnx backend) and exercise it plus the friendly errors on TF paths.
deps =
.[tests,onnx]
commands =
pytest -m "no_tf" -n auto
pyproject-build -o dist/
python -m twine check dist/*

[testenv:py312-coverage]
deps =
.[tests]
Expand Down
22 changes: 22 additions & 0 deletions src/birdnet/model_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,25 @@ def _validate_optional_backend_runtime(backend: MODEL_BACKENDS) -> None:
)


def _validate_tf_backend_runtime(backend: MODEL_BACKENDS) -> None:
"""Guard the TensorFlow-backed backends when TensorFlow is unavailable.

The 'tf' (TFLite/LiteRT) and 'pb' (SavedModel) backends both require
TensorFlow. TensorFlow does not yet ship wheels for Python 3.14+, so on such
interpreters birdnet installs without it. Fail early with an actionable
message instead of a bare ImportError deep in the backend (issue #55).
"""
if backend in (MODEL_BACKEND_TF, MODEL_BACKEND_PB) and not tf_installed():
raise ValueError(
f"Parameter 'backend': Backend '{backend}' requires TensorFlow, which is "
"not installed. TensorFlow currently provides no wheels for Python 3.14+; "
"use Python 3.11-3.13 to run TensorFlow-based models (all geo models and "
"the acoustic 2.4/Perch models are TensorFlow-only), or use the 'onnx' or "
"'pt' backend with the acoustic 3.0 model, e.g. "
"birdnet.load('acoustic', '3.0', 'onnx')."
)
Comment thread
Josef-Haupt marked this conversation as resolved.
Outdated


def _raise_unsupported_backend(
model_type: MODEL_TYPES,
version: str,
Expand Down Expand Up @@ -361,6 +380,7 @@ def load_perch_v2(device: str) -> AcousticModelPerchV2:
raise OSError("The Perch v2 model is not supported on Intel macOS systems.")

device = _validate_device(device)
_validate_tf_backend_runtime(MODEL_BACKEND_PB)
check_tf_version_for_perch_v2()
model_path, species_list = AcousticPBDownloaderPerchV2.get_model_path_and_labels(
device
Expand Down Expand Up @@ -389,6 +409,7 @@ def load(
) -> ModelBase:
model_type = _validate_model_type(model_type)
backend = _validate_backend(backend)
_validate_tf_backend_runtime(backend)
precision = _validate_precision(precision)

if model_type == MODEL_TYPE_ACOUSTIC:
Expand Down Expand Up @@ -658,6 +679,7 @@ def load_custom(
) -> ModelBase:
model_type = _validate_model_type(model_type)
backend = _validate_backend(backend)
_validate_tf_backend_runtime(backend)
model = _validate_path(model)
species_list = _validate_species_list_path(species_list)
precision = _validate_precision(precision)
Expand Down
65 changes: 65 additions & 0 deletions src/birdnet_tests/issues/test_issue55.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Issue #55: Python 3.14 support.

TensorFlow does not yet publish wheels for Python 3.14, so on that interpreter
birdnet installs *without* TensorFlow. This module documents and pins the
TensorFlow-free surface:

- ``import birdnet`` works without TensorFlow installed.
- The acoustic 3.0 model runs via the ``onnx`` (and ``pt``) backend.
- Every TensorFlow-only path (``tf``/``pb`` backends, all geo models, the
acoustic 2.4 and Perch models) fails with a clear, actionable ``ValueError``
instead of a bare ``ModuleNotFoundError``.

The tests only run when TensorFlow is absent (i.e. on Python 3.14); they are
skipped on 3.11-3.13 where TensorFlow is installed.
"""

import pytest

import birdnet
from birdnet.core.backends import onnxruntime_installed, tf_installed

pytestmark = [
pytest.mark.no_tf,
pytest.mark.skipif(
tf_installed(),
reason="TensorFlow-free surface; runs only when TF is absent (Python 3.14).",
),
]


def test_import_birdnet_without_tensorflow() -> None:
assert not tf_installed()
assert birdnet is not None


@pytest.mark.parametrize(
("model_type", "version", "backend"),
[
("acoustic", "2.4", "tf"),
("acoustic", "2.4", "pb"),
("acoustic", "3.0", "tf"),
("acoustic", "3.0", "pb"),
("geo", "2.4", "tf"),
("geo", "2.4", "pb"),
("geo", "3.0", "tf"),
("geo", "3.0", "pb"),
],
)
def test_tf_backends_raise_clear_error_without_tensorflow(
model_type: str, version: str, backend: str
) -> None:
with pytest.raises(ValueError, match="TensorFlow"):
birdnet.load(model_type, version, backend)


def test_perch_raises_clear_error_without_tensorflow() -> None:
with pytest.raises(ValueError, match="TensorFlow"):
birdnet.load_perch_v2("CPU")


@pytest.mark.skipif(not onnxruntime_installed(), reason="onnxruntime not installed")
def test_acoustic_v3_onnx_predicts_without_tensorflow() -> None:
model = birdnet.load("acoustic", "3.0", "onnx")
predictions = model.predict("example/soundscape.wav")
assert predictions is not None
Loading