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
cc9e0786c2
189 files changed
+29792
No files matched your search
@@ -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 %}
|
||||
Reference in new issue
Block a user