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

+159
View File
@@ -0,0 +1,159 @@
{% extends "dex/base.html" %}
{% load i18n dex_extras %}
{% block title %}{% trans "Vogelalbum — Übersicht" %}{% endblock %}
{% comment %}
Der Fortschrittsbalken steht auf allen Seiten oben. Hier nicht: er ist Teil
des Blickfangs geworden. Zweimal dieselbe Leiste untereinander sähe nach
Versehen aus.
{% endcomment %}
{% block progress %}{% endblock %}
{% block content %}
{% comment %}
🔑 Die Startseite begann vorher mit vier Kacheln, von denen an einem ruhigen
Tag zwei auf null standen — "0 Besuche heute, 0 Arten heute". Das ist die
erste Seite, die ein Fremder sieht, und sie sagte ihm: hier passiert nichts.
Jetzt steht oben der letzte Gast in groß. Die Zahlen sind nicht weg, sie
stehen dort, wo sie etwas erklären: neben der Überschrift des Abschnitts,
zu dem sie gehören.
{% endcomment %}
<section class="held">
<div class="held-text">
{% comment %}
🔑 Die Überschrift kommt aus ``views.blickfang_titel`` und richtet sich
nach dem letzten Besuch. Vorher stand hier fest „Wer war heute im
Futterhaus?" — fünf Tage nach dem letzten Gast behauptete die Seite damit
etwas, das das Bild daneben widerlegte.
{% endcomment %}
<h1>{{ blickfang_titel }}</h1>
<p>{% trans "Jeder Vogel wird bestimmt, gezählt und gesammelt — aus dem Kamerabild oder vom Handyfoto. Alles auf dem eigenen Server." %}</p>
<div class="progress-bar">
<div class="level-badge">{{ progress.level }}</div>
<div style="flex:1 1 200px; min-width:170px">
<div class="rank">{{ progress.rank }}</div>
<div class="xp-track" style="margin:8px 0 6px">
<div class="xp-fill" style="width: {% widthratio progress.level_progress 1 100 %}%"></div>
</div>
<div class="sub">{% blocktrans with xp=progress.xp rest=progress.xp_to_next lvl=progress.level|add:1 %}{{ xp }} XP · noch {{ rest }} XP bis Level {{ lvl }}{% endblocktrans %}</div>
</div>
<div style="text-align:right">
<div class="rank">{{ progress.species_seen }} / {{ progress.species_total }}</div>
<div class="sub">{% trans "Arten" %}</div>
</div>
</div>
</div>
{% with letzte=recent.0 %}
{% if letzte %}
<a class="held-bild" href="{% url 'species' letzte.effective_species.slug %}">
{% if letzte.image %}
<img src="{{ letzte.image.url }}" alt="{{ letzte.effective_species.name }}">
{% elif letzte.kachel_url %}
<img src="{{ letzte.kachel_url }}" alt="{{ letzte.effective_species.name }}">
{% endif %}
<span class="frisch">{% trans "ZULETZT" %}</span>
<div class="schild">
<b>{{ letzte.effective_species.name }}</b>
<small>{{ letzte.started_at|zeitpunkt }} · {{ letzte.duration_s|floatformat:0 }}s · {% widthratio letzte.confidence 1 100 %}%</small>
</div>
</a>
{% else %}
<div class="held-leer">
{% trans "Noch kein Gast aufgezeichnet." %}<br>
{% trans "Der erste bestimmte Vogel steht hier." %}
</div>
{% endif %}
{% endwith %}
</section>
{% if new_entries %}
<h2>{% trans "Neu in der Sammlung" %} <span class="count">{% trans "letzte 7 Tage" %}</span></h2>
<div class="grid">
{% for entry in new_entries %}
<a class="card" href="{% url 'species' entry.species.slug %}"
style="{{ entry.species.typ_vars }}">
<span class="nr">{{ entry.species.dex_nr }}</span>
<div class="thumb">
{% if entry.best_sighting and entry.best_sighting.kachel_url %}
<img src="{{ entry.best_sighting.kachel_url }}" alt="{{ entry.species.name }}" loading="lazy">
{% elif entry.species.reference_image %}
<img src="{{ entry.species.reference_image.url }}" alt="{{ entry.species.name }}" loading="lazy">
{% endif %}
</div>
<div class="body">
<div class="name">{{ entry.species.name }}</div>
<div class="meta">
{% if entry.species.typ %}<span class="pill">{{ entry.species.typ_label }}</span>{% endif %}
<span class="tag-new">{% trans "Neu" %}</span>
</div>
</div>
</a>
{% endfor %}
</div>
{% endif %}
<h2>{% trans "Zuletzt zu Besuch" %}
<span class="count">{% blocktrans with n=progress.sightings %}{{ n }} Besuche insgesamt{% endblocktrans %}</span></h2>
{% if recent %}
{% comment %}
⚠ Bildkarten statt Zeilen. Die Zeilen liefen über die ganze Breite, mit dem
Namen links und dem Prozentwert rechts — dazwischen auf einem großen
Bildschirm tausend Pixel Nichts. Eine Karte ist so breit wie ihr Inhalt.
Der erste Eintrag fehlt hier mit Absicht: der steht oben im Blickfang.
{% endcomment %}
<div class="besuche">
{% for s in recent|slice:"1:5" %}
<a class="besuch" href="{% url 'species' s.effective_species.slug %}">
{% if s.kachel_url %}
<img src="{{ s.kachel_url }}" alt="" loading="lazy">
{% else %}<div class="leer"></div>{% endif %}
<div class="f">
{% if s.xp_awarded %}<span class="xp">+{{ s.xp_awarded }} XP</span>{% endif %}
<b>{{ s.effective_species.name }}</b>
<small>
{% if s.is_first_of_species %}<span class="tag-new">{% trans "Erstfang" %}</span>
{% else %}{{ s.started_at|zeitpunkt }}{% endif %}
</small>
</div>
</a>
{% endfor %}
</div>
{% else %}
<div class="empty">
{% trans "Noch keine Sichtung aufgezeichnet." %}<br>
{% trans "Sobald der Detector einen Vogel erkennt oder das erste Foto eingeliefert ist, erscheint hier der erste Gast." %}
</div>
{% endif %}
{% if top %}
<h2>{% trans "Stammgäste" %}</h2>
<div class="grid">
{% for entry in top %}
<a class="card" href="{% url 'species' entry.species.slug %}"
style="{{ entry.species.typ_vars }}">
<span class="nr">{{ entry.species.dex_nr }}</span>
<div class="thumb">
{% if entry.best_sighting and entry.best_sighting.kachel_url %}
<img src="{{ entry.best_sighting.kachel_url }}" alt="{{ entry.species.name }}" loading="lazy">
{% elif entry.species.reference_image %}
<img src="{{ entry.species.reference_image.url }}" alt="{{ entry.species.name }}" loading="lazy">
{% endif %}
</div>
<div class="body">
<div class="name">{{ entry.species.name }}</div>
<div class="meta">
{% if entry.species.typ %}<span class="pill">{{ entry.species.typ_label }}</span>{% endif %}
<span class="times">×{{ entry.count }}</span>
</div>
</div>
</a>
{% endfor %}
</div>
{% endif %}
{% endblock %}