"""database.py — Persistent storage for groups, students, attendance""" import json, uuid, os from pathlib import Path from datetime import datetime DATA_DIR = Path("data") EMBED_FILE = DATA_DIR / "embeddings.json" GROUPS_FILE = DATA_DIR / "groups.json" ATT_DIR = DATA_DIR / "attendance" def _init(): DATA_DIR.mkdir(exist_ok=True) ATT_DIR.mkdir(exist_ok=True) # ── Groups ──────────────────────────────────────────────────── def load_groups(): _init() if not GROUPS_FILE.exists(): return {} try: return json.loads(GROUPS_FILE.read_text()) except: return {} def save_groups(g): _init(); GROUPS_FILE.write_text(json.dumps(g, indent=2)) def create_group(name, color="#a855f7"): g = load_groups() if any(v["name"].lower()==name.lower() for v in g.values()): return None, "Group already exists" gid = str(uuid.uuid4())[:8] g[gid] = {"name": name, "color": color, "created": datetime.now().isoformat()} save_groups(g); return gid, "OK" def delete_group(gid): g = load_groups() if gid not in g: return False del g[gid]; save_groups(g) # remove students e = load_embeddings() for sid in [k for k,v in e.items() if v.get("group_id")==gid]: del e[sid] save_embeddings(e); return True # ── Students / Embeddings ───────────────────────────────────── def load_embeddings(): _init() if not EMBED_FILE.exists(): return {} try: return json.loads(EMBED_FILE.read_text()) except: return {} def save_embeddings(e): _init(); EMBED_FILE.write_text(json.dumps(e, indent=2)) def add_student(name, group_id, fingerprint): """ fingerprint = list of vectors (one per captured pose/angle) OR a single plain vector (legacy) OR { 'insight': list-of-vectors|vector|None, 'facenet':..., 'geo':... } Each pose is stored separately so matching can pick the best-fitting angle instead of averaging poses into one blurry vector. One registration per name per group. """ e = load_embeddings() if any(v["group_id"]==group_id and v["name"].lower()==name.lower() for v in e.values()): return None, f"'{name}' already registered in this group" sid = str(uuid.uuid4())[:12] def _as_pose_list(v): if v is None: return None # list of vectors (each vector is itself a list of floats) if isinstance(v, list) and v and isinstance(v[0], list): return v # single flat vector -> wrap as a 1-pose list if isinstance(v, list): return [v] return None if isinstance(fingerprint, dict): fp_dict = fingerprint insight = _as_pose_list(fp_dict.get("insight")) facenet = _as_pose_list(fp_dict.get("facenet")) geo = _as_pose_list(fp_dict.get("geo")) else: insight = _as_pose_list(fingerprint) facenet = None geo = None e[sid] = { "name": name, "group_id": group_id, "insight": insight, # list of pose vectors, e.g. [front, left, right] "facenet": facenet, "geo": geo, "registered": datetime.now().isoformat(), } save_embeddings(e); return sid, "OK" def delete_student(sid): e = load_embeddings() if sid not in e: return False del e[sid]; save_embeddings(e); return True def get_students_in_group(gid): e = load_embeddings() return [{"id":k,"name":v["name"],"registered":v.get("registered","")} for k,v in e.items() if v.get("group_id")==gid] def get_group_embeddings(gid): e = load_embeddings() return {k:v for k,v in e.items() if v.get("group_id")==gid} def get_all_embeddings(): return load_embeddings() # ── Attendance ──────────────────────────────────────────────── def today(): return datetime.now().strftime("%Y-%m-%d") def _att_file(d=None): _init(); return ATT_DIR / f"{d or today()}.json" def load_att(): f = _att_file() if not f.exists(): return {} try: return json.loads(f.read_text()) except: return {} def save_att(a): _att_file().write_text(json.dumps(a, indent=2)) def mark_present(sid, name, gid): a = load_att() now = datetime.now() if sid in a: last = datetime.fromisoformat(a[sid]["time"]) if (now-last).total_seconds() < 600: return False a[sid] = {"name":name,"group_id":gid,"time":now.isoformat(),"status":"present"} save_att(a); return True def get_summary(gid=None): a = load_att() e = load_embeddings() all_s = {k:v for k,v in e.items() if gid is None or v.get("group_id")==gid} present = [{"id":k,"name":v["name"],"group_id":v.get("group_id",""),"time":a[k]["time"]} for k,v in all_s.items() if k in a] absent = [{"id":k,"name":v["name"],"group_id":v.get("group_id","")} for k,v in all_s.items() if k not in a] return {"present":present,"absent":absent} def reset_att(): f = _att_file() if f.exists(): f.unlink() reset_today = reset_att # alias used in app.py def export_csv(gid=None): s = get_summary(gid); g = load_groups() gn = lambda id: g.get(id,{}).get("name",id) rows = ["Name,Group,Status,Time"] for x in s["present"]: rows.append(f"{x['name']},{gn(x['group_id'])},Present,{x['time'][:19].replace('T',' ')}") for x in s["absent"]: rows.append(f"{x['name']},{gn(x['group_id'])},Absent,") return "\n".join(rows)