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,105 @@
|
||||
{% extends "dex/base.html" %}
|
||||
{% load dex_extras i18n %}
|
||||
|
||||
{% block title %}{% trans "Sichtungen" %} — Vogelalbum{% endblock %}
|
||||
|
||||
{% block kopf %}
|
||||
<style>
|
||||
/* Der Einlieferungs-Knopf sitzt in der Zeile der Ueberschrift, rechts.
|
||||
Auf dem Telefon rutscht er darunter, statt die Zeile zu sprengen. */
|
||||
.kopf-zeile {
|
||||
display: flex; align-items: baseline; justify-content: space-between;
|
||||
flex-wrap: wrap; gap: 10px;
|
||||
}
|
||||
.kopf-zeile h2 { margin: 0; }
|
||||
.hochladen-link {
|
||||
padding: 7px 16px; border-radius: 999px;
|
||||
background: var(--accent); color: var(--accent-on);
|
||||
font-weight: 600; font-size: .92em; text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.hochladen-link:hover { filter: brightness(1.08); }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="kopf-zeile">
|
||||
<h2>{% trans "Sichtungen" %} <span class="count">{% blocktrans with n=total %}{{ n }} insgesamt{% endblocktrans %}</span></h2>
|
||||
{% comment %}
|
||||
Nur fuer Angemeldete. Ein oeffentlicher Besucher soll keinen Knopf sehen,
|
||||
der ihn bloss ins Login schickt -- und einliefern darf er ohnehin nicht.
|
||||
{% endcomment %}
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'hochladen' %}" class="hochladen-link">{% trans "Foto hochladen" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<a href="{% url 'sightings' %}" {% if not nur_bestimmt %}class="on"{% endif %}>{% trans "Alle Besuche" %}</a>
|
||||
<a href="?nur_bestimmt=1" {% if nur_bestimmt %}class="on"{% endif %}>{% trans "Nur bestimmte Arten" %}</a>
|
||||
</div>
|
||||
|
||||
{% if cameras %}
|
||||
<div class="filters">
|
||||
<a href="?{% if nur_bestimmt %}nur_bestimmt=1{% endif %}" {% if not kamera %}class="on"{% endif %}>{% trans "Alle Kameras" %}</a>
|
||||
{% for c in cameras %}
|
||||
<a href="?kamera={{ c.slug }}{% if nur_bestimmt %}&nur_bestimmt=1{% endif %}"
|
||||
{% if kamera == c.slug %}class="on"{% endif %}>
|
||||
{% if c.is_stale %}⚠ {% endif %}{{ c.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if sightings %}
|
||||
<div class="rows">
|
||||
{% for s in sightings %}
|
||||
<div class="row">
|
||||
{% if s.kachel_url %}
|
||||
{# Klick zeigt das grosse Foto -- siehe base.html #}
|
||||
<img class="shot" src="{{ s.kachel_url }}" alt="" loading="lazy"
|
||||
{% if s.image %}data-gross="{{ s.image.url }}"
|
||||
data-beschriftung="{% if s.effective_species %}{{ s.effective_species.name }} · {% endif %}{{ s.started_at|date:"SHORT_DATETIME_FORMAT" }}{% if s.camera %} · {{ s.camera.name }}{% endif %}"
|
||||
style="cursor:zoom-in"{% endif %}>
|
||||
{% else %}<div class="shot"></div>{% endif %}
|
||||
<div>
|
||||
<div class="who">
|
||||
{% if s.effective_species %}
|
||||
<a href="{% url 'species' s.effective_species.slug %}">{{ s.effective_species.name }}</a>
|
||||
{% if s.confirmed_species %}<span class="pill ghost">{% trans "von Hand" %}</span>{% endif %}
|
||||
{% else %}
|
||||
<span style="color:var(--muted)">{% trans "Unbestimmt" %}</span>
|
||||
{% endif %}
|
||||
{% if s.is_first_of_species %}<span class="tag-new">{% trans "Erstfang" %}</span>{% endif %}
|
||||
</div>
|
||||
<div class="when">
|
||||
{{ s.started_at|date:"DATETIME_FORMAT" }} · {{ s.duration_s|floatformat:0 }}s
|
||||
{% if s.camera %} · {{ s.camera.name }}{% endif %}
|
||||
</div>
|
||||
{% with alt=s.votes|alternativen %}
|
||||
{% if alt %}
|
||||
<div class="when" style="margin-top:2px">
|
||||
{% trans "Alternativen" %}:
|
||||
{% for a in alt %}{{ a.species.name }} {{ a.anteil }}%{% if not forloop.last %}, {% endif %}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="conf">{{ s.confidence|prozent }}%</div>
|
||||
{% if s.xp_awarded %}<div class="xp">+{{ s.xp_awarded }} XP</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="pager">
|
||||
{% if page > 1 %}<a href="?seite={{ page|add:'-1' }}{% if nur_bestimmt %}&nur_bestimmt=1{% endif %}">← {% trans "Neuer" %}</a>{% endif %}
|
||||
{% if has_next %}<a href="?seite={{ page|add:'1' }}{% if nur_bestimmt %}&nur_bestimmt=1{% endif %}">{% trans "Älter" %} →</a>{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty">{% trans "Noch nichts aufgezeichnet." %}</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user