* fix onvif reinitialization

* api docs: clarify usage of clip.mp4 endpoint

* Always show train tab

* Add description to API

* catch lpr model inference exceptions

* always apply motion mask when using yolov9 plate detection

* lpr faq

* fix incorrect focus when reopening search detail dialog on video tab

* only use keyboard listener in face library when train tab is active

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
Josh Hawkins
2025-05-07 17:31:24 -05:00
committed by GitHub
parent da1fb935b4
commit ac8e647b92
7 changed files with 135 additions and 105 deletions

View File

@@ -1234,55 +1234,58 @@ export function VideoTab({ search }: VideoTabProps) {
const source = `${baseUrl}vod/${search.camera}/start/${search.start_time}/end/${endTime}/index.m3u8`;
return (
<GenericVideoPlayer source={source}>
{reviewItem && (
<div
className={cn(
"absolute top-2 z-10 flex items-center gap-2",
isIOS ? "right-8" : "right-2",
)}
>
<Tooltip>
<TooltipTrigger>
<Chip
className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
onClick={() => {
if (reviewItem?.id) {
const params = new URLSearchParams({
id: reviewItem.id,
}).toString();
navigate(`/review?${params}`);
}
}}
>
<FaHistory className="size-4 text-white" />
</Chip>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{t("itemMenu.viewInHistory.label")}
</TooltipContent>
</TooltipPortal>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<a
download
href={`${baseUrl}api/${search.camera}/start/${search.start_time}/end/${endTime}/clip.mp4`}
>
<Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500">
<FaDownload className="size-4 text-white" />
<>
<span tabIndex={0} className="sr-only" />
<GenericVideoPlayer source={source}>
{reviewItem && (
<div
className={cn(
"absolute top-2 z-10 flex items-center gap-2",
isIOS ? "right-8" : "right-2",
)}
>
<Tooltip>
<TooltipTrigger>
<Chip
className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
onClick={() => {
if (reviewItem?.id) {
const params = new URLSearchParams({
id: reviewItem.id,
}).toString();
navigate(`/review?${params}`);
}
}}
>
<FaHistory className="size-4 text-white" />
</Chip>
</a>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{t("button.download", { ns: "common" })}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
)}
</GenericVideoPlayer>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{t("itemMenu.viewInHistory.label")}
</TooltipContent>
</TooltipPortal>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<a
download
href={`${baseUrl}api/${search.camera}/start/${search.start_time}/end/${endTime}/clip.mp4`}
>
<Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500">
<FaDownload className="size-4 text-white" />
</Chip>
</a>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{t("button.download", { ns: "common" })}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
)}
</GenericVideoPlayer>
</>
);
}

View File

@@ -46,6 +46,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { isDesktop, isMobile } from "react-device-detect";
import { useTranslation } from "react-i18next";
import {
LuFolderCheck,
LuImagePlus,
LuInfo,
LuPencil,
@@ -69,7 +70,7 @@ export default function FaceLibrary() {
document.title = t("documentTitle");
}, [t]);
const [page, setPage] = useState<string>();
const [page, setPage] = useState<string>("train");
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
// face data
@@ -92,20 +93,6 @@ export default function FaceLibrary() {
[faceData],
);
useEffect(() => {
if (!pageToggle) {
if (trainImages.length > 0) {
setPageToggle("train");
} else if (faces) {
setPageToggle(faces[0]);
}
} else if (pageToggle == "train" && trainImages.length == 0) {
setPageToggle(faces[0]);
}
// we need to listen on the value of the faces list
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [trainImages, faces]);
// upload
const [upload, setUpload] = useState(false);
@@ -257,26 +244,29 @@ export default function FaceLibrary() {
// keyboard
useKeyboardListener(["a", "Escape"], (key, modifiers) => {
if (modifiers.repeat || !modifiers.down) {
return;
}
useKeyboardListener(
page === "train" ? ["a", "Escape"] : [],
(key, modifiers) => {
if (modifiers.repeat || !modifiers.down) {
return;
}
switch (key) {
case "a":
if (modifiers.ctrl) {
if (selectedFaces.length) {
setSelectedFaces([]);
} else {
setSelectedFaces([...trainImages]);
switch (key) {
case "a":
if (modifiers.ctrl) {
if (selectedFaces.length) {
setSelectedFaces([]);
} else {
setSelectedFaces([...trainImages]);
}
}
}
break;
case "Escape":
setSelectedFaces([]);
break;
}
});
break;
case "Escape":
setSelectedFaces([]);
break;
}
},
);
if (!config) {
return <ActivityIndicator />;
@@ -371,7 +361,7 @@ type LibrarySelectorProps = {
faceData?: FaceLibraryData;
faces: string[];
trainImages: string[];
setPageToggle: (toggle: string | undefined) => void;
setPageToggle: (toggle: string) => void;
onDelete: (name: string, ids: string[], isName: boolean) => void;
onRename: (old_name: string, new_name: string) => void;
};
@@ -463,18 +453,16 @@ function LibrarySelector({
className="scrollbar-container max-h-[40dvh] min-w-[220px] overflow-y-auto"
align="start"
>
{trainImages.length > 0 && (
<DropdownMenuItem
className="flex cursor-pointer items-center justify-start gap-2"
aria-label={t("train.aria")}
onClick={() => setPageToggle("train")}
>
<div>{t("train.title")}</div>
<div className="text-secondary-foreground">
({trainImages.length})
</div>
</DropdownMenuItem>
)}
<DropdownMenuItem
className="flex cursor-pointer items-center justify-start gap-2"
aria-label={t("train.aria")}
onClick={() => setPageToggle("train")}
>
<div>{t("train.title")}</div>
<div className="text-secondary-foreground">
({trainImages.length})
</div>
</DropdownMenuItem>
{trainImages.length > 0 && faces.length > 0 && (
<>
<DropdownMenuSeparator />
@@ -624,6 +612,15 @@ function TrainingGrid({
config?.ui.timezone,
);
if (attemptImages.length == 0) {
return (
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
<LuFolderCheck className="size-16" />
{t("train.empty")}
</div>
);
}
return (
<>
<Dialog