diff --git a/frigate/api/classification.py b/frigate/api/classification.py index d862008c8..9cd3e0db1 100644 --- a/frigate/api/classification.py +++ b/frigate/api/classification.py @@ -1,11 +1,13 @@ """Object classification APIs.""" import logging +import os from fastapi import APIRouter, Request, UploadFile from fastapi.responses import JSONResponse from frigate.api.defs.tags import Tags +from frigate.const import FACE_DIR from frigate.embeddings import EmbeddingsContext logger = logging.getLogger(__name__) @@ -15,7 +17,14 @@ router = APIRouter(tags=[Tags.events]) @router.get("/faces") def get_faces(): - return JSONResponse(content={"message": "there are faces"}) + face_dict: dict[str, list[str]] = {} + + for name in os.listdir(FACE_DIR): + face_dict[name] = [] + for file in os.listdir(os.path.join(FACE_DIR, name)): + face_dict[name].append(file) + + return JSONResponse(status_code=200, content=face_dict) @router.post("/faces/{name}") diff --git a/web/src/App.tsx b/web/src/App.tsx index 3bc2e7836..ef0a9497e 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -19,6 +19,7 @@ const ConfigEditor = lazy(() => import("@/pages/ConfigEditor")); const System = lazy(() => import("@/pages/System")); const Settings = lazy(() => import("@/pages/Settings")); const UIPlayground = lazy(() => import("@/pages/UIPlayground")); +const FaceLibrary = lazy(() => import("@/pages/FaceLibrary")); const Logs = lazy(() => import("@/pages/Logs")); function App() { @@ -51,6 +52,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/web/src/hooks/use-navigation.ts b/web/src/hooks/use-navigation.ts index 06ebd6c1d..d9f45dfa4 100644 --- a/web/src/hooks/use-navigation.ts +++ b/web/src/hooks/use-navigation.ts @@ -1,20 +1,28 @@ import { ENV } from "@/env"; +import { FrigateConfig } from "@/types/frigateConfig"; import { NavData } from "@/types/navigation"; import { useMemo } from "react"; import { FaCompactDisc, FaVideo } from "react-icons/fa"; import { IoSearch } from "react-icons/io5"; import { LuConstruction } from "react-icons/lu"; import { MdVideoLibrary } from "react-icons/md"; +import { TbFaceId } from "react-icons/tb"; +import useSWR from "swr"; export const ID_LIVE = 1; export const ID_REVIEW = 2; export const ID_EXPLORE = 3; export const ID_EXPORT = 4; export const ID_PLAYGROUND = 5; +export const ID_FACE_LIBRARY = 6; export default function useNavigation( variant: "primary" | "secondary" = "primary", ) { + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + return useMemo( () => [ @@ -54,7 +62,15 @@ export default function useNavigation( url: "/playground", enabled: ENV !== "production", }, + { + id: ID_FACE_LIBRARY, + variant, + icon: TbFaceId, + title: "Face Library", + url: "/faces", + enabled: config?.face_recognition.enabled, + }, ] as NavData[], - [variant], + [config?.face_recognition.enabled, variant], ); } diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx new file mode 100644 index 000000000..419692a0b --- /dev/null +++ b/web/src/pages/FaceLibrary.tsx @@ -0,0 +1,107 @@ +import { baseUrl } from "@/api/baseUrl"; +import Chip from "@/components/indicators/Chip"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; +import useOptimisticState from "@/hooks/use-optimistic-state"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { isDesktop } from "react-device-detect"; +import { LuTrash } from "react-icons/lu"; +import useSWR from "swr"; + +export default function FaceLibrary() { + const [page, setPage] = useState(); + const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); + const tabsRef = useRef(null); + + // face data + + const { data: faceData } = useSWR("faces"); + + const faces = useMemo( + () => (faceData ? Object.keys(faceData) : []), + [faceData], + ); + const faceImages = useMemo( + () => (pageToggle && faceData ? faceData[pageToggle] : []), + [pageToggle, faceData], + ); + + useEffect(() => { + if (!pageToggle && faces) { + setPageToggle(faces[0]); + } + // we need to listen on the value of the faces list + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [faces]); + + return ( +
+
+ +
+ { + if (value) { + setPageToggle(value); + } + }} + > + {Object.values(faces).map((item) => ( + +
{item}
+
+ ))} +
+ +
+
+
+ {pageToggle && ( +
+ {faceImages.map((image: string) => ( + + ))} +
+ )} +
+ ); +} + +type FaceImageProps = { + name: string; + image: string; +}; +function FaceImage({ name, image }: FaceImageProps) { + const [hovered, setHovered] = useState(false); + + return ( +
setHovered(true) : undefined} + onMouseLeave={isDesktop ? () => setHovered(false) : undefined} + onClick={isDesktop ? undefined : () => setHovered(!hovered)} + > + {hovered && ( +
+ + + +
+ )} + +
+ ); +} diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts index 5c5971fc0..1413efbad 100644 --- a/web/src/types/frigateConfig.ts +++ b/web/src/types/frigateConfig.ts @@ -287,6 +287,10 @@ export interface FrigateConfig { environment_vars: Record; + face_recognition: { + enabled: boolean; + }; + ffmpeg: { global_args: string[]; hwaccel_args: string;