diff --git a/frigate/api/classification.py b/frigate/api/classification.py index 9cd3e0db1..e142735be 100644 --- a/frigate/api/classification.py +++ b/frigate/api/classification.py @@ -2,6 +2,7 @@ import logging import os +from pathvalidate import sanitize_filename from fastapi import APIRouter, Request, UploadFile from fastapi.responses import JSONResponse @@ -46,8 +47,8 @@ async def register_face(request: Request, name: str, file: UploadFile): ) -@router.delete("/faces") -def deregister_faces(request: Request, body: dict = None): +@router.post("/faces/{name}/delete") +def deregister_faces(request: Request, name: str, body: dict = None): json: dict[str, any] = body or {} list_of_ids = json.get("ids", "") @@ -58,7 +59,9 @@ def deregister_faces(request: Request, body: dict = None): ) context: EmbeddingsContext = request.app.embeddings - context.delete_face_ids(list_of_ids) + context.delete_face_ids( + name, map(lambda file: sanitize_filename(file), list_of_ids) + ) return JSONResponse( content=({"success": True, "message": "Successfully deleted faces."}), status_code=200, diff --git a/frigate/embeddings/__init__.py b/frigate/embeddings/__init__.py index 235b15df3..9836ae28e 100644 --- a/frigate/embeddings/__init__.py +++ b/frigate/embeddings/__init__.py @@ -14,7 +14,7 @@ from setproctitle import setproctitle from frigate.comms.embeddings_updater import EmbeddingsRequestEnum, EmbeddingsRequestor from frigate.config import FrigateConfig -from frigate.const import CONFIG_DIR +from frigate.const import CONFIG_DIR, FACE_DIR from frigate.db.sqlitevecq import SqliteVecQueueDatabase from frigate.models import Event from frigate.util.builtin import serialize @@ -209,8 +209,13 @@ class EmbeddingsContext: return self.db.execute_sql(sql_query).fetchall() - def delete_face_ids(self, ids: list[str]) -> None: - self.db.delete_embeddings_face(ids) + def delete_face_ids(self, face: str, ids: list[str]) -> None: + folder = os.path.join(FACE_DIR, face) + for id in ids: + file_path = os.path.join(folder, id) + + if os.path.isfile(file_path): + os.unlink(file_path) def update_description(self, event_id: str, description: str) -> None: self.requestor.send_data( diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx index 381c2f14d..fd6d50223 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -3,13 +3,16 @@ import Chip from "@/components/indicators/Chip"; import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { Toaster } from "@/components/ui/sonner"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import useOptimisticState from "@/hooks/use-optimistic-state"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect, useMemo, useRef, useState } from "react"; +import axios from "axios"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { isDesktop } from "react-device-detect"; import { useForm } from "react-hook-form"; import { LuTrash } from "react-icons/lu"; +import { toast } from "sonner"; import useSWR from "swr"; import { z } from "zod"; @@ -60,6 +63,8 @@ export default function FaceLibrary() { return (
+ +
{pageToggle && ( -
+
{faceImages.map((image: string) => ( ))} @@ -125,6 +130,27 @@ type FaceImageProps = { function FaceImage({ name, image }: FaceImageProps) { const [hovered, setHovered] = useState(false); + const onDelete = useCallback(() => { + axios + .post(`/faces/${name}/delete`, { ids: [image] }) + .then((resp) => { + if (resp.status == 200) { + toast.error(`Successfully deleted face.`, { position: "top-center" }); + } + }) + .catch((error) => { + if (error.response?.data?.message) { + toast.error(`Failed to delete: ${error.response.data.message}`, { + position: "top-center", + }); + } else { + toast.error(`Failed to delete: ${error.message}`, { + position: "top-center", + }); + } + }); + }, [name, image]); + return (
{hovered && (
- + onDelete()} + >