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.
162 lines
5.4 KiB
HTML
162 lines
5.4 KiB
HTML
{% 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 %}
|