diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index e53e121a1..8765ad884 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -924,10 +924,13 @@ cameras: type: thumbnail # Reference data for matching, either an event ID for `thumbnail` or a text string for `description`. (default: none) data: 1751565549.853251-b69j73 - # Similarity threshold for triggering. (default: none) - threshold: 0.7 + # Similarity threshold for triggering. (default: shown below) + threshold: 0.8 # List of actions to perform when the trigger fires. (default: none) - # Available options: `notification` (send a webpush notification) + # Available options: + # - `notification` (send a webpush notification) + # - `sub_label` (add trigger friendly name as a sub label to the triggering tracked object) + # - `attribute` (add trigger's name and similarity score as a data attribute to the triggering tracked object) actions: - notification diff --git a/docs/docs/configuration/semantic_search.md b/docs/docs/configuration/semantic_search.md index 9950a3c8a..70ef161be 100644 --- a/docs/docs/configuration/semantic_search.md +++ b/docs/docs/configuration/semantic_search.md @@ -119,7 +119,7 @@ Semantic Search must be enabled to use Triggers. ### Configuration -Triggers are defined within the `semantic_search` configuration for each camera in your Frigate configuration file or through the UI. Each trigger consists of a `friendly_name`, a `type` (either `thumbnail` or `description`), a `data` field (the reference image event ID or text), a `threshold` for similarity matching, and a list of `actions` to perform when the trigger fires. +Triggers are defined within the `semantic_search` configuration for each camera in your Frigate configuration file or through the UI. Each trigger consists of a `friendly_name`, a `type` (either `thumbnail` or `description`), a `data` field (the reference image event ID or text), a `threshold` for similarity matching, and a list of `actions` to perform when the trigger fires - `notification`, `sub_label`, and `attribute`. Triggers are best configured through the Frigate UI. @@ -128,17 +128,20 @@ Triggers are best configured through the Frigate UI. 1. Navigate to the **Settings** page and select the **Triggers** tab. 2. Choose a camera from the dropdown menu to view or manage its triggers. 3. Click **Add Trigger** to create a new trigger or use the pencil icon to edit an existing one. -4. In the **Create Trigger** dialog: - - Enter a **Name** for the trigger (e.g., "red_car_alert"). +4. In the **Create Trigger** wizard: + - Enter a **Name** for the trigger (e.g., "Red Car Alert"). - Enter a descriptive **Friendly Name** for the trigger (e.g., "Red car on the driveway camera"). - Select the **Type** (`Thumbnail` or `Description`). - For `Thumbnail`, select an image to trigger this action when a similar thumbnail image is detected, based on the threshold. - For `Description`, enter text to trigger this action when a similar tracked object description is detected. - Set the **Threshold** for similarity matching. - Select **Actions** to perform when the trigger fires. + If native webpush notifications are enabled, check the `Send Notification` box to send a notification. + Check the `Add Sub Label` box to add the trigger's friendly name as a sub label to any triggering tracked objects. + Check the `Add Attribute` box to add the trigger's internal ID (e.g., "red_car_alert") to a data attribute on the tracked object that can be processed via the API or MQTT. 5. Save the trigger to update the configuration and store the embedding in the database. -When a trigger fires, the UI highlights the trigger with a blue outline for 3 seconds for easy identification. +When a trigger fires, the UI highlights the trigger with a blue dot for 3 seconds for easy identification. ### Usage and Best Practices diff --git a/frigate/config/classification.py b/frigate/config/classification.py index 5b6cb8cec..bdcbf48f1 100644 --- a/frigate/config/classification.py +++ b/frigate/config/classification.py @@ -33,6 +33,8 @@ class TriggerType(str, Enum): class TriggerAction(str, Enum): NOTIFICATION = "notification" + SUB_LABEL = "sub_label" + ATTRIBUTE = "attribute" class ObjectClassificationType(str, Enum): diff --git a/frigate/data_processing/post/semantic_trigger.py b/frigate/data_processing/post/semantic_trigger.py index db1aff751..40eed0c10 100644 --- a/frigate/data_processing/post/semantic_trigger.py +++ b/frigate/data_processing/post/semantic_trigger.py @@ -10,6 +10,10 @@ import cv2 import numpy as np from peewee import DoesNotExist +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataTypeEnum, +) from frigate.comms.inter_process import InterProcessRequestor from frigate.config import FrigateConfig from frigate.const import CONFIG_DIR @@ -34,6 +38,7 @@ class SemanticTriggerProcessor(PostProcessorApi): db: SqliteVecQueueDatabase, config: FrigateConfig, requestor: InterProcessRequestor, + sub_label_publisher: EventMetadataPublisher, metrics: DataProcessorMetrics, embeddings, ): @@ -41,6 +46,7 @@ class SemanticTriggerProcessor(PostProcessorApi): self.db = db self.embeddings = embeddings self.requestor = requestor + self.sub_label_publisher = sub_label_publisher self.trigger_embeddings: list[np.ndarray] = [] self.thumb_stats = ZScoreNormalization() @@ -184,14 +190,44 @@ class SemanticTriggerProcessor(PostProcessorApi): ), ) + friendly_name = ( + self.config.cameras[camera] + .semantic_search.triggers[trigger["name"]] + .friendly_name + ) + if ( self.config.cameras[camera] .semantic_search.triggers[trigger["name"]] .actions ): - # TODO: handle actions for the trigger + # handle actions for the trigger # notifications already handled by webpush - pass + if ( + "sub_label" + in self.config.cameras[camera] + .semantic_search.triggers[trigger["name"]] + .actions + ): + self.sub_label_publisher.publish( + (event_id, friendly_name, similarity), + EventMetadataTypeEnum.sub_label, + ) + if ( + "attribute" + in self.config.cameras[camera] + .semantic_search.triggers[trigger["name"]] + .actions + ): + self.sub_label_publisher.publish( + ( + event_id, + trigger["name"], + trigger["type"], + similarity, + ), + EventMetadataTypeEnum.attribute.value, + ) if WRITE_DEBUG_IMAGES: try: diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index fe04d8b17..10895d592 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -233,6 +233,7 @@ class EmbeddingMaintainer(threading.Thread): db, self.config, self.requestor, + self.event_metadata_publisher, metrics, self.embeddings, ) diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 8bed74c01..3600529f0 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -903,8 +903,9 @@ "description": "Description" }, "actions": { - "alert": "Mark as Alert", - "notification": "Send Notification" + "notification": "Send Notification", + "sub_label": "Add Sub Label", + "attribute": "Add Attribute" }, "dialog": { "createTrigger": { @@ -959,7 +960,7 @@ }, "actions": { "title": "Actions", - "desc": "By default, Frigate fires an MQTT message for all triggers. Choose an additional action to perform when this trigger fires.", + "desc": "By default, Frigate fires an MQTT message for all triggers. Sub labels add the trigger name to the object label. Attributes are searchable metadata stored separately in the tracked object metadata.", "error": { "min": "At least one action must be selected." } diff --git a/web/src/components/overlay/CreateTriggerDialog.tsx b/web/src/components/overlay/CreateTriggerDialog.tsx index efbb1dd7b..fc6a28160 100644 --- a/web/src/components/overlay/CreateTriggerDialog.tsx +++ b/web/src/components/overlay/CreateTriggerDialog.tsx @@ -79,6 +79,15 @@ export default function CreateTriggerDialog({ const { t } = useTranslation("views/settings"); const { data: config } = useSWR("config"); + const availableActions = useMemo(() => { + if (!config) return []; + + if (config.cameras[selectedCamera].notifications.enabled_in_config) { + return ["notification", "sub_label", "attribute"]; + } + return ["sub_label", "attribute"]; + }, [config, selectedCamera]); + const existingTriggerNames = useMemo(() => { if ( !config || @@ -132,7 +141,7 @@ export default function CreateTriggerDialog({ .number() .min(0, t("triggers.dialog.form.threshold.error.min")) .max(1, t("triggers.dialog.form.threshold.error.max")), - actions: z.array(z.enum(["notification"])), + actions: z.array(z.enum(["notification", "sub_label", "attribute"])), }); const form = useForm>({ @@ -383,7 +392,7 @@ export default function CreateTriggerDialog({ {t("triggers.dialog.form.actions.title")}
- {["notification"].map((action) => ( + {availableActions.map((action) => (
void; onBack: () => void; isLoading?: boolean; @@ -34,18 +37,29 @@ type Step3ThresholdAndActionsProps = { export default function Step3ThresholdAndActions({ initialData, trigger, + camera, onNext, onBack, isLoading = false, }: Step3ThresholdAndActionsProps) { const { t } = useTranslation("views/settings"); + const { data: config } = useSWR("config"); + + const availableActions = useMemo(() => { + if (!config) return []; + + if (config.cameras[camera].notifications.enabled_in_config) { + return ["notification", "sub_label", "attribute"]; + } + return ["sub_label", "attribute"]; + }, [config, camera]); const formSchema = z.object({ threshold: z .number() .min(0, t("triggers.dialog.form.threshold.error.min")) .max(1, t("triggers.dialog.form.threshold.error.max")), - actions: z.array(z.enum(["notification"])), + actions: z.array(z.enum(["notification", "sub_label", "attribute"])), }); const form = useForm>({ @@ -127,7 +141,7 @@ export default function Step3ThresholdAndActions({ {t("triggers.dialog.form.actions.title")}
- {["notification"].map((action) => ( + {availableActions.map((action) => (