""" 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/ — Lấy ảnh crop từ MongoDB GET /api/face_crop/ — 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") @bp.post("/faces") 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 @bp.post("/upload") 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 ───────────────────────────────────────────────────────── @bp.get("/face-crops/") def get_face_crop(crop_id: str): """GET /api/face-crops/ — 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") @bp.get("/face_crop/") def get_face_crop_legacy(crop_id: str): """Legacy alias → GET /api/face-crops/""" return get_face_crop(crop_id)