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:
Nicolas Mowen 2025-03-14 10:23:37 -06:00 committed by GitHub
parent d2368f5cbb
commit a995872d1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 508 additions and 310 deletions

View File

@ -3,19 +3,55 @@ id: face_recognition
title: 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. 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 ## 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 ```yaml
face_recognition: face_recognition:
enabled: true 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 ## Dataset
The number of images needed for a sufficient training set for face recognition varies depending on several factors: The number of images needed for a sufficient training set for face recognition varies depending on several factors:

View File

@ -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. - **`detection_threshold`**: License plate object detection confidence score required before recognition runs.
- Default: `0.7` - 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. - **`min_area`**: Defines the minimum size (in pixels) a license plate must be before recognition runs.
- Default: `1000` pixels. - Default: `1000` pixels.
- Depending on the resolution of your camera's `detect` stream, you can increase this value to ignore small or distant plates. - Depending on the resolution of your camera's `detect` stream, you can increase this value to ignore small or distant plates.

View File

@ -937,12 +937,6 @@ class LicensePlateProcessingMixin:
if not license_plate: if not license_plate:
return 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") license_plate_box = license_plate.get("box")
# check that license plate is valid # check that license plate is valid

View File

@ -255,6 +255,19 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
</Link> </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> </DropdownMenuGroup>
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}> <DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
Appearance Appearance

View File

@ -344,7 +344,7 @@ function TrainingGrid({
key={image} key={image}
image={image} image={image}
faceNames={faceNames} faceNames={faceNames}
threshold={config.face_recognition.threshold} threshold={config.face_recognition.recognition_threshold}
selected={selectedFaces.includes(image)} selected={selectedFaces.includes(image)}
onClick={() => onClickFace(image)} onClick={() => onClickFace(image)}
onRefresh={onRefresh} onRefresh={onRefresh}

View File

@ -35,7 +35,7 @@ import MotionTunerView from "@/views/settings/MotionTunerView";
import MasksAndZonesView from "@/views/settings/MasksAndZonesView"; import MasksAndZonesView from "@/views/settings/MasksAndZonesView";
import AuthenticationView from "@/views/settings/AuthenticationView"; import AuthenticationView from "@/views/settings/AuthenticationView";
import NotificationView from "@/views/settings/NotificationsSettingsView"; 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 UiSettingsView from "@/views/settings/UiSettingsView";
import { useSearchEffect } from "@/hooks/use-overlay-state"; import { useSearchEffect } from "@/hooks/use-overlay-state";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
@ -46,7 +46,7 @@ import { useIsAdmin } from "@/hooks/use-is-admin";
const allSettingsViews = [ const allSettingsViews = [
"UI settings", "UI settings",
"explore settings", "classification settings",
"camera settings", "camera settings",
"masks / zones", "masks / zones",
"motion tuner", "motion tuner",
@ -245,8 +245,8 @@ export default function Settings() {
</div> </div>
<div className="mt-2 flex h-full w-full flex-col items-start md:h-dvh md:pb-24"> <div className="mt-2 flex h-full w-full flex-col items-start md:h-dvh md:pb-24">
{page == "UI settings" && <UiSettingsView />} {page == "UI settings" && <UiSettingsView />}
{page == "explore settings" && ( {page == "classification settings" && (
<SearchSettingsView setUnsavedChanges={setUnsavedChanges} /> <ClassificationSettingsView setUnsavedChanges={setUnsavedChanges} />
)} )}
{page == "debug" && ( {page == "debug" && (
<ObjectSettingsView selectedCamera={selectedCamera} /> <ObjectSettingsView selectedCamera={selectedCamera} />

View File

@ -333,7 +333,8 @@ export interface FrigateConfig {
face_recognition: { face_recognition: {
enabled: boolean; enabled: boolean;
threshold: number; detection_threshold: number;
recognition_threshold: number;
}; };
ffmpeg: { ffmpeg: {

View 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>
);
}

View File

@ -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>
);
}