From 08cf0def6e50b8101233ad49dd97075e17500b3e Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 21 Mar 2025 11:47:32 -0600 Subject: [PATCH] Face tweaks (#17290) * Ensure doesn't fail due to missing dir * Remove redundant settings from tabs * Adjust selection method for mobile * Fix button descendent error * Ensure train is option on mobile * Cleanup face images * Cleanup --- frigate/api/classification.py | 3 + frigate/data_processing/real_time/face.py | 1 + web/src/pages/FaceLibrary.tsx | 164 +++++++++++++++------- web/src/pages/Settings.tsx | 35 +++-- web/src/types/face.ts | 4 + 5 files changed, 137 insertions(+), 70 deletions(-) diff --git a/frigate/api/classification.py b/frigate/api/classification.py index 498158ff2..d3ee9c3d9 100644 --- a/frigate/api/classification.py +++ b/frigate/api/classification.py @@ -30,6 +30,9 @@ router = APIRouter(tags=[Tags.events]) def get_faces(): face_dict: dict[str, list[str]] = {} + if not os.path.exists(FACE_DIR): + return JSONResponse(status_code=200, content={}) + for name in os.listdir(FACE_DIR): face_dir = os.path.join(FACE_DIR, name) diff --git a/frigate/data_processing/real_time/face.py b/frigate/data_processing/real_time/face.py index 61baba98e..ce2ea85c1 100644 --- a/frigate/data_processing/real_time/face.py +++ b/frigate/data_processing/real_time/face.py @@ -506,6 +506,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): if self.config.face_recognition.save_attempts: # write face to library folder = os.path.join(FACE_DIR, "train") + os.makedirs(folder, exist_ok=True) new_file = os.path.join( folder, f"{id}-{sub_label}-{score}-{face_score}.webp" ) diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx index 73fd54c7a..0ac937283 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -32,11 +32,11 @@ import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import useKeyboardListener from "@/hooks/use-keyboard-listener"; import useOptimisticState from "@/hooks/use-optimistic-state"; import { cn } from "@/lib/utils"; -import { RecognizedFaceData } from "@/types/face"; +import { FaceLibraryData, RecognizedFaceData } from "@/types/face"; import { FrigateConfig } from "@/types/frigateConfig"; import axios from "axios"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { isDesktop } from "react-device-detect"; +import { isDesktop, isMobile } from "react-device-detect"; import { useTranslation } from "react-i18next"; import { LuImagePlus, LuRefreshCw, LuScanFace, LuTrash2 } from "react-icons/lu"; import { toast } from "sonner"; @@ -55,11 +55,11 @@ export default function FaceLibrary() { const [page, setPage] = useState(); const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); - const tabsRef = useRef(null); // face data - const { data: faceData, mutate: refreshFaces } = useSWR("faces"); + const { data: faceData, mutate: refreshFaces } = + useSWR("faces"); const faces = useMemo( () => @@ -233,50 +233,13 @@ export default function FaceLibrary() { />
- -
- { - if (value) { - setPageToggle(value); - } - }} - > - {trainImages.length > 0 && ( - <> - -
{t("train.title")}
-
-
|
- - )} - - {Object.values(faces).map((item) => ( - -
- {item} ({faceData[item].length}) -
-
- ))} -
- -
-
+ {selectedFaces?.length > 0 ? (
+ + + {trainImages.length > 0 && ( + setPageToggle("train")} + > +
{t("train.title")}
+
+ )} + {Object.values(faces).map((face) => ( + setPageToggle(face)} + > + {face} ({faceData?.[face].length}) + + ))} +
+ + ); +} + type TrainingGridProps = { config: FrigateConfig; attemptImages: string[]; @@ -536,7 +588,7 @@ function FaceAttempt({
- + @@ -579,7 +631,12 @@ type FaceGridProps = { }; function FaceGrid({ faceImages, pageToggle, onDelete }: FaceGridProps) { return ( -
+
{faceImages.map((image: string) => ( -
+
diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 6ccda34f3..b00d3255c 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -47,9 +47,9 @@ import { useIsAdmin } from "@/hooks/use-is-admin"; import { useTranslation } from "react-i18next"; const allSettingsViews = [ - "uiSettings", - "classificationSettings", - "cameraSettings", + "ui", + "classification", + "cameras", "masksAndZones", "motionTuner", "debug", @@ -61,7 +61,7 @@ type SettingsType = (typeof allSettingsViews)[number]; export default function Settings() { const { t } = useTranslation(["views/settings"]); - const [page, setPage] = useState("uiSettings"); + const [page, setPage] = useState("ui"); const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); const tabsRef = useRef(null); @@ -73,7 +73,7 @@ export default function Settings() { const isAdmin = useIsAdmin(); - const allowedViewsForViewer: SettingsType[] = ["uiSettings", "debug"]; + const allowedViewsForViewer: SettingsType[] = ["ui", "debug"]; const visibleSettingsViews = !isAdmin ? allowedViewsForViewer : allSettingsViews; @@ -135,10 +135,7 @@ export default function Settings() { const firstEnabledCamera = cameras.find((cam) => cameraEnabledStates[cam.name]) || cameras[0]; setSelectedCamera(firstEnabledCamera.name); - } else if ( - !cameraEnabledStates[selectedCamera] && - page !== "cameraSettings" - ) { + } else if (!cameraEnabledStates[selectedCamera] && page !== "cameras") { // Switch to first enabled camera if current one is disabled, unless on "camera settings" page const firstEnabledCamera = cameras.find((cam) => cameraEnabledStates[cam.name]) || cameras[0]; @@ -167,8 +164,8 @@ export default function Settings() { useSearchEffect("page", (page: string) => { if (allSettingsViews.includes(page as SettingsType)) { // Restrict viewer to UI settings - if (!isAdmin && !["uiSettings", "debug"].includes(page)) { - setPage("uiSettings"); + if (!isAdmin && !["ui", "debug"].includes(page)) { + setPage("ui"); } else { setPage(page as SettingsType); } @@ -203,8 +200,8 @@ export default function Settings() { onValueChange={(value: SettingsType) => { if (value) { // Restrict viewer navigation - if (!isAdmin && !["uiSettings", "debug"].includes(value)) { - setPageToggle("uiSettings"); + if (!isAdmin && !["ui", "debug"].includes(value)) { + setPageToggle("ui"); } else { setPageToggle(value); } @@ -214,7 +211,7 @@ export default function Settings() { {visibleSettingsViews.map((item) => ( {(page == "debug" || - page == "cameraSettings" || + page == "cameras" || page == "masksAndZones" || page == "motionTuner") && (
@@ -251,14 +248,14 @@ export default function Settings() { )}
- {page == "uiSettings" && } - {page == "classificationSettings" && ( + {page == "ui" && } + {page == "classification" && ( )} {page == "debug" && ( )} - {page == "cameraSettings" && ( + {page == "cameras" && ( {allCameras.map((item) => { const isEnabled = cameraEnabledStates[item.name]; - const isCameraSettingsPage = currentPage === "cameraSettings"; + const isCameraSettingsPage = currentPage === "cameras"; return (