Files
frigate/frigate/review/maintainer.py
T

913 lines
36 KiB
Python
Raw Normal View History

"""Maintain review segments in db."""
2025-05-09 08:36:44 -05:00
import copy
import datetime
import json
import logging
import os
import random
import string
import sys
import threading
from multiprocessing.synchronize import Event as MpEvent
2024-04-19 16:11:41 -06:00
from pathlib import Path
2026-07-06 09:28:02 -08:00
from typing import Any
import cv2
import numpy as np
from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum
from frigate.comms.inter_process import InterProcessRequestor
from frigate.comms.review_updater import ReviewDataPublisher
from frigate.config import CameraConfig, FrigateConfig
2025-05-22 12:16:51 -06:00
from frigate.config.camera.updater import (
CameraConfigUpdateEnum,
CameraConfigUpdateSubscriber,
)
2024-04-24 07:44:28 -06:00
from frigate.const import (
CLEAR_ONGOING_REVIEW_SEGMENTS,
CLIPS_DIR,
UPSERT_REVIEW_SEGMENT,
)
from frigate.models import ReviewSegment
from frigate.review.types import SeverityEnum
2026-03-25 18:30:59 -06:00
from frigate.track.object_processing import ManualEventState
from frigate.util.image import SharedMemoryFrameManager, calculate_16_9_crop
logger = logging.getLogger(__name__)
THUMB_HEIGHT = 180
THUMB_WIDTH = 320
class PendingReviewSegment:
def __init__(
self,
camera: str,
frame_time: float,
severity: SeverityEnum,
2024-03-30 12:45:42 -06:00
detections: dict[str, str],
2024-10-24 16:00:39 -06:00
sub_labels: dict[str, str],
2024-06-25 06:38:37 -06:00
zones: list[str],
audio: set[str],
):
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
self.id = f"{frame_time}-{rand_id}"
self.camera = camera
self.start_time = frame_time
self.severity = severity
self.detections = detections
self.sub_labels = sub_labels
self.zones = zones
self.audio = audio
self.thumb_time: float | None = None
self.last_alert_time: float | None = None
self.last_detection_time: float = frame_time
if severity == SeverityEnum.alert:
self.last_alert_time = frame_time
# thumbnail
2026-03-25 18:30:59 -06:00
self._frame: np.ndarray[Any, Any] = np.zeros(
(THUMB_HEIGHT * 3 // 2, THUMB_WIDTH), np.uint8
)
2024-05-14 07:38:03 -06:00
self.has_frame = False
self.frame_active_count = 0
2024-04-19 16:11:41 -06:00
self.frame_path = os.path.join(
CLIPS_DIR, f"review/thumb-{self.camera}-{self.id}.webp"
)
def update_frame(
2026-03-25 18:30:59 -06:00
self,
camera_config: CameraConfig,
frame: np.ndarray,
objects: list[dict[str, Any]],
) -> None:
min_x = camera_config.frame_shape[1]
min_y = camera_config.frame_shape[0]
max_x = 0
max_y = 0
# find bounds for all boxes
for o in objects:
min_x = min(o["box"][0], min_x)
min_y = min(o["box"][1], min_y)
max_x = max(o["box"][2], max_x)
max_y = max(o["box"][3], max_y)
region = calculate_16_9_crop(
camera_config.frame_shape, min_x, min_y, max_x, max_y
)
# could not find suitable 16:9 region
if not region:
return
self.frame_active_count = len(objects)
color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
color_frame = color_frame[region[1] : region[3], region[0] : region[2]]
width = int(THUMB_HEIGHT * color_frame.shape[1] / color_frame.shape[0])
2024-05-14 07:38:03 -06:00
self._frame = cv2.resize(
color_frame, dsize=(width, THUMB_HEIGHT), interpolation=cv2.INTER_AREA
)
2024-05-14 07:38:03 -06:00
if self._frame is not None:
self.thumb_time = datetime.datetime.now().timestamp()
2024-05-14 07:38:03 -06:00
self.has_frame = True
2024-04-11 06:42:16 -06:00
cv2.imwrite(
2024-05-14 07:38:03 -06:00
self.frame_path, self._frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60]
2024-04-11 06:42:16 -06:00
)
2026-03-25 18:30:59 -06:00
def save_full_frame(self, camera_config: CameraConfig, frame: np.ndarray) -> None:
2024-05-09 07:20:33 -06:00
color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
width = int(THUMB_HEIGHT * color_frame.shape[1] / color_frame.shape[0])
2024-05-14 07:38:03 -06:00
self._frame = cv2.resize(
2024-05-09 07:20:33 -06:00
color_frame, dsize=(width, THUMB_HEIGHT), interpolation=cv2.INTER_AREA
)
2024-05-14 07:38:03 -06:00
if self._frame is not None:
self.has_frame = True
2024-05-09 07:20:33 -06:00
cv2.imwrite(
2024-05-14 07:38:03 -06:00
self.frame_path, self._frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60]
2024-05-09 07:20:33 -06:00
)
2024-04-11 06:42:16 -06:00
def get_data(self, ended: bool) -> dict:
end_time = None
if ended:
if self.severity == SeverityEnum.alert:
end_time = self.last_alert_time
else:
end_time = self.last_detection_time
2025-05-09 08:36:44 -05:00
return copy.deepcopy(
{
ReviewSegment.id.name: self.id,
ReviewSegment.camera.name: self.camera,
ReviewSegment.start_time.name: self.start_time,
ReviewSegment.end_time.name: end_time,
2025-05-09 08:36:44 -05:00
ReviewSegment.severity.name: self.severity.value,
ReviewSegment.thumb_path.name: self.frame_path,
ReviewSegment.data.name: {
"detections": list(set(self.detections.keys())),
"objects": list(set(self.detections.values())),
"verified_objects": [
o for o in self.detections.values() if "-verified" in o
],
2025-05-09 08:36:44 -05:00
"sub_labels": list(self.sub_labels.values()),
"zones": self.zones,
"audio": list(self.audio),
"thumb_time": self.thumb_time,
2025-08-10 05:57:54 -06:00
"metadata": None,
2025-05-09 08:36:44 -05:00
},
}
)
class ActiveObjects:
def __init__(
self,
frame_time: float,
camera_config: CameraConfig,
2026-03-25 18:30:59 -06:00
all_objects: list[dict[str, Any]],
):
self.camera_config = camera_config
# get current categorization of objects to know if
# these objects are currently being categorized
2026-03-25 18:30:59 -06:00
self.categorized_objects: dict[str, list[dict[str, Any]]] = {
"alerts": [],
"detections": [],
}
for o in all_objects:
if (
o["motionless_count"] >= camera_config.detect.stationary.threshold
and not o["pending_loitering"]
):
# no stationary objects unless loitering
continue
if o["position_changes"] == 0:
# object must have moved at least once
continue
if o["frame_time"] != frame_time:
# object must be detected in this frame
continue
if o["false_positive"]:
# object must not be a false positive
continue
if (
o["label"] in camera_config.review.alerts.labels
and (
not camera_config.review.alerts.required_zones
or (
len(o["current_zones"]) > 0
and set(o["current_zones"])
& set(camera_config.review.alerts.required_zones)
)
)
and camera_config.review.alerts.enabled
):
self.categorized_objects["alerts"].append(o)
continue
if (
(
camera_config.review.detections.labels is None
or o["label"] in camera_config.review.detections.labels
)
and (
not camera_config.review.detections.required_zones
or (
len(o["current_zones"]) > 0
and set(o["current_zones"])
& set(camera_config.review.detections.required_zones)
)
)
and camera_config.review.detections.enabled
):
self.categorized_objects["detections"].append(o)
continue
def has_active_objects(self) -> bool:
return (
len(self.categorized_objects["alerts"]) > 0
or len(self.categorized_objects["detections"]) > 0
)
def has_activity_category(self, severity: SeverityEnum) -> bool:
if (
severity == SeverityEnum.alert
and len(self.categorized_objects["alerts"]) > 0
):
return True
if (
severity == SeverityEnum.detection
and len(self.categorized_objects["detections"]) > 0
):
return True
return False
2026-03-25 18:30:59 -06:00
def get_all_objects(self) -> list[dict[str, Any]]:
return (
self.categorized_objects["alerts"] + self.categorized_objects["detections"]
)
class ReviewSegmentMaintainer(threading.Thread):
"""Maintain review segments."""
def __init__(self, config: FrigateConfig, stop_event: MpEvent):
super().__init__(name="review_segment_maintainer")
self.config = config
2026-07-06 09:28:02 -08:00
self.active_review_segments: dict[str, PendingReviewSegment | None] = {}
self.frame_manager = SharedMemoryFrameManager()
# create communication for review segments
self.requestor = InterProcessRequestor()
2025-05-22 12:16:51 -06:00
self.config_subscriber = CameraConfigUpdateSubscriber(
2025-06-11 11:25:30 -06:00
config,
2025-05-22 12:16:51 -06:00
config.cameras,
[
2025-06-11 11:25:30 -06:00
CameraConfigUpdateEnum.add,
2025-05-22 12:16:51 -06:00
CameraConfigUpdateEnum.enabled,
CameraConfigUpdateEnum.record,
2025-06-11 11:25:30 -06:00
CameraConfigUpdateEnum.remove,
2025-05-22 12:16:51 -06:00
CameraConfigUpdateEnum.review,
],
)
2025-08-08 06:08:37 -06:00
self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all.value)
self.review_publisher = ReviewDataPublisher("")
# manual events
2025-05-13 16:27:20 +02:00
self.indefinite_events: dict[str, dict[str, Any]] = {}
2024-04-19 16:11:41 -06:00
# ensure dirs
Path(os.path.join(CLIPS_DIR, "review")).mkdir(exist_ok=True)
self.stop_event = stop_event
2024-04-24 07:44:28 -06:00
# clear ongoing review segments from last instance
self.requestor.send_data(CLEAR_ONGOING_REVIEW_SEGMENTS, "")
2024-10-08 08:41:54 -06:00
def _publish_segment_start(
2024-04-22 20:20:30 -06:00
self,
segment: PendingReviewSegment,
) -> None:
"""New segment."""
new_data = segment.get_data(ended=False)
self.requestor.send_data(UPSERT_REVIEW_SEGMENT, new_data)
start_data = {k: v for k, v in new_data.items()}
review_update = {
"type": "new",
"before": start_data,
"after": start_data,
}
2024-04-11 06:42:16 -06:00
self.requestor.send_data(
"reviews",
json.dumps(review_update),
2024-04-22 20:20:30 -06:00
)
2026-03-25 18:30:59 -06:00
self.review_publisher.publish(review_update, segment.camera) # type: ignore[arg-type]
self.requestor.send_data(
f"{segment.camera}/review_status", segment.severity.value.upper()
)
2024-04-22 20:20:30 -06:00
2024-10-08 08:41:54 -06:00
def _publish_segment_update(
2024-04-22 20:20:30 -06:00
self,
segment: PendingReviewSegment,
camera_config: CameraConfig,
2026-07-06 09:28:02 -08:00
frame: np.ndarray | None,
2026-03-25 18:30:59 -06:00
objects: list[dict[str, Any]],
2025-05-13 16:27:20 +02:00
prev_data: dict[str, Any],
2024-04-22 20:20:30 -06:00
) -> None:
"""Update segment."""
2024-05-14 07:38:03 -06:00
if frame is not None:
segment.update_frame(camera_config, frame, objects)
2024-04-22 20:20:30 -06:00
new_data = segment.get_data(ended=False)
self.requestor.send_data(UPSERT_REVIEW_SEGMENT, new_data)
review_update = {
"type": "update",
"before": {k: v for k, v in prev_data.items()},
"after": {k: v for k, v in new_data.items()},
}
2024-04-22 20:20:30 -06:00
self.requestor.send_data(
"reviews",
json.dumps(review_update),
2024-04-11 06:42:16 -06:00
)
2026-03-25 18:30:59 -06:00
self.review_publisher.publish(review_update, segment.camera) # type: ignore[arg-type]
self.requestor.send_data(
f"{segment.camera}/review_status", segment.severity.value.upper()
)
2024-04-11 06:42:16 -06:00
2024-10-08 08:41:54 -06:00
def _publish_segment_end(
self,
segment: PendingReviewSegment,
2025-05-13 16:27:20 +02:00
prev_data: dict[str, Any],
2026-03-25 18:30:59 -06:00
) -> Any:
"""End segment."""
2024-04-22 20:20:30 -06:00
final_data = segment.get_data(ended=True)
end_time = final_data[ReviewSegment.end_time.name]
2024-04-22 20:20:30 -06:00
self.requestor.send_data(UPSERT_REVIEW_SEGMENT, final_data)
review_update = {
"type": "end",
"before": {k: v for k, v in prev_data.items()},
"after": {k: v for k, v in final_data.items()},
}
self.requestor.send_data(
"reviews",
json.dumps(review_update),
)
2026-03-25 18:30:59 -06:00
self.review_publisher.publish(review_update, segment.camera) # type: ignore[arg-type]
self.requestor.send_data(f"{segment.camera}/review_status", "NONE")
self.active_review_segments[segment.camera] = None
return end_time
2026-03-25 18:30:59 -06:00
def forcibly_end_segment(self, camera: str) -> Any:
"""Forcibly end the pending segment for a camera."""
segment = self.active_review_segments.get(camera)
if segment:
prev_data = segment.get_data(False)
return self._publish_segment_end(segment, prev_data)
2026-03-25 18:30:59 -06:00
return None
def update_existing_segment(
self,
segment: PendingReviewSegment,
2024-11-16 16:00:19 -07:00
frame_name: str,
frame_time: float,
2026-03-25 18:30:59 -06:00
objects: list[dict[str, Any]],
) -> None:
"""Validate if existing review segment should continue."""
camera_config = self.config.cameras[segment.camera]
2024-10-10 07:09:12 -06:00
# get active objects + objects loitering in loitering zones
activity = ActiveObjects(frame_time, camera_config, objects)
2024-06-12 16:48:54 -05:00
prev_data = segment.get_data(False)
2024-10-08 08:41:54 -06:00
has_activity = False
if activity.has_active_objects():
2024-10-08 08:41:54 -06:00
has_activity = True
should_update_image = False
should_update_state = False
2024-06-11 09:19:17 -05:00
if activity.has_activity_category(SeverityEnum.alert):
# update current time for last alert activity
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
# update this segment to be an alert
segment.severity = SeverityEnum.alert
should_update_state = True
should_update_image = True
if activity.has_activity_category(SeverityEnum.detection):
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():
2025-11-09 07:38:38 -07:00
# Alert-level objects should always be added (they extend/upgrade the segment)
# Detection-level objects should only be added if:
# - The segment is a detection segment (matching severity), OR
# - The segment is an alert AND the object started before the alert ended
# (objects starting after will be in the new detection segment)
is_alert_object = object in activity.categorized_objects["alerts"]
if not is_alert_object and segment.severity == SeverityEnum.alert:
# This is a detection-level object
# Only add if it started during the alert's active period
if object["start_time"] > segment.last_alert_time:
continue
2024-03-30 12:45:42 -06:00
if not object["sub_label"]:
segment.detections[object["id"]] = object["label"]
2024-09-28 07:49:04 -06:00
elif object["sub_label"][0] in self.config.model.all_attributes:
2024-03-30 12:45:42 -06:00
segment.detections[object["id"]] = object["sub_label"][0]
else:
2025-01-11 08:04:11 -06:00
segment.detections[object["id"]] = f"{object['label']}-verified"
2024-10-24 16:00:39 -06:00
segment.sub_labels[object["id"]] = object["sub_label"][0]
# keep zones up to date
if len(object["current_zones"]) > 0:
2024-06-25 06:38:37 -06:00
for zone in object["current_zones"]:
if zone not in segment.zones:
segment.zones.append(zone)
if len(activity.get_all_objects()) > segment.frame_active_count:
should_update_state = True
should_update_image = True
2024-06-11 09:19:17 -05:00
if prev_data["data"]["sub_labels"] != list(segment.sub_labels.values()):
should_update_state = True
if should_update_state:
try:
if should_update_image:
yuv_frame = self.frame_manager.get(
frame_name, camera_config.frame_shape_yuv
)
2024-09-03 10:22:30 -06:00
if yuv_frame is None:
logger.debug(f"Failed to get frame {frame_name} from SHM")
return
else:
yuv_frame = None
2024-09-03 10:22:30 -06:00
2024-10-08 08:41:54 -06:00
self._publish_segment_update(
segment,
camera_config,
yuv_frame,
activity.get_all_objects(),
prev_data,
2024-04-22 20:20:30 -06:00
)
2024-11-16 16:00:19 -07:00
self.frame_manager.close(frame_name)
except FileNotFoundError:
return
2024-10-08 08:41:54 -06:00
if not has_activity:
2024-05-14 07:38:03 -06:00
if not segment.has_frame:
try:
yuv_frame = self.frame_manager.get(
2024-11-16 16:00:19 -07:00
frame_name, camera_config.frame_shape_yuv
2024-05-14 07:38:03 -06:00
)
2024-09-03 10:22:30 -06:00
if yuv_frame is None:
2024-11-16 16:00:19 -07:00
logger.debug(f"Failed to get frame {frame_name} from SHM")
2024-09-03 10:22:30 -06:00
return
2024-05-14 07:38:03 -06:00
segment.save_full_frame(camera_config, yuv_frame)
2024-11-16 16:00:19 -07:00
self.frame_manager.close(frame_name)
2024-10-08 08:41:54 -06:00
self._publish_segment_update(
segment, camera_config, None, [], prev_data
)
2024-05-14 07:38:03 -06:00
except FileNotFoundError:
return
2026-03-25 18:30:59 -06:00
if (
segment.severity == SeverityEnum.alert
and segment.last_alert_time is not None
and frame_time
> (segment.last_alert_time + camera_config.review.alerts.cutoff_time)
):
needs_new_detection = (
segment.last_detection_time > segment.last_alert_time
and (
segment.last_detection_time
+ camera_config.review.detections.cutoff_time
)
> frame_time
)
last_detection_time = segment.last_detection_time
end_time = self._publish_segment_end(segment, prev_data)
if needs_new_detection:
new_detections: dict[str, str] = {}
new_zones = set()
for o in activity.categorized_objects["detections"]:
new_detections[o["id"]] = o["label"]
new_zones.update(o["current_zones"])
if new_detections:
2026-03-25 18:30:59 -06:00
new_segment = PendingReviewSegment(
segment.camera,
end_time,
SeverityEnum.detection,
new_detections,
sub_labels={},
audio=set(),
zones=list(new_zones),
)
2026-03-25 18:30:59 -06:00
self.active_review_segments[segment.camera] = new_segment
self._publish_segment_start(new_segment)
new_segment.last_detection_time = last_detection_time
elif segment.severity == SeverityEnum.detection and frame_time > (
segment.last_detection_time
+ camera_config.review.detections.cutoff_time
):
2024-10-08 08:41:54 -06:00
self._publish_segment_end(segment, prev_data)
def check_if_new_segment(
self,
camera: str,
2024-11-16 16:00:19 -07:00
frame_name: str,
frame_time: float,
2026-03-25 18:30:59 -06:00
objects: list[dict[str, Any]],
) -> None:
"""Check if a new review segment should be created."""
camera_config = self.config.cameras[camera]
activity = ActiveObjects(frame_time, camera_config, objects)
if activity.has_active_objects():
2024-03-30 12:45:42 -06:00
detections: dict[str, str] = {}
2024-10-25 06:47:56 -06:00
sub_labels: dict[str, str] = {}
2024-06-25 06:38:37 -06:00
zones: list[str] = []
severity: SeverityEnum | None = None
# if activity is alert category mark this review as alert
if severity != SeverityEnum.alert and activity.has_activity_category(
SeverityEnum.alert
):
severity = SeverityEnum.alert
# if object is detection label and not already higher severity
# mark this review as detection
if not severity and activity.has_activity_category(SeverityEnum.detection):
severity = SeverityEnum.detection
for object in activity.get_all_objects():
2024-03-30 12:45:42 -06:00
if not object["sub_label"]:
detections[object["id"]] = object["label"]
2024-09-28 07:49:04 -06:00
elif object["sub_label"][0] in self.config.model.all_attributes:
2024-03-30 12:45:42 -06:00
detections[object["id"]] = object["sub_label"][0]
else:
2025-01-11 08:04:11 -06:00
detections[object["id"]] = f"{object['label']}-verified"
2024-10-24 16:00:39 -06:00
sub_labels[object["id"]] = object["sub_label"][0]
2024-06-25 06:38:37 -06:00
for zone in object["current_zones"]:
if zone not in zones:
zones.append(zone)
if severity:
2026-03-25 18:30:59 -06:00
new_segment = PendingReviewSegment(
camera,
frame_time,
severity,
detections,
sub_labels=sub_labels,
audio=set(),
zones=zones,
)
2026-03-25 18:30:59 -06:00
self.active_review_segments[camera] = new_segment
try:
yuv_frame = self.frame_manager.get(
2024-11-16 16:00:19 -07:00
frame_name, camera_config.frame_shape_yuv
)
2024-09-03 10:22:30 -06:00
if yuv_frame is None:
2024-11-16 16:00:19 -07:00
logger.debug(f"Failed to get frame {frame_name} from SHM")
2024-09-03 10:22:30 -06:00
return
2026-03-25 18:30:59 -06:00
new_segment.update_frame(
camera_config, yuv_frame, activity.get_all_objects()
)
2024-11-16 16:00:19 -07:00
self.frame_manager.close(frame_name)
2026-03-25 18:30:59 -06:00
self._publish_segment_start(new_segment)
except FileNotFoundError:
return
def run(self) -> None:
while not self.stop_event.is_set():
# check if there is an updated config
2025-05-22 12:16:51 -06:00
updated_topics = self.config_subscriber.check_for_updates()
2025-05-22 12:16:51 -06:00
if "record" in updated_topics:
for camera in updated_topics["record"]:
self.forcibly_end_segment(camera)
2025-05-22 12:16:51 -06:00
if "enabled" in updated_topics:
for camera in updated_topics["enabled"]:
self.forcibly_end_segment(camera)
2025-04-05 07:47:58 -06:00
2026-03-25 18:30:59 -06:00
result = self.detection_subscriber.check_for_update(timeout=1)
2026-03-25 18:30:59 -06:00
if not result:
continue
topic, data = result
if not topic or not data:
continue
2025-08-08 06:08:37 -06:00
if topic == DetectionTypeEnum.video.value:
(
camera,
2024-11-16 16:00:19 -07:00
frame_name,
frame_time,
current_tracked_objects,
2024-11-16 16:00:19 -07:00
_,
_,
) = data
2025-08-08 06:08:37 -06:00
elif topic == DetectionTypeEnum.audio.value:
(
camera,
frame_time,
2024-11-16 16:00:19 -07:00
_,
audio_detections,
) = data
elif (
topic == DetectionTypeEnum.api.value
or topic == DetectionTypeEnum.lpr.value
):
(
camera,
frame_time,
manual_info,
) = data
if camera not in self.indefinite_events:
self.indefinite_events[camera] = {}
2026-03-04 10:07:34 -06:00
if camera not in self.config.cameras:
continue
2025-03-06 06:59:35 -07:00
if (
not self.config.cameras[camera].enabled
or not self.config.cameras[camera].record.enabled
):
2024-05-17 07:26:42 -06:00
continue
2025-04-05 07:47:58 -06:00
current_segment = self.active_review_segments.get(camera)
# Check if the current segment should be processed based on enabled settings
if current_segment:
if (
current_segment.severity == SeverityEnum.alert
and not self.config.cameras[camera].review.alerts.enabled
) or (
current_segment.severity == SeverityEnum.detection
and not self.config.cameras[camera].review.detections.enabled
):
self.forcibly_end_segment(camera)
continue
# If we reach here, the segment can be processed (if it exists)
if current_segment is not None:
if topic == DetectionTypeEnum.video:
self.update_existing_segment(
current_segment,
2024-11-16 16:00:19 -07:00
frame_name,
frame_time,
current_tracked_objects,
)
elif topic == DetectionTypeEnum.audio and len(audio_detections) > 0:
camera_config = self.config.cameras[camera]
for audio in audio_detections:
if (
audio in camera_config.review.alerts.labels
and camera_config.review.alerts.enabled
):
current_segment.audio.add(audio)
current_segment.severity = SeverityEnum.alert
current_segment.last_alert_time = frame_time
elif (
camera_config.review.detections.labels is None
or audio in camera_config.review.detections.labels
) and camera_config.review.detections.enabled:
current_segment.audio.add(audio)
current_segment.last_detection_time = frame_time
2025-03-23 14:30:48 -05:00
elif topic == DetectionTypeEnum.api or topic == DetectionTypeEnum.lpr:
if manual_info["state"] == ManualEventState.complete:
current_segment.detections[manual_info["event_id"]] = (
manual_info["label"]
)
if topic == DetectionTypeEnum.api:
# manual_info["label"] contains 'label: sub_label'
# so split out the label without modifying manual_info
2026-03-25 18:30:59 -06:00
det_labels = self.config.cameras[
camera
].review.detections.labels
if (
self.config.cameras[camera].review.detections.enabled
2026-03-25 18:30:59 -06:00
and det_labels is not None
and manual_info["label"].split(": ")[0] in det_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"
]
2025-03-23 14:30:48 -05:00
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"]
elif manual_info["state"] == ManualEventState.start:
self.indefinite_events[camera][manual_info["event_id"]] = (
manual_info["label"]
)
current_segment.detections[manual_info["event_id"]] = (
manual_info["label"]
)
2025-03-23 14:30:48 -05:00
if (
topic == DetectionTypeEnum.api
and self.config.cameras[camera].review.alerts.enabled
):
# manual_info["label"] contains 'label: sub_label'
# so split out the label without modifying manual_info
2026-03-25 18:30:59 -06:00
det_labels = self.config.cameras[
camera
].review.detections.labels
if (
not self.config.cameras[
camera
].review.detections.enabled
2026-03-25 18:30:59 -06:00
or det_labels is None
or manual_info["label"].split(": ")[0] not in det_labels
):
current_segment.severity = SeverityEnum.alert
2025-03-23 14:30:48 -05:00
elif (
topic == DetectionTypeEnum.lpr
and self.config.cameras[camera].review.detections.enabled
):
current_segment.severity = SeverityEnum.detection
# temporarily make it so this event can not end
current_segment.last_alert_time = sys.maxsize
current_segment.last_detection_time = sys.maxsize
elif manual_info["state"] == ManualEventState.end:
event_id = manual_info["event_id"]
if event_id in self.indefinite_events[camera]:
self.indefinite_events[camera].pop(event_id)
if len(self.indefinite_events[camera]) == 0:
current_segment.last_alert_time = manual_info[
"end_time"
]
current_segment.last_detection_time = manual_info[
"end_time"
]
else:
logger.error(
f"Event with ID {event_id} has a set duration and can not be ended manually."
)
else:
if topic == DetectionTypeEnum.video:
if (
self.config.cameras[camera].review.alerts.enabled
or self.config.cameras[camera].review.detections.enabled
):
self.check_if_new_segment(
camera,
frame_name,
frame_time,
current_tracked_objects,
)
elif topic == DetectionTypeEnum.audio and len(audio_detections) > 0:
severity = None
camera_config = self.config.cameras[camera]
detections = set()
for audio in audio_detections:
if (
audio in camera_config.review.alerts.labels
and camera_config.review.alerts.enabled
):
detections.add(audio)
severity = SeverityEnum.alert
elif (
camera_config.review.detections.labels is None
or audio in camera_config.review.detections.labels
) and camera_config.review.detections.enabled:
detections.add(audio)
if not severity:
severity = SeverityEnum.detection
if severity:
self.active_review_segments[camera] = PendingReviewSegment(
camera,
frame_time,
severity,
{},
2024-10-24 16:00:39 -06:00
{},
2024-06-25 06:38:37 -06:00
[],
detections,
)
elif topic == DetectionTypeEnum.api:
severity = None
# manual_info["label"] contains 'label: sub_label'
# so split out the label without modifying manual_info
2026-03-25 18:30:59 -06:00
det_labels = self.config.cameras[camera].review.detections.labels
if (
self.config.cameras[camera].review.detections.enabled
2026-03-25 18:30:59 -06:00
and det_labels is not None
and manual_info["label"].split(": ")[0] in det_labels
):
severity = SeverityEnum.detection
elif self.config.cameras[camera].review.alerts.enabled:
severity = SeverityEnum.alert
if severity:
2026-03-25 18:30:59 -06:00
api_segment = PendingReviewSegment(
camera,
frame_time,
severity,
{manual_info["event_id"]: manual_info["label"]},
{},
[],
set(),
)
2026-03-25 18:30:59 -06:00
self.active_review_segments[camera] = api_segment
if manual_info["state"] == ManualEventState.start:
self.indefinite_events[camera][manual_info["event_id"]] = (
manual_info["label"]
)
# temporarily make it so this event can not end
2026-03-25 18:30:59 -06:00
api_segment.last_alert_time = sys.maxsize
api_segment.last_detection_time = sys.maxsize
elif manual_info["state"] == ManualEventState.complete:
2026-03-25 18:30:59 -06:00
api_segment.last_alert_time = manual_info["end_time"]
api_segment.last_detection_time = manual_info["end_time"]
else:
logger.warning(
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."
)
2025-03-23 14:30:48 -05:00
elif topic == DetectionTypeEnum.lpr:
if self.config.cameras[camera].review.detections.enabled:
2026-03-25 18:30:59 -06:00
lpr_segment = PendingReviewSegment(
2025-03-23 14:30:48 -05:00
camera,
frame_time,
SeverityEnum.detection,
{manual_info["event_id"]: manual_info["label"]},
{},
[],
set(),
)
2026-03-25 18:30:59 -06:00
self.active_review_segments[camera] = lpr_segment
2025-03-23 14:30:48 -05:00
if manual_info["state"] == ManualEventState.start:
self.indefinite_events[camera][manual_info["event_id"]] = (
manual_info["label"]
)
# temporarily make it so this event can not end
2026-03-25 18:30:59 -06:00
lpr_segment.last_alert_time = sys.maxsize
lpr_segment.last_detection_time = sys.maxsize
2025-03-23 14:30:48 -05:00
elif manual_info["state"] == ManualEventState.complete:
2026-03-25 18:30:59 -06:00
lpr_segment.last_alert_time = manual_info["end_time"]
lpr_segment.last_detection_time = manual_info["end_time"]
2025-03-23 14:30:48 -05:00
else:
logger.warning(
f"Dedicated LPR camera API has been called for {camera}, but detections are disabled. LPR events will not appear as a detection."
)
2025-05-22 12:16:51 -06:00
self.config_subscriber.stop()
2024-04-11 06:42:16 -06:00
self.requestor.stop()
self.detection_subscriber.stop()
logger.info("Exiting review maintainer...")