mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-07 02:18:07 +01:00
Fixes (#18220)
* 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:
@@ -525,7 +525,10 @@ export default function ObjectLifecycle({
|
||||
{t("objectLifecycle.scrollViewTips")}
|
||||
</div>
|
||||
<div className="min-w-20 text-right text-sm text-muted-foreground">
|
||||
{current + 1} of {eventSequence.length}
|
||||
{t("objectLifecycle.count", {
|
||||
first: current + 1,
|
||||
second: eventSequence.length,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{config?.cameras[event.camera]?.onvif.autotracking.enabled_in_config && (
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import { getLifecycleItemDescription } from "@/utils/lifecycleUtil";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type ObjectPathProps = {
|
||||
positions?: Position[];
|
||||
@@ -40,6 +41,7 @@ export function ObjectPath({
|
||||
onPointClick,
|
||||
visible = true,
|
||||
}: ObjectPathProps) {
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
const getAbsolutePositions = useCallback(() => {
|
||||
if (!imgRef.current || !positions) return [];
|
||||
const imgRect = imgRef.current.getBoundingClientRect();
|
||||
@@ -103,7 +105,7 @@ export function ObjectPath({
|
||||
<TooltipContent side="top" className="smart-capitalize">
|
||||
{pos.lifecycle_item
|
||||
? getLifecycleItemDescription(pos.lifecycle_item)
|
||||
: "Tracked point"}
|
||||
: t("objectLifecycle.trackedPoint")}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
|
||||
@@ -864,16 +864,14 @@ function ObjectDetailsTab({
|
||||
className={cn("flex w-full flex-row gap-2", isMobile && "flex-col")}
|
||||
>
|
||||
{config?.semantic_search.enabled &&
|
||||
setSimilarity != undefined &&
|
||||
search.data.type == "object" && (
|
||||
<Button
|
||||
className="w-full"
|
||||
aria-label={t("itemMenu.findSimilar.aria")}
|
||||
onClick={() => {
|
||||
setSearch(undefined);
|
||||
|
||||
if (setSimilarity) {
|
||||
setSimilarity();
|
||||
}
|
||||
setSimilarity();
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-1">
|
||||
@@ -1101,7 +1099,7 @@ export function ObjectSnapshotTab({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href={`${baseUrl}api/events/${search?.id}/snapshot.jpg`}
|
||||
href={`${baseUrl}api/events/${search?.id}/snapshot.jpg?bbox=1`}
|
||||
download={`${search?.camera}_${search?.label}.jpg`}
|
||||
>
|
||||
<Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500">
|
||||
@@ -1270,7 +1268,7 @@ export function VideoTab({ search }: VideoTabProps) {
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
download
|
||||
href={`${baseUrl}api/${search.camera}/start/${search.start_time}/end/${endTime}/clip.mp4`}
|
||||
href={`${baseUrl}api/${search.camera}/start/${search.start_time}/end/${endTime}/clip.mp4?trim=end`}
|
||||
>
|
||||
<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" />
|
||||
|
||||
@@ -91,7 +91,7 @@ export function PlatformAwareSheet({
|
||||
className="mx-2"
|
||||
onClose={() => onOpenChange(false)}
|
||||
>
|
||||
<MobilePageTitle>More Filters</MobilePageTitle>
|
||||
<MobilePageTitle>{title}</MobilePageTitle>
|
||||
</MobilePageHeader>
|
||||
<div className={contentClassName}>{content}</div>
|
||||
</MobilePageContent>
|
||||
|
||||
@@ -224,6 +224,7 @@ export default function SearchFilterDialog({
|
||||
return (
|
||||
<PlatformAwareSheet
|
||||
trigger={trigger}
|
||||
title={t("more")}
|
||||
content={content}
|
||||
contentClassName={cn(
|
||||
"w-auto lg:min-w-[275px] scrollbar-container h-full overflow-auto px-4",
|
||||
|
||||
@@ -369,7 +369,11 @@ export function DateRangePicker({
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="mx-auto mb-2 w-[180px]">
|
||||
<SelectValue placeholder="Select..." />
|
||||
<SelectValue
|
||||
placeholder={t("dates.selectPreset", {
|
||||
ns: "components/filter",
|
||||
})}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PRESETS.map((preset) => (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -631,6 +631,7 @@ export default function LiveCameraView({
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<PtzControlPanel
|
||||
camera={camera.name}
|
||||
enabled={cameraEnabled}
|
||||
clickOverlay={clickOverlay}
|
||||
setClickOverlay={setClickOverlay}
|
||||
/>
|
||||
@@ -689,15 +690,19 @@ function TooltipButton({
|
||||
|
||||
function PtzControlPanel({
|
||||
camera,
|
||||
enabled,
|
||||
clickOverlay,
|
||||
setClickOverlay,
|
||||
}: {
|
||||
camera: string;
|
||||
enabled: boolean;
|
||||
clickOverlay: boolean;
|
||||
setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
const { data: ptz } = useSWR<CameraPtzInfo>(`${camera}/ptz/info`);
|
||||
const { data: ptz } = useSWR<CameraPtzInfo>(
|
||||
enabled ? `${camera}/ptz/info` : null,
|
||||
);
|
||||
|
||||
const { send: sendPtz } = usePtzCommand(camera);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user