From 5514fc11b9ba2a24805b08e731fcc8e59a869ccb Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 18 Mar 2025 08:32:15 -0600 Subject: [PATCH] Face tweaks (#17225) * Always use white text * Add right click as well * Add face details dialog * Clenaup --- web/public/locales/en/views/faceLibrary.json | 6 + web/src/pages/FaceLibrary.tsx | 124 +++++++++++++++---- web/src/types/face.ts | 6 + 3 files changed, 113 insertions(+), 23 deletions(-) create mode 100644 web/src/types/face.ts diff --git a/web/public/locales/en/views/faceLibrary.json b/web/public/locales/en/views/faceLibrary.json index 46842b7ea..57d78b18a 100644 --- a/web/public/locales/en/views/faceLibrary.json +++ b/web/public/locales/en/views/faceLibrary.json @@ -2,6 +2,12 @@ "description": { "addFace": "Walk through adding a new face to the Face Library." }, + "details": { + "confidence": "Confidence", + "face": "Face Details", + "faceDesc": "Details for the face and associated object", + "timestamp": "Timestamp" + }, "documentTitle": "Face Library - Frigate", "uploadFaceImage": { "title": "Upload Face Image", diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx index 94a7f6947..a9b7dc230 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -5,6 +5,13 @@ import ActivityIndicator from "@/components/indicators/activity-indicator"; import CreateFaceWizardDialog from "@/components/overlay/detail/FaceCreateWizardDialog"; import UploadImageDialog from "@/components/overlay/dialog/UploadImageDialog"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, @@ -20,9 +27,12 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import useContextMenu from "@/hooks/use-contextmenu"; +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 { FrigateConfig } from "@/types/frigateConfig"; import axios from "axios"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -329,20 +339,76 @@ function TrainingGrid({ onClickFace, onRefresh, }: TrainingGridProps) { + const { t } = useTranslation(["views/faceLibrary"]); + + // face data + + const [selectedEvent, setSelectedEvent] = useState(); + + const formattedDate = useFormattedTimestamp( + selectedEvent?.timestamp ?? 0, + config?.ui.time_format == "24hour" + ? t("time.formattedTimestampWithYear.24hour", { ns: "common" }) + : t("time.formattedTimestampWithYear.12hour", { ns: "common" }), + config?.ui.timezone, + ); + return ( -
- {attemptImages.map((image: string) => ( - onClickFace(image, meta)} - onRefresh={onRefresh} - /> - ))} -
+ <> + { + if (!open) { + setSelectedEvent(undefined); + } + }} + > + + + {t("details.face")} + {t("details.faceDesc")} + +
+
+ {t("details.confidence")} +
+
+ {(selectedEvent?.score || 0) * 100}% +
+
+
+
+ {t("details.timestamp")} +
+
{formattedDate}
+
+ +
+
+ +
+ {attemptImages.map((image: string) => ( + { + if (meta) { + onClickFace(image, meta); + } else { + setSelectedEvent(data); + } + }} + onRefresh={onRefresh} + /> + ))} +
+ ); } @@ -351,7 +417,7 @@ type FaceAttemptProps = { faceNames: string[]; threshold: number; selected: boolean; - onClick: (meta: boolean) => void; + onClick: (data: RecognizedFaceData, meta: boolean) => void; onRefresh: () => void; }; function FaceAttempt({ @@ -363,17 +429,27 @@ function FaceAttempt({ onRefresh, }: FaceAttemptProps) { const { t } = useTranslation(["views/faceLibrary"]); - const data = useMemo(() => { + const data = useMemo(() => { const parts = image.split("-"); return { timestamp: Number.parseFloat(parts[0]), eventId: `${parts[0]}-${parts[1]}`, name: parts[2], - score: parts[3], + score: Number.parseFloat(parts[3]), }; }, [image]); + // interaction + + const imgRef = useRef(null); + + useContextMenu(imgRef, () => { + onClick(data, true); + }); + + // api calls + const onTrainAttempt = useCallback( (trainName: string) => { axios @@ -429,12 +505,16 @@ function FaceAttempt({ ? "shadow-selected outline-selected" : "outline-transparent duration-500", )} - onClick={(e) => onClick(e.metaKey || e.ctrlKey)} >
- + onClick(data, e.metaKey || e.ctrlKey)} + />
- +
@@ -443,12 +523,10 @@ function FaceAttempt({
{data.name}
= threshold - ? "text-success" - : "text-danger", + data.score >= threshold ? "text-success" : "text-danger", )} > - {Number.parseFloat(data.score) * 100}% + {data.score * 100}%
diff --git a/web/src/types/face.ts b/web/src/types/face.ts new file mode 100644 index 000000000..e8f426a5b --- /dev/null +++ b/web/src/types/face.ts @@ -0,0 +1,6 @@ +export type RecognizedFaceData = { + timestamp: number; + eventId: string; + name: string; + score: number; +};