AvianVisitors: BirdNET-Pi collage frontend overlay
Adds avian/ on top of upstream BirdNET-Pi: - frontend/ static collage UI (mask-packed bird tiles, sized by detection count) - assets/ ~450 bundled kachō-e illustrations + cutouts + binary masks - api/ PHP shims for recent/lifelist/firstseen/timeseries JSON - scripts/ installer + Gemini pregen + editable prompt template - caddy/ snippet mounting /collage on existing Caddy - forwarding/ optional HA / MQTT / Cloudflare configs Default install: http://birdnet.local/collage/ with no auth. Upstream BirdNET-Pi README preserved at README.upstream.md.
@@ -1,183 +1,122 @@
|
||||
<h1 align="center"><a href="https://github.com/mcguirepr89/BirdNET-Pi/blob/main/LICENSE">Review the license!!</a></h1>
|
||||
<h1 align="center">You may not use BirdNET-Pi to develop a commercial product!!!!</h1>
|
||||
<h1 align="center">
|
||||
BirdNET-Pi
|
||||
</h1>
|
||||
<p align="center">
|
||||
A realtime acoustic bird classification system for the Raspberry Pi 5, 4B, 400, 3B+, and 0W2
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/60325264/140656397-bf76bad4-f110-467c-897d-992ff0f96476.png" />
|
||||
</p>
|
||||
<p align="center">
|
||||
Icon made by <a href="https://www.freepik.com" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
|
||||
</p>
|
||||
# AvianVisitors
|
||||
|
||||
## About this fork:
|
||||
I've been building on [mcguirepr89's](https://github.com/mcguirepr89/BirdNET-Pi) most excellent work to further update and improve BirdNET-Pi. Maybe someone will find it useful.
|
||||
*A live bird collage from your apartment window.*
|
||||
|
||||
Changes include:
|
||||
A frontend overlay for [BirdNET-Pi](https://github.com/Nachtzuster/BirdNET-Pi). Listens to your balcony, identifies every passing bird with Cornell's [BirdNET](https://birdnet.cornell.edu/), and renders the detections as a tile-packed kachō-e print at `http://birdnet.local/collage/`. Full project writeup at [teddywarner.org/Projects/AvianVisitors](https://teddywarner.org/Projects/AvianVisitors/).
|
||||
|
||||
- Backup & Restore
|
||||
- Web ui is much more responsive
|
||||
- Daily charts now include all species, not just top/bottom 10
|
||||
- Bump apprise version, so more notification type are possible
|
||||
- Swipe events on Daily Charts (by @croisez)
|
||||
- Support for 'Species range model V2.4 - V2'
|
||||
- Bookworm and Trixie support
|
||||
- Experimental support for writing transient files to tmpfs
|
||||
- Rework analysis to consolidate analysis/server/extraction. Should make analysis more robust and slightly more efficient, especially on installations with a large number of recordings
|
||||
- Bump tflite_runtime to 2.17.1, it is faster
|
||||
- Rework daily_plot.py (chart_viewer) to run as a daemon to avoid the very expensive startup
|
||||
- Lots of fixes & cleanups
|
||||
<img alt="avianvisitors collage" src="docs/thumb.png" />
|
||||
|
||||
!! note: see 'Migrating' on how to migrate from mcguirepr89
|
||||
---
|
||||
|
||||
## Introduction
|
||||
BirdNET-Pi is built on the [BirdNET framework](https://github.com/kahst/BirdNET-Analyzer) by [**@kahst**](https://github.com/kahst) <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/"><img src="https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg"></a> using [pre-built TFLite binaries](https://github.com/PINTO0309/TensorflowLite-bin) by [**@PINTO0309**](https://github.com/PINTO0309) . It is able to recognize bird sounds from a USB microphone or sound card in realtime and share its data with the rest of the world.
|
||||
## BOM
|
||||
|
||||
Check out birds from around the world
|
||||
- [BirdWeather](https://app.birdweather.com)<br>
|
||||
| Qty | Description | Price | Link | Notes |
|
||||
|-----|-------------|-------|------|-------|
|
||||
| 1 | Raspberry Pi (4B / 5 / Zero 2W) | ~$35–80 | [Raspberry Pi](https://www.raspberrypi.com/products/) | 4B+ recommended |
|
||||
| 1 | Micro SD Card | ~$10 | [Amazon](https://a.co/d/08aiL8c) | ≥32 GB |
|
||||
| 1 | USB Lavalier Microphone | $14.99 | [Amazon](https://www.amazon.com/dp/B0176NRE1G) | The one I used. Any USB mic with a half-decent capsule works. |
|
||||
| 1 | Pi Power Supply | ~$10 | — | Matched to your Pi model |
|
||||
|
||||
## Features
|
||||
* **24/7 recording and automatic identification** of bird songs, chirps, and peeps using BirdNET machine learning
|
||||
* **Automatic extraction and cataloguing** of bird clips from full-length recordings
|
||||
* **Tools to visualize your recorded bird data** and analyze trends
|
||||
* **Live audio stream and spectrogram**
|
||||
* **Automatic disk space management** that periodically purges old audio files
|
||||
* [BirdWeather](https://app.birdweather.com) integration -- you can request a BirdWeather ID from BirdNET-Pi's "Tools" > "Settings" page
|
||||
* Web interface access to all data and logs provided by [Caddy](https://caddyserver.com)
|
||||
* [GoTTY](https://github.com/yudai/gotty) and [GoTTY x86](https://github.com/sorenisanerd/gotty) Web Terminal
|
||||
* [Tiny File Manager](https://tinyfilemanager.github.io/)
|
||||
* FTP server included
|
||||
* SQLite3 Database
|
||||
* [Adminer](https://www.adminer.org/) database maintenance
|
||||
* [phpSysInfo](https://github.com/phpsysinfo/phpsysinfo)
|
||||
* [Apprise Notifications](https://github.com/caronc/apprise) supporting 90+ notification platforms
|
||||
* Localization supported
|
||||
You'll also need a [Gemini API key](https://aistudio.google.com/apikey) (free tier is enough for an apartment-scale lifelist) and, optionally, an [eBird API key](https://ebird.org/api/keygen) for region-filtering the species pre-gen.
|
||||
|
||||
## Requirements
|
||||
* A Raspberry Pi 5, Raspberry 4B, Raspberry Pi 400, Raspberry Pi 3B+, or Raspberry Pi 0W2 (The 3B+ and 0W2 must run on RaspiOS-ARM64-**Lite**)
|
||||
* An SD Card with the **_64-bit version of RaspiOS_** installed (please use Trixie) -- Lite is recommended, but the installation works on RaspiOS-ARM64-Full as well. Downloads available within the [Raspberry Pi Imager](https://www.raspberrypi.com/software/).
|
||||
* A USB Microphone or Sound Card
|
||||
---
|
||||
|
||||
## Installation
|
||||
[A comprehensive installation guide is available here](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Installation-Guide). This guide is slightly out-dated: make sure to pick Bookworm, also the curl command is still pointing to mcguirepr89's repo.
|
||||
## 1. Install BirdNET-Pi
|
||||
|
||||
Please note that installing BirdNET-Pi on top of other servers is not supported. If this is something that you require, please open a discussion for your idea and inquire about how to contribute to development.
|
||||
Follow the [BirdNET-Pi installation guide](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Installation-Guide) on your Pi. Confirm the stock BirdNET-Pi UI is reachable at `http://birdnet.local/` before continuing.
|
||||
|
||||
[Raspberry Pi 3B[+] and 0W2 installation guide available here](https://github.com/mcguirepr89/BirdNET-Pi/wiki/RPi0W2-Installation-Guide)
|
||||
---
|
||||
|
||||
The system can be installed with:
|
||||
```
|
||||
curl -s https://raw.githubusercontent.com/Nachtzuster/BirdNET-Pi/main/newinstaller.sh | bash
|
||||
```
|
||||
The installer takes care of any and all necessary updates, so you can run that as the very first command upon the first boot, if you'd like.
|
||||
## 2. Clone AvianVisitors
|
||||
|
||||
The installation creates a log in `$HOME/installation-$(date "+%F").txt`.
|
||||
## Access
|
||||
The BirdNET-Pi can be accessed from any web browser on the same network:
|
||||
- http://birdnetpi.local OR your Pi's IP address
|
||||
- Default Basic Authentication Username: birdnet
|
||||
- Password is empty by default. Set this in "Tools" > "Settings" > "Advanced Settings"
|
||||
|
||||
Please take a look at the [wiki](https://github.com/mcguirepr89/BirdNET-Pi/wiki) and [discussions](https://github.com/mcguirepr89/BirdNET-Pi/discussions) for information on
|
||||
- [BirdNET-Pi's Deep Convolutional Neural Network(s)](https://github.com/mcguirepr89/BirdNET-Pi/wiki/BirdNET-Pi:-some-theory-on-classification-&-some-practical-hints)
|
||||
- [making your installation public](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Sharing-Your-BirdNET-Pi)
|
||||
- [backing up and restoring your database](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Backup-and-Restore-the-Database)
|
||||
- [adjusting your sound card settings](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Adjusting-your-sound-card)
|
||||
- [suggested USB microphones](https://github.com/mcguirepr89/BirdNET-Pi/discussions/39)
|
||||
- [building your own microphone](https://github.com/DD4WH/SASS/wiki/Stereo--(Mono)-recording-low-noise-low-cost-system)
|
||||
- [privacy concerns and options](https://github.com/mcguirepr89/BirdNET-Pi/discussions/166)
|
||||
- [beta testing](https://github.com/mcguirepr89/BirdNET-Pi/discussions/11)
|
||||
- [and more!](https://github.com/mcguirepr89/BirdNET-Pi/discussions)
|
||||
|
||||
|
||||
## Updating
|
||||
|
||||
Use the web interface and go to "Tools" > "System Controls" > "Update". If you encounter any issues with that, or suspect that the update did not work for some reason, please save its output and post it in an issue where we can help.
|
||||
|
||||
## Backup and Restore
|
||||
Use the web interface and go to "Tools" > "System Controls" > "Backup" or "Restore". Backup/Restore is primary meant for migrating your data for one system to another. Since the time required to create or restore a backup depends on the size of the data set and the speed of the storage, this could take quite a while.
|
||||
|
||||
Alternatively, the backup script can be used directly. These examples assume the backup medium is mounted on `/mnt`
|
||||
|
||||
To backup:
|
||||
```commandline
|
||||
./scripts/backup_data.sh -a backup -f /mnt/birds/backup-2024-07-09.tar
|
||||
```
|
||||
To restore:
|
||||
```commandline
|
||||
./scripts/backup_data.sh -a restore -f /mnt/birds/backup-2024-07-09.tar
|
||||
```bash
|
||||
ssh monalisa@birdnet.local
|
||||
git clone https://github.com/Twarner491/AvianVisitors.git ~/AvianVisitors
|
||||
cd ~/AvianVisitors
|
||||
```
|
||||
|
||||
## x86_64 support
|
||||
x86_64 support is mainly there for developers or otherwise more Linux savvy people.
|
||||
That being said, some pointers:
|
||||
- Use Debian 12 or 13
|
||||
- The user needs passwordless sudo
|
||||
---
|
||||
|
||||
For Proxmox, a user has reported adding this in their `cpu-models.conf`, in order for the custom TFLite build to work.
|
||||
```
|
||||
cpu-model: BirdNet
|
||||
flags +sse4.1
|
||||
reported-model host
|
||||
## 3. Pre-generate illustrations for your region (optional but recommended)
|
||||
|
||||
The repo ships with ~450 bundled illustrations. To restyle, regenerate, or add region-specific species, run the pregen script with your Gemini key.
|
||||
|
||||
```bash
|
||||
export GEMINI_API_KEY='your-gemini-key'
|
||||
|
||||
# Generate every species BirdNET-Pi knows:
|
||||
python3 avian/scripts/pregen.py --labels ~/BirdNET-Pi/model/labels.txt
|
||||
|
||||
# Or filter to species observed in your state/county via eBird:
|
||||
export EBIRD_API_KEY='your-ebird-key'
|
||||
python3 avian/scripts/pregen.py \
|
||||
--labels ~/BirdNET-Pi/model/labels.txt \
|
||||
--ebird-region US-CA # state, or US-CA-085 for a county
|
||||
```
|
||||
|
||||
## Uninstallation
|
||||
To change the art style, edit [`avian/scripts/prompt.template.md`](avian/scripts/prompt.template.md) and re-run with `--force`. The template has `{sci_name}`, `{com_name}`, and `{pose}` placeholders; swap the body for whatever style you want (woodblock, ink wash, scientific plate, etc.).
|
||||
|
||||
---
|
||||
|
||||
## 4. Install the frontend
|
||||
|
||||
One-shot installer — drops the static collage UI into `/var/www/avian`, the JSON shims into BirdNET-Pi's PHP root, and a Caddy snippet that mounts everything at `http://birdnet.local/collage/`.
|
||||
|
||||
```bash
|
||||
bash avian/scripts/install.sh
|
||||
```
|
||||
/usr/local/bin/uninstall.sh && cd ~ && rm -drf BirdNET-Pi
|
||||
|
||||
That's it. Open `http://birdnet.local/collage/` from any device on your network. As BirdNET-Pi accumulates detections, they fade into the collage — sized by how often each species has been heard.
|
||||
|
||||
---
|
||||
|
||||
## 5. Loading the Gemini key on the Pi (for live JIT generation)
|
||||
|
||||
The bundled illustrations cover most North-American species. For new birds the Pi hasn't seen before, AvianVisitors can render them on the fly. Drop the key into a systemd environment file so the JIT generator picks it up:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/avian
|
||||
echo "GEMINI_API_KEY=your-gemini-key" | sudo tee /etc/avian/env >/dev/null
|
||||
sudo chmod 600 /etc/avian/env
|
||||
sudo systemctl restart php8.2-fpm
|
||||
```
|
||||
## Migrating
|
||||
Before switching, make sure your installation is fully up-to-date. Also make sure to have a backup, that is also the only way to get back to the original BirdNET-Pi.
|
||||
Please note that upgrading your underlying OS to Bookworm is not going to work. Please stick to Bullseye. If you do want Bookworm, you need to start from a fresh install and copy back your data. (remember the backup!)
|
||||
|
||||
Run these commands to migrate to this repo:
|
||||
The PHP shim at `avian/api/cutout.php` reads `/etc/avian/env` and falls through to a Wikipedia photo if the key is missing — so you can run completely Gemini-free if you'd rather.
|
||||
|
||||
---
|
||||
|
||||
## 6. Optional: forward off your local network
|
||||
|
||||
For public access, Home Assistant integration, or MQTT fan-out, see [`avian/forwarding/`](avian/forwarding/). Each recipe is independent:
|
||||
|
||||
- **Cloudflare Tunnel** — public HTTPS URL, no port forwarding, optional Cloudflare Access for password protection.
|
||||
- **Home Assistant REST sensor** — surfaces the latest detection as a sensor.value for automations.
|
||||
- **MQTT bridge** — publishes every new detection to a broker as JSON.
|
||||
|
||||
---
|
||||
|
||||
## Repo layout
|
||||
|
||||
```
|
||||
git remote remove origin
|
||||
git remote add origin https://github.com/Nachtzuster/BirdNET-Pi.git
|
||||
./scripts/update_birdnet.sh
|
||||
avian/
|
||||
├── frontend/ # static HTML/JS/CSS for the collage
|
||||
├── assets/ # bundled illustrations + cutouts + masks
|
||||
├── api/ # PHP shims served by BirdNET-Pi's existing PHP-FPM
|
||||
├── scripts/ # installer + Gemini pregen + editable prompt
|
||||
├── caddy/ # snippet mounting /collage on existing Caddy
|
||||
└── forwarding/ # optional HA / MQTT / Cloudflare configs
|
||||
```
|
||||
## Troubleshooting and Ideas
|
||||
*Hint: A lot of weird problems can be solved by simply restarting the core services. Do this from the web interface "Tools" > "Services" > "Restart Core Services"
|
||||
Having trouble or have an idea? *Submit an issue for trouble* and a *discussion for ideas*. Please do *not* submit an issue as a discussion -- the issue tracker solicits information that is needed for anyone to help -- discussions are *not for issues*.
|
||||
|
||||
PLEASE search the repo for your issue before creating a new one. This repo has nothing to do with the validity of the detection results, so please do not start any issues around "False positives."
|
||||
Everything outside `avian/` is upstream BirdNET-Pi.
|
||||
|
||||
## Sharing
|
||||
Please join a Discussion!! and please join [BirdWeather!!](https://app.birdweather.com)
|
||||
I hope that if you find BirdNET-Pi has been worth your time, you will share your setup, results, customizations, etc. [HERE](https://github.com/mcguirepr89/BirdNET-Pi/discussions/69) and will consider [making your installation public](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Sharing-Your-BirdNET-Pi).
|
||||
---
|
||||
|
||||
## Homeassistant addon
|
||||
## License
|
||||
|
||||
BirdNET-Pi can also be run as a [Homeassistant](https://www.home-assistant.io/) addon through docker.
|
||||
For more information : https://github.com/alexbelgium/hassio-addons/blob/master/birdnet-pi/README.md
|
||||
CC-BY-NC-SA-4.0 (inherited from BirdNET-Pi). Non-commercial use only; share-alike on derivatives. See [`LICENSE`](LICENSE) and [`README.upstream.md`](README.upstream.md).
|
||||
|
||||
## Docker
|
||||
---
|
||||
|
||||
BirdNET-Pi can also be run as as a docker container.
|
||||
For more information : https://github.com/alexbelgium/hassio-addons/blob/master/birdnet-pi/README_standalone.md
|
||||
|
||||
## Cool Links
|
||||
|
||||
- [Marie Lelouche's <i>Out of Spaces</i>](https://www.lestanneries.fr/exposition/marie-lelouche-out-of-spaces/) using BirdNET-Pi in post-sculpture VR! [Press Kit](https://github.com/mcguirepr89/BirdNET-Pi-assets/blob/main/dp_out_of_spaces_marie_lelouche_digital_05_01_22.pdf)
|
||||
- [Research on noded BirdNET-Pi networks for farming](https://github.com/mcguirepr89/BirdNET-Pi-assets/blob/main/G23_Report_ModelBasedSysEngineering_FarmMarkBirdDetector_V1__Copy_.pdf)
|
||||
- [PixCams Build Guide](https://pixcams.com/building-a-birdnet-pi-real-time-acoustic-bird-id-station/)
|
||||
- [Core-Electronics](https://core-electronics.com.au/projects/bird-calls-raspberry-pi) Build Article
|
||||
- [RaspberryPi.com Blog Post](https://www.raspberrypi.com/news/classify-birds-acoustically-with-birdnet-pi/)
|
||||
- [MagPi Issue 119 Showcase Article](https://magpi.raspberrypi.com/issues/119/pdf)
|
||||
|
||||
|
||||
### Internationalization:
|
||||
The bird names are in English by default, but other localized versions are available thanks to the wonderful efforts of [@patlevin](https://github.com/patlevin) and Wikipedia. Use the web interface's "Tools" > "Settings" and select your "Database Language" to have the detections in your language.
|
||||
|
||||
[Internationalization](docs/translations.md)
|
||||
|
||||
|
||||
## Screenshots
|
||||

|
||||

|
||||
|
||||
|
||||
## :thinking:
|
||||
Are you a lucky ducky with a spare Raspberry Pi? [Try Folding@home!](https://foldingathome.org/)
|
||||
- [Fork this repository](https://github.com/Twarner491/AvianVisitors/fork)
|
||||
- [Watch this repo](https://github.com/Twarner491/AvianVisitors/subscription)
|
||||
- [Create issue](https://github.com/Twarner491/AvianVisitors/issues/new)
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
<h1 align="center"><a href="https://github.com/mcguirepr89/BirdNET-Pi/blob/main/LICENSE">Review the license!!</a></h1>
|
||||
<h1 align="center">You may not use BirdNET-Pi to develop a commercial product!!!!</h1>
|
||||
<h1 align="center">
|
||||
BirdNET-Pi
|
||||
</h1>
|
||||
<p align="center">
|
||||
A realtime acoustic bird classification system for the Raspberry Pi 5, 4B, 400, 3B+, and 0W2
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/60325264/140656397-bf76bad4-f110-467c-897d-992ff0f96476.png" />
|
||||
</p>
|
||||
<p align="center">
|
||||
Icon made by <a href="https://www.freepik.com" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
|
||||
</p>
|
||||
|
||||
## About this fork:
|
||||
I've been building on [mcguirepr89's](https://github.com/mcguirepr89/BirdNET-Pi) most excellent work to further update and improve BirdNET-Pi. Maybe someone will find it useful.
|
||||
|
||||
Changes include:
|
||||
|
||||
- Backup & Restore
|
||||
- Web ui is much more responsive
|
||||
- Daily charts now include all species, not just top/bottom 10
|
||||
- Bump apprise version, so more notification type are possible
|
||||
- Swipe events on Daily Charts (by @croisez)
|
||||
- Support for 'Species range model V2.4 - V2'
|
||||
- Bookworm and Trixie support
|
||||
- Experimental support for writing transient files to tmpfs
|
||||
- Rework analysis to consolidate analysis/server/extraction. Should make analysis more robust and slightly more efficient, especially on installations with a large number of recordings
|
||||
- Bump tflite_runtime to 2.17.1, it is faster
|
||||
- Rework daily_plot.py (chart_viewer) to run as a daemon to avoid the very expensive startup
|
||||
- Lots of fixes & cleanups
|
||||
|
||||
!! note: see 'Migrating' on how to migrate from mcguirepr89
|
||||
|
||||
## Introduction
|
||||
BirdNET-Pi is built on the [BirdNET framework](https://github.com/kahst/BirdNET-Analyzer) by [**@kahst**](https://github.com/kahst) <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/"><img src="https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg"></a> using [pre-built TFLite binaries](https://github.com/PINTO0309/TensorflowLite-bin) by [**@PINTO0309**](https://github.com/PINTO0309) . It is able to recognize bird sounds from a USB microphone or sound card in realtime and share its data with the rest of the world.
|
||||
|
||||
Check out birds from around the world
|
||||
- [BirdWeather](https://app.birdweather.com)<br>
|
||||
|
||||
## Features
|
||||
* **24/7 recording and automatic identification** of bird songs, chirps, and peeps using BirdNET machine learning
|
||||
* **Automatic extraction and cataloguing** of bird clips from full-length recordings
|
||||
* **Tools to visualize your recorded bird data** and analyze trends
|
||||
* **Live audio stream and spectrogram**
|
||||
* **Automatic disk space management** that periodically purges old audio files
|
||||
* [BirdWeather](https://app.birdweather.com) integration -- you can request a BirdWeather ID from BirdNET-Pi's "Tools" > "Settings" page
|
||||
* Web interface access to all data and logs provided by [Caddy](https://caddyserver.com)
|
||||
* [GoTTY](https://github.com/yudai/gotty) and [GoTTY x86](https://github.com/sorenisanerd/gotty) Web Terminal
|
||||
* [Tiny File Manager](https://tinyfilemanager.github.io/)
|
||||
* FTP server included
|
||||
* SQLite3 Database
|
||||
* [Adminer](https://www.adminer.org/) database maintenance
|
||||
* [phpSysInfo](https://github.com/phpsysinfo/phpsysinfo)
|
||||
* [Apprise Notifications](https://github.com/caronc/apprise) supporting 90+ notification platforms
|
||||
* Localization supported
|
||||
|
||||
## Requirements
|
||||
* A Raspberry Pi 5, Raspberry 4B, Raspberry Pi 400, Raspberry Pi 3B+, or Raspberry Pi 0W2 (The 3B+ and 0W2 must run on RaspiOS-ARM64-**Lite**)
|
||||
* An SD Card with the **_64-bit version of RaspiOS_** installed (please use Trixie) -- Lite is recommended, but the installation works on RaspiOS-ARM64-Full as well. Downloads available within the [Raspberry Pi Imager](https://www.raspberrypi.com/software/).
|
||||
* A USB Microphone or Sound Card
|
||||
|
||||
## Installation
|
||||
[A comprehensive installation guide is available here](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Installation-Guide). This guide is slightly out-dated: make sure to pick Bookworm, also the curl command is still pointing to mcguirepr89's repo.
|
||||
|
||||
Please note that installing BirdNET-Pi on top of other servers is not supported. If this is something that you require, please open a discussion for your idea and inquire about how to contribute to development.
|
||||
|
||||
[Raspberry Pi 3B[+] and 0W2 installation guide available here](https://github.com/mcguirepr89/BirdNET-Pi/wiki/RPi0W2-Installation-Guide)
|
||||
|
||||
The system can be installed with:
|
||||
```
|
||||
curl -s https://raw.githubusercontent.com/Nachtzuster/BirdNET-Pi/main/newinstaller.sh | bash
|
||||
```
|
||||
The installer takes care of any and all necessary updates, so you can run that as the very first command upon the first boot, if you'd like.
|
||||
|
||||
The installation creates a log in `$HOME/installation-$(date "+%F").txt`.
|
||||
## Access
|
||||
The BirdNET-Pi can be accessed from any web browser on the same network:
|
||||
- http://birdnetpi.local OR your Pi's IP address
|
||||
- Default Basic Authentication Username: birdnet
|
||||
- Password is empty by default. Set this in "Tools" > "Settings" > "Advanced Settings"
|
||||
|
||||
Please take a look at the [wiki](https://github.com/mcguirepr89/BirdNET-Pi/wiki) and [discussions](https://github.com/mcguirepr89/BirdNET-Pi/discussions) for information on
|
||||
- [BirdNET-Pi's Deep Convolutional Neural Network(s)](https://github.com/mcguirepr89/BirdNET-Pi/wiki/BirdNET-Pi:-some-theory-on-classification-&-some-practical-hints)
|
||||
- [making your installation public](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Sharing-Your-BirdNET-Pi)
|
||||
- [backing up and restoring your database](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Backup-and-Restore-the-Database)
|
||||
- [adjusting your sound card settings](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Adjusting-your-sound-card)
|
||||
- [suggested USB microphones](https://github.com/mcguirepr89/BirdNET-Pi/discussions/39)
|
||||
- [building your own microphone](https://github.com/DD4WH/SASS/wiki/Stereo--(Mono)-recording-low-noise-low-cost-system)
|
||||
- [privacy concerns and options](https://github.com/mcguirepr89/BirdNET-Pi/discussions/166)
|
||||
- [beta testing](https://github.com/mcguirepr89/BirdNET-Pi/discussions/11)
|
||||
- [and more!](https://github.com/mcguirepr89/BirdNET-Pi/discussions)
|
||||
|
||||
|
||||
## Updating
|
||||
|
||||
Use the web interface and go to "Tools" > "System Controls" > "Update". If you encounter any issues with that, or suspect that the update did not work for some reason, please save its output and post it in an issue where we can help.
|
||||
|
||||
## Backup and Restore
|
||||
Use the web interface and go to "Tools" > "System Controls" > "Backup" or "Restore". Backup/Restore is primary meant for migrating your data for one system to another. Since the time required to create or restore a backup depends on the size of the data set and the speed of the storage, this could take quite a while.
|
||||
|
||||
Alternatively, the backup script can be used directly. These examples assume the backup medium is mounted on `/mnt`
|
||||
|
||||
To backup:
|
||||
```commandline
|
||||
./scripts/backup_data.sh -a backup -f /mnt/birds/backup-2024-07-09.tar
|
||||
```
|
||||
To restore:
|
||||
```commandline
|
||||
./scripts/backup_data.sh -a restore -f /mnt/birds/backup-2024-07-09.tar
|
||||
```
|
||||
|
||||
## x86_64 support
|
||||
x86_64 support is mainly there for developers or otherwise more Linux savvy people.
|
||||
That being said, some pointers:
|
||||
- Use Debian 12 or 13
|
||||
- The user needs passwordless sudo
|
||||
|
||||
For Proxmox, a user has reported adding this in their `cpu-models.conf`, in order for the custom TFLite build to work.
|
||||
```
|
||||
cpu-model: BirdNet
|
||||
flags +sse4.1
|
||||
reported-model host
|
||||
```
|
||||
|
||||
## Uninstallation
|
||||
```
|
||||
/usr/local/bin/uninstall.sh && cd ~ && rm -drf BirdNET-Pi
|
||||
```
|
||||
## Migrating
|
||||
Before switching, make sure your installation is fully up-to-date. Also make sure to have a backup, that is also the only way to get back to the original BirdNET-Pi.
|
||||
Please note that upgrading your underlying OS to Bookworm is not going to work. Please stick to Bullseye. If you do want Bookworm, you need to start from a fresh install and copy back your data. (remember the backup!)
|
||||
|
||||
Run these commands to migrate to this repo:
|
||||
```
|
||||
git remote remove origin
|
||||
git remote add origin https://github.com/Nachtzuster/BirdNET-Pi.git
|
||||
./scripts/update_birdnet.sh
|
||||
```
|
||||
## Troubleshooting and Ideas
|
||||
*Hint: A lot of weird problems can be solved by simply restarting the core services. Do this from the web interface "Tools" > "Services" > "Restart Core Services"
|
||||
Having trouble or have an idea? *Submit an issue for trouble* and a *discussion for ideas*. Please do *not* submit an issue as a discussion -- the issue tracker solicits information that is needed for anyone to help -- discussions are *not for issues*.
|
||||
|
||||
PLEASE search the repo for your issue before creating a new one. This repo has nothing to do with the validity of the detection results, so please do not start any issues around "False positives."
|
||||
|
||||
## Sharing
|
||||
Please join a Discussion!! and please join [BirdWeather!!](https://app.birdweather.com)
|
||||
I hope that if you find BirdNET-Pi has been worth your time, you will share your setup, results, customizations, etc. [HERE](https://github.com/mcguirepr89/BirdNET-Pi/discussions/69) and will consider [making your installation public](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Sharing-Your-BirdNET-Pi).
|
||||
|
||||
## Homeassistant addon
|
||||
|
||||
BirdNET-Pi can also be run as a [Homeassistant](https://www.home-assistant.io/) addon through docker.
|
||||
For more information : https://github.com/alexbelgium/hassio-addons/blob/master/birdnet-pi/README.md
|
||||
|
||||
## Docker
|
||||
|
||||
BirdNET-Pi can also be run as as a docker container.
|
||||
For more information : https://github.com/alexbelgium/hassio-addons/blob/master/birdnet-pi/README_standalone.md
|
||||
|
||||
## Cool Links
|
||||
|
||||
- [Marie Lelouche's <i>Out of Spaces</i>](https://www.lestanneries.fr/exposition/marie-lelouche-out-of-spaces/) using BirdNET-Pi in post-sculpture VR! [Press Kit](https://github.com/mcguirepr89/BirdNET-Pi-assets/blob/main/dp_out_of_spaces_marie_lelouche_digital_05_01_22.pdf)
|
||||
- [Research on noded BirdNET-Pi networks for farming](https://github.com/mcguirepr89/BirdNET-Pi-assets/blob/main/G23_Report_ModelBasedSysEngineering_FarmMarkBirdDetector_V1__Copy_.pdf)
|
||||
- [PixCams Build Guide](https://pixcams.com/building-a-birdnet-pi-real-time-acoustic-bird-id-station/)
|
||||
- [Core-Electronics](https://core-electronics.com.au/projects/bird-calls-raspberry-pi) Build Article
|
||||
- [RaspberryPi.com Blog Post](https://www.raspberrypi.com/news/classify-birds-acoustically-with-birdnet-pi/)
|
||||
- [MagPi Issue 119 Showcase Article](https://magpi.raspberrypi.com/issues/119/pdf)
|
||||
|
||||
|
||||
### Internationalization:
|
||||
The bird names are in English by default, but other localized versions are available thanks to the wonderful efforts of [@patlevin](https://github.com/patlevin) and Wikipedia. Use the web interface's "Tools" > "Settings" and select your "Database Language" to have the detections in your language.
|
||||
|
||||
[Internationalization](docs/translations.md)
|
||||
|
||||
|
||||
## Screenshots
|
||||

|
||||

|
||||
|
||||
|
||||
## :thinking:
|
||||
Are you a lucky ducky with a spare Raspberry Pi? [Try Folding@home!](https://foldingathome.org/)
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
// /home/monalisa/BirdSongs/Extracted/api.php — JSON facade over BirdNET-Pi's
|
||||
// birds.db, queryable by the bird.onethreenine.net Cloudflare Worker.
|
||||
//
|
||||
// Lives in the Caddy file-server root (NOT in BirdNET-Pi/scripts/) so the
|
||||
// Sunday auto-update doesn't clobber it. Reachable as /api.php on the Pi.
|
||||
//
|
||||
// Auth: callers must send X-BirdNET-Proxy-Token matching the Caddy gate set
|
||||
// up earlier — Caddy 403s anything missing it before this script runs, so we
|
||||
// inherit that protection for free. The Cloudflare Worker is the only thing
|
||||
// that ever sets the header.
|
||||
//
|
||||
// Endpoints (?action=...):
|
||||
// stats — totals: detections, unique species, today, last hour, etc.
|
||||
// lifelist — every species with first_seen, last_seen, total_count
|
||||
// recent — &hours=N (default 24): every detection in the window
|
||||
// species — &sci=<sci_name>: detail page for one species
|
||||
|
||||
declare(strict_types=1);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: public, max-age=30');
|
||||
|
||||
$DB_PATH = '/home/monalisa/BirdNET-Pi/scripts/birds.db';
|
||||
|
||||
if (!file_exists($DB_PATH)) {
|
||||
http_response_code(503);
|
||||
echo json_encode(['error' => 'birds.db not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = new SQLite3($DB_PATH, SQLITE3_OPEN_READONLY);
|
||||
$db->busyTimeout(2000);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'db open failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
function rows(SQLite3 $db, string $sql, array $bind = []): array {
|
||||
$stmt = $db->prepare($sql);
|
||||
foreach ($bind as $k => $v) $stmt->bindValue($k, $v);
|
||||
$res = $stmt->execute();
|
||||
$out = [];
|
||||
while ($r = $res->fetchArray(SQLITE3_ASSOC)) $out[] = $r;
|
||||
return $out;
|
||||
}
|
||||
function one(SQLite3 $db, string $sql, array $bind = []) {
|
||||
$r = rows($db, $sql, $bind);
|
||||
return $r[0] ?? null;
|
||||
}
|
||||
|
||||
$action = $_GET['action'] ?? 'stats';
|
||||
|
||||
switch ($action) {
|
||||
|
||||
case 'stats': {
|
||||
$total = (int)(one($db, 'SELECT COUNT(*) AS n FROM detections')['n'] ?? 0);
|
||||
$species = (int)(one($db, 'SELECT COUNT(DISTINCT Sci_Name) AS n FROM detections')['n'] ?? 0);
|
||||
$today = (int)(one($db, "SELECT COUNT(*) AS n FROM detections WHERE Date = DATE('now','localtime')")['n'] ?? 0);
|
||||
$todaySpec = (int)(one($db, "SELECT COUNT(DISTINCT Sci_Name) AS n FROM detections WHERE Date = DATE('now','localtime')")['n'] ?? 0);
|
||||
$lastHour = (int)(one($db, "SELECT COUNT(*) AS n FROM detections WHERE Date = DATE('now','localtime') AND Time >= TIME('now','localtime','-1 hour')")['n'] ?? 0);
|
||||
$week = (int)(one($db, "SELECT COUNT(*) AS n FROM detections WHERE Date >= DATE('now','localtime','-7 day')")['n'] ?? 0);
|
||||
$weekSpec = (int)(one($db, "SELECT COUNT(DISTINCT Sci_Name) AS n FROM detections WHERE Date >= DATE('now','localtime','-7 day')")['n'] ?? 0);
|
||||
$first = one($db, 'SELECT MIN(Date) AS d FROM detections');
|
||||
echo json_encode([
|
||||
'totals' => ['detections' => $total, 'species' => $species],
|
||||
'today' => ['detections' => $today, 'species' => $todaySpec],
|
||||
'last_hour' => ['detections' => $lastHour],
|
||||
'week' => ['detections' => $week, 'species' => $weekSpec],
|
||||
'started' => $first['d'] ?? null,
|
||||
'as_of' => date('c'),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'lifelist': {
|
||||
$rs = rows($db,
|
||||
"SELECT Sci_Name AS sci, Com_Name AS com, MIN(Date||' '||Time) AS first_seen, "
|
||||
. " MAX(Date||' '||Time) AS last_seen, COUNT(*) AS total, MAX(Confidence) AS best_conf "
|
||||
. "FROM detections GROUP BY Sci_Name ORDER BY first_seen ASC"
|
||||
);
|
||||
echo json_encode(['species' => $rs, 'as_of' => date('c')]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'recent': {
|
||||
// Cap raised to 1,000,000 hours (~114 years) so the frontend's
|
||||
// "ALL" button can effectively turn off the time filter without
|
||||
// needing a separate code path.
|
||||
$hours = max(1, min(1000000, (int)($_GET['hours'] ?? 24)));
|
||||
// species-collapsed view: one row per species seen in the window,
|
||||
// with the file of its highest-confidence detection inside the window.
|
||||
$rs = rows($db,
|
||||
"SELECT Sci_Name AS sci, Com_Name AS com, COUNT(*) AS n, MAX(Confidence) AS best_conf, "
|
||||
. " MAX(Date||' '||Time) AS last_seen "
|
||||
. "FROM detections "
|
||||
. "WHERE (julianday('now','localtime') - julianday(Date||' '||Time)) * 24 <= :hrs "
|
||||
. "GROUP BY Sci_Name ORDER BY last_seen DESC",
|
||||
[':hrs' => $hours]
|
||||
);
|
||||
// for each row, attach the file of the top-confidence detection in the window
|
||||
foreach ($rs as &$r) {
|
||||
$best = one($db,
|
||||
"SELECT File_Name AS file, Date AS d, Time AS t, Confidence AS conf "
|
||||
. "FROM detections "
|
||||
. "WHERE Sci_Name = :sn "
|
||||
. "AND (julianday('now','localtime') - julianday(Date||' '||Time)) * 24 <= :hrs "
|
||||
. "ORDER BY Confidence DESC LIMIT 1",
|
||||
[':sn' => $r['sci'], ':hrs' => $hours]
|
||||
);
|
||||
$r['top_file'] = $best['file'] ?? null;
|
||||
$r['top_at'] = isset($best['d']) ? ($best['d'].' '.$best['t']) : null;
|
||||
}
|
||||
echo json_encode(['hours' => $hours, 'species' => $rs, 'as_of' => date('c')]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'species': {
|
||||
$sci = $_GET['sci'] ?? '';
|
||||
if ($sci === '') { http_response_code(400); echo json_encode(['error' => 'sci= required']); break; }
|
||||
$detections = rows($db,
|
||||
"SELECT Date AS d, Time AS t, File_Name AS file, Confidence AS conf "
|
||||
. "FROM detections WHERE Sci_Name = :sn ORDER BY Date DESC, Time DESC LIMIT 500",
|
||||
[':sn' => $sci]
|
||||
);
|
||||
$summary = one($db,
|
||||
"SELECT Com_Name AS com, COUNT(*) AS total, MIN(Date||' '||Time) AS first_seen, "
|
||||
. " MAX(Date||' '||Time) AS last_seen, MAX(Confidence) AS best_conf "
|
||||
. "FROM detections WHERE Sci_Name = :sn",
|
||||
[':sn' => $sci]
|
||||
);
|
||||
echo json_encode(['sci' => $sci, 'summary' => $summary, 'detections' => $detections]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'timeseries': {
|
||||
// Aggregated time-bucketed counts for the stats charts.
|
||||
// daily — last $days days, detections + unique species per day
|
||||
// by_hour — detections grouped by hour of day, last 30 days
|
||||
// The frontend backfills missing dates with zero — sparse data days
|
||||
// are otherwise dropped by the GROUP BY.
|
||||
$days = max(1, min(90, (int)($_GET['days'] ?? 30)));
|
||||
$daily = rows($db,
|
||||
"SELECT Date AS date, COUNT(*) AS detections, COUNT(DISTINCT Sci_Name) AS species "
|
||||
. "FROM detections "
|
||||
. "WHERE Date >= DATE('now','localtime','-".($days - 1)." day') "
|
||||
. "GROUP BY Date ORDER BY Date"
|
||||
);
|
||||
$by_hour = rows($db,
|
||||
"SELECT CAST(strftime('%H', Time) AS INT) AS hour, COUNT(*) AS detections "
|
||||
. "FROM detections "
|
||||
. "WHERE Date >= DATE('now','localtime','-30 day') "
|
||||
. "GROUP BY hour ORDER BY hour"
|
||||
);
|
||||
echo json_encode([
|
||||
'days' => $days,
|
||||
'daily' => $daily,
|
||||
'by_hour' => $by_hour,
|
||||
'as_of' => date('c'),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'firstseen': {
|
||||
// Most recent additions to the life list — first detection per
|
||||
// species, sorted by first_seen DESC. Powers the "First Detections"
|
||||
// section on the stats view.
|
||||
$limit = max(1, min(50, (int)($_GET['limit'] ?? 10)));
|
||||
$rs = rows($db,
|
||||
"SELECT Sci_Name AS sci, Com_Name AS com, MIN(Date||' '||Time) AS first_seen, "
|
||||
. " COUNT(*) AS total "
|
||||
. "FROM detections GROUP BY Sci_Name ORDER BY first_seen DESC LIMIT :lim",
|
||||
[':lim' => $limit]
|
||||
);
|
||||
echo json_encode(['species' => $rs, 'as_of' => date('c')]);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'unknown action']);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
// /home/monalisa/BirdSongs/Extracted/cutout.php — dynamic background-removed
|
||||
// bird-photo facade. Called by the bird.onethreenine.net Cloudflare Worker
|
||||
// (which fronts /api/img?sci=<name>).
|
||||
//
|
||||
// Lives in the Caddy file-server root so the BirdNET-Pi auto-update doesn't
|
||||
// touch it. Each new species hits this once: download Wikipedia/Macaulay
|
||||
// image → rembg via /usr/local/bin/rembg-cli → cache transparent PNG to
|
||||
// disk under cutouts/. Subsequent requests are instant readfile() from cache.
|
||||
//
|
||||
// Auth: the Caddy site block 403s anything missing X-BirdNET-Proxy-Token at
|
||||
// the top, so this script inherits the Worker-only access guarantee.
|
||||
//
|
||||
// Pre-reqs (run once via /install/rembg-setup.sh):
|
||||
// - /home/monalisa/rembg-env/bin/rembg (Python venv)
|
||||
// - /usr/local/bin/rembg-cli (wrapper)
|
||||
// - /home/monalisa/BirdSongs/Extracted/cutouts/ (cache dir, monalisa-owned)
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$sci = trim((string)($_GET['sci'] ?? ''));
|
||||
if ($sci === '') {
|
||||
http_response_code(400);
|
||||
echo 'sci required';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Slugify scientific name for the cache filename.
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '-', strtolower($sci));
|
||||
$slug = trim((string)$slug, '-');
|
||||
$cacheDir = '/home/monalisa/BirdSongs/Extracted/cutouts';
|
||||
$cachePath = "$cacheDir/$slug.png";
|
||||
|
||||
function serve_png(string $path): void {
|
||||
header('Content-Type: image/png');
|
||||
header('Cache-Control: public, max-age=2592000');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
readfile($path);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cache hit — short-circuit.
|
||||
if (is_file($cachePath) && filesize($cachePath) > 1024) {
|
||||
serve_png($cachePath);
|
||||
}
|
||||
|
||||
// Make sure the cache dir exists (first run).
|
||||
if (!is_dir($cacheDir)) @mkdir($cacheDir, 0755, true);
|
||||
|
||||
// Resolve a source image URL. Wikipedia summary first (free, fast,
|
||||
// no auth, has a clean originalimage for most birds).
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'header' => "User-Agent: apartment-birds/1.0 (twarner491@gmail.com)\r\n",
|
||||
'timeout' => 12,
|
||||
],
|
||||
]);
|
||||
$wpUrl = 'https://en.wikipedia.org/api/rest_v1/page/summary/' . rawurlencode($sci);
|
||||
$wpJson = @file_get_contents($wpUrl, false, $ctx);
|
||||
$srcUrl = null;
|
||||
if ($wpJson !== false) {
|
||||
$j = json_decode($wpJson, true);
|
||||
$srcUrl = $j['originalimage']['source'] ?? $j['thumbnail']['source'] ?? null;
|
||||
}
|
||||
if (!$srcUrl) {
|
||||
http_response_code(404);
|
||||
echo 'no upstream image for ' . htmlspecialchars($sci);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Download the source image.
|
||||
$imgBytes = @file_get_contents($srcUrl, false, $ctx);
|
||||
if (!$imgBytes || strlen($imgBytes) < 1024) {
|
||||
http_response_code(503);
|
||||
echo 'failed to fetch source image';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Run rembg via the wrapper. We use temp files because rembg's CLI prefers
|
||||
// real paths. u2netp is the lightweight model (~50MB peak RAM) — important
|
||||
// on the Pi 3B+ (1GB total RAM). Call with --post-process-mask to clean
|
||||
// up edges.
|
||||
$tmpIn = tempnam('/tmp', 'rembg-in-') . '.jpg';
|
||||
$tmpOut = tempnam('/tmp', 'rembg-out-') . '.png';
|
||||
file_put_contents($tmpIn, $imgBytes);
|
||||
|
||||
$cmd = sprintf(
|
||||
'/usr/local/bin/rembg-cli i -m u2netp -ppm %s %s 2>&1',
|
||||
escapeshellarg($tmpIn),
|
||||
escapeshellarg($tmpOut)
|
||||
);
|
||||
$out = shell_exec($cmd);
|
||||
@unlink($tmpIn);
|
||||
|
||||
if (!is_file($tmpOut) || filesize($tmpOut) < 1024) {
|
||||
@unlink($tmpOut);
|
||||
http_response_code(500);
|
||||
header('Content-Type: text/plain');
|
||||
echo "rembg failed:\n" . ($out ?? '(no output)');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 1. Alpha-crop to bounding box so each PNG == the bird's actual shape
|
||||
// (no transparent padding around it). Lets the layout pack tiles tight.
|
||||
// 2. Resize down to a max edge of 800px so the cache stays small.
|
||||
$im = @imagecreatefrompng($tmpOut);
|
||||
if ($im !== false) {
|
||||
$cropped = @imagecropauto($im, IMG_CROP_TRANSPARENT);
|
||||
if ($cropped !== false) {
|
||||
imagedestroy($im);
|
||||
$im = $cropped;
|
||||
}
|
||||
$w = imagesx($im); $h = imagesy($im);
|
||||
$max = 800;
|
||||
if ($w > $max || $h > $max) {
|
||||
$scale = $max / max($w, $h);
|
||||
$nw = (int)($w * $scale); $nh = (int)($h * $scale);
|
||||
$resized = imagecreatetruecolor($nw, $nh);
|
||||
imagealphablending($resized, false);
|
||||
imagesavealpha($resized, true);
|
||||
imagecopyresampled($resized, $im, 0, 0, 0, 0, $nw, $nh, $w, $h);
|
||||
imagedestroy($im);
|
||||
$im = $resized;
|
||||
}
|
||||
imagealphablending($im, false);
|
||||
imagesavealpha($im, true);
|
||||
imagepng($im, $tmpOut, 6);
|
||||
imagedestroy($im);
|
||||
}
|
||||
|
||||
// Cache + serve.
|
||||
copy($tmpOut, $cachePath);
|
||||
@unlink($tmpOut);
|
||||
serve_png($cachePath);
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
// /home/monalisa/BirdSongs/Extracted/recording.php — serves the most-recent
|
||||
// detection mp3 for a given scientific name. Called by the Cloudflare Worker
|
||||
// at /api/recording?sci=<name>.
|
||||
//
|
||||
// BirdNET-Pi writes audio + spectrograms to
|
||||
// ~/BirdSongs/Extracted/By_Date/YYYY-MM-DD/<Common_Name>/<base>.mp3
|
||||
// (with a matching .png next to it). Common_Name is the SPACE-stripped
|
||||
// English common name (e.g. "Anna's_Hummingbird"), NOT the scientific name.
|
||||
// We resolve sci → common via the same lookup the Pi's web UI uses
|
||||
// (birds.json under /scripts/) and then walk the directory tree newest-first.
|
||||
//
|
||||
// Auth: the Caddy site block 403s anything missing X-BirdNET-Proxy-Token.
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$sci = trim((string)($_GET['sci'] ?? ''));
|
||||
$file = trim((string)($_GET['file'] ?? ''));
|
||||
|
||||
if ($sci === '' && $file === '') {
|
||||
http_response_code(400);
|
||||
echo 'sci or file required';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Reject any sci-name that isn't a clean Genus species[ subspecies[ tri]]
|
||||
// pattern. resolve_common() below falls back to str_replace(' ', '_', $sci)
|
||||
// when there's no birds.json match — without this guard, `?sci=../etc` would
|
||||
// flow through unmodified into a filesystem path.
|
||||
if ($sci !== '' && !preg_match('/^[A-Za-z]{2,40}(?:[ ][a-z]{2,40}){1,3}$/', $sci)) {
|
||||
http_response_code(400);
|
||||
echo 'invalid sci';
|
||||
exit;
|
||||
}
|
||||
|
||||
$BY_DATE = '/home/monalisa/BirdSongs/Extracted/By_Date';
|
||||
|
||||
// ---- Direct-by-file lookup ----
|
||||
// Used by the atlas detail modal to play any past recording.
|
||||
// Filename schema (BirdNET-Pi):
|
||||
// <Common_Name>-<conf>-<YYYY-MM-DD>-birdnet-<HH-MM-SS>.mp3
|
||||
// We pull the date out of the filename to locate the species
|
||||
// directory under By_Date/. Whitelisted character set keeps this
|
||||
// safe against path-traversal payloads.
|
||||
if ($file !== '') {
|
||||
if (!preg_match('/^[A-Za-z0-9_.:-]+\.mp3$/', $file)) {
|
||||
http_response_code(400);
|
||||
echo 'invalid file name';
|
||||
exit;
|
||||
}
|
||||
// Extract the YYYY-MM-DD from the filename if present.
|
||||
$date = null;
|
||||
if (preg_match('/(\d{4}-\d{2}-\d{2})/', $file, $m)) $date = $m[1];
|
||||
// The species directory is the prefix before the first dash-digit.
|
||||
// Try a couple of strategies: explicit species_dir param, dir from file
|
||||
// prefix, then a broader recursive search as fallback.
|
||||
$candidates = [];
|
||||
if ($date) {
|
||||
// Look in that specific date dir across every species subdir.
|
||||
$dayDir = "$BY_DATE/$date";
|
||||
if (is_dir($dayDir)) {
|
||||
foreach (scandir($dayDir) as $sub) {
|
||||
if ($sub[0] === '.') continue;
|
||||
$p = "$dayDir/$sub/$file";
|
||||
if (is_file($p)) { $candidates[] = $p; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$candidates) {
|
||||
// Fall back to scanning every date dir (slower).
|
||||
if (is_dir($BY_DATE)) {
|
||||
foreach (scandir($BY_DATE) as $d) {
|
||||
if ($d[0] === '.') continue;
|
||||
$dayDir = "$BY_DATE/$d";
|
||||
if (!is_dir($dayDir)) continue;
|
||||
foreach (scandir($dayDir) as $sub) {
|
||||
if ($sub[0] === '.') continue;
|
||||
$p = "$dayDir/$sub/$file";
|
||||
if (is_file($p)) { $candidates[] = $p; break 2; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$candidates || filesize($candidates[0]) < 64) {
|
||||
http_response_code(404);
|
||||
echo 'recording not found';
|
||||
exit;
|
||||
}
|
||||
$path = $candidates[0];
|
||||
header('Content-Type: audio/mpeg');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('Cache-Control: public, max-age=86400');
|
||||
header('Accept-Ranges: bytes');
|
||||
readfile($path);
|
||||
exit;
|
||||
}
|
||||
$BIRDS_JSON_CANDIDATES = [
|
||||
'/home/monalisa/BirdNET-Pi/scripts/birds.json',
|
||||
'/home/monalisa/BirdNET-Pi/model/labels.txt',
|
||||
];
|
||||
|
||||
// ---- Resolve scientific name → common name (with underscores) ----
|
||||
function resolve_common(string $sci): ?string {
|
||||
// Try birds.json first (preferred — has clean sci/com pairs).
|
||||
foreach (['/home/monalisa/BirdNET-Pi/scripts/birds.json'] as $f) {
|
||||
if (is_readable($f)) {
|
||||
$list = json_decode((string)file_get_contents($f), true);
|
||||
if (is_array($list)) {
|
||||
foreach ($list as $row) {
|
||||
if (!is_array($row)) continue;
|
||||
$rowSci = $row['sci'] ?? $row['scientific'] ?? $row['scientificName'] ?? '';
|
||||
$rowCom = $row['com'] ?? $row['common'] ?? $row['commonName'] ?? '';
|
||||
if (strcasecmp(trim((string)$rowSci), $sci) === 0 && $rowCom) {
|
||||
return str_replace(' ', '_', (string)$rowCom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: labels.txt has "<sci>_<com>" or "<sci>, <com>" per line.
|
||||
$labels = '/home/monalisa/BirdNET-Pi/model/labels.txt';
|
||||
if (is_readable($labels)) {
|
||||
foreach (file($labels, FILE_IGNORE_NEW_LINES) as $line) {
|
||||
if (strpos($line, '_') !== false) {
|
||||
[$s, $c] = explode('_', $line, 2);
|
||||
if (strcasecmp(trim($s), $sci) === 0) {
|
||||
return str_replace(' ', '_', trim($c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$common = resolve_common($sci);
|
||||
if ($common === null) {
|
||||
// Last-ditch: try the scientific name itself, with spaces → underscores.
|
||||
// (Some BirdNET dirs are keyed by sci name.)
|
||||
$common = str_replace(' ', '_', $sci);
|
||||
}
|
||||
|
||||
// ---- Find newest matching file ----
|
||||
// Walk By_Date/* newest-first; inside each date dir, look for a
|
||||
// subdirectory named exactly $common, return the newest .mp3 inside.
|
||||
function newest_recording(string $rootDir, string $common): ?string {
|
||||
if (!is_dir($rootDir)) return null;
|
||||
$dates = scandir($rootDir, SCANDIR_SORT_DESCENDING);
|
||||
if (!$dates) return null;
|
||||
foreach ($dates as $date) {
|
||||
if ($date[0] === '.') continue;
|
||||
$speciesDir = "$rootDir/$date/$common";
|
||||
if (!is_dir($speciesDir)) continue;
|
||||
$files = scandir($speciesDir, SCANDIR_SORT_DESCENDING);
|
||||
if (!$files) continue;
|
||||
foreach ($files as $f) {
|
||||
if (substr($f, -4) === '.mp3') {
|
||||
return "$speciesDir/$f";
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = newest_recording($BY_DATE, $common);
|
||||
if ($path === null || !is_file($path) || filesize($path) < 64) {
|
||||
http_response_code(404);
|
||||
echo 'no recording for ' . htmlspecialchars($sci);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ---- Serve ----
|
||||
header('Content-Type: audio/mpeg');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('Cache-Control: public, max-age=60');
|
||||
header('Accept-Ranges: bytes');
|
||||
readfile($path);
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
// /home/monalisa/BirdSongs/Extracted/spectrogram.php — serves the
|
||||
// spectrogram PNG that BirdNET-Pi generates alongside each detection mp3.
|
||||
// Same lookup logic as recording.php (find the matching file under
|
||||
// By_Date/<date>/<Common_Name>/) — just .png instead of .mp3.
|
||||
//
|
||||
// Endpoints:
|
||||
// ?sci=<sci_name> → newest spectrogram for that species
|
||||
// ?file=<original-base>.mp3 → spectrogram next to that specific
|
||||
// recording (atlas modal uses this so the
|
||||
// strip below each play button is the
|
||||
// spectrogram for that recording, not
|
||||
// "the most recent" — they can differ).
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$sci = trim((string)($_GET['sci'] ?? ''));
|
||||
$file = trim((string)($_GET['file'] ?? ''));
|
||||
|
||||
if ($sci === '' && $file === '') {
|
||||
http_response_code(400);
|
||||
echo 'sci or file required';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Reject any sci-name that isn't a clean Genus species[ subspecies[ tri]]
|
||||
// pattern. Same defence-in-depth check as recording.php.
|
||||
if ($sci !== '' && !preg_match('/^[A-Za-z]{2,40}(?:[ ][a-z]{2,40}){1,3}$/', $sci)) {
|
||||
http_response_code(400);
|
||||
echo 'invalid sci';
|
||||
exit;
|
||||
}
|
||||
|
||||
$BY_DATE = '/home/monalisa/BirdSongs/Extracted/By_Date';
|
||||
|
||||
// ---- Direct-by-file lookup ----
|
||||
// BirdNET-Pi writes <base>.mp3 and <base>.png next to each other under
|
||||
// By_Date/<date>/<Common_Name>/. So we accept the mp3 filename, swap
|
||||
// the extension, and search the same way recording.php does.
|
||||
if ($file !== '') {
|
||||
if (!preg_match('/^[A-Za-z0-9_.:-]+\.(mp3|png)$/', $file)) {
|
||||
http_response_code(400);
|
||||
echo 'invalid file name';
|
||||
exit;
|
||||
}
|
||||
// BirdNET-Pi names the spectrogram as the FULL mp3 filename plus
|
||||
// ".png" — e.g. "American_Crow-82-…-20:25:29.mp3" pairs with
|
||||
// "American_Crow-82-…-20:25:29.mp3.png" (not "…-20:25:29.png").
|
||||
// Accept either form gracefully.
|
||||
if (substr($file, -4) === '.png') {
|
||||
$png = $file;
|
||||
} else {
|
||||
$png = $file . '.png';
|
||||
}
|
||||
$date = null;
|
||||
if (preg_match('/(\d{4}-\d{2}-\d{2})/', $png, $m)) $date = $m[1];
|
||||
$candidates = [];
|
||||
if ($date) {
|
||||
$dayDir = "$BY_DATE/$date";
|
||||
if (is_dir($dayDir)) {
|
||||
foreach (scandir($dayDir) as $sub) {
|
||||
if ($sub[0] === '.') continue;
|
||||
$p = "$dayDir/$sub/$png";
|
||||
if (is_file($p)) { $candidates[] = $p; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$candidates) {
|
||||
if (is_dir($BY_DATE)) {
|
||||
foreach (scandir($BY_DATE) as $d) {
|
||||
if ($d[0] === '.') continue;
|
||||
$dayDir = "$BY_DATE/$d";
|
||||
if (!is_dir($dayDir)) continue;
|
||||
foreach (scandir($dayDir) as $sub) {
|
||||
if ($sub[0] === '.') continue;
|
||||
$p = "$dayDir/$sub/$png";
|
||||
if (is_file($p)) { $candidates[] = $p; break 2; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$candidates || filesize($candidates[0]) < 64) {
|
||||
http_response_code(404);
|
||||
echo 'spectrogram not found';
|
||||
exit;
|
||||
}
|
||||
$path = $candidates[0];
|
||||
header('Content-Type: image/png');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('Cache-Control: public, max-age=86400');
|
||||
readfile($path);
|
||||
exit;
|
||||
}
|
||||
|
||||
function resolve_common(string $sci): ?string {
|
||||
$f = '/home/monalisa/BirdNET-Pi/scripts/birds.json';
|
||||
if (is_readable($f)) {
|
||||
$list = json_decode((string)file_get_contents($f), true);
|
||||
if (is_array($list)) {
|
||||
foreach ($list as $row) {
|
||||
if (!is_array($row)) continue;
|
||||
$rowSci = $row['sci'] ?? $row['scientific'] ?? $row['scientificName'] ?? '';
|
||||
$rowCom = $row['com'] ?? $row['common'] ?? $row['commonName'] ?? '';
|
||||
if (strcasecmp(trim((string)$rowSci), $sci) === 0 && $rowCom) {
|
||||
return str_replace(' ', '_', (string)$rowCom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$labels = '/home/monalisa/BirdNET-Pi/model/labels.txt';
|
||||
if (is_readable($labels)) {
|
||||
foreach (file($labels, FILE_IGNORE_NEW_LINES) as $line) {
|
||||
if (strpos($line, '_') !== false) {
|
||||
[$s, $c] = explode('_', $line, 2);
|
||||
if (strcasecmp(trim($s), $sci) === 0) {
|
||||
return str_replace(' ', '_', trim($c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$common = resolve_common($sci) ?? str_replace(' ', '_', $sci);
|
||||
|
||||
function newest_spectrogram(string $rootDir, string $common): ?string {
|
||||
if (!is_dir($rootDir)) return null;
|
||||
$dates = scandir($rootDir, SCANDIR_SORT_DESCENDING);
|
||||
if (!$dates) return null;
|
||||
foreach ($dates as $date) {
|
||||
if ($date[0] === '.') continue;
|
||||
$speciesDir = "$rootDir/$date/$common";
|
||||
if (!is_dir($speciesDir)) continue;
|
||||
$files = scandir($speciesDir, SCANDIR_SORT_DESCENDING);
|
||||
if (!$files) continue;
|
||||
foreach ($files as $f) {
|
||||
if (substr($f, -4) === '.png') {
|
||||
return "$speciesDir/$f";
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = newest_spectrogram($BY_DATE, $common);
|
||||
if ($path === null || !is_file($path) || filesize($path) < 64) {
|
||||
http_response_code(404);
|
||||
echo 'no spectrogram for ' . htmlspecialchars($sci);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: image/png');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('Cache-Control: public, max-age=60');
|
||||
readfile($path);
|
||||
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 424 KiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 406 KiB |
|
After Width: | Height: | Size: 389 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 364 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 618 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 414 KiB |
|
After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 453 KiB |
|
After Width: | Height: | Size: 679 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 272 KiB |
|
After Width: | Height: | Size: 383 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 549 KiB |
|
After Width: | Height: | Size: 622 KiB |
|
After Width: | Height: | Size: 447 KiB |
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 558 KiB |
|
After Width: | Height: | Size: 413 KiB |
|
After Width: | Height: | Size: 429 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 467 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 308 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 427 KiB |
|
After Width: | Height: | Size: 554 KiB |
|
After Width: | Height: | Size: 503 KiB |
|
After Width: | Height: | Size: 633 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 436 KiB |
|
After Width: | Height: | Size: 453 KiB |
|
After Width: | Height: | Size: 493 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 561 KiB |
|
After Width: | Height: | Size: 404 KiB |
|
After Width: | Height: | Size: 373 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 462 KiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 231 KiB |
|
After Width: | Height: | Size: 635 KiB |
|
After Width: | Height: | Size: 409 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 323 KiB |
|
After Width: | Height: | Size: 442 KiB |
|
After Width: | Height: | Size: 567 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 435 KiB |
|
After Width: | Height: | Size: 347 KiB |
|
After Width: | Height: | Size: 556 KiB |
|
After Width: | Height: | Size: 356 KiB |
|
After Width: | Height: | Size: 385 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 488 KiB |
|
After Width: | Height: | Size: 466 KiB |
|
After Width: | Height: | Size: 327 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 702 KiB |
|
After Width: | Height: | Size: 421 KiB |
|
After Width: | Height: | Size: 420 KiB |
|
After Width: | Height: | Size: 379 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 526 KiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 457 KiB |
|
After Width: | Height: | Size: 490 KiB |
|
After Width: | Height: | Size: 289 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 569 KiB |
|
After Width: | Height: | Size: 499 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 562 KiB |