Add face library page

This commit is contained in:
Nicolas Mowen 2024-11-22 07:23:47 -07:00
parent c4e944dc93
commit 36cd46c871
5 changed files with 140 additions and 2 deletions

View File

@ -1,11 +1,13 @@
"""Object classification APIs.""" """Object classification APIs."""
import logging import logging
import os
from fastapi import APIRouter, Request, UploadFile from fastapi import APIRouter, Request, UploadFile
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
from frigate.const import FACE_DIR
from frigate.embeddings import EmbeddingsContext from frigate.embeddings import EmbeddingsContext
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -15,7 +17,14 @@ router = APIRouter(tags=[Tags.events])
@router.get("/faces") @router.get("/faces")
def 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}") @router.post("/faces/{name}")

View File

@ -19,6 +19,7 @@ const ConfigEditor = lazy(() => import("@/pages/ConfigEditor"));
const System = lazy(() => import("@/pages/System")); const System = lazy(() => import("@/pages/System"));
const Settings = lazy(() => import("@/pages/Settings")); const Settings = lazy(() => import("@/pages/Settings"));
const UIPlayground = lazy(() => import("@/pages/UIPlayground")); const UIPlayground = lazy(() => import("@/pages/UIPlayground"));
const FaceLibrary = lazy(() => import("@/pages/FaceLibrary"));
const Logs = lazy(() => import("@/pages/Logs")); const Logs = lazy(() => import("@/pages/Logs"));
function App() { function App() {
@ -51,6 +52,7 @@ function App() {
<Route path="/config" element={<ConfigEditor />} /> <Route path="/config" element={<ConfigEditor />} />
<Route path="/logs" element={<Logs />} /> <Route path="/logs" element={<Logs />} />
<Route path="/playground" element={<UIPlayground />} /> <Route path="/playground" element={<UIPlayground />} />
<Route path="/faces" element={<FaceLibrary />} />
<Route path="*" element={<Redirect to="/" />} /> <Route path="*" element={<Redirect to="/" />} />
</Routes> </Routes>
</Suspense> </Suspense>

View File

@ -1,20 +1,28 @@
import { ENV } from "@/env"; import { ENV } from "@/env";
import { FrigateConfig } from "@/types/frigateConfig";
import { NavData } from "@/types/navigation"; import { NavData } from "@/types/navigation";
import { useMemo } from "react"; import { useMemo } from "react";
import { FaCompactDisc, FaVideo } from "react-icons/fa"; import { FaCompactDisc, FaVideo } from "react-icons/fa";
import { IoSearch } from "react-icons/io5"; import { IoSearch } from "react-icons/io5";
import { LuConstruction } from "react-icons/lu"; import { LuConstruction } from "react-icons/lu";
import { MdVideoLibrary } from "react-icons/md"; import { MdVideoLibrary } from "react-icons/md";
import { TbFaceId } from "react-icons/tb";
import useSWR from "swr";
export const ID_LIVE = 1; export const ID_LIVE = 1;
export const ID_REVIEW = 2; export const ID_REVIEW = 2;
export const ID_EXPLORE = 3; export const ID_EXPLORE = 3;
export const ID_EXPORT = 4; export const ID_EXPORT = 4;
export const ID_PLAYGROUND = 5; export const ID_PLAYGROUND = 5;
export const ID_FACE_LIBRARY = 6;
export default function useNavigation( export default function useNavigation(
variant: "primary" | "secondary" = "primary", variant: "primary" | "secondary" = "primary",
) { ) {
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
return useMemo( return useMemo(
() => () =>
[ [
@ -54,7 +62,15 @@ export default function useNavigation(
url: "/playground", url: "/playground",
enabled: ENV !== "production", enabled: ENV !== "production",
}, },
{
id: ID_FACE_LIBRARY,
variant,
icon: TbFaceId,
title: "Face Library",
url: "/faces",
enabled: config?.face_recognition.enabled,
},
] as NavData[], ] as NavData[],
[variant], [config?.face_recognition.enabled, variant],
); );
} }

View File

@ -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<string>();
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
const tabsRef = useRef<HTMLDivElement | null>(null);
// face data
const { data: faceData } = useSWR("faces");
const faces = useMemo<string[]>(
() => (faceData ? Object.keys(faceData) : []),
[faceData],
);
const faceImages = useMemo<string[]>(
() => (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 (
<div className="flex size-full flex-col p-2">
<div className="relative flex h-11 w-full items-center justify-between">
<ScrollArea className="w-full whitespace-nowrap">
<div ref={tabsRef} className="flex flex-row">
<ToggleGroup
className="*:rounded-md *:px-3 *:py-4"
type="single"
size="sm"
value={pageToggle}
onValueChange={(value: string) => {
if (value) {
setPageToggle(value);
}
}}
>
{Object.values(faces).map((item) => (
<ToggleGroupItem
key={item}
className={`flex scroll-mx-10 items-center justify-between gap-2 ${page == "UI settings" ? "last:mr-20" : ""} ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
value={item}
data-nav-item={item}
aria-label={`Select ${item}`}
>
<div className="capitalize">{item}</div>
</ToggleGroupItem>
))}
</ToggleGroup>
<ScrollBar orientation="horizontal" className="h-0" />
</div>
</ScrollArea>
</div>
{pageToggle && (
<div className="flex gap-2">
{faceImages.map((image: string) => (
<FaceImage name={pageToggle} image={image} />
))}
</div>
)}
</div>
);
}
type FaceImageProps = {
name: string;
image: string;
};
function FaceImage({ name, image }: FaceImageProps) {
const [hovered, setHovered] = useState(false);
return (
<div
className="relative h-40"
onMouseEnter={isDesktop ? () => setHovered(true) : undefined}
onMouseLeave={isDesktop ? () => setHovered(false) : undefined}
onClick={isDesktop ? undefined : () => setHovered(!hovered)}
>
{hovered && (
<div className="absolute right-1 top-1">
<Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500">
<LuTrash className="size-4 fill-destructive text-destructive" />
</Chip>
</div>
)}
<img
className="h-40 rounded-md"
src={`${baseUrl}clips/faces/${name}/${image}`}
/>
</div>
);
}

View File

@ -287,6 +287,10 @@ export interface FrigateConfig {
environment_vars: Record<string, unknown>; environment_vars: Record<string, unknown>;
face_recognition: {
enabled: boolean;
};
ffmpeg: { ffmpeg: {
global_args: string[]; global_args: string[];
hwaccel_args: string; hwaccel_args: string;