Fotos: Fotowand je Verband + Gesichtssuche (lokales SCRFD/MobileFaceNet via onnxruntime)

This commit is contained in:
Nicolas Gruber
2026-10-19 11:28:59 +02:00
parent 32b4dc4823
commit d97164cddc
12 changed files with 640 additions and 2 deletions
+1
View File
@@ -18,6 +18,7 @@
<a href="/plan" class="{{ 'active' if path.startswith('/plan') }}">Studentenplan</a>
<a href="/stg" class="{{ 'active' if path.startswith('/stg') }}">Studiengänge</a>
<a href="/liste" class="{{ 'active' if path.startswith('/liste') }}">Namenslisten</a>
<a href="/fotos" class="{{ 'active' if path.startswith('/fotos') }}">Fotos</a>
<a href="/" class="{{ 'active' if path == '/' }}">Start</a>
</nav>
</div>
+75
View File
@@ -0,0 +1,75 @@
{% extends "base.html" %}
{% block title %}Fotos{% endblock %}
{% block content %}
<h1 class="page-title">Fotos
{% if stg %}<span class="badge">{{ stg.code }} - {{ stg.name }}</span>{% endif %}
<span class="badge badge-muted">{{ ver == 'V' and 'Vollzeit' or ver }}</span>
</h1>
<div class="card">
<form class="form-row" method="get" action="/fotos">
<div class="field">
<label>Studiengang</label>
<select name="kz" required>
{% for s in meta.stgs %}
<option value="{{ s.kz }}" {{ 'selected' if s.kz == kz }}>{{ s.code }} - {{ s.name }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label>Studien-Semester</label>
<select name="sem">
{% for n in range(1, 9) %}
<option value="{{ n }}" {{ 'selected' if (n | string) == sem }}>{{ n }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label>Verband</label>
<select name="ver">
{% for v in verbands %}
<option value="{{ v }}" {{ 'selected' if v == ver }}>{{ v }}{{ verband_label(v) }}</option>
{% endfor %}
</select>
</div>
<button class="btn btn-primary" type="submit">Laden</button>
</form>
</div>
<div class="card">
<h2>Fotowand <span class="count-pill">{{ wall | selectattr('has_photo') | list | length }}/{{ wall | length }}</span></h2>
<div class="photo-wall" id="photo-wall">
{% for p in wall %}
<div class="photo-tile" data-uid="{{ p.matrikel }}" data-initials="{{ (p.nachname[:1] ~ p.vorname[:1]) | upper }}">
<div class="photo-frame">
{% if p.person_id %}
<img loading="lazy" src="/fotos/img/{{ p.person_id }}" alt="{{ p.nachname }}"
onerror="this.closest('.photo-frame').innerHTML='<div class=&quot;avatar-initials&quot;>' + this.closest('.photo-tile').dataset.initials + '</div>'">
{% else %}
<div class="avatar-initials">{{ (p.nachname[:1] ~ p.vorname[:1]) | upper }}</div>
{% endif %}
</div>
<div class="photo-name">{{ p.nachname }}{% if p.vorname %}, {{ p.vorname }}{% endif %}</div>
{% if p.matrikel == own_uid %}<div class="photo-own">Das bist du</div>{% endif %}
</div>
{% else %}
<p class="empty">Keine Studierenden in diesem Verband/Semester.</p>
{% endfor %}
</div>
</div>
<div class="card">
<h2>Gesichtssuche</h2>
<p class="hint">Foto von dir hochladen - die App findet die passenden Profile in diesem Verband.</p>
<form class="form-row form-stack" method="post" action="/fotos/suche" enctype="multipart/form-data">
<input type="hidden" name="kz" value="{{ kz }}">
<input type="hidden" name="sem" value="{{ sem }}">
<input type="hidden" name="ver" value="{{ ver }}">
<div class="field">
<label>Foto</label>
<input type="file" name="file" accept="image/*" required>
</div>
<button class="btn btn-primary" type="submit">Suchen</button>
</form>
</div>
{% endblock %}
+74
View File
@@ -0,0 +1,74 @@
{% extends "base.html" %}
{% block title %}Gesichtssuche{% endblock %}
{% block content %}
<h1 class="page-title">Gesichtssuche
{% if stg %}<span class="badge">{{ stg.code }} - {{ stg.name }}</span>{% endif %}
</h1>
<div class="card">
<form class="form-row" method="get" action="/fotos">
<div class="field">
<label>Studiengang</label>
<select name="kz">
{% for s in meta.stgs %}
<option value="{{ s.kz }}" {{ 'selected' if s.kz == kz }}>{{ s.code }} - {{ s.name }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label>Studien-Semester</label>
<select name="sem">
{% for n in range(1, 9) %}
<option value="{{ n }}" {{ 'selected' if (n | string) == sem }}>{{ n }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label>Verband</label>
<select name="ver">
{% for v in verbands %}
<option value="{{ v }}" {{ 'selected' if v == ver }}>{{ v }}{{ verband_label(v) }}</option>
{% endfor %}
</select>
</div>
<button class="btn btn-primary" type="submit">Zurück zur Fotowand</button>
</form>
</div>
{% if error %}
<div class="card">
<p class="empty" style="color:#b3261e">{{ error }}</p>
</div>
{% else %}
<div class="card">
<h2>
Ergebnisse
<span class="count-pill">{{ results | selectattr('sim', 'ge', 0.35) | list | length }} Treffer</span>
{% if took is defined %}<span class="badge badge-muted">{{ took }} s</span>{% endif %}
</h2>
{% if results | selectattr('sim', 'ge', 0.35) | list | length %}
<div class="match-grid">
{% for r in results[:30] %}
<div class="match-tile {{ 'match-strong' if r.sim >= 0.5 else ('match-mid' if r.sim >= 0.35 else '') }}">
<img src="{{ r.img }}" alt="{{ r.nachname }}">
<div class="match-name">
<a href="/plan?uid={{ r.matrikel }}">{{ r.nachname }}{% if r.vorname %}, {{ r.vorname }}{% endif %}</a>
</div>
<div class="match-sim">
<div class="sim-bar"><div style="width: {{ [r.sim * 100, 0] | max | round(0) }}%"></div></div>
<span class="sim-num">{{ [r.sim * 100, 0] | max | round(0) }} %</span>
</div>
</div>
{% endfor %}
</div>
<p class="hint" style="margin-top:12px">
<strong>Hinweis:</strong> Hohe Werte (grün, &ge; 50 %) deuten auf dieselbe Person hin.
Werte um 35–50 % (gelb) sind mögliche Treffer - bitte prüfen. Unter 35 % wird nicht als Treffer gezählt.
</p>
{% else %}
<p class="empty">Kein Treffer mit &ge; 35 % Ähnlichkeit gefunden.
Prüfe, ob das richtige Semester/Verband gewählt ist und das Foto ein klar sichtbares Gesicht zeigt.</p>
{% endif %}
</div>
{% endif %}
{% endblock %}