Files
vogelalbum/docs/betrieb.md
T
christian cc9e0786c2 Vogelalbum 1.0.0
Eine Kamera am Vogelhaus, ein Rechner der erkennt wer da war — und eine
Sammlung, die sich von allein fuellt.

Zwei neuronale Netze: eines findet den Vogel im Bild, eines bestimmt die
Art. 148 heimische Arten, auf Deutschland eingeschraenkt und neu normiert —
ohne diese Einschraenkung wird aus einer Kohlmeise eine nordamerikanische
Schwesterart. Daraus entsteht ein Sammelalbum: nummerierte Seiten,
gefundene Arten in Farbe, fehlende als Schatten, dazu Erstfang, Punkte,
Stufen und 15 Abzeichen.

Laeuft vollstaendig auf eigener Hardware. Kein Cloud-Dienst, kein Konto,
keine Telemetrie; die Bilder verlassen das eigene Netz nicht.

Zwei getrennt deploybare Teile: die Sammlung als Django-Anwendung, die
Erkennung als eigener Dienst. Der Erkennungs-Dienst fragt selbst an und
braucht deshalb weder eine erreichbare Adresse noch einen offenen Port —
er laeuft hinter einem gewoehnlichen Router.

Dabei: Firmware fuer Raspberry Pi und ESP32-S3, Anbindung an Home
Assistant ueber MQTT, Oberflaeche auf Deutsch und Englisch, Einstellungen
je Kamera aus dem Browser, und ein Weg fuer Fotos von unterwegs.

AGPL-3.0.
2026-10-14 18:55:23 +00:00

161 lines
6.7 KiB
Markdown

# Betrieb
<sub>[← zurück zum Überblick](../README.md) · [alle Kapitel](README.md)</sub>
Live zuschauen, mehrere Kameras, der Detector auf einem eigenen Host und das
Deployen der Web-App. Also alles, was nach dem ersten erkannten Vogel kommt.
## Live zuschauen
Eine Kamera liefert **einen** Stream. Wer direkt bei ihr zuschaut, nimmt ihn
der Erkennung weg — auf einem Pi Zero W ist das keine Einstellung, sondern
Physik: die Funkstrecke trägt rund 2 Mbit/s, ein zweiter Client sättigt sie.
Der Detector liest den Stream aber ohnehin rund um die Uhr. Also verteilt er
ihn weiter:
```bash
python -m detector watch --source http://kamera:8080/stream --live-port 8131
```
Damit schauen beliebig viele Leute zu, und die Kamera merkt nichts davon — sie
hat weiterhin genau einen Zuschauer. Solange niemand hinsieht, wird auch nichts
umkodiert: bei MJPEG-Quellen gehen die JPEG-Bytes unverändert durch, und das
Kodieren für andere Quellen passiert im HTTP-Thread, einmal je Bild statt
einmal je Zuschauer. Die Erkennung wird davon nicht langsamer.
| Adresse | wozu |
|---|---|
| `http://<detector>:8131/live` | MJPEG, für mehrere Zuschauer |
| `http://<detector>:8131/bild.jpg` | Einzelbild |
| `http://<detector>:8131/` | Seite mit Livebild |
### In der Web-App
Eingetragen wird das **im Django-Admin bei der Kamera**: `Live-Adresse` auf
`http://<detector>:8131/live`, dann `Live-Bild zeigen` ankreuzen. Danach gibt
es die Seite `/live/` und einen Eintrag in der Navigation. Ohne Freigabe
existiert beides nicht — kein Link, keine Seite, 404.
Das Bild lädt der Browser direkt beim Verteiler; die Web-App reicht nichts
durch. Sonst hinge an jedem Zuschauer eine offene Verbindung im Webserver.
> **Für eine öffentliche Instanz: `VOGELALBUM_LIVE_ERLAUBT=0`.** Dann ist Live
> nicht einschaltbar, auch nicht mit einem Fehlklick im Admin, und die
> Admin-Liste zeigt „gesperrt". Ein Livebild aus einem Privatgarten soll nicht
> an einem Häkchen hängen — für einen Showcase ist die Sammlung ohnehin das
> Interessantere.
Der Verteiler selbst kennt keine Anmeldung. Er gehört ins Heimnetz.
## Mehrere Kameras
Jede Kamera bekommt einen eigenen Token. Der Token identifiziert sie — die
Web-App muss nie wissen, wo sie steht, und ein kompromittierter Standort lässt
sich einzeln stilllegen, ohne die anderen anzufassen.
```bash
manage.py camera add futterhaus --name "Futterhaus Garten" --location "Garten"
manage.py camera add nistkasten --name "Nistkasten Apfelbaum"
manage.py camera list
manage.py camera rotate futterhaus # Token erneuern
manage.py camera disable nistkasten
```
Der Token wird **genau einmal** ausgegeben; gespeichert ist nur sein SHA-256.
Bei 256 Bit Entropie braucht es kein Salt, und wer die Datenbank in die Hand
bekommt, kann damit trotzdem nichts einliefern.
Pro Kamera läuft eine eigene `detector watch`-Instanz. Sie können auf
demselben Host laufen; rechne mit rund 40 % eines Kerns je Kamera bei
`--classify-every 2` und 4 fps.
Die Sammlung ist dabei **gemeinsam**: Findet die Gartenkamera die erste Amsel,
ist sie im Dex freigeschaltet — die Nistkastenkamera bekommt für dieselbe Art
keinen zweiten Erstfang. Das ist Absicht, es ist die Sammlung eines Menschen
und nicht die einer Kamera. Die Sichtungsliste lässt sich trotzdem nach Kamera
filtern.
`manage.py camera list` markiert Kameras, die sich über eine Stunde nicht
gemeldet haben — der schnellste Weg zu „warum kommt da nichts mehr".
## Betrieb
### Detector auf einem eigenen Host
Ein frischer Debian- oder Ubuntu-Container, dann:
```bash
sudo ./scripts/setup_detector_host.sh
```
Das Skript legt den Dienstbenutzer `vogelalbum` an, klont nach `/opt/vogelalbum`,
baut das venv, holt die Modelle, schreibt `/etc/vogelalbum.env` und installiert
die systemd-Unit. Idempotent — ein zweiter Lauf aktualisiert nur.
Danach `/etc/vogelalbum.env` ausfüllen und `systemctl enable --now
vogelalbum-detector`.
**Dimensionierung**
| | |
|---|---|
| CPU | 1 Kern je Kamera (bei `--classify-every 2` und ~4 fps rund 40 % Last) |
| RAM | 512 MB je Instanz, 1 GB für Luft |
| Platte | 4 GB fürs System, plus ~150 kB je gespeicherter Sichtung |
**Netz:** ausgehend zur Kamera und zur Web-App. **Eingehend nichts.**
Achtung, wenn Kamera und Erkennungs-Host in verschiedenen Netzsegmenten
liegen: Die Route muss stehen. Bei uns hängt die Kamera im WLAN-Segment und
der Server im LAN — ein Scan des falschen Segments findet sie schlicht nicht.
**Zweite Kamera:** eigene `/etc/vogelalbum-<name>.env`, die Unit als
`vogelalbum-detector@.service` ablegen, dann `systemctl enable --now
vogelalbum-detector@nistkasten`.
## Die Web-App deployen
Nur die Web-App wird deployt. Der Detector bleibt im Heimnetz — er braucht
Sichtverbindung zur Kamera.
1. DNS auf den Host zeigen lassen.
2. Anwendung aus dem Repo anlegen, Build über `docker-compose.yml`.
⚠️ **Der Eingang ist `medien` auf Port 8080, nicht `web` auf 8000.** Das
compose startet zwei Dienste: Gunicorn und einen nginx davor, der die
Vogelfotos vom Volume ausliefert. Zeigt der Reverse Proxy direkt auf
`web`, lädt die Seite — aber **jede Karte der Sammlung hat ein kaputtes
Bild**, denn Django liefert unter `DJANGO_DEBUG=0` keine Medien aus.
Begründung ausführlich in [deploy/nginx-media.conf](../deploy/nginx-media.conf).
3. Persistentes Volume auf `/data` mounten. **Das ist der Schritt, den man
vergisst** — ohne ihn löscht jeder Deploy Sammlung und Fotos.
4. Umgebungsvariablen setzen (siehe [Einstellungen](einstellungen.md)).
5. Denselben Token auf dem Erkennungs-Host eintragen und `VOGELALBUM_API` auf die
öffentliche Adresse setzen.
6. Auf einer öffentlichen Instanz `VOGELALBUM_LIVE_ERLAUBT=0` setzen — steht im
`docker-compose.yml` schon drin. Ohne die Angabe wäre das Livebild aus dem
eigenen Garten erlaubt, denn der Standard im Code ist `1`.
Der erste Start dauert ein paar Minuten: Der Entrypoint holt einmalig die
Referenzfotos. Danach kostet der Schritt nichts mehr. `migrate`,
`collectstatic` und `seed_dex` erledigt er ebenfalls — neue Arten im Katalog
landen also mit dem nächsten Deploy automatisch in der Datenbank.
📘 **Schritt für Schritt mit Coolify, inklusive Umzug einer bestehenden
Sammlung: [deploy/COOLIFY.md](../deploy/COOLIFY.md).** Dort steht auch, wie
Datenbank und Fotos als 1:1-Kopie auf das Volume kommen — Kamera-Tokens,
Handbestimmungen und Abzeichen bleiben dabei erhalten, und die Kameras zu
Hause liefern mit ihrem bisherigen Token weiter.
### Einbinden auf einer anderen Seite
`/widget/` ist dafür da — keine Navigation, transparenter Hintergrund:
```html
<iframe src="https://vogelalbum.example.org/widget/"
style="width:100%;height:110px;border:0" loading="lazy"></iframe>
```
Wer lieber selbst gestaltet, nimmt `/api/stats/` — Level, Artenzahl und die
letzte Sichtung als JSON.