* Add option to not trim clip

* Improve API

* Update snapshot for new best objects

* Fix missing strings

* Convert to separate key

* Always include bounding box on snapshots

* improve autotracking relative zooming time calculation

* update proxy docs to note the need for comma separated header roles

* Add count translation

* tracked object lifecycle i18n fix

* update speed estimation docs

* clarity

* Re-initialize onvif information when toggling camera on live view

* Move time ago to card info and add face area

* Clarify face recognition docs

* Increase minimum face recognition area

* use clipFrom to in vod module endpoint to start at the correct time

* Cleanup media api

* Don't change duration

* Use search detail dialog for face library

* Move to segment based

* Cleanup

* Add back duration modification

* clean up docs

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
Nicolas Mowen
2025-05-14 16:44:06 -06:00
committed by GitHub
co-authored by Josh Hawkins
parent 1fa7ce5486
commit d3d05fa397
17 changed files with 121 additions and 121 deletions
+40 -91
View File
@@ -31,11 +31,6 @@ import {
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Toaster } from "@/components/ui/sonner";
import {
Tooltip,
@@ -43,7 +38,6 @@ import {
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";
@@ -58,7 +52,6 @@ import { Trans, useTranslation } from "react-i18next";
import {
LuFolderCheck,
LuImagePlus,
LuInfo,
LuPencil,
LuRefreshCw,
LuScanFace,
@@ -68,6 +61,10 @@ import {
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import useSWR from "swr";
import SearchDetailDialog, {
SearchTab,
} from "@/components/overlay/detail/SearchDetailDialog";
import { SearchResult } from "@/types/search";
export default function FaceLibrary() {
const { t } = useTranslation(["views/faceLibrary"]);
@@ -663,18 +660,7 @@ function TrainingGrid({
// selection
const [selectedEvent, setSelectedEvent] = useState<Event>();
const formattedDate = useFormattedTimestamp(
selectedEvent?.start_time ?? 0,
config?.ui.time_format == "24hour"
? t("time.formattedTimestampMonthDayYearHourMinute.24hour", {
ns: "common",
})
: t("time.formattedTimestampMonthDayYearHourMinute.12hour", {
ns: "common",
}),
config?.ui.timezone,
);
const [dialogTab, setDialogTab] = useState<SearchTab>("details");
if (attemptImages.length == 0) {
return (
@@ -687,66 +673,16 @@ function TrainingGrid({
return (
<>
<Dialog
open={selectedEvent != undefined}
onOpenChange={(open) => {
if (!open) {
setSelectedEvent(undefined);
}
}}
>
<DialogContent
className={cn(
"",
selectedEvent?.has_snapshot && isDesktop && "max-w-7xl",
)}
>
<DialogHeader>
<DialogTitle>{t("details.face")}</DialogTitle>
<DialogDescription>{t("details.faceDesc")}</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">{t("details.person")}</div>
<div className="text-sm smart-capitalize">
{selectedEvent?.sub_label ?? t("details.unknown")}
</div>
</div>
{selectedEvent?.data.sub_label_score && (
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">
<div className="flex flex-row items-center gap-1">
{t("details.subLabelScore")}
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">Info</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-80">
{t("details.scoreInfo")}
</PopoverContent>
</Popover>
</div>
</div>
<div className="text-sm smart-capitalize">
{Math.round((selectedEvent?.data?.sub_label_score || 0) * 100)}%
</div>
</div>
)}
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">
{t("details.timestamp")}
</div>
<div className="text-sm">{formattedDate}</div>
</div>
<img
className="mx-auto max-h-[60dvh] object-contain"
loading="lazy"
src={`${baseUrl}api/events/${selectedEvent?.id}/${selectedEvent?.has_snapshot ? "snapshot.jpg" : "thumbnail.jpg"}`}
/>
</DialogContent>
</Dialog>
<SearchDetailDialog
search={
selectedEvent ? (selectedEvent as unknown as SearchResult) : undefined
}
page={dialogTab}
setSimilarity={undefined}
setSearchPage={setDialogTab}
setSearch={(search) => setSelectedEvent(search as unknown as Event)}
setInputFocused={() => {}}
/>
<div className="scrollbar-container flex flex-wrap gap-2 overflow-y-scroll p-1">
{Object.entries(faceGroups).map(([key, group]) => {
@@ -853,11 +789,18 @@ function FaceAttemptGroup({
}}
>
<div className="flex flex-row justify-between">
<div className="select-none smart-capitalize">
Person
{event?.sub_label
? `: ${event.sub_label} (${Math.round((event.data.sub_label_score || 0) * 100)}%)`
: ": " + t("details.unknown")}
<div className="flex flex-col gap-1">
<div className="select-none smart-capitalize">
Person
{event?.sub_label
? `: ${event.sub_label} (${Math.round((event.data.sub_label_score || 0) * 100)}%)`
: ": " + t("details.unknown")}
</div>
<TimeAgo
className="text-sm text-secondary-foreground"
time={group[0].timestamp * 1000}
dense
/>
</div>
{event && (
<Tooltip>
@@ -950,6 +893,14 @@ function FaceAttempt({
onClick(data, true);
});
const imageArea = useMemo(() => {
if (!imgRef.current) {
return undefined;
}
return imgRef.current.naturalWidth * imgRef.current.naturalHeight;
}, [imgRef]);
// api calls
const onTrainAttempt = useCallback(
@@ -1021,13 +972,11 @@ function FaceAttempt({
onClick(data, e.metaKey || e.ctrlKey);
}}
/>
<div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white">
<TimeAgo
className="text-white"
time={data.timestamp * 1000}
dense
/>
</div>
{imageArea != undefined && (
<div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white">
{imageArea}px
</div>
)}
</div>
<div className="select-none p-2">
<div className="flex w-full flex-row items-center justify-between gap-2">