Spaces:
Sleeping
Sleeping
| """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) | |