Spaces:
Runtime error
Runtime error
| """ | |
| api/faces.py — Endpoints quản lý khuôn mặt (upload & lấy ảnh) | |
| POST /api/faces — Upload & đăng ký khuôn mặt mới | |
| GET /api/face-crops/<id> — Lấy ảnh crop từ MongoDB | |
| GET /api/face_crop/<id> — Legacy alias | |
| """ | |
| import logging | |
| import os | |
| from flask import Blueprint, Response, jsonify, request | |
| import config | |
| from face_model.analyzer import face_analyzer | |
| from storage.mongo_storage import mongo_storage | |
| from storage.pg_storage import pg_storage | |
| from utils.image import crop_face, decode_image, image_to_bytes | |
| logger = logging.getLogger("api.faces") | |
| bp = Blueprint("faces", __name__, url_prefix="/api") | |
| def upload_face(): | |
| """ | |
| POST /api/faces | |
| Body: multipart/form-data | |
| - file: ảnh JPEG/PNG | |
| - name: tên người (form field hoặc query param, tùy chọn) | |
| Luồng: | |
| 1. Detect khuôn mặt | |
| 2. Crop → lưu MongoDB face_crops (image_type='registration') | |
| 3. Đăng ký persons + face_embeddings trên Supabase | |
| 4. Cập nhật RAM cache | |
| """ | |
| if "file" not in request.files: | |
| return jsonify({"error": { | |
| "code": "FILE_REQUIRED", | |
| "message": "Không tìm thấy file ảnh trong request.", | |
| "status": 400, | |
| }}), 400 | |
| file = request.files["file"] | |
| if file.filename == "": | |
| return jsonify({"error": { | |
| "code": "EMPTY_FILENAME", | |
| "message": "Tên file rỗng.", | |
| "status": 400, | |
| }}), 400 | |
| # Lấy tên người từ form/query | |
| name = ( | |
| request.args.get("name") | |
| or request.form.get("name") | |
| or os.path.splitext(file.filename)[0] | |
| ) | |
| try: | |
| file_bytes = file.read() | |
| img = decode_image(file_bytes) | |
| if img is None: | |
| return jsonify({"error": { | |
| "code": "INVALID_IMAGE", | |
| "message": "Không đọc được ảnh.", | |
| "status": 400, | |
| }}), 400 | |
| # Detect face | |
| faces = face_analyzer.get_faces(img) | |
| if not faces: | |
| return jsonify({"error": { | |
| "code": "NO_FACE_DETECTED", | |
| "message": "Không tìm thấy khuôn mặt trong ảnh.", | |
| "status": 422, | |
| }}), 422 | |
| # Crop khuôn mặt đầu tiên | |
| face = faces[0] | |
| bbox_arr = face.bbox.astype(int) | |
| embedding = face.normed_embedding | |
| det_score = float(face.det_score) if hasattr(face, "det_score") else 0.0 | |
| face_img, bbox_coords = crop_face(img, bbox_arr, padding=20) | |
| crop_bytes = image_to_bytes(face_img) | |
| # Lưu raw image vào MongoDB (source_type = upload) | |
| h, w = img.shape[:2] | |
| raw_image_id = mongo_storage.save_raw_image( | |
| img_bytes=file_bytes, | |
| source_type="upload", | |
| width=w, | |
| height=h, | |
| fmt="jpeg", | |
| ) | |
| # Lưu face_crop vào MongoDB (image_type = registration) | |
| crop_id = mongo_storage.save_face_crop( | |
| img_bytes=crop_bytes, | |
| raw_image_id=raw_image_id, | |
| bbox=bbox_coords, | |
| image_type="registration", | |
| confidence=det_score, | |
| quality_score=det_score, | |
| model_name=config.MODEL_NAME, | |
| ) | |
| if not crop_id: | |
| return jsonify({"error": { | |
| "code": "STORAGE_FAILED", | |
| "message": "Không thể lưu ảnh vào MongoDB.", | |
| "status": 500, | |
| }}), 500 | |
| # Đăng ký lên Supabase | |
| person_id, embedding_id = pg_storage.register_new_face( | |
| name=name, | |
| embedding=embedding.tolist(), | |
| face_crop_mongo_id=crop_id, | |
| ) | |
| # Cập nhật links trong face_crop | |
| mongo_storage.update_face_crop_links( | |
| crop_id=crop_id, | |
| person_id=person_id, | |
| visit_id="", | |
| recognition_log_id="", | |
| ) | |
| # Cập nhật RAM cache | |
| face_analyzer.add_to_cache( | |
| embedding=embedding, | |
| name=name, | |
| person_id=person_id, | |
| embedding_id=embedding_id, | |
| mongo_id=crop_id, | |
| ) | |
| return jsonify({ | |
| "success": True, | |
| "person_id": person_id, | |
| "name": name, | |
| "crop_id": crop_id, | |
| "url": f"/api/face-crops/{crop_id}", | |
| "total_faces": face_analyzer.total, | |
| }), 201 | |
| except Exception as e: | |
| logger.error(f"[upload_face] Error: {e}", exc_info=True) | |
| return jsonify({"error": { | |
| "code": "UPLOAD_FAILED", | |
| "message": f"Lỗi khi đăng ký khuôn mặt: {e}", | |
| "status": 500, | |
| }}), 500 | |
| def upload_legacy(): | |
| """ | |
| Legacy alias → POST /api/faces (backward compatibility với frontend). | |
| Response format giữ nguyên để frontend không cần sửa. | |
| """ | |
| if "file" not in request.files: | |
| return jsonify({"detail": "Không tìm thấy file tải lên."}), 400 | |
| file = request.files["file"] | |
| name = ( | |
| request.args.get("name") | |
| or request.form.get("name") | |
| or os.path.splitext(file.filename or "face")[0] | |
| ) | |
| try: | |
| file_bytes = file.read() | |
| img = decode_image(file_bytes) | |
| if img is None: | |
| return jsonify({"detail": "Không đọc được ảnh."}), 400 | |
| faces = face_analyzer.get_faces(img) | |
| if not faces: | |
| return jsonify({"detail": "Không tìm thấy khuôn mặt trong ảnh."}), 422 | |
| face = faces[0] | |
| bbox_arr = face.bbox.astype(int) | |
| embedding = face.normed_embedding | |
| det_score = float(face.det_score) if hasattr(face, "det_score") else 0.0 | |
| face_img, bbox_coords = crop_face(img, bbox_arr, padding=20) | |
| crop_bytes = image_to_bytes(face_img) | |
| h, w = img.shape[:2] | |
| raw_image_id = mongo_storage.save_raw_image( | |
| img_bytes=file_bytes, source_type="upload", width=w, height=h, fmt="jpeg" | |
| ) | |
| crop_id = mongo_storage.save_face_crop( | |
| img_bytes=crop_bytes, raw_image_id=raw_image_id, | |
| bbox=bbox_coords, image_type="registration", | |
| confidence=det_score, quality_score=det_score, | |
| model_name=config.MODEL_NAME, | |
| ) | |
| if not crop_id: | |
| return jsonify({"detail": "Lỗi lưu trữ hình ảnh vào MongoDB Atlas."}), 500 | |
| person_id, embedding_id = pg_storage.register_new_face( | |
| name=name, embedding=embedding.tolist(), face_crop_mongo_id=crop_id | |
| ) | |
| mongo_storage.update_face_crop_links( | |
| crop_id=crop_id, person_id=person_id, visit_id="", recognition_log_id="" | |
| ) | |
| face_analyzer.add_to_cache( | |
| embedding=embedding, name=name, | |
| person_id=person_id, embedding_id=embedding_id, mongo_id=crop_id, | |
| ) | |
| return jsonify({ | |
| "success": True, | |
| "filename": f"{name}.jpg", | |
| "name": name, | |
| "url": f"/api/face-crops/{crop_id}", | |
| "total_faces": face_analyzer.total, | |
| }), 200 | |
| except Exception as e: | |
| logger.error(f"[upload_legacy] Error: {e}", exc_info=True) | |
| return jsonify({"detail": f"Lỗi khi đăng ký khuôn mặt: {e}"}), 500 | |
| # ── Serve ảnh crop ───────────────────────────────────────────────────────── | |
| def get_face_crop(crop_id: str): | |
| """GET /api/face-crops/<crop_id> — Lấy ảnh crop từ MongoDB.""" | |
| img_bytes = mongo_storage.get_face_crop_bytes(crop_id) | |
| if not img_bytes: | |
| return jsonify({"error": { | |
| "code": "NOT_FOUND", | |
| "message": "Không tìm thấy hình ảnh khuôn mặt.", | |
| "status": 404, | |
| }}), 404 | |
| return Response(img_bytes, mimetype="image/jpeg") | |
| def get_face_crop_legacy(crop_id: str): | |
| """Legacy alias → GET /api/face-crops/<crop_id>""" | |
| return get_face_crop(crop_id) | |