diff --git a/frigate/api/classification.py b/frigate/api/classification.py index c79229de5..df0d3b45e 100644 --- a/frigate/api/classification.py +++ b/frigate/api/classification.py @@ -117,7 +117,24 @@ def train_face(request: Request, name: str, body: dict = None): ) -@router.post("/faces/{name}") +@router.post("/faces/{name}/create") +async def create_face(request: Request, name: str, file: UploadFile): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + os.makedirs( + sanitize_filename(os.path.join(FACE_DIR, name.replace(" ", "_"))), exist_ok=True + ) + return JSONResponse( + status_code=200, + content={"success": False, "message": "Successfully created face folder."}, + ) + + +@router.post("/faces/{name}/register") async def register_face(request: Request, name: str, file: UploadFile): if not request.app.frigate_config.face_recognition.enabled: return JSONResponse( diff --git a/web/src/components/overlay/dialog/TextEntryDialog.tsx b/web/src/components/overlay/dialog/TextEntryDialog.tsx new file mode 100644 index 000000000..1b0655078 --- /dev/null +++ b/web/src/components/overlay/dialog/TextEntryDialog.tsx @@ -0,0 +1,88 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useCallback } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +type TextEntryDialogProps = { + open: boolean; + title: string; + description?: string; + setOpen: (open: boolean) => void; + onSave: (text: string) => void; +}; +export default function TextEntryDialog({ + open, + title, + description, + setOpen, + onSave, +}: TextEntryDialogProps) { + const formSchema = z.object({ + text: z.string(), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + }); + const fileRef = form.register("text"); + + // upload handler + + const onSubmit = useCallback( + (data: z.infer) => { + if (!data["text"]) { + return; + } + + onSave(data["text"]); + }, + [onSave], + ); + + return ( + + + + {title} + {description && {description}} + +
+ + ( + + + + + + )} + /> + + + + + + +
+
+ ); +} diff --git a/web/src/components/overlay/dialog/UploadImageDialog.tsx b/web/src/components/overlay/dialog/UploadImageDialog.tsx index b4fbd5065..6a01a7fab 100644 --- a/web/src/components/overlay/dialog/UploadImageDialog.tsx +++ b/web/src/components/overlay/dialog/UploadImageDialog.tsx @@ -41,7 +41,7 @@ export default function UploadImageDialog({ const onSubmit = useCallback( (data: z.infer) => { - if (!data["file"]) { + if (!data["file"] || Object.keys(data.file).length == 0) { return; } diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx index 610f3b5c4..a096bb28c 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -1,6 +1,7 @@ import { baseUrl } from "@/api/baseUrl"; import AddFaceIcon from "@/components/icons/AddFaceIcon"; import ActivityIndicator from "@/components/indicators/activity-indicator"; +import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog"; import UploadImageDialog from "@/components/overlay/dialog/UploadImageDialog"; import { Button } from "@/components/ui/button"; import { @@ -23,7 +24,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, LuRefreshCw, LuTrash2 } from "react-icons/lu"; +import { LuImagePlus, LuRefreshCw, LuScanFace, LuTrash2 } from "react-icons/lu"; import { toast } from "sonner"; import useSWR from "swr"; @@ -76,13 +77,14 @@ export default function FaceLibrary() { // upload const [upload, setUpload] = useState(false); + const [addFace, setAddFace] = useState(false); const onUploadImage = useCallback( (file: File) => { const formData = new FormData(); formData.append("file", file); axios - .post(`faces/${pageToggle}`, formData, { + .post(`faces/${pageToggle}/register`, formData, { headers: { "Content-Type": "multipart/form-data", }, @@ -113,6 +115,40 @@ export default function FaceLibrary() { [pageToggle, refreshFaces], ); + const onAddName = useCallback( + (name: string) => { + axios + .post(`faces/${name}/create`, { + headers: { + "Content-Type": "multipart/form-data", + }, + }) + .then((resp) => { + if (resp.status == 200) { + setUpload(false); + refreshFaces(); + toast.success( + "Successfully uploaded image. View the file in the /exports folder.", + { position: "top-center" }, + ); + } + }) + .catch((error) => { + if (error.response?.data?.message) { + toast.error( + `Failed to upload image: ${error.response.data.message}`, + { position: "top-center" }, + ); + } else { + toast.error(`Failed to upload image: ${error.message}`, { + position: "top-center", + }); + } + }); + }, + [refreshFaces], + ); + if (!config) { return ; } @@ -129,6 +165,14 @@ export default function FaceLibrary() { onSave={onUploadImage} /> + +
@@ -174,10 +218,16 @@ export default function FaceLibrary() {
- +
+ + +
{pageToggle && (pageToggle == "train" ? (