From 754ae4db3762b34609cc108b78fd256dc0ffcf50 Mon Sep 17 00:00:00 2001 From: Weitheng Haw Date: Tue, 28 Jan 2025 15:32:47 +0000 Subject: [PATCH] Add rename api --- frigate/api/classification.py | 73 +++++++++++++++++-- .../real_time/face_processor.py | 43 ----------- web/src/pages/FaceLibrary.tsx | 30 ++------ 3 files changed, 74 insertions(+), 72 deletions(-) diff --git a/frigate/api/classification.py b/frigate/api/classification.py index fd23cb9d4..1694b41c1 100644 --- a/frigate/api/classification.py +++ b/frigate/api/classification.py @@ -109,16 +109,22 @@ def deregister_faces(request: Request, name: str, body: dict = None): ) json: dict[str, any] = body or {} - list_of_ids = json.get("ids", "") + list_of_ids = json.get("ids", []) - if not list_of_ids or len(list_of_ids) == 0: + if not list_of_ids: return JSONResponse( - content=({"success": False, "message": "Not a valid list of ids"}), + content={"success": False, "message": "Not a valid list of ids"}, status_code=404, ) face_dir = os.path.join(FACE_DIR, name) + if not os.path.exists(face_dir): + return JSONResponse( + status_code=404, + content={"message": f"Face '{name}' not found", "success": False}, + ) + context: EmbeddingsContext = request.app.embeddings context.delete_face_ids( name, map(lambda file: sanitize_filename(file), list_of_ids) @@ -130,12 +136,12 @@ def deregister_faces(request: Request, name: str, body: dict = None): except Exception as e: logger.error(f"Failed to remove directory {face_dir}: {str(e)}") return JSONResponse( - content=({"success": False, "message": f"Failed to remove directory: {str(e)}"}), + content={"success": False, "message": f"Failed to remove directory: {str(e)}"}, status_code=500, ) return JSONResponse( - content=({"success": True, "message": "Successfully deleted faces."}), + content={"success": True, "message": "Successfully deleted faces."}, status_code=200, ) @@ -155,3 +161,60 @@ def create_face(name: str): status_code=200, content={"message": "Successfully created face", "success": True}, ) + + +@router.post("/faces/{name}/rename") +def rename_face(request: Request, name: str, body: dict = None): + """Rename a face directory.""" + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + json: dict[str, any] = body or {} + new_name = json.get("new_name") + + if not new_name: + return JSONResponse( + status_code=400, + content={"message": "New name is required", "success": False}, + ) + + old_folder = os.path.join(FACE_DIR, name) + new_folder = os.path.join(FACE_DIR, new_name) + + if not os.path.exists(old_folder): + return JSONResponse( + status_code=404, + content={"message": f"Face '{name}' not found", "success": False}, + ) + + if os.path.exists(new_folder): + return JSONResponse( + status_code=400, + content={"message": f"Face '{new_name}' already exists", "success": False}, + ) + + try: + # Use atomic operation when possible + try: + os.rename(old_folder, new_folder) + except OSError: + # Fallback to copy+delete if rename fails + shutil.copytree(old_folder, new_folder) + shutil.rmtree(old_folder) + + context: EmbeddingsContext = request.app.embeddings + context.clear_face_classifier() + + return JSONResponse( + status_code=200, + content={"message": "Successfully renamed face", "success": True}, + ) + except Exception as e: + logger.error(f"Failed to rename face: {str(e)}") + return JSONResponse( + status_code=500, + content={"message": f"Failed to rename face: {str(e)}", "success": False}, + ) diff --git a/frigate/data_processing/real_time/face_processor.py b/frigate/data_processing/real_time/face_processor.py index b4ea317a7..5fc7a6161 100644 --- a/frigate/data_processing/real_time/face_processor.py +++ b/frigate/data_processing/real_time/face_processor.py @@ -423,49 +423,6 @@ class FaceProcessor(RealTimeProcessorApi): "message": "Internal server error", "success": False, } - elif topic == "rename_face": - old_name = request_data.get("old_name") - new_name = request_data.get("new_name") - - if not self.__validate_face_name(new_name): - return { - "message": "Invalid new face name", - "success": False, - } - - try: - old_folder = os.path.join(FACE_DIR, old_name) - new_folder = os.path.join(FACE_DIR, new_name) - - if not os.path.exists(old_folder): - return { - "message": f"Face '{old_name}' not found", - "success": False, - } - - if os.path.exists(new_folder): - return { - "message": f"Face name '{new_name}' already exists", - "success": False, - } - - shutil.copytree(old_folder, new_folder) - shutil.rmtree(old_folder) - - # Clear and rebuild classifier with new names - self.__clear_classifier() - self.__build_classifier() - - return { - "message": "Successfully renamed face", - "success": True, - } - except Exception as e: - logger.error(f"Failed to rename face: {str(e)}") - return { - "message": f"Failed to rename face: {str(e)}", - "success": False, - } def expire_object(self, object_id: str): if object_id in self.detected_faces: diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx index bc6004ebe..590c4c475 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -164,28 +164,8 @@ export default function FaceLibrary() { setIsRenaming(true); try { - await axios.post(`/faces/${renameData.newName}/create`); - - const oldFaceImages = faceData[renameData.oldName] || []; - const copyPromises = oldFaceImages.map(async (image: string) => { - const response = await fetch(`${baseUrl}clips/faces/${renameData.oldName}/${image}`); - const blob = await response.blob(); - - const formData = new FormData(); - formData.append('file', new File([blob], image)); - formData.append('cropped', 'true'); - - return axios.post(`/faces/${renameData.newName}`, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - }); - - await Promise.all(copyPromises); - - await axios.post(`/faces/${renameData.oldName}/delete`, { - ids: oldFaceImages.length ? oldFaceImages : ['dummy'] + await axios.post(`/faces/${renameData.oldName}/rename`, { + new_name: renameData.newName }); setRenameDialog(false); @@ -201,7 +181,7 @@ export default function FaceLibrary() { } finally { setIsRenaming(false); } - }, [renameData, faceData, refreshFaces]); + }, [renameData, refreshFaces]); const deleteFace = useCallback(async () => { try { @@ -210,7 +190,9 @@ export default function FaceLibrary() { ids: images.length ? images : ['dummy'] }); setRenameDialog(false); - setPageToggle(faces[0]); + + const nextFace = faces.find(face => face !== renameData.oldName) || null; + setPageToggle(nextFace); await refreshFaces(); toast.success("Successfully deleted face", { position: "top-center" }); } catch (error) {