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

+149
View File
@@ -0,0 +1,149 @@
{% extends "dex/base.html" %}
{% load dex_extras i18n %}
{% block title %}{{ species.name }} — Vogelalbum{% endblock %}
{% block content %}
{# Die Typfarbe faerbt die ganze Seite: Hero, Balken, Initiale, Diagramm. #}
<div style="{{ species.typ_vars }}">
<div class="blaettern">
{% if vorher %}<a href="{% url 'species' vorher.slug %}">← {{ vorher.dex_nr }} {{ vorher.name }}</a>{% else %}<span></span>{% endif %}
{% if nachher %}<a href="{% url 'species' nachher.slug %}">{{ nachher.dex_nr }} {{ nachher.name }} →</a>{% endif %}
</div>
<div class="species-hero">
<div class="hero-grid">
<div>
<div class="hero-photo">
{% if entry and entry.best_sighting and entry.best_sighting.image %}
<img src="{{ entry.best_sighting.image.url }}" alt="{{ species.name }}">
{% elif species.reference_image %}
<img src="{{ species.reference_image.url }}" alt="{{ species.german }}"
style="filter: saturate(.7) brightness(.72)">
{% endif %}
</div>
<div class="sub" style="margin-top:9px">
{% if entry and entry.best_sighting and entry.best_sighting.image %}
{% trans "Bestes eigenes Foto" %} · {{ entry.best_sighting.started_at|date:"SHORT_DATETIME_FORMAT" }}
{% elif species.reference_image %}
{% trans "Referenzfoto — diese Art war noch nicht vor unserer Kamera." %}
{% if species.reference_credit %}
{% trans "Foto" %}: <a href="{{ species.reference_source }}" rel="nofollow noopener" target="_blank"
style="text-decoration:underline">{{ species.reference_credit }}</a>{% if species.reference_license %}, {{ species.reference_license }}{% endif %}
{% endif %}
{% endif %}
</div>
</div>
<div>
<div class="dexnr">{{ species.dex_nr }}</div>
<h1>{{ species.name }}</h1>
{% if species.kategorie_lokal %}<div class="kategorie">{{ species.kategorie_lokal }}</div>{% endif %}
<div class="sci">{{ species.scientific }}{% if species.zweitname %} · {{ species.zweitname }}{% endif %}</div>
<div class="badge-row">
{% if species.typ %}<span class="pill">{{ species.typ_label }}</span>{% endif %}
<span class="pill rar" style="background:{{ species.rarity_color }}">{{ species.rarity_label }}</span>
{% if species.zug %}<span class="pill ghost">{{ species.zug_label }}</span>{% endif %}
<span class="pill ghost">{{ species.xp }} XP</span>
{% if entry %}<span class="pill ghost">{% blocktrans count n=entry.count %}{{ n }}× seen{% plural %}{{ n }}× seen{% endblocktrans %}</span>{% endif %}
</div>
{% if species.balken %}
<div class="werte">
{% for b in species.balken %}
<div class="wert">
<div class="k">{{ b.label }}</div>
<div class="track"><div class="fill" style="width:{{ b.pct }}%"></div></div>
<div class="v">{{ b.wert }}</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
{% if species.absaetze %}
<h2>{% trans "Steckbrief" %}</h2>
<div class="prose">
{% for p in species.absaetze %}<p>{{ p }}</p>{% endfor %}
</div>
{% elif species.fact %}
<h2>{% trans "Steckbrief" %}</h2>
<div class="prose"><p>{{ species.fact }}</p></div>
{% endif %}
<div class="fakten">
{% if species.lebensraum_lokal %}
<div class="fakt"><div class="k">{% trans "Lebensraum" %}</div><div class="v">{{ species.lebensraum_lokal }}</div></div>
{% endif %}
{% if species.nahrung_lokal %}
<div class="fakt"><div class="k">{% trans "Nahrung" %}</div><div class="v">{{ species.nahrung_lokal }}</div></div>
{% endif %}
{% if species.stimme_lokal %}
<div class="fakt"><div class="k">{% trans "Stimme" %}</div><div class="v">{{ species.stimme_lokal }}</div></div>
{% endif %}
{% if species.zug %}
<div class="fakt"><div class="k">{% trans "Zugverhalten" %}</div><div class="v">{{ species.zug_label }}</div></div>
{% endif %}
</div>
{% if aehnliche %}
<h2>{% trans "Verwechslungsgefahr" %} <span class="count">{% trans "Arten, die man nicht auf Anhieb unterscheidet" %}</span></h2>
<div class="verwechslung">
{% for a in aehnliche %}
<a class="vw" href="{% url 'species' a.slug %}">
{% if a.reference_image %}<img src="{{ a.reference_image.url }}" alt="" loading="lazy">{% endif %}
<span class="n">{{ a.name }}</span>
</a>
{% endfor %}
</div>
{% endif %}
{% if entry %}
<h2>{% blocktrans with n=species.name %}Wann ist {{ n }} da?{% endblocktrans %} <span class="count">{% trans "Besuche über den Tag" %}</span></h2>
<div class="hours">
{% for h in hours %}<div style="height:{{ h.pct }}%" title="{{ h.h }} Uhr: {{ h.n }}"></div>{% endfor %}
</div>
<div class="hours-axis"><span>{% trans "0 Uhr" %}</span><span>6</span><span>12</span><span>18</span><span>23</span></div>
{% endif %}
{% if sightings %}
<h2>{% trans "Alle Besuche" %} <span class="count">{% blocktrans with n=entry.count %}{{ n }} insgesamt{% endblocktrans %}</span></h2>
<div class="rows">
{% for s in sightings %}
<div class="row">
{% if s.kachel_url %}
<img class="shot" src="{{ s.kachel_url }}" alt="" loading="lazy"
{% if s.image %}data-gross="{{ s.image.url }}"
data-beschriftung="{{ species.name }} · {{ 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">
{{ s.started_at|date:"DATETIME_FORMAT" }}
{% if s.is_first_of_species %}<span class="tag-new">{% trans "Erstfang" %}</span>{% endif %}
</div>
<div class="when">
{{ s.duration_s|floatformat:0 }}s · {% blocktrans with a=s.frames_with_bird b=s.frames_total %}{{ a }} von {{ b }} Bildern mit Vogel{% endblocktrans %}
{% if s.camera %} · {{ s.camera.name }}{% endif %}
</div>
</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>
{% elif not entry %}
<div class="empty" style="margin-top:24px">
<strong style="color:var(--text); font-size:17px">{% trans "Noch nicht gesammelt." %}</strong><br>
{% blocktrans with n=species.name %}Sobald {{ n }} vor der Kamera landet, wird dieser Eintrag freigeschaltet — mit eurem eigenen Foto.{% endblocktrans %}
</div>
{% endif %}
</div>
{% endblock %}