Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
30 changes: 30 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Version control and CI
.git
.github

# Python build/cache artifacts
__pycache__
*.pyc
*.egg-info
build
dist

# Local environments
.venv*
venv

# Large local-only artifacts (untracked, not needed at runtime).
# The BirdNET model is downloaded by the `birdnet` dependency on first run,
# so bundled checkpoints are not required in the image.
installers
birdnet_analyzer/checkpoints

# Not needed inside the image
tests
docs
.pytest_cache
.ruff_cache
.vscode
.idea
Dockerfile
.dockerignore
161 changes: 125 additions & 36 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
@@ -1,48 +1,137 @@
name: Docker Build
name: Docker

on:
push:
branches: [ main ]
branches: [main]
paths:
- 'Dockerfile'
- '.github/workflows/docker-build.yml'
- "Dockerfile"
- ".dockerignore"
- ".github/workflows/docker-build.yml"
pull_request:
branches: [ main ]
branches: [main]
paths:
- 'Dockerfile'
- '.github/workflows/docker-build.yml'
- "Dockerfile"
- ".dockerignore"
- ".github/workflows/docker-build.yml"
release:
types: [published]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
REGISTRY: ghcr.io
# metadata-action lowercases this to ghcr.io/birdnet-team/birdnet-analyzer
IMAGE_NAME: ${{ github.repository }}

jobs:
build:
test:
name: Build and test image
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Build Docker image
run: docker build . --file Dockerfile --tag birdnet:local-test

# Optional: Add basic test to verify the image works
- name: Test Docker image
run: |
docker run -v $PWD/birdnet_analyzer/example:/audio birdnet:local-test -m birdnet_analyzer.analyze /audio --slist /audio

# Verify output file content
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)"
expected_first_line="1 Spectrogram 1 1 0.0 3.0 0 15000 Black-capped Chickadee bkcchi 0.8141 /audio/soundscape.wav 0.0"

actual_header=$(head -n 1 birdnet_analyzer/example/soundscape.BirdNET.selection.table.txt)
actual_first_line=$(head -n 2 birdnet_analyzer/example/soundscape.BirdNET.selection.table.txt | tail -n 1)

if [ "$actual_header" != "$expected_header" ] || [ "$actual_first_line" != "$expected_first_line" ]
then
echo "Output file content does not match expected content"
echo "Expected header: $expected_header"
echo "Actual header: $actual_header"
echo "Expected first line: $expected_first_line"
echo "Actual first line: $actual_first_line"
- name: Checkout repository
uses: actions/checkout@v6

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build image
uses: docker/build-push-action@v6
with:
context: .
load: true
tags: birdnet:test
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Smoke test image
run: |
# Run inference end-to-end: mount the example audio read-only and
# write results to a separate directory so nothing is polluted.
mkdir -p out
docker run --rm \
-v "$PWD/birdnet_analyzer/example:/audio:ro" \
-v "$PWD/out:/out" \
birdnet:test -m birdnet_analyzer.analyze /audio --slist /audio/species_list.txt -o /out

output_file="out/BirdNET_SelectionTable.txt"

# The container must produce a non-empty Raven selection table.
if [ ! -s "$output_file" ]; then
echo "::error::Expected output '$output_file' was not created or is empty"
ls -la out
exit 1
fi

# It must contain a header plus at least one detection row.
line_count=$(wc -l < "$output_file")
if [ "$line_count" -lt 2 ]; then
echo "::error::Output table has no detection rows ($line_count lines)"
cat "$output_file"
exit 1
else
echo "test received expected output file contents"
fi
fi

# Sanity-check the header exposes the expected Raven columns
# (structure only — exact model scores are covered by the pytest suite).
header=$(head -n 1 "$output_file")
for col in "Selection" "Begin Time (s)" "Confidence" "Begin Path"; do
case "$header" in
*"$col"*) ;;
*) echo "::error::Output header is missing column: $col"; echo "Header: $header"; exit 1 ;;
esac
done

echo "Docker image smoke test passed ($((line_count - 1)) detections)"

publish:
name: Publish to GitHub Container Registry
needs: test
if: github.event_name == 'release'
runs-on: ubuntu-latest

permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# A release tagged v2.4.0 produces the tags 2.4.0, 2.4 and latest
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}

Comment thread
Josef-Haupt marked this conversation as resolved.
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Avoid "unknown/unknown" attestation entries in the GHCR package UI
provenance: false
13 changes: 9 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
# Build from Python slim
FROM python:3.11
# Match the Python version used to build the release (see publish.yml / CI matrix)
FROM python:3.12-slim

# Install required packages while keeping the image small
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*

# Import all scripts
WORKDIR /app
COPY . ./

# Install required Python packages
RUN pip3 install --no-cache-dir .
# Install the package, then replace the default TensorFlow build with the
# CPU-only wheel. BirdNET-Analyzer runs inference on CPU (the birdnet library
# uses ai-edge-litert), so the GPU-enabled TensorFlow build is dead weight.
RUN pip3 install --no-cache-dir . \
&& pip3 uninstall -y tensorflow \
&& pip3 install --no-cache-dir "tensorflow-cpu>=2.20"

# Add entry point to run the script
ENTRYPOINT [ "python3" ]
Expand Down
29 changes: 28 additions & 1 deletion docs/usage/docker.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,31 @@
Docker
======

We are currently re-working our Docker setup. Please check back later for updates.
Official Docker images are published to the GitHub Container Registry with every release, for ``linux/amd64`` and ``linux/arm64``:

.. code-block:: bash

docker pull ghcr.io/birdnet-team/birdnet-analyzer:latest

Version tags follow the GitHub releases, so ``2.4.0`` and ``2.4`` point to that release while ``latest`` always points to the most recent one.

Usage
-----

The image runs the command line interface. Mount your audio data into the container and pass the usual command line arguments:

.. code-block:: bash

# Analyze audio files in the current directory
docker run --rm -v "$PWD:/audio" ghcr.io/birdnet-team/birdnet-analyzer -m birdnet_analyzer.analyze /audio -o /audio/output

Any of the CLI entry points can be used, e.g. ``-m birdnet_analyzer.species`` or ``-m birdnet_analyzer.segments``.

Building locally
----------------

.. code-block:: bash

git clone https://github.com/birdnet-team/BirdNET-Analyzer.git
cd BirdNET-Analyzer
docker build -t birdnet-analyzer .
Loading