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:
christian committed 2026-10-15 14:30:02 +00:00
commit c11fe9ab04
189 files changed
+29806

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
{% extends "dex/base.html" %}
{% load i18n %}
{% block title %}{% trans "Fotos hochladen" %} — Vogelalbum{% endblock %}
{% block kopf %}
<style>
.hochladen-form {
display: grid;
gap: 16px;
max-width: 560px;
margin-bottom: 32px;
}
.hochladen-form label { display: block; font-weight: 600; margin-bottom: 6px; }
.hochladen-form input[type="file"],
.hochladen-form select,
.hochladen-form input[type="text"] {
width: 100%;
padding: 10px;
border-radius: 8px;
border: 1px solid color-mix(in srgb, var(--text) 20%, transparent);
background: var(--card);
color: var(--text);
}
.hochladen-form .hinweis {
font-size: .9em;
opacity: .75;
margin-top: 4px;
}
.ort-zeile { display: flex; gap: 12px; }
.ort-zeile > div { flex: 1; }
.meldung {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
background: color-mix(in srgb, var(--accent) 18%, transparent);
}
.meldung.fehler {
background: color-mix(in srgb, crimson 22%, transparent);
}
/* Die wartenden Fotos als schlichte Reihe -- sie sollen zeigen, dass
etwas passiert, und nicht mit der Sammlung konkurrieren. */
.wartend-liste { display: flex; flex-wrap: wrap; gap: 10px; }
.wartend-liste figure { margin: 0; width: 110px; }
.wartend-liste img {
width: 110px; height: 110px; object-fit: cover;
border-radius: 8px; display: block;
/* Solange nicht bestimmt ist, bleibt das Bild bewusst zurückhaltend. */
opacity: .72;
}
.wartend-liste figcaption { font-size: .78em; opacity: .7; margin-top: 4px; }
/* ⚠ Es gibt im Projekt-CSS keine allgemeine Knopfklasse — die Knöpfe im
Kopfbereich sind eigene Fälle. Also hier einer, der sich an die
Farbvariablen hält statt an den Browserstandard. */
.hochladen-form button {
padding: 10px 22px;
border: 0;
border-radius: 999px;
background: var(--accent);
color: var(--accent-on);
font: inherit;
font-weight: 600;
cursor: pointer;
}
.hochladen-form button:hover { filter: brightness(1.08); }
/* Auf dem Telefon untereinander: zwei Koordinatenfelder nebeneinander
sind bei 390 px so schmal, dass der Platzhalter abgeschnitten wird. */
@media (max-width: 460px) {
.ort-zeile { flex-direction: column; gap: 10px; }
}
</style>
{% endblock %}
{% block content %}
<h2>{% trans "Fotos hochladen" %}</h2>
<p>
{% comment %}
⚠ In einer Zeile lassen. Ein Umbruch im Quelltext landet als ``\n`` mitten
im Schlüssel der .po-Datei — der Text ist dann nur noch mit genau diesem
Umbruch übersetzbar, und jede Einrückungsänderung macht ihn wieder deutsch.
{% endcomment %}
{% blocktrans %}Ein Vogel unterwegs? Foto hier einliefern — der Detector bestimmt es beim nächsten Durchgang und trägt es in die Sammlung ein.{% endblocktrans %}
</p>
{% if angelegt %}
<div class="meldung">
{% blocktrans count n=angelegt %}{{ n }} Foto angenommen. Es wartet auf den Detector.{% plural %}{{ n }} Fotos angenommen. Sie warten auf den Detector.{% endblocktrans %}
</div>
{% endif %}
{% if fehler %}
<div class="meldung fehler">{{ fehler }}</div>
{% endif %}
<form class="hochladen-form" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div>
<label for="bilder">{% trans "Bilder" %}</label>
{% comment %}
multiple ist Absicht: Von einem Spaziergang kommen selten einzelne
Fotos. Je Bild entsteht eine eigene Sichtung — zehn Fotos können
fünf verschiedene Arten sein.
{% endcomment %}
<input type="file" name="bilder" id="bilder" accept="image/*" multiple required>
<div class="hinweis">{% trans "Mehrere Bilder auf einmal sind möglich. Jedes wird einzeln bestimmt." %}</div>
</div>
{% if kameras %}
<div>
<label for="kamera">{% trans "Gerät" %}</label>
<select name="kamera" id="kamera">
<option value="">{% trans "— ohne Zuordnung —" %}</option>
{% for k in kameras %}
<option value="{{ k.slug }}">{{ k.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
<div>
<label>{% trans "Ort (freiwillig)" %}</label>
<div class="ort-zeile">
<div><input type="text" name="latitude" inputmode="decimal" placeholder="{% trans "Breite, z. B. 49.49" %}"></div>
<div><input type="text" name="longitude" inputmode="decimal" placeholder="{% trans "Länge, z. B. 8.47" %}"></div>
</div>
<div class="hinweis">
{% blocktrans %}Wird auf etwa einen Kilometer gerundet gespeichert — die Gegend, nicht die Adresse.{% endblocktrans %}
</div>
</div>
<div>
<button type="submit" class="knopf">{% trans "Hochladen" %}</button>
</div>
</form>
{% if wartende_fotos %}
<h3>
{% trans "Warten auf Bestimmung" %}
<span class="count">{{ wartend }}</span>
</h3>
{% comment %}
Sichtbar, obwohl noch nichts bestimmt ist. Wäre hier nichts zu sehen,
hielte man den Upload für fehlgeschlagen und lüde dasselbe Foto erneut
hoch — und die Doppel sortiert hinterher niemand auseinander.
{% endcomment %}
<div class="wartend-liste">
{% for s in wartende_fotos %}
<figure>
{% if s.image %}<img src="{{ s.image.url }}" alt="{% trans "Noch nicht bestimmt" %}" loading="lazy">{% endif %}
<figcaption>{{ s.started_at|date:"SHORT_DATETIME_FORMAT" }}</figcaption>
</figure>
{% endfor %}
</div>
<p class="hinweis">
{% blocktrans %}Bleiben die Bilder länger stehen, läuft der Detector nicht.{% endblocktrans %}
</p>
{% endif %}
{% endblock %}