Spaces:
Sleeping
Sleeping
| """ | |
| Read-only lookup against the Attendance roster (attendance_store.json), for pipelines | |
| that want to recognize an enrolled student from a face embedding without depending on | |
| the Flask attendance_service package (which has side effects like incremental gallery | |
| growth that shouldn't run from an anonymous video-analysis context). | |
| Uses the same embedding space and matching formula as | |
| activity_web/backend/attendance_service.py:match_student (max of prototype similarity | |
| and best individual stored embedding similarity), so the same threshold applies | |
| regardless of which pipeline produced the query embedding. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| import numpy as np | |
| # Higher than Attendance's photo-based 0.28 (attendance_service.py, unaffected by this | |
| # module) — video frames tend to be lower-quality/more-varied-angle than a still | |
| # classroom photo, and this is a read-only "bonus" recognition, not the primary | |
| # attendance record, so it's worth erring conservative here. | |
| DEFAULT_MATCH_THRESHOLD = 0.35 | |
| # Minimum gap over the second-best candidate student to accept a match — guards | |
| # against accepting a mediocre match just because it's the best of a bad bunch. | |
| DEFAULT_MATCH_MARGIN = 0.05 | |
| def _normalize(vector: np.ndarray) -> np.ndarray: | |
| vector = np.asarray(vector, dtype=np.float32).reshape(-1) | |
| norm = float(np.linalg.norm(vector)) | |
| return vector / norm if norm > 0 else vector | |
| def load_roster(store_path: str | Path) -> list[dict]: | |
| """Load the enrolled students list from an attendance_store.json path. | |
| Returns [] if the file doesn't exist or can't be parsed.""" | |
| store_path = Path(store_path) | |
| if not store_path.exists(): | |
| return [] | |
| try: | |
| data = json.loads(store_path.read_text()) | |
| except Exception: | |
| return [] | |
| return data.get("students", []) or [] | |
| def match_against_roster( | |
| embedding: np.ndarray, | |
| students: list[dict], | |
| threshold: float = DEFAULT_MATCH_THRESHOLD, | |
| margin: float = DEFAULT_MATCH_MARGIN, | |
| ) -> tuple[str | None, float]: | |
| """Compare an embedding against every enrolled student's prototype + stored | |
| embeddings and return (name, similarity) for the best match, requiring it to | |
| both clear `threshold` and beat the second-best candidate student by at least | |
| `margin`. Returns (None, best_similarity) if nothing qualifies.""" | |
| if not students: | |
| return None, -1.0 | |
| q = _normalize(embedding) | |
| best_name, best_similarity = None, -1.0 | |
| second_best_similarity = -1.0 | |
| for student in students: | |
| prototype = np.asarray(student.get("prototype") or [], dtype=np.float32) | |
| sim = float(np.dot(q, prototype)) if prototype.size > 0 else -1.0 | |
| stored = student.get("embeddings") or [] | |
| if stored: | |
| mat = np.asarray(stored, dtype=np.float32) | |
| sim = max(sim, float((mat @ q).max())) | |
| if sim > best_similarity: | |
| second_best_similarity = best_similarity | |
| best_similarity = sim | |
| best_name = student.get("name") | |
| elif sim > second_best_similarity: | |
| second_best_similarity = sim | |
| if ( | |
| best_name is None | |
| or best_similarity < threshold | |
| or (best_similarity - second_best_similarity) < margin | |
| ): | |
| return None, best_similarity | |
| return best_name, best_similarity | |