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.
This commit is contained in:
commit
c11fe9ab04
189 files changed
+29806
No files matched your search
@@ -0,0 +1,135 @@
|
||||
{% extends "dex/base.html" %}
|
||||
{% load i18n l10n %}
|
||||
|
||||
{% block title %}{% trans "Live" %} — Vogelalbum{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2>{% trans "Live" %}
|
||||
<span class="count">
|
||||
{% blocktrans count n=cameras|length %}{{ n }} Kamera{% plural %}{{ n }} Kameras{% endblocktrans %}
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div class="live-grid">
|
||||
{% for kamera in cameras %}
|
||||
<figure class="live-karte">
|
||||
{% comment %}
|
||||
Das Bild kommt vom Live-Verteiler des Detectors, nicht von der Kamera —
|
||||
die liefert nur einen Stream, und den braucht die Erkennung. Der Browser
|
||||
lädt es direkt, der Server reicht nichts durch: sonst hinge an jedem
|
||||
Zuschauer eine offene Verbindung in der Web-App.
|
||||
{% endcomment %}
|
||||
<div class="live-bild">
|
||||
<img alt="{{ kamera.name }}"
|
||||
data-quelle="{{ kamera.live_bild_url }}"
|
||||
data-rate="{{ kamera.live_bildrate|unlocalize }}">
|
||||
{% comment %}
|
||||
Der Name liegt AUF dem Bild: In einer Reihe gleich aussehender
|
||||
Gartenbilder erkennt man sonst nicht, welche Kamera welche ist —
|
||||
und beim Scrollen ist die Bildunterschrift oft aus dem Blick.
|
||||
|
||||
⚠ Diese lange Kommentarform benutzen, nicht die kurze mit der
|
||||
Raute: Die kurze gilt nur für EINE Zeile. Über mehrere Zeilen wird
|
||||
sie nicht entfernt, sondern steht wörtlich auf der Seite. Genau das
|
||||
ist am 23.09.2026 passiert, und man sieht es erst im Browser.
|
||||
{% endcomment %}
|
||||
<span class="warte">Bild wird geholt …</span>
|
||||
<span class="live-schild">{{ kamera.name }}</span>
|
||||
</div>
|
||||
<figcaption>
|
||||
<strong>{{ kamera.name }}</strong>
|
||||
{% if kamera.location %}<span class="live-ort">{{ kamera.location }}</span>{% endif %}
|
||||
</figcaption>
|
||||
</figure>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<p class="d" style="margin-top:14px">
|
||||
{% trans "Bleibt das Bild schwarz, läuft der Detector nicht — die Erkennung verteilt das Livebild aus ihrem eigenen Bildstrom." %}
|
||||
</p>
|
||||
|
||||
<script>
|
||||
/*
|
||||
* Einzelbilder statt Dauerstrom.
|
||||
*
|
||||
* 🔑 Am 24.09.2026 gemessen und an der Anlage bestaetigt: EINE Kamera
|
||||
* erscheint sofort, ZWEI nebeneinander lassen die Seite haengen. Ein
|
||||
* MJPEG-Strom belegt seine Verbindung fuer immer; mit zwei solchen kommt
|
||||
* mancher Browser durcheinander -- auf dem Handy besonders.
|
||||
*
|
||||
* Ein Einzelbild ist eine kurze, gewoehnliche Anfrage. Sie blockiert
|
||||
* nichts, und nach einem Fehler wird beim naechsten Mal einfach wieder
|
||||
* gefragt. Wie oft, sagt die Bildrate der Kamera aus dem Backend.
|
||||
*
|
||||
* ⚠ Der Zeitstempel an der Adresse ist Pflicht: Ohne ihn liefert der
|
||||
* Browser sein zwischengespeichertes Bild aus und man sieht ein
|
||||
* Standbild, das aussieht wie ein Livebild.
|
||||
*/
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.querySelectorAll(".live-bild img").forEach(function (bild) {
|
||||
var warte = bild.parentElement.querySelector(".warte");
|
||||
var quelle = bild.dataset.quelle;
|
||||
var rate = parseFloat(bild.dataset.rate) || 2;
|
||||
var pause = Math.max(200, 1000 / rate);
|
||||
var laeuft = false;
|
||||
|
||||
if (!quelle) return;
|
||||
|
||||
bild.addEventListener("load", function () {
|
||||
if (warte) warte.style.display = "none";
|
||||
laeuft = false;
|
||||
});
|
||||
|
||||
bild.addEventListener("error", function () {
|
||||
if (warte) {
|
||||
warte.style.display = "";
|
||||
warte.textContent = "Kein Bild — neuer Versuch …";
|
||||
}
|
||||
laeuft = false;
|
||||
});
|
||||
|
||||
function holen() {
|
||||
// Nicht nachlegen, solange das vorige Bild noch unterwegs ist --
|
||||
// sonst stapeln sich Anfragen, wenn die Strecke langsam ist.
|
||||
if (!laeuft) {
|
||||
laeuft = true;
|
||||
bild.src = quelle + "?t=" + Date.now();
|
||||
}
|
||||
setTimeout(holen, pause);
|
||||
}
|
||||
holen();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.live-grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}
|
||||
.live-karte{margin:0;background:#000;border-radius:10px;overflow:hidden}
|
||||
.live-karte img{display:block;width:100%;height:auto}
|
||||
/* ⚠ Die Karte ist schwarz, die Schriftfarbe der Seite aber fast auch
|
||||
(--text: #16211a). Ohne eigene Farbe stand die Beschriftung schwarz
|
||||
auf schwarz — sie war da und trotzdem unsichtbar. */
|
||||
.live-karte figcaption{padding:8px 12px;display:flex;gap:10px;
|
||||
align-items:baseline;color:#fff}
|
||||
.live-karte figcaption .live-ort{color:#9fb3a6;font-size:13px}
|
||||
|
||||
/* ⚠ Feste Form, auch wenn kein Bild da ist. Ohne das faellt die Karte
|
||||
in sich zusammen, sobald der Bildstrom abreisst -- dann steht da nur
|
||||
noch die Beschriftung auf einem schwarzen Strich, und es sieht nach
|
||||
einem kaputten Programm aus statt nach einer Kamera ohne Bild. */
|
||||
.live-bild{position:relative;aspect-ratio:16/9;background:#000;
|
||||
display:flex;align-items:center;justify-content:center}
|
||||
.live-bild img{position:absolute;inset:0;width:100%;height:100%;
|
||||
object-fit:cover}
|
||||
.live-bild .warte{color:#8a8a94;font-size:14px;text-align:center;
|
||||
padding:0 20px;line-height:1.5}
|
||||
.live-schild{position:absolute;top:10px;left:10px;
|
||||
padding:4px 10px;border-radius:999px;
|
||||
background:rgba(0,0,0,.55);color:#fff;
|
||||
font-size:13px;font-weight:600;letter-spacing:.01em;
|
||||
/* Damit der Name auch auf hellem Himmel lesbar bleibt. */
|
||||
backdrop-filter:blur(4px);text-shadow:0 1px 2px rgba(0,0,0,.6)}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user