Fotos: Fotowand je Verband + Gesichtssuche (lokales SCRFD/MobileFaceNet via onnxruntime)
This commit is contained in:
+155
-2
@@ -1,10 +1,12 @@
|
||||
"""CIS Enhanced - FastAPI app."""
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI, Query, Request
|
||||
from fastapi import FastAPI, File, Form, Query, Request, UploadFile
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, Response
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
@@ -17,9 +19,12 @@ for _var in ("CIS_USER", "CIS_PASSWORD"):
|
||||
|
||||
from fastapi.staticfiles import StaticFiles # noqa: E402
|
||||
|
||||
import numpy as np # noqa: E402
|
||||
|
||||
from .cis import CISClient, SLOTS # noqa: E402
|
||||
from .face import engine # noqa: E402
|
||||
from .util import (current_semester, decode_matrikel, events_to_json, fmt_range, # noqa: E402
|
||||
lv_color, week_label, week_start)
|
||||
lv_color, week_label, week_start)
|
||||
|
||||
BASE = os.environ["CIS_BASE_URL"].rstrip("/")
|
||||
client = CISClient(os.environ["CIS_USER"], os.environ["CIS_PASSWORD"], BASE)
|
||||
@@ -158,6 +163,154 @@ def liste(request: Request,
|
||||
})
|
||||
|
||||
|
||||
# ---------- Fotos / Gesichter ----------
|
||||
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
|
||||
EMB_CACHE = DATA_DIR / "embeddings.json"
|
||||
MAX_UPLOAD = 8 * 1024 * 1024
|
||||
|
||||
|
||||
def _emb_cache() -> dict:
|
||||
if EMB_CACHE.exists():
|
||||
try:
|
||||
return json.loads(EMB_CACHE.read_text())
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def _embed_person(pid: str, photo: bytes) -> np.ndarray | None:
|
||||
cache = _emb_cache()
|
||||
if pid in cache:
|
||||
return np.asarray(cache[pid], dtype=np.float32)
|
||||
v = engine.embed(photo)
|
||||
if v is None:
|
||||
return None
|
||||
cache[pid] = v.tolist()
|
||||
try:
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
EMB_CACHE.write_text(json.dumps(cache))
|
||||
except OSError:
|
||||
pass
|
||||
return v
|
||||
|
||||
|
||||
def _photo_wall(kz: str, sem: str, ver: str) -> list[dict]:
|
||||
"""Students of a verband with person_id + whether a photo exists (cached check)."""
|
||||
students = client.namensliste(kz, sem, ver)
|
||||
wall = []
|
||||
for s in students:
|
||||
pid = client.person_id_for(s.matrikel)
|
||||
wall.append({
|
||||
"nachname": s.nachname, "vorname": s.vorname,
|
||||
"matrikel": s.matrikel, "person_id": pid,
|
||||
"has_photo": bool(pid),
|
||||
})
|
||||
return wall
|
||||
|
||||
|
||||
@app.get("/fotos", response_class=HTMLResponse)
|
||||
def fotos_page(request: Request,
|
||||
kz: str = Query("640"),
|
||||
sem: str = Query("1"),
|
||||
ver: str = Query("V")):
|
||||
meta = _meta()
|
||||
stg = next((x for x in meta["stgs"] if x["kz"] == kz), None)
|
||||
verbands = client.verbands_for(kz, sem)
|
||||
wall = _photo_wall(kz, sem, ver)
|
||||
return templates.TemplateResponse(request, "fotos.html", {
|
||||
"request": request,
|
||||
"meta": meta,
|
||||
"kz": kz,
|
||||
"sem": sem,
|
||||
"ver": ver,
|
||||
"verbands": verbands,
|
||||
"stg": stg,
|
||||
"wall": wall,
|
||||
"wall_json": wall,
|
||||
"own_uid": OWN_UID,
|
||||
})
|
||||
|
||||
|
||||
@app.get("/fotos/img/{person_id}")
|
||||
def fotos_img(person_id: str):
|
||||
if not person_id.isdigit():
|
||||
return JSONResponse({"error": "Ungültige ID"}, status_code=400)
|
||||
res = client.photo_bytes(person_id)
|
||||
if not res:
|
||||
return JSONResponse({"error": "Kein Foto vorhanden"}, status_code=404)
|
||||
data, ct = res
|
||||
return Response(data, media_type=ct)
|
||||
|
||||
|
||||
@app.post("/fotos/suche", response_class=HTMLResponse)
|
||||
def fotos_suche(request: Request,
|
||||
kz: str = Form("640"),
|
||||
sem: str = Form("1"),
|
||||
ver: str = Form("V"),
|
||||
file: UploadFile = File(...)):
|
||||
data = file.file.read()
|
||||
if not data:
|
||||
return templates.TemplateResponse(request, "fotos_result.html", {
|
||||
"request": request, "error": "Leere Datei hochgeladen.",
|
||||
"kz": kz, "sem": sem, "ver": ver, "results": [], "meta": _meta(),
|
||||
"stg": next((x for x in _meta()["stgs"] if x["kz"] == kz), None),
|
||||
"verbands": client.verbands_for(kz, sem),
|
||||
"own_uid": OWN_UID,
|
||||
}, status_code=400)
|
||||
if len(data) > MAX_UPLOAD:
|
||||
return templates.TemplateResponse(request, "fotos_result.html", {
|
||||
"request": request, "error": "Datei zu groß (max. 8 MB).",
|
||||
"kz": kz, "sem": sem, "ver": ver, "results": [], "meta": _meta(),
|
||||
"stg": next((x for x in _meta()["stgs"] if x["kz"] == kz), None),
|
||||
"verbands": client.verbands_for(kz, sem),
|
||||
"own_uid": OWN_UID,
|
||||
}, status_code=400)
|
||||
emb = engine.embed(data)
|
||||
if emb is None:
|
||||
return templates.TemplateResponse(request, "fotos_result.html", {
|
||||
"request": request, "error": "Kein Gesicht im hochgeladenen Bild gefunden.",
|
||||
"kz": kz, "sem": sem, "ver": ver, "results": [], "meta": _meta(),
|
||||
"stg": next((x for x in _meta()["stgs"] if x["kz"] == kz), None),
|
||||
"verbands": client.verbands_for(kz, sem),
|
||||
"own_uid": OWN_UID,
|
||||
}, status_code=422)
|
||||
wall = _photo_wall(kz, sem, ver)
|
||||
results = []
|
||||
t0 = time.time()
|
||||
for p in wall:
|
||||
pid = p["person_id"]
|
||||
if not pid:
|
||||
continue
|
||||
res = client.photo_bytes(pid)
|
||||
if not res:
|
||||
continue
|
||||
e = _embed_person(pid, res[0])
|
||||
if e is None:
|
||||
continue
|
||||
sim = engine.similarity(emb, e)
|
||||
results.append({**p, "sim": round(sim, 4),
|
||||
"img": f"/fotos/img/{pid}"})
|
||||
results.sort(key=lambda r: r["sim"], reverse=True)
|
||||
meta = _meta()
|
||||
return templates.TemplateResponse(request, "fotos_result.html", {
|
||||
"request": request,
|
||||
"error": None,
|
||||
"took": round(time.time() - t0, 1),
|
||||
"query": {"kz": kz, "sem": sem, "ver": ver},
|
||||
"kz": kz, "sem": sem, "ver": ver,
|
||||
"results": results[:30],
|
||||
"meta": meta,
|
||||
"stg": next((x for x in meta["stgs"] if x["kz"] == kz), None),
|
||||
"verbands": client.verbands_for(kz, sem),
|
||||
"own_uid": OWN_UID,
|
||||
})
|
||||
|
||||
|
||||
@app.get("/api/person_id")
|
||||
def api_person_id(uid: str = Query(...)):
|
||||
return {"uid": uid, "person_id": client.person_id_for(uid)}
|
||||
|
||||
|
||||
@app.get("/api/meta")
|
||||
def api_meta():
|
||||
return _meta()
|
||||
|
||||
Reference in New Issue
Block a user