diff --git a/frigate/api/event.py b/frigate/api/event.py index 525fb9515..aeea871c4 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -379,7 +379,12 @@ def events_search(): n_results=limit, where=where, ) - thumb_ids = dict(zip(thumb_result["ids"][0], thumb_result["distances"][0])) + thumb_ids = dict( + zip( + thumb_result["ids"][0], + context.thumb_stats.normalize(thumb_result["distances"][0]), + ) + ) else: thumb_result = context.embeddings.thumbnail.query( query_texts=[query], diff --git a/web/src/components/overlay/LogInfoDialog.tsx b/web/src/components/overlay/LogInfoDialog.tsx index dd49a97e6..4cebb4a6e 100644 --- a/web/src/components/overlay/LogInfoDialog.tsx +++ b/web/src/components/overlay/LogInfoDialog.tsx @@ -1,7 +1,19 @@ import { LogLine } from "@/types/log"; import { isDesktop } from "react-device-detect"; -import { Sheet, SheetContent } from "../ui/sheet"; -import { Drawer, DrawerContent } from "../ui/drawer"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "../ui/sheet"; +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, +} from "../ui/drawer"; import { LogChip } from "../indicators/Chip"; import { useMemo } from "react"; import { Link } from "react-router-dom"; @@ -16,6 +28,9 @@ export default function LogInfoDialog({ }: LogInfoDialogProps) { const Overlay = isDesktop ? Sheet : Drawer; const Content = isDesktop ? SheetContent : DrawerContent; + const Header = isDesktop ? SheetHeader : DrawerHeader; + const Title = isDesktop ? SheetTitle : DrawerTitle; + const Description = isDesktop ? SheetDescription : DrawerDescription; const helpfulLinks = useHelpfulLinks(logLine?.content); @@ -31,6 +46,10 @@ export default function LogInfoDialog({ +
+ Log Details + Log details +
{logLine && (
diff --git a/web/src/components/overlay/detail/ObjectLifecycle.tsx b/web/src/components/overlay/detail/ObjectLifecycle.tsx index 4aef59fb3..3cb7945ce 100644 --- a/web/src/components/overlay/detail/ObjectLifecycle.tsx +++ b/web/src/components/overlay/detail/ObjectLifecycle.tsx @@ -44,6 +44,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { AnnotationSettingsPane } from "./AnnotationSettingsPane"; +import { TooltipPortal } from "@radix-ui/react-tooltip"; type ObjectLifecycleProps = { review: ReviewSegment; @@ -185,7 +186,6 @@ export default function ObjectLifecycle({ if (!mainApi || !thumbnailApi) { return; } - thumbnailApi.scrollTo(index); mainApi.scrollTo(index); setCurrent(index); }; @@ -210,18 +210,10 @@ export default function ObjectLifecycle({ thumbnailApi.scrollTo(selected); }; - const handleBottomSelect = () => { - const selected = thumbnailApi.selectedScrollSnap(); - setCurrent(selected); - mainApi.scrollTo(selected); - }; - - mainApi.on("select", handleTopSelect); - thumbnailApi.on("select", handleBottomSelect); + mainApi.on("select", handleTopSelect).on("reInit", handleTopSelect); return () => { mainApi.off("select", handleTopSelect); - thumbnailApi.off("select", handleBottomSelect); }; // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps @@ -467,15 +459,22 @@ export default function ObjectLifecycle({ - + 4 ? "justify-start" : "justify-center", + )} + > {eventSequence.map((item, index) => ( handleThumbnailClick(index)} >
@@ -486,15 +485,24 @@ export default function ObjectLifecycle({ index === current && "bg-selected", )} > - + + + + + + + {getLifecycleItemDescription(item)} + + +
diff --git a/web/src/components/overlay/detail/ReviewDetailDialog.tsx b/web/src/components/overlay/detail/ReviewDetailDialog.tsx index 1742c1fa2..55c94f009 100644 --- a/web/src/components/overlay/detail/ReviewDetailDialog.tsx +++ b/web/src/components/overlay/detail/ReviewDetailDialog.tsx @@ -1,6 +1,18 @@ import { isDesktop, isIOS, isMobile } from "react-device-detect"; -import { Sheet, SheetContent } from "../../ui/sheet"; -import { Drawer, DrawerContent } from "../../ui/drawer"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "../../ui/sheet"; +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, +} from "../../ui/drawer"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { useFormattedTimestamp } from "@/hooks/use-date-utils"; @@ -66,6 +78,9 @@ export default function ReviewDetailDialog({ const Overlay = isDesktop ? Sheet : Drawer; const Content = isDesktop ? SheetContent : DrawerContent; + const Header = isDesktop ? SheetHeader : DrawerHeader; + const Title = isDesktop ? SheetTitle : DrawerTitle; + const Description = isDesktop ? SheetDescription : DrawerDescription; if (!review) { return; @@ -102,6 +117,10 @@ export default function ReviewDetailDialog({ : "max-h-[80dvh] overflow-hidden p-2 pb-4", )} > +
+ Review Item Details + Review item details +
{pane == "overview" && (
diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 4a726a246..368d88dea 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -1,6 +1,18 @@ import { isDesktop, isIOS } from "react-device-detect"; -import { Sheet, SheetContent } from "../../ui/sheet"; -import { Drawer, DrawerContent } from "../../ui/drawer"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "../../ui/sheet"; +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, +} from "../../ui/drawer"; import { SearchResult } from "@/types/search"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; @@ -71,6 +83,9 @@ export default function SearchDetailDialog({ const Overlay = isDesktop ? Sheet : Drawer; const Content = isDesktop ? SheetContent : DrawerContent; + const Header = isDesktop ? SheetHeader : DrawerHeader; + const Title = isDesktop ? SheetTitle : DrawerTitle; + const Description = isDesktop ? SheetDescription : DrawerDescription; return ( +
+ Tracked Object Details + Tracked object details +
{search && (
@@ -93,7 +112,7 @@ export default function SearchDetailDialog({
Label
- {getIconForLabel(search.label, "size-4 text-white")} + {getIconForLabel(search.label, "size-4 text-primary")} {search.label}
diff --git a/web/src/components/player/SearchThumbnailPlayer.tsx b/web/src/components/player/SearchThumbnailPlayer.tsx index 3391bf445..52d4f640d 100644 --- a/web/src/components/player/SearchThumbnailPlayer.tsx +++ b/web/src/components/player/SearchThumbnailPlayer.tsx @@ -287,17 +287,6 @@ function PreviewContent({ /> ); } else if (isCurrentHour(searchResult.start_time)) { - return ( - /* { }} - />*/ -
- ); + return
; } } diff --git a/web/src/pages/Search.tsx b/web/src/pages/Search.tsx index e6725f6dd..0ffa02718 100644 --- a/web/src/pages/Search.tsx +++ b/web/src/pages/Search.tsx @@ -53,7 +53,7 @@ export default function Search() { setTimeout(() => { setSearchTimeout(undefined); setSearchTerm(search); - }, 500), + }, 750), ); // we only want to update the searchTerm when search changes // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/web/src/types/search.ts b/web/src/types/search.ts index 27f2d9e4b..249e6ebc7 100644 --- a/web/src/types/search.ts +++ b/web/src/types/search.ts @@ -12,6 +12,7 @@ export type SearchResult = { thumb_path?: string; zones: string[]; search_source: SearchSource; + search_distance: number; }; export type SearchFilter = { diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index 065478c31..0fea41f77 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -1,17 +1,26 @@ import SearchFilterGroup from "@/components/filter/SearchFilterGroup"; import ActivityIndicator from "@/components/indicators/activity-indicator"; +import Chip from "@/components/indicators/Chip"; import SearchDetailDialog from "@/components/overlay/detail/SearchDetailDialog"; import SearchThumbnailPlayer from "@/components/player/SearchThumbnailPlayer"; import { Input } from "@/components/ui/input"; import { Toaster } from "@/components/ui/sonner"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { Preview } from "@/types/preview"; import { SearchFilter, SearchResult } from "@/types/search"; -import { useCallback, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; +import { isMobileOnly } from "react-device-detect"; import { LuExternalLink, + LuImage, LuSearchCheck, LuSearchX, + LuText, LuXCircle, } from "react-icons/lu"; import { Link } from "react-router-dom"; @@ -40,6 +49,15 @@ export default function SearchView({ onUpdateFilter, onOpenSearch, }: SearchViewProps) { + // remove duplicate event ids + + const uniqueResults = useMemo(() => { + return searchResults?.filter( + (value, index, self) => + index === self.findIndex((v) => v.id === value.id), + ); + }, [searchResults]); + // detail const [searchDetail, setSearchDetail] = useState(); @@ -57,6 +75,25 @@ export default function SearchView({ [onOpenSearch], ); + // confidence score - probably needs tweaking + + const zScoreToConfidence = (score: number, source: string) => { + let midpoint, scale; + + if (source === "thumbnail") { + midpoint = 2; + scale = 0.5; + } else { + midpoint = 0.5; + scale = 1.5; + } + + // Sigmoid function: 1 / (1 + e^x) + const confidence = 1 / (1 + Math.exp((score - midpoint) * scale)); + + return Math.round(confidence * 100); + }; + return (
@@ -69,10 +106,12 @@ export default function SearchView({ />
-
+
setSearch(e.target.value)} /> @@ -124,8 +163,8 @@ export default function SearchView({ )}
- {searchResults && - searchResults.map((value) => { + {uniqueResults && + uniqueResults.map((value) => { const selected = false; return ( @@ -145,6 +184,34 @@ export default function SearchView({ scrollLock={false} onClick={onSelectSearch} /> +
+ + + + {value.search_source == "thumbnail" ? ( + + ) : ( + + )} + {zScoreToConfidence( + value.search_distance, + value.search_source, + )} + % + + + + Matched {value.search_source} at{" "} + {zScoreToConfidence( + value.search_distance, + value.search_source, + )} + % + + +