From bb6e889449e2256586ece8d8924b4e5bdf57b421 Mon Sep 17 00:00:00 2001 From: nulledy <254504350+nulledy@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:09:26 -0500 Subject: [PATCH] Allow API Events to be Detections or Alerts, depending on the Event Label (#21923) * - API created events will be alerts OR detections, depending on the event label, defaulting to alerts - Indefinite API events will extend the recording segment until those events are ended - API event start time is the actual start time, instead of having a pre-buffer of record.event_pre_capture * Instead of checking for indefinite events on a camera before deciding if we should end the segment, only update last_detection_time and last_alert_time if frame_time is greater, which should have the same effect * Add the ability to set a pre_capture number of seconds when creating a manual event via the API. Default behavior unchanged * Remove unnecessary _publish_segment_start() call * Formatting * handle last_alert_time or last_detection_time being None when checking them against the frame_time * comment manual_info["label"].split(": ")[0] for clarity --- docs/static/frigate-api.yaml | 7 +++ frigate/api/defs/request/events_body.py | 1 + frigate/api/event.py | 1 + frigate/review/maintainer.py | 66 ++++++++++++++++++++----- frigate/track/object_processing.py | 9 +++- 5 files changed, 70 insertions(+), 14 deletions(-) diff --git a/docs/static/frigate-api.yaml b/docs/static/frigate-api.yaml index 36b346422..2063514ac 100644 --- a/docs/static/frigate-api.yaml +++ b/docs/static/frigate-api.yaml @@ -3200,6 +3200,7 @@ paths: duration: 30 include_recording: true draw: {} + pre_capture: null responses: "200": description: Successful Response @@ -5002,6 +5003,12 @@ components: - type: "null" title: Draw default: {} + pre_capture: + anyOf: + - type: integer + - type: "null" + title: Pre Capture Seconds + default: null type: object title: EventsCreateBody EventsDeleteBody: diff --git a/frigate/api/defs/request/events_body.py b/frigate/api/defs/request/events_body.py index 50754e92a..d844c31ca 100644 --- a/frigate/api/defs/request/events_body.py +++ b/frigate/api/defs/request/events_body.py @@ -41,6 +41,7 @@ class EventsCreateBody(BaseModel): duration: Optional[int] = 30 include_recording: Optional[bool] = True draw: Optional[dict] = {} + pre_capture: Optional[int] = None class EventsEndBody(BaseModel): diff --git a/frigate/api/event.py b/frigate/api/event.py index c03cfb431..b0a749018 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -1782,6 +1782,7 @@ def create_event( body.duration, "api", body.draw, + body.pre_capture, ), EventMetadataTypeEnum.manual_event_create.value, ) diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index 917c0c5ac..6afdc8de9 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -394,7 +394,11 @@ class ReviewSegmentMaintainer(threading.Thread): if activity.has_activity_category(SeverityEnum.alert): # update current time for last alert activity - segment.last_alert_time = frame_time + if ( + segment.last_alert_time is None + or frame_time > segment.last_alert_time + ): + segment.last_alert_time = frame_time if segment.severity != SeverityEnum.alert: # if segment is not alert category but current activity is @@ -404,7 +408,11 @@ class ReviewSegmentMaintainer(threading.Thread): should_update_image = True if activity.has_activity_category(SeverityEnum.detection): - segment.last_detection_time = frame_time + if ( + segment.last_detection_time is None + or frame_time > segment.last_detection_time + ): + segment.last_detection_time = frame_time for object in activity.get_all_objects(): # Alert-level objects should always be added (they extend/upgrade the segment) @@ -695,17 +703,28 @@ class ReviewSegmentMaintainer(threading.Thread): current_segment.detections[manual_info["event_id"]] = ( manual_info["label"] ) - if ( - topic == DetectionTypeEnum.api - and self.config.cameras[camera].review.alerts.enabled - ): - current_segment.severity = SeverityEnum.alert + if topic == DetectionTypeEnum.api: + # manual_info["label"] contains 'label: sub_label' + # so split out the label without modifying manual_info + if ( + self.config.cameras[camera].review.detections.enabled + and manual_info["label"].split(": ")[0] + in self.config.cameras[camera].review.detections.labels + ): + current_segment.last_detection_time = manual_info[ + "end_time" + ] + elif self.config.cameras[camera].review.alerts.enabled: + current_segment.severity = SeverityEnum.alert + current_segment.last_alert_time = manual_info[ + "end_time" + ] elif ( topic == DetectionTypeEnum.lpr and self.config.cameras[camera].review.detections.enabled ): current_segment.severity = SeverityEnum.detection - current_segment.last_alert_time = manual_info["end_time"] + current_segment.last_alert_time = manual_info["end_time"] elif manual_info["state"] == ManualEventState.start: self.indefinite_events[camera][manual_info["event_id"]] = ( manual_info["label"] @@ -717,7 +736,18 @@ class ReviewSegmentMaintainer(threading.Thread): topic == DetectionTypeEnum.api and self.config.cameras[camera].review.alerts.enabled ): - current_segment.severity = SeverityEnum.alert + # manual_info["label"] contains 'label: sub_label' + # so split out the label without modifying manual_info + if ( + not self.config.cameras[ + camera + ].review.detections.enabled + or manual_info["label"].split(": ")[0] + not in self.config.cameras[ + camera + ].review.detections.labels + ): + current_segment.severity = SeverityEnum.alert elif ( topic == DetectionTypeEnum.lpr and self.config.cameras[camera].review.detections.enabled @@ -789,11 +819,23 @@ class ReviewSegmentMaintainer(threading.Thread): detections, ) elif topic == DetectionTypeEnum.api: - if self.config.cameras[camera].review.alerts.enabled: + severity = None + # manual_info["label"] contains 'label: sub_label' + # so split out the label without modifying manual_info + if ( + self.config.cameras[camera].review.detections.enabled + and manual_info["label"].split(": ")[0] + in self.config.cameras[camera].review.detections.labels + ): + severity = SeverityEnum.detection + elif self.config.cameras[camera].review.alerts.enabled: + severity = SeverityEnum.alert + + if severity: self.active_review_segments[camera] = PendingReviewSegment( camera, frame_time, - SeverityEnum.alert, + severity, {manual_info["event_id"]: manual_info["label"]}, {}, [], @@ -820,7 +862,7 @@ class ReviewSegmentMaintainer(threading.Thread): ].last_detection_time = manual_info["end_time"] else: logger.warning( - f"Manual event API has been called for {camera}, but alerts are disabled. This manual event will not appear as an alert." + f"Manual event API has been called for {camera}, but alerts and detections are disabled. This manual event will not appear as an alert or detection." ) elif topic == DetectionTypeEnum.lpr: if self.config.cameras[camera].review.detections.enabled: diff --git a/frigate/track/object_processing.py b/frigate/track/object_processing.py index f44f21be3..9ac04b42a 100644 --- a/frigate/track/object_processing.py +++ b/frigate/track/object_processing.py @@ -515,6 +515,7 @@ class TrackedObjectProcessor(threading.Thread): duration, source_type, draw, + pre_capture, ) = payload # save the snapshot image @@ -522,6 +523,11 @@ class TrackedObjectProcessor(threading.Thread): None, event_id, label, draw ) end_time = frame_time + duration if duration is not None else None + start_time = ( + frame_time - self.config.cameras[camera_name].record.event_pre_capture + if pre_capture is None + else frame_time - pre_capture + ) # send event to event maintainer self.event_sender.publish( @@ -536,8 +542,7 @@ class TrackedObjectProcessor(threading.Thread): "sub_label": sub_label, "score": score, "camera": camera_name, - "start_time": frame_time - - self.config.cameras[camera_name].record.event_pre_capture, + "start_time": start_time, "end_time": end_time, "has_clip": self.config.cameras[camera_name].record.enabled and include_recording,