diff --git a/frigate/data_processing/real_time/face_processor.py b/frigate/data_processing/real_time/face_processor.py index 47abfdd9c..199dd6bd4 100644 --- a/frigate/data_processing/real_time/face_processor.py +++ b/frigate/data_processing/real_time/face_processor.py @@ -422,6 +422,49 @@ 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, + } + + # Rename the directory + os.rename(old_folder, new_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 5d8496e96..e9889a7f1 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -23,7 +23,7 @@ import { cn } from "@/lib/utils"; import { FrigateConfig } from "@/types/frigateConfig"; import axios from "axios"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { LuImagePlus, LuTrash2, LuUserPlus } from "react-icons/lu"; +import { LuImagePlus, LuTrash2, LuUserPlus, LuEdit } from "react-icons/lu"; import { toast } from "sonner"; import useSWR from "swr"; import { Input } from "@/components/ui/input"; @@ -150,6 +150,37 @@ export default function FaceLibrary() { } }, [newFaceName, refreshFaces]); + const [renameDialog, setRenameDialog] = useState(false); + const [isRenaming, setIsRenaming] = useState(false); + const [renameData, setRenameData] = useState<{ oldName: string; newName: string }>({ oldName: '', newName: '' }); + + const renameFace = useCallback(async () => { + if (!renameData.newName.trim()) { + toast.error("Face name cannot be empty", { position: "top-center" }); + return; + } + + setIsRenaming(true); + try { + const resp = await axios.post(`/faces/${renameData.oldName}/rename`, { + new_name: renameData.newName + }); + if (resp.status === 200) { + setRenameDialog(false); + setRenameData({ oldName: '', newName: '' }); + refreshFaces(); + toast.success("Successfully renamed face", { position: "top-center" }); + } + } catch (error) { + toast.error( + `Failed to rename face: ${error.response?.data?.message || error.message}`, + { position: "top-center" } + ); + } finally { + setIsRenaming(false); + } + }, [renameData, refreshFaces]); + if (!config) { return ; } @@ -178,6 +209,26 @@ export default function FaceLibrary() { + + + + Rename Face + + + setRenameData(prev => ({ ...prev, newName: e.target.value }))} + onKeyPress={(e) => e.key === 'Enter' && renameFace()} + disabled={isRenaming} + /> + + {isRenaming ? "Renaming..." : "Rename"} + + + + + ( {item} ({faceData[item].length}) + + + { + e.stopPropagation(); + setRenameData({ oldName: item, newName: item }); + setRenameDialog(true); + }} + > + + + + Rename Face + ))}