From 4716ab202fd40fbcc86bc6af65af061144db8682 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 15 Oct 2024 14:52:06 -0600 Subject: [PATCH] Add search settings and rename general to ui settings --- web/src/pages/Settings.tsx | 15 +- web/src/types/frigateConfig.ts | 5 +- web/src/views/settings/SearchSettingsView.tsx | 284 ++++++++++++++++++ ...ralSettingsView.tsx => UiSettingsView.tsx} | 2 +- 4 files changed, 299 insertions(+), 7 deletions(-) create mode 100644 web/src/views/settings/SearchSettingsView.tsx rename web/src/views/settings/{GeneralSettingsView.tsx => UiSettingsView.tsx} (99%) diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 6d147b332..4ba29dd08 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -29,16 +29,18 @@ import { ZoneMaskFilterButton } from "@/components/filter/ZoneMaskFilter"; import { PolygonType } from "@/types/canvas"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import scrollIntoView from "scroll-into-view-if-needed"; -import GeneralSettingsView from "@/views/settings/GeneralSettingsView"; import CameraSettingsView from "@/views/settings/CameraSettingsView"; import ObjectSettingsView from "@/views/settings/ObjectSettingsView"; import MotionTunerView from "@/views/settings/MotionTunerView"; import MasksAndZonesView from "@/views/settings/MasksAndZonesView"; import AuthenticationView from "@/views/settings/AuthenticationView"; import NotificationView from "@/views/settings/NotificationsSettingsView"; +import SearchSettingsView from "@/views/settings/SearchSettingsView"; +import UiSettingsView from "@/views/settings/UiSettingsView"; const allSettingsViews = [ - "general", + "UI settings", + "search settings", "camera settings", "masks / zones", "motion tuner", @@ -49,7 +51,7 @@ const allSettingsViews = [ type SettingsType = (typeof allSettingsViews)[number]; export default function Settings() { - const [page, setPage] = useState("general"); + const [page, setPage] = useState("UI settings"); const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); const tabsRef = useRef(null); @@ -140,7 +142,7 @@ export default function Settings() { {Object.values(settingsViews).map((item) => (
- {page == "general" && } + {page == "UI settings" && } + {page == "search settings" && ( + + )} {page == "debug" && ( )} diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts index 2c54b289e..76d9cfa67 100644 --- a/web/src/types/frigateConfig.ts +++ b/web/src/types/frigateConfig.ts @@ -27,6 +27,8 @@ export const ATTRIBUTE_LABELS = [ "ups", ]; +export type SearchModelSize = "small" | "large"; + export interface CameraConfig { audio: { enabled: boolean; @@ -418,7 +420,8 @@ export interface FrigateConfig { semantic_search: { enabled: boolean; - model_size: string; + reindex: boolean; + model_size: SearchModelSize; }; snapshots: { diff --git a/web/src/views/settings/SearchSettingsView.tsx b/web/src/views/settings/SearchSettingsView.tsx new file mode 100644 index 000000000..0bbb8a15a --- /dev/null +++ b/web/src/views/settings/SearchSettingsView.tsx @@ -0,0 +1,284 @@ +import Heading from "@/components/ui/heading"; +import { FrigateConfig, SearchModelSize } from "@/types/frigateConfig"; +import useSWR from "swr"; +import axios from "axios"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { useCallback, useContext, useEffect, useState } from "react"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Switch } from "@/components/ui/switch"; +import { Toaster } from "@/components/ui/sonner"; +import { toast } from "sonner"; +import { Separator } from "@/components/ui/separator"; +import { Link } from "react-router-dom"; +import { LuExternalLink } from "react-icons/lu"; +import { StatusBarMessagesContext } from "@/context/statusbar-provider"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, +} from "@/components/ui/select"; + +type SearchSettingsViewProps = { + setUnsavedChanges: React.Dispatch>; +}; + +type SearchSettings = { + enabled?: boolean; + reindex?: boolean; + model_size?: SearchModelSize; +}; + +export default function SearchSettingsView({ + setUnsavedChanges, +}: SearchSettingsViewProps) { + const { data: config, mutate: updateConfig } = + useSWR("config"); + const [changedValue, setChangedValue] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; + + const [searchSettings, setSearchSettings] = useState({ + enabled: undefined, + reindex: undefined, + model_size: undefined, + }); + + const [origSearchSettings, setOrigSearchSettings] = useState({ + enabled: undefined, + reindex: undefined, + model_size: undefined, + }); + + useEffect(() => { + if (config) { + if (searchSettings?.enabled == undefined) { + setSearchSettings({ + enabled: config.semantic_search.enabled, + reindex: config.semantic_search.reindex, + model_size: config.semantic_search.model_size, + }); + } + + setOrigSearchSettings({ + enabled: config.semantic_search.enabled, + reindex: config.semantic_search.reindex, + model_size: config.semantic_search.model_size, + }); + } + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config]); + + const handleSearchConfigChange = (newConfig: Partial) => { + setSearchSettings((prevConfig) => ({ ...prevConfig, ...newConfig })); + setUnsavedChanges(true); + setChangedValue(true); + }; + + const saveToConfig = useCallback(async () => { + setIsLoading(true); + + axios + .put( + `config/set?semantic_search.enabled=${searchSettings.enabled}&semantic_search.reindex=${searchSettings.reindex}&semantic_search.model_size=${searchSettings.model_size}`, + ) + .then((res) => { + if (res.status === 200) { + toast.success("Search settings have been saved.", { + position: "top-center", + }); + setChangedValue(false); + updateConfig(); + } else { + toast.error(`Failed to save config changes: ${res.statusText}`, { + position: "top-center", + }); + } + }) + .catch((error) => { + toast.error( + `Failed to save config changes: ${error.response.data.message}`, + { position: "top-center" }, + ); + }) + .finally(() => { + setIsLoading(false); + }); + }, [ + updateConfig, + searchSettings.enabled, + searchSettings.reindex, + searchSettings.model_size, + ]); + + const onCancel = useCallback(() => { + setSearchSettings(origSearchSettings); + setChangedValue(false); + removeMessage("search_settings", "search_settings"); + }, [origSearchSettings, removeMessage]); + + useEffect(() => { + if (changedValue) { + addMessage( + "search_settings", + `Unsaved search settings changes)`, + undefined, + "search_settings", + ); + } else { + removeMessage("search_settings", "search_settings"); + } + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [changedValue]); + + useEffect(() => { + document.title = "Search Settings - Frigate"; + }, []); + + if (!config) { + return ; + } + + return ( +
+ +
+ + Search Settings + +
+

+ Semantic Search in Frigate allows you to find tracked objects within + your review items using either the image itself, a user-defined text + description, or an automatically generated one. This feature works + by creating embeddings — numerical vector representations — for both + the images and text descriptions of your tracked objects. By + comparing these embeddings, Frigate assesses their similarities to + deliver relevant search results. +

+ +
+ + Read the semantic search docs + + +
+
+ +
+
+
+ +
+ { + handleSearchConfigChange({ enabled: isChecked }); + }} + /> +
+
+
+ +
+ Re-indexing will reprocess all thumbnails and descriptions (if + enabled) and apply the embeddings on each startup. Don't forget + to disable the option after restarting! +
+
+ { + handleSearchConfigChange({ reindex: isChecked }); + }} + /> +
+ +
+
+
Model Size
+
+

+ Configure the size of the model used for semantic search + embeddings: +

+

+ • Configuring the small model employs a quantized + version of the model that uses much less RAM and runs faster + on CPU with a very negligible difference in embedding quality. +

+

+ • Configuring the large model employs the full Jina + model and will automatically run on the GPU if applicable. +

+
+
+ +
+
+
+
+ + +
+
+
+
+ ); +} diff --git a/web/src/views/settings/GeneralSettingsView.tsx b/web/src/views/settings/UiSettingsView.tsx similarity index 99% rename from web/src/views/settings/GeneralSettingsView.tsx rename to web/src/views/settings/UiSettingsView.tsx index 0cb7689f6..c212073c1 100644 --- a/web/src/views/settings/GeneralSettingsView.tsx +++ b/web/src/views/settings/UiSettingsView.tsx @@ -22,7 +22,7 @@ import { const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16]; const WEEK_STARTS_ON = ["Sunday", "Monday"]; -export default function GeneralSettingsView() { +export default function UiSettingsView() { const { data: config } = useSWR("config"); const clearStoredLayouts = useCallback(() => {