mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Add face recognition and license plate recognition to settings UI (#17152)
* Refactor explore settings to classification settings * Cleanup * Add face config section * Add license plate recognition to settings * Update face recognition docs * Fix variable usage * Fix typo * Update docs/docs/configuration/face_recognition.md Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Improve spacing and add face library to mobile * Clarify docs --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									d2368f5cbb
								
							
						
					
					
						commit
						a995872d1c
					
				| @ -3,19 +3,55 @@ id: face_recognition | ||||
| title: Face Recognition | ||||
| --- | ||||
| 
 | ||||
| Face recognition allows people to be assigned names and when their face is recognized Frigate will assign the person's name as a sub label. This information is included in the UI, filters, as well as in notifications. | ||||
| Face recognition identifies known individuals by matching detected faces with previously learned facial data. When a known person is recognized, their name will be added as a `sub_label`. This information is included in the UI, filters, as well as in notifications. | ||||
| 
 | ||||
| ## Model Requirements | ||||
| 
 | ||||
| Frigate has support for CV2 Local Binary Pattern Face Recognizer to recognize faces, which runs locally. A lightweight face landmark detection model is also used to align faces before running them through the face recognizer. | ||||
| 
 | ||||
| Users running a Frigate+ model (or any custom model that natively detects faces) should ensure that `face` is added to the [list of objects to track](../plus/#available-label-types) either globally or for a specific camera. This will allow face detection to run at the same time as object detection and be more efficient. | ||||
| 
 | ||||
| Users without a model that detects faces can still run face recognition. Frigate uses a lightweight DNN face detection model that runs on the CPU. In this case, you should _not_ define `face` in your list of objects to track. | ||||
| 
 | ||||
| :::note | ||||
| 
 | ||||
| Frigate needs to first detect a `face` before it can recognize a face. | ||||
| 
 | ||||
| ::: | ||||
| 
 | ||||
| ## Minimum System Requirements | ||||
| 
 | ||||
| Face recognition is lightweight and runs on the CPU, there are no significantly different system requirements than running Frigate itself. | ||||
| 
 | ||||
| ## Configuration | ||||
| 
 | ||||
| Face recognition is disabled by default, face recognition must be enabled in your config file before it can be used. Face recognition is a global configuration setting. | ||||
| Face recognition is disabled by default, face recognition must be enabled in the UI or in your config file before it can be used. Face recognition is a global configuration setting. | ||||
| 
 | ||||
| ```yaml | ||||
| face_recognition: | ||||
|   enabled: true | ||||
| ``` | ||||
| 
 | ||||
| ## Advanced Configuration | ||||
| 
 | ||||
| Fine-tune face recognition with these optional parameters: | ||||
| 
 | ||||
| ### Detection | ||||
| 
 | ||||
| - `detection_threshold`: Face detection confidence score required before recognition runs: | ||||
|   - Default: `0.7` | ||||
|   - Note: This is field only applies to the standalone face detection model, `min_score` should be used to filter for models that have face detection built in. | ||||
| - `min_area`: Defines the minimum size (in pixels) a face must be before recognition runs. | ||||
|   - Default: `500` pixels. | ||||
|   - Depending on the resolution of your camera's `detect` stream, you can increase this value to ignore small or distant faces. | ||||
| 
 | ||||
| ### Recognition | ||||
| 
 | ||||
| - `recognition_threshold`: Recognition confidence score required to add the face to the object as a sub label. | ||||
|   - Default: `0.9`. | ||||
| - `blur_confidence_filter`: Enables a filter that calculates how blurry the face is and adjusts the confidence based on this. | ||||
|   - Default: `True`. | ||||
| 
 | ||||
| ## Dataset | ||||
| 
 | ||||
| The number of images needed for a sufficient training set for face recognition varies depending on several factors: | ||||
|  | ||||
| @ -51,7 +51,7 @@ Fine-tune the LPR feature using these optional parameters: | ||||
| 
 | ||||
| - **`detection_threshold`**: License plate object detection confidence score required before recognition runs. | ||||
|   - Default: `0.7` | ||||
|   - Note: If you are using a Frigate+ model and you set the `threshold` in your objects config for `license_plate` higher than this value, recognition will never run. It's best to ensure these values match, or this `detection_threshold` is lower than your object config `threshold`. | ||||
|   - Note: This is field only applies to the standalone license plate detection model, `min_score` should be used to filter for models that have license plate detection built in. | ||||
| - **`min_area`**: Defines the minimum size (in pixels) a license plate must be before recognition runs. | ||||
|   - Default: `1000` pixels. | ||||
|   - Depending on the resolution of your camera's `detect` stream, you can increase this value to ignore small or distant plates. | ||||
|  | ||||
| @ -937,12 +937,6 @@ class LicensePlateProcessingMixin: | ||||
|             if not license_plate: | ||||
|                 return | ||||
| 
 | ||||
|             if license_plate.get("score") < self.lpr_config.detection_threshold: | ||||
|                 logger.debug( | ||||
|                     f"Plate detection score is less than the threshold ({license_plate['score']:0.2f} < {self.lpr_config.detection_threshold})" | ||||
|                 ) | ||||
|                 return | ||||
| 
 | ||||
|             license_plate_box = license_plate.get("box") | ||||
| 
 | ||||
|             # check that license plate is valid | ||||
|  | ||||
| @ -255,6 +255,19 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { | ||||
|                   </Link> | ||||
|                 </> | ||||
|               )} | ||||
|               {isAdmin && isMobile && ( | ||||
|                 <> | ||||
|                   <Link to="/faces"> | ||||
|                     <MenuItem | ||||
|                       className="flex w-full items-center p-2 text-sm" | ||||
|                       aria-label="Face Library" | ||||
|                     > | ||||
|                       <LuSquarePen className="mr-2 size-4" /> | ||||
|                       <span>Configuration editor</span> | ||||
|                     </MenuItem> | ||||
|                   </Link> | ||||
|                 </> | ||||
|               )} | ||||
|             </DropdownMenuGroup> | ||||
|             <DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}> | ||||
|               Appearance | ||||
|  | ||||
| @ -344,7 +344,7 @@ function TrainingGrid({ | ||||
|           key={image} | ||||
|           image={image} | ||||
|           faceNames={faceNames} | ||||
|           threshold={config.face_recognition.threshold} | ||||
|           threshold={config.face_recognition.recognition_threshold} | ||||
|           selected={selectedFaces.includes(image)} | ||||
|           onClick={() => onClickFace(image)} | ||||
|           onRefresh={onRefresh} | ||||
|  | ||||
| @ -35,7 +35,7 @@ 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 ClassificationSettingsView from "@/views/settings/ClassificationSettingsView"; | ||||
| import UiSettingsView from "@/views/settings/UiSettingsView"; | ||||
| import { useSearchEffect } from "@/hooks/use-overlay-state"; | ||||
| import { useSearchParams } from "react-router-dom"; | ||||
| @ -46,7 +46,7 @@ import { useIsAdmin } from "@/hooks/use-is-admin"; | ||||
| 
 | ||||
| const allSettingsViews = [ | ||||
|   "UI settings", | ||||
|   "explore settings", | ||||
|   "classification settings", | ||||
|   "camera settings", | ||||
|   "masks / zones", | ||||
|   "motion tuner", | ||||
| @ -245,8 +245,8 @@ export default function Settings() { | ||||
|       </div> | ||||
|       <div className="mt-2 flex h-full w-full flex-col items-start md:h-dvh md:pb-24"> | ||||
|         {page == "UI settings" && <UiSettingsView />} | ||||
|         {page == "explore settings" && ( | ||||
|           <SearchSettingsView setUnsavedChanges={setUnsavedChanges} /> | ||||
|         {page == "classification settings" && ( | ||||
|           <ClassificationSettingsView setUnsavedChanges={setUnsavedChanges} /> | ||||
|         )} | ||||
|         {page == "debug" && ( | ||||
|           <ObjectSettingsView selectedCamera={selectedCamera} /> | ||||
|  | ||||
| @ -333,7 +333,8 @@ export interface FrigateConfig { | ||||
| 
 | ||||
|   face_recognition: { | ||||
|     enabled: boolean; | ||||
|     threshold: number; | ||||
|     detection_threshold: number; | ||||
|     recognition_threshold: number; | ||||
|   }; | ||||
| 
 | ||||
|   ffmpeg: { | ||||
|  | ||||
							
								
								
									
										449
									
								
								web/src/views/settings/ClassificationSettingsView.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										449
									
								
								web/src/views/settings/ClassificationSettingsView.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,449 @@ | ||||
| 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 ClassificationSettings = { | ||||
|   search: { | ||||
|     enabled?: boolean; | ||||
|     reindex?: boolean; | ||||
|     model_size?: SearchModelSize; | ||||
|   }; | ||||
|   face: { | ||||
|     enabled?: boolean; | ||||
|   }; | ||||
|   lpr: { | ||||
|     enabled?: boolean; | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| type ClassificationSettingsViewProps = { | ||||
|   setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>; | ||||
| }; | ||||
| export default function ClassificationSettingsView({ | ||||
|   setUnsavedChanges, | ||||
| }: ClassificationSettingsViewProps) { | ||||
|   const { data: config, mutate: updateConfig } = | ||||
|     useSWR<FrigateConfig>("config"); | ||||
|   const [changedValue, setChangedValue] = useState(false); | ||||
|   const [isLoading, setIsLoading] = useState(false); | ||||
| 
 | ||||
|   const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; | ||||
| 
 | ||||
|   const [classificationSettings, setClassificationSettings] = | ||||
|     useState<ClassificationSettings>({ | ||||
|       search: { | ||||
|         enabled: undefined, | ||||
|         reindex: undefined, | ||||
|         model_size: undefined, | ||||
|       }, | ||||
|       face: { | ||||
|         enabled: undefined, | ||||
|       }, | ||||
|       lpr: { | ||||
|         enabled: undefined, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|   const [origSearchSettings, setOrigSearchSettings] = | ||||
|     useState<ClassificationSettings>({ | ||||
|       search: { | ||||
|         enabled: undefined, | ||||
|         reindex: undefined, | ||||
|         model_size: undefined, | ||||
|       }, | ||||
|       face: { | ||||
|         enabled: undefined, | ||||
|       }, | ||||
|       lpr: { | ||||
|         enabled: undefined, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (config) { | ||||
|       if (classificationSettings?.search.enabled == undefined) { | ||||
|         setClassificationSettings({ | ||||
|           search: { | ||||
|             enabled: config.semantic_search.enabled, | ||||
|             reindex: config.semantic_search.reindex, | ||||
|             model_size: config.semantic_search.model_size, | ||||
|           }, | ||||
|           face: { | ||||
|             enabled: config.face_recognition.enabled, | ||||
|           }, | ||||
|           lpr: { | ||||
|             enabled: config.lpr.enabled, | ||||
|           }, | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       setOrigSearchSettings({ | ||||
|         search: { | ||||
|           enabled: config.semantic_search.enabled, | ||||
|           reindex: config.semantic_search.reindex, | ||||
|           model_size: config.semantic_search.model_size, | ||||
|         }, | ||||
|         face: { | ||||
|           enabled: config.face_recognition.enabled, | ||||
|         }, | ||||
|         lpr: { | ||||
|           enabled: config.lpr.enabled, | ||||
|         }, | ||||
|       }); | ||||
|     } | ||||
|     // we know that these deps are correct
 | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [config]); | ||||
| 
 | ||||
|   const handleClassificationConfigChange = ( | ||||
|     newConfig: Partial<ClassificationSettings>, | ||||
|   ) => { | ||||
|     setClassificationSettings((prevConfig) => ({ | ||||
|       search: { | ||||
|         ...prevConfig.search, | ||||
|         ...newConfig.search, | ||||
|       }, | ||||
|       face: { ...prevConfig.face, ...newConfig.face }, | ||||
|       lpr: { ...prevConfig.lpr, ...newConfig.lpr }, | ||||
|     })); | ||||
|     setUnsavedChanges(true); | ||||
|     setChangedValue(true); | ||||
|   }; | ||||
| 
 | ||||
|   const saveToConfig = useCallback(async () => { | ||||
|     setIsLoading(true); | ||||
| 
 | ||||
|     axios | ||||
|       .put( | ||||
|         `config/set?semantic_search.enabled=${classificationSettings.search.enabled ? "True" : "False"}&semantic_search.reindex=${classificationSettings.search.reindex ? "True" : "False"}&semantic_search.model_size=${classificationSettings.search.model_size}`, | ||||
|         { | ||||
|           requires_restart: 0, | ||||
|         }, | ||||
|       ) | ||||
|       .then((res) => { | ||||
|         if (res.status === 200) { | ||||
|           toast.success("Classification 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) => { | ||||
|         const errorMessage = | ||||
|           error.response?.data?.message || | ||||
|           error.response?.data?.detail || | ||||
|           "Unknown error"; | ||||
|         toast.error(`Failed to save config changes: ${errorMessage}`, { | ||||
|           position: "top-center", | ||||
|         }); | ||||
|       }) | ||||
|       .finally(() => { | ||||
|         setIsLoading(false); | ||||
|       }); | ||||
|   }, [updateConfig, classificationSettings.search]); | ||||
| 
 | ||||
|   const onCancel = useCallback(() => { | ||||
|     setClassificationSettings(origSearchSettings); | ||||
|     setChangedValue(false); | ||||
|     removeMessage("search_settings", "search_settings"); | ||||
|   }, [origSearchSettings, removeMessage]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (changedValue) { | ||||
|       addMessage( | ||||
|         "search_settings", | ||||
|         `Unsaved Classification 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 = "Classification Settings - Frigate"; | ||||
|   }, []); | ||||
| 
 | ||||
|   if (!config) { | ||||
|     return <ActivityIndicator />; | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="flex size-full flex-col md:flex-row"> | ||||
|       <Toaster position="top-center" closeButton={true} /> | ||||
|       <div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0"> | ||||
|         <Heading as="h3" className="my-2"> | ||||
|           Classification Settings | ||||
|         </Heading> | ||||
|         <Separator className="my-2 flex bg-secondary" /> | ||||
|         <Heading as="h4" className="my-2"> | ||||
|           Semantic Search | ||||
|         </Heading> | ||||
|         <div className="max-w-6xl"> | ||||
|           <div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant"> | ||||
|             <p> | ||||
|               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. | ||||
|             </p> | ||||
| 
 | ||||
|             <div className="flex items-center text-primary"> | ||||
|               <Link | ||||
|                 to="https://docs.frigate.video/configuration/semantic_search" | ||||
|                 target="_blank" | ||||
|                 rel="noopener noreferrer" | ||||
|                 className="inline" | ||||
|               > | ||||
|                 Read the Documentation | ||||
|                 <LuExternalLink className="ml-2 inline-flex size-3" /> | ||||
|               </Link> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className="flex w-full max-w-lg flex-col space-y-6"> | ||||
|           <div className="flex flex-row items-center"> | ||||
|             <Switch | ||||
|               id="enabled" | ||||
|               className="mr-3" | ||||
|               disabled={classificationSettings.search.enabled === undefined} | ||||
|               checked={classificationSettings.search.enabled === true} | ||||
|               onCheckedChange={(isChecked) => { | ||||
|                 handleClassificationConfigChange({ | ||||
|                   search: { enabled: isChecked }, | ||||
|                 }); | ||||
|               }} | ||||
|             /> | ||||
|             <div className="space-y-0.5"> | ||||
|               <Label htmlFor="enabled">Enabled</Label> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div className="flex flex-col"> | ||||
|             <div className="flex flex-row items-center"> | ||||
|               <Switch | ||||
|                 id="reindex" | ||||
|                 className="mr-3" | ||||
|                 disabled={classificationSettings.search.reindex === undefined} | ||||
|                 checked={classificationSettings.search.reindex === true} | ||||
|                 onCheckedChange={(isChecked) => { | ||||
|                   handleClassificationConfigChange({ | ||||
|                     search: { reindex: isChecked }, | ||||
|                   }); | ||||
|                 }} | ||||
|               /> | ||||
|               <div className="space-y-0.5"> | ||||
|                 <Label htmlFor="reindex">Re-Index On Startup</Label> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div className="mt-3 text-sm text-muted-foreground"> | ||||
|               Re-indexing will reprocess all thumbnails and descriptions (if | ||||
|               enabled) and apply the embeddings on each startup.{" "} | ||||
|               <em>Don't forget to disable the option after restarting!</em> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div className="mt-2 flex flex-col space-y-6"> | ||||
|             <div className="space-y-0.5"> | ||||
|               <div className="text-md">Model Size</div> | ||||
|               <div className="space-y-1 text-sm text-muted-foreground"> | ||||
|                 <p> | ||||
|                   The size of the model used for Semantic Search embeddings. | ||||
|                 </p> | ||||
|                 <ul className="list-disc pl-5 text-sm"> | ||||
|                   <li> | ||||
|                     Using <em>small</em> employs a quantized version of the | ||||
|                     model that uses less RAM and runs faster on CPU with a very | ||||
|                     negligible difference in embedding quality. | ||||
|                   </li> | ||||
|                   <li> | ||||
|                     Using <em>large</em> employs the full Jina model and will | ||||
|                     automatically run on the GPU if applicable. | ||||
|                   </li> | ||||
|                 </ul> | ||||
|               </div> | ||||
|             </div> | ||||
|             <Select | ||||
|               value={classificationSettings.search.model_size} | ||||
|               onValueChange={(value) => | ||||
|                 handleClassificationConfigChange({ | ||||
|                   search: { | ||||
|                     model_size: value as SearchModelSize, | ||||
|                   }, | ||||
|                 }) | ||||
|               } | ||||
|             > | ||||
|               <SelectTrigger className="w-20"> | ||||
|                 {classificationSettings.search.model_size} | ||||
|               </SelectTrigger> | ||||
|               <SelectContent> | ||||
|                 <SelectGroup> | ||||
|                   {["small", "large"].map((size) => ( | ||||
|                     <SelectItem | ||||
|                       key={size} | ||||
|                       className="cursor-pointer" | ||||
|                       value={size} | ||||
|                     > | ||||
|                       {size} | ||||
|                     </SelectItem> | ||||
|                   ))} | ||||
|                 </SelectGroup> | ||||
|               </SelectContent> | ||||
|             </Select> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className="my-2 space-y-6"> | ||||
|           <Separator className="my-2 flex bg-secondary" /> | ||||
| 
 | ||||
|           <Heading as="h4" className="my-2"> | ||||
|             Face Recognition | ||||
|           </Heading> | ||||
|           <div className="max-w-6xl"> | ||||
|             <div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant"> | ||||
|               <p> | ||||
|                 Face recognition allows people to be assigned names and when | ||||
|                 their face is recognized Frigate will assign the person's name | ||||
|                 as a sub label. This information is included in the UI, filters, | ||||
|                 as well as in notifications. | ||||
|               </p> | ||||
| 
 | ||||
|               <div className="flex items-center text-primary"> | ||||
|                 <Link | ||||
|                   to="https://docs.frigate.video/configuration/face_recognition" | ||||
|                   target="_blank" | ||||
|                   rel="noopener noreferrer" | ||||
|                   className="inline" | ||||
|                 > | ||||
|                   Read the Documentation | ||||
|                   <LuExternalLink className="ml-2 inline-flex size-3" /> | ||||
|                 </Link> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className="flex w-full max-w-lg flex-col space-y-6"> | ||||
|             <div className="flex flex-row items-center"> | ||||
|               <Switch | ||||
|                 id="enabled" | ||||
|                 className="mr-3" | ||||
|                 disabled={classificationSettings.face.enabled === undefined} | ||||
|                 checked={classificationSettings.face.enabled === true} | ||||
|                 onCheckedChange={(isChecked) => { | ||||
|                   handleClassificationConfigChange({ | ||||
|                     face: { enabled: isChecked }, | ||||
|                   }); | ||||
|                 }} | ||||
|               /> | ||||
|               <div className="space-y-0.5"> | ||||
|                 <Label htmlFor="enabled">Enabled</Label> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <Separator className="my-2 flex bg-secondary" /> | ||||
| 
 | ||||
|           <Heading as="h4" className="my-2"> | ||||
|             License Plate Recognition | ||||
|           </Heading> | ||||
|           <div className="max-w-6xl"> | ||||
|             <div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant"> | ||||
|               <p> | ||||
|                 Frigate can recognize license plates on vehicles and | ||||
|                 automatically add the detected characters to the | ||||
|                 recognized_license_plate field or a known name as a sub_label to | ||||
|                 objects that are of type car. A common use case may be to read | ||||
|                 the license plates of cars pulling into a driveway or cars | ||||
|                 passing by on a street. | ||||
|               </p> | ||||
| 
 | ||||
|               <div className="flex items-center text-primary"> | ||||
|                 <Link | ||||
|                   to="https://docs.frigate.video/configuration/license_plate_recognition" | ||||
|                   target="_blank" | ||||
|                   rel="noopener noreferrer" | ||||
|                   className="inline" | ||||
|                 > | ||||
|                   Read the Documentation | ||||
|                   <LuExternalLink className="ml-2 inline-flex size-3" /> | ||||
|                 </Link> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className="flex w-full max-w-lg flex-col space-y-6"> | ||||
|             <div className="flex flex-row items-center"> | ||||
|               <Switch | ||||
|                 id="enabled" | ||||
|                 className="mr-3" | ||||
|                 disabled={classificationSettings.lpr.enabled === undefined} | ||||
|                 checked={classificationSettings.lpr.enabled === true} | ||||
|                 onCheckedChange={(isChecked) => { | ||||
|                   handleClassificationConfigChange({ | ||||
|                     lpr: { enabled: isChecked }, | ||||
|                   }); | ||||
|                 }} | ||||
|               /> | ||||
|               <div className="space-y-0.5"> | ||||
|                 <Label htmlFor="enabled">Enabled</Label> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <Separator className="my-2 flex bg-secondary" /> | ||||
| 
 | ||||
|           <div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]"> | ||||
|             <Button | ||||
|               className="flex flex-1" | ||||
|               aria-label="Reset" | ||||
|               onClick={onCancel} | ||||
|             > | ||||
|               Reset | ||||
|             </Button> | ||||
|             <Button | ||||
|               variant="select" | ||||
|               disabled={!changedValue || isLoading} | ||||
|               className="flex flex-1" | ||||
|               aria-label="Save" | ||||
|               onClick={saveToConfig} | ||||
|             > | ||||
|               {isLoading ? ( | ||||
|                 <div className="flex flex-row items-center gap-2"> | ||||
|                   <ActivityIndicator /> | ||||
|                   <span>Saving...</span> | ||||
|                 </div> | ||||
|               ) : ( | ||||
|                 "Save" | ||||
|               )} | ||||
|             </Button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| @ -1,295 +0,0 @@ | ||||
| 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<React.SetStateAction<boolean>>; | ||||
| }; | ||||
| 
 | ||||
| type SearchSettings = { | ||||
|   enabled?: boolean; | ||||
|   reindex?: boolean; | ||||
|   model_size?: SearchModelSize; | ||||
| }; | ||||
| 
 | ||||
| export default function SearchSettingsView({ | ||||
|   setUnsavedChanges, | ||||
| }: SearchSettingsViewProps) { | ||||
|   const { data: config, mutate: updateConfig } = | ||||
|     useSWR<FrigateConfig>("config"); | ||||
|   const [changedValue, setChangedValue] = useState(false); | ||||
|   const [isLoading, setIsLoading] = useState(false); | ||||
| 
 | ||||
|   const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; | ||||
| 
 | ||||
|   const [searchSettings, setSearchSettings] = useState<SearchSettings>({ | ||||
|     enabled: undefined, | ||||
|     reindex: undefined, | ||||
|     model_size: undefined, | ||||
|   }); | ||||
| 
 | ||||
|   const [origSearchSettings, setOrigSearchSettings] = useState<SearchSettings>({ | ||||
|     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<SearchSettings>) => { | ||||
|     setSearchSettings((prevConfig) => ({ ...prevConfig, ...newConfig })); | ||||
|     setUnsavedChanges(true); | ||||
|     setChangedValue(true); | ||||
|   }; | ||||
| 
 | ||||
|   const saveToConfig = useCallback(async () => { | ||||
|     setIsLoading(true); | ||||
| 
 | ||||
|     axios | ||||
|       .put( | ||||
|         `config/set?semantic_search.enabled=${searchSettings.enabled ? "True" : "False"}&semantic_search.reindex=${searchSettings.reindex ? "True" : "False"}&semantic_search.model_size=${searchSettings.model_size}`, | ||||
|         { | ||||
|           requires_restart: 0, | ||||
|         }, | ||||
|       ) | ||||
|       .then((res) => { | ||||
|         if (res.status === 200) { | ||||
|           toast.success("Explore 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) => { | ||||
|         const errorMessage = | ||||
|           error.response?.data?.message || | ||||
|           error.response?.data?.detail || | ||||
|           "Unknown error"; | ||||
|         toast.error(`Failed to save config changes: ${errorMessage}`, { | ||||
|           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 Explore 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 = "Explore Settings - Frigate"; | ||||
|   }, []); | ||||
| 
 | ||||
|   if (!config) { | ||||
|     return <ActivityIndicator />; | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="flex size-full flex-col md:flex-row"> | ||||
|       <Toaster position="top-center" closeButton={true} /> | ||||
|       <div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0"> | ||||
|         <Heading as="h3" className="my-2"> | ||||
|           Explore Settings | ||||
|         </Heading> | ||||
|         <Separator className="my-2 flex bg-secondary" /> | ||||
|         <Heading as="h4" className="my-2"> | ||||
|           Semantic Search | ||||
|         </Heading> | ||||
|         <div className="max-w-6xl"> | ||||
|           <div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant"> | ||||
|             <p> | ||||
|               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. | ||||
|             </p> | ||||
| 
 | ||||
|             <div className="flex items-center text-primary"> | ||||
|               <Link | ||||
|                 to="https://docs.frigate.video/configuration/semantic_search" | ||||
|                 target="_blank" | ||||
|                 rel="noopener noreferrer" | ||||
|                 className="inline" | ||||
|               > | ||||
|                 Read the Documentation | ||||
|                 <LuExternalLink className="ml-2 inline-flex size-3" /> | ||||
|               </Link> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className="flex w-full max-w-lg flex-col space-y-6"> | ||||
|           <div className="flex flex-row items-center"> | ||||
|             <Switch | ||||
|               id="enabled" | ||||
|               className="mr-3" | ||||
|               disabled={searchSettings.enabled === undefined} | ||||
|               checked={searchSettings.enabled === true} | ||||
|               onCheckedChange={(isChecked) => { | ||||
|                 handleSearchConfigChange({ enabled: isChecked }); | ||||
|               }} | ||||
|             /> | ||||
|             <div className="space-y-0.5"> | ||||
|               <Label htmlFor="enabled">Enabled</Label> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div className="flex flex-col"> | ||||
|             <div className="flex flex-row items-center"> | ||||
|               <Switch | ||||
|                 id="reindex" | ||||
|                 className="mr-3" | ||||
|                 disabled={searchSettings.reindex === undefined} | ||||
|                 checked={searchSettings.reindex === true} | ||||
|                 onCheckedChange={(isChecked) => { | ||||
|                   handleSearchConfigChange({ reindex: isChecked }); | ||||
|                 }} | ||||
|               /> | ||||
|               <div className="space-y-0.5"> | ||||
|                 <Label htmlFor="reindex">Re-Index On Startup</Label> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div className="mt-3 text-sm text-muted-foreground"> | ||||
|               Re-indexing will reprocess all thumbnails and descriptions (if | ||||
|               enabled) and apply the embeddings on each startup.{" "} | ||||
|               <em>Don't forget to disable the option after restarting!</em> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div className="mt-2 flex flex-col space-y-6"> | ||||
|             <div className="space-y-0.5"> | ||||
|               <div className="text-md">Model Size</div> | ||||
|               <div className="space-y-1 text-sm text-muted-foreground"> | ||||
|                 <p> | ||||
|                   The size of the model used for Semantic Search embeddings. | ||||
|                 </p> | ||||
|                 <ul className="list-disc pl-5 text-sm"> | ||||
|                   <li> | ||||
|                     Using <em>small</em> employs a quantized version of the | ||||
|                     model that uses less RAM and runs faster on CPU with a very | ||||
|                     negligible difference in embedding quality. | ||||
|                   </li> | ||||
|                   <li> | ||||
|                     Using <em>large</em> employs the full Jina model and will | ||||
|                     automatically run on the GPU if applicable. | ||||
|                   </li> | ||||
|                 </ul> | ||||
|               </div> | ||||
|             </div> | ||||
|             <Select | ||||
|               value={searchSettings.model_size} | ||||
|               onValueChange={(value) => | ||||
|                 handleSearchConfigChange({ | ||||
|                   model_size: value as SearchModelSize, | ||||
|                 }) | ||||
|               } | ||||
|             > | ||||
|               <SelectTrigger className="w-20"> | ||||
|                 {searchSettings.model_size} | ||||
|               </SelectTrigger> | ||||
|               <SelectContent> | ||||
|                 <SelectGroup> | ||||
|                   {["small", "large"].map((size) => ( | ||||
|                     <SelectItem | ||||
|                       key={size} | ||||
|                       className="cursor-pointer" | ||||
|                       value={size} | ||||
|                     > | ||||
|                       {size} | ||||
|                     </SelectItem> | ||||
|                   ))} | ||||
|                 </SelectGroup> | ||||
|               </SelectContent> | ||||
|             </Select> | ||||
|           </div> | ||||
|         </div> | ||||
|         <Separator className="my-2 flex bg-secondary" /> | ||||
| 
 | ||||
|         <div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]"> | ||||
|           <Button className="flex flex-1" aria-label="Reset" onClick={onCancel}> | ||||
|             Reset | ||||
|           </Button> | ||||
|           <Button | ||||
|             variant="select" | ||||
|             disabled={!changedValue || isLoading} | ||||
|             className="flex flex-1" | ||||
|             aria-label="Save" | ||||
|             onClick={saveToConfig} | ||||
|           > | ||||
|             {isLoading ? ( | ||||
|               <div className="flex flex-row items-center gap-2"> | ||||
|                 <ActivityIndicator /> | ||||
|                 <span>Saving...</span> | ||||
|               </div> | ||||
|             ) : ( | ||||
|               "Save" | ||||
|             )} | ||||
|           </Button> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user