Files
frigate/frigate/events/maintainer.py
T
Josh HawkinsandGitHub 79177db969 Update deps (#24623)
* bump backend and frontend dependencies

Bumps every outdated dependency that passed testing, with the code changes each one needs.

- peewee 4.5 and peewee_migrate 2.3: use SqliteDatabase in place of the removed SqliteExtDatabase, and fix types for peewee's bundled stubs
- mypy 2.4.0: remove unused type ignores and add the annotations mypy 2 needs
- pandas 3.0: convert the motion activity index with as_unit("s"), because pandas 3 infers second resolution for whole-second timestamps and the old // 10**9 returned 1
- transformers 5.19: load Jina tokenizers from the saved directory with trust_remote_code off and use CLIPImageProcessor, since v5 removed CLIPFeatureExtractor and remote code pulled in torch
- tensorflow-cpu 2.21: pin protobuf to 7.36 in the TensorRT amd64 image, because the old 3.20.3 pin was installed over TensorFlow and broke its import
- pydantic 2.13 and google-genai 2.29: regenerate the API spec
- ruff 0.16: set an explicit lint select, since 0.16 widened the default rules
- react-router-dom 7: set useTransitions={false} on BrowserRouter to keep v6 update timing, which useSearchEffect relies on
- typescript 6: drop baseUrl and add node types in tsconfig
- radix: update to the latest release and remove the slot patch, which upstream now includes
- apexcharts 7.8: guard the tooltip formatter against null values
- cryptography 50, joserfc 1.7, pywebpush 2.5, openai 3.26, aiohttp, uvicorn, onvif-zeep-async, scipy, framer-motion 14, react-day-picker 10 and other minor and patch bumps

* silence httpx2 request logging

* redownload an incomplete jina tokenizer

An interrupted tokenizer download left the Hugging Face cache directory without the saved tokenizer files, and since the download is skipped whenever that directory exists, every restart failed to load the tokenizer. The Jina constructors now remove a tokenizer directory that has no tokenizer_config.json so the download runs again.
2026-10-10 15:01:33 -05:00

346 lines
13 KiB
Python

import logging
import threading
from multiprocessing import Queue
from multiprocessing.synchronize import Event as MpEvent
from typing import Any
from frigate.comms.events_updater import EventEndPublisher, EventUpdateSubscriber
from frigate.config import FrigateConfig
from frigate.config.classification import ObjectClassificationType
from frigate.const import REPLAY_CAMERA_PREFIX
from frigate.events.types import EventStateEnum, EventTypeEnum
from frigate.models import Event
from frigate.util.builtin import to_relative_box
logger = logging.getLogger(__name__)
def should_update_db(prev_event: dict[str, Any], current_event: dict[str, Any]) -> bool:
"""If current_event has updated fields and (clip or snapshot)."""
# If event is ending and was previously saved, always update to set end_time
# This ensures events are properly ended even when alerts/detections are disabled
# mid-event (which can cause has_clip/has_snapshot to become False)
if (
prev_event["end_time"] is None
and current_event["end_time"] is not None
and (prev_event["has_clip"] or prev_event["has_snapshot"])
):
return True
if current_event["has_clip"] or current_event["has_snapshot"]:
# if this is the first time has_clip or has_snapshot turned true
if not prev_event["has_clip"] and not prev_event["has_snapshot"]:
return True
# or if any of the following values changed
if (
prev_event["top_score"] != current_event["top_score"]
or prev_event["entered_zones"] != current_event["entered_zones"]
or prev_event["end_time"] != current_event["end_time"]
or prev_event["average_estimated_speed"]
!= current_event["average_estimated_speed"]
or prev_event["velocity_angle"] != current_event["velocity_angle"]
or prev_event["recognized_license_plate"]
!= current_event["recognized_license_plate"]
or prev_event["path_data"] != current_event["path_data"]
):
return True
return False
def should_update_state(
prev_event: dict[str, Any], current_event: dict[str, Any]
) -> bool:
"""If current event should update state, but not necessarily update the db."""
if prev_event["stationary"] != current_event["stationary"]:
return True
if prev_event["attributes"] != current_event["attributes"]:
return True
if prev_event["sub_label"] != current_event["sub_label"]:
return True
if set(prev_event["current_zones"]) != set(current_event["current_zones"]):
return True
return False
class EventProcessor(threading.Thread):
def __init__(
self,
config: FrigateConfig,
timeline_queue: Queue,
stop_event: MpEvent,
):
super().__init__(name="event_processor")
self.config = config
self.timeline_queue = timeline_queue
self.events_in_process: dict[str, dict[str, Any]] = {}
self.stop_event = stop_event
self.event_receiver = EventUpdateSubscriber()
self.event_end_publisher = EventEndPublisher()
def run(self) -> None:
# set an end_time on events without an end_time on startup
Event.update(end_time=Event.start_time + 30).where(
Event.end_time == None
).execute()
while not self.stop_event.is_set():
update = self.event_receiver.check_for_update(timeout=1)
if update == None:
continue
source_type, event_type, camera, _, event_data = update
logger.debug(
f"Event received: {source_type} {event_type} {camera} {event_data['id']}"
)
if source_type == EventTypeEnum.tracked_object:
id = event_data["id"]
self.timeline_queue.put(
(
camera,
source_type,
event_type,
self.events_in_process.get(id),
event_data,
)
)
# if this is the first message, just store it and continue, its not time to insert it in the db
if (
event_type == EventStateEnum.start
or id not in self.events_in_process
):
self.events_in_process[id] = event_data
continue
self.handle_object_detection(event_type, camera, event_data)
elif source_type == EventTypeEnum.api:
self.timeline_queue.put(
(
camera,
source_type,
event_type,
{},
event_data,
)
)
self.handle_external_detection(event_type, event_data)
self.event_receiver.stop()
self.event_end_publisher.stop()
logger.info("Exiting event processor...")
def handle_object_detection(
self,
event_type: str,
camera: str,
event_data: dict[str, Any],
) -> None:
"""handle tracked object event updates."""
updated_db = False
if should_update_db(self.events_in_process[event_data["id"]], event_data):
updated_db = True
camera_config = self.config.cameras.get(camera)
if camera_config is None:
return
width = camera_config.detect.width
height = camera_config.detect.height
if width is None or height is None:
return
camera_model = self.config.model_for_camera(camera)
camera_detector = self.config.devices_for_model(camera_model)[0].detector
start_time = event_data["start_time"]
end_time = (
None if event_data["end_time"] is None else event_data["end_time"]
)
snapshot = event_data["snapshot"]
# score of the snapshot
score = None if snapshot is None else snapshot["score"]
# detection region in the snapshot
region = (
None
if snapshot is None
else to_relative_box(
width,
height,
snapshot["region"],
)
)
# bounding box for the snapshot
box = (
None
if snapshot is None
else to_relative_box(
width,
height,
snapshot["box"],
)
)
attributes = (
None
if snapshot is None
else [
{
"box": to_relative_box(
width,
height,
a["box"],
),
"label": a["label"],
"score": a["score"],
}
for a in snapshot["attributes"]
]
)
snapshot_frame_time = None if snapshot is None else snapshot["frame_time"]
snapshot_area = None if snapshot is None else snapshot["area"]
snapshot_estimated_speed = (
None if snapshot is None else snapshot["current_estimated_speed"]
)
# keep these from being set back to false because the event
# may have started while recordings/snapshots/alerts/detections were enabled
# this would be an issue for long running events
if self.events_in_process[event_data["id"]]["has_clip"]:
event_data["has_clip"] = True
if self.events_in_process[event_data["id"]]["has_snapshot"]:
event_data["has_snapshot"] = True
event = {
Event.id: event_data["id"],
Event.label: event_data["label"],
Event.camera: camera,
Event.start_time: start_time,
Event.end_time: end_time,
Event.zones: list(event_data["entered_zones"]),
Event.thumbnail: event_data.get("thumbnail"),
Event.has_clip: event_data["has_clip"],
Event.has_snapshot: event_data["has_snapshot"],
Event.model_hash: camera_model.model_hash,
Event.model_type: camera_model.model_type,
Event.detector_type: camera_detector,
Event.data: {
"box": box,
"region": region,
"score": score,
"top_score": event_data["top_score"],
"attributes": attributes,
"snapshot_clean": event_data.get("snapshot_clean", False),
"snapshot_frame_time": snapshot_frame_time,
"snapshot_area": snapshot_area,
"snapshot_estimated_speed": snapshot_estimated_speed,
"average_estimated_speed": event_data["average_estimated_speed"],
"velocity_angle": event_data["velocity_angle"],
"type": "object",
"max_severity": event_data.get("max_severity"),
"path_data": event_data.get("path_data"),
},
}
# only overwrite the sub_label in the database if it's set
if event_data.get("sub_label") is not None:
event[Event.sub_label] = event_data["sub_label"][0]
event[Event.data]["sub_label_score"] = event_data["sub_label"][1]
# only overwrite the recognized_license_plate in the database if it's set
if event_data.get("recognized_license_plate") is not None:
event[Event.data]["recognized_license_plate"] = event_data[
"recognized_license_plate"
][0]
event[Event.data]["recognized_license_plate_score"] = event_data[
"recognized_license_plate"
][1]
# only overwrite attribute-type custom model fields in the database if they're set
for name, model_config in self.config.classification.custom.items():
if (
model_config.object_config
and model_config.object_config.classification_type
== ObjectClassificationType.attribute
):
value = event_data.get(name)
if value is not None:
event[Event.data][name] = value[0]
event[Event.data][f"{name}_score"] = value[1]
(
Event.insert(event)
.on_conflict(
conflict_target=[Event.id],
update=event,
)
.execute()
)
# check if the stored event_data should be updated
if updated_db or should_update_state(
self.events_in_process[event_data["id"]], event_data
):
# update the stored copy for comparison on future update messages
self.events_in_process[event_data["id"]] = event_data
if event_type == EventStateEnum.end:
del self.events_in_process[event_data["id"]]
self.event_end_publisher.publish((event_data["id"], camera, updated_db)) # type: ignore[arg-type]
def handle_external_detection(
self, event_type: EventStateEnum, event_data: dict[str, Any]
) -> None:
# Skip replay cameras
if event_data.get("camera", "").startswith(REPLAY_CAMERA_PREFIX):
return
if event_type == EventStateEnum.start:
event = {
Event.id: event_data["id"],
Event.label: event_data["label"],
Event.sub_label: event_data["sub_label"],
Event.camera: event_data["camera"],
Event.start_time: event_data["start_time"],
Event.end_time: event_data["end_time"],
Event.thumbnail: event_data.get("thumbnail"),
Event.has_clip: event_data["has_clip"],
Event.has_snapshot: event_data["has_snapshot"],
Event.zones: [],
Event.data: {
"type": event_data["type"],
"score": event_data["score"],
"top_score": event_data["score"],
"snapshot_clean": event_data.get("snapshot_clean", False),
},
}
if event_data.get("draw") is not None:
event[Event.data]["draw"] = event_data["draw"]
if event_data.get("recognized_license_plate") is not None:
event[Event.data]["recognized_license_plate"] = event_data[
"recognized_license_plate"
]
event[Event.data]["recognized_license_plate_score"] = event_data[
"score"
]
Event.insert(event).execute()
elif event_type == EventStateEnum.end:
event = {
Event.id: event_data["id"],
Event.end_time: event_data["end_time"],
}
try:
Event.update(event).where(Event.id == event_data["id"]).execute()
except Exception:
logger.warning(f"Failed to update manual event: {event_data['id']}")