mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Ability to manually create events through the API (#3184)
* Move to events package * Improve handling of external events * Handle external events in the event queue * Pass in event processor * Check event json * Fix json parsing and change defaults * Fix snapshot saving * Hide % score when not available * Correct docs and add json example * Save event png db * Adjust image * Formatting * Add catch for failure ending event * Add init to modules * Fix naming * Formatting * Fix http creation * fix test * Change to PUT and include response in docs * Add ability to set bounding box locations in snapshot * Support multiple box annotations * Cleanup docs example response Co-authored-by: Blake Blackshear <blake@frigate.video> * Cleanup docs wording Co-authored-by: Blake Blackshear <blake@frigate.video> * Store full frame for thumbnail * Formatting * Set thumbnail height to 175 * Formatting --------- Co-authored-by: Blake Blackshear <blake@frigate.video>
This commit is contained in:
parent
6d0c2ec5c8
commit
e357715a8c
@ -295,3 +295,41 @@ Get ffprobe output for camera feed paths.
|
||||
### `GET /api/<camera_name>/ptz/info`
|
||||
|
||||
Get PTZ info for the camera.
|
||||
|
||||
### `POST /api/events/<camera_name>/<label>/create`
|
||||
|
||||
Create a manual API with a given `label` (ex: doorbell press) to capture a specific event besides an object being detected.
|
||||
|
||||
**Optional Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"subLabel": "some_string", // add sub label to event
|
||||
"duration": 30, // predetermined length of event (default: 30 seconds) or can be to null for indeterminate length event
|
||||
"include_recording": true, // whether the event should save recordings along with the snapshot that is taken
|
||||
"draw": {
|
||||
// optional annotations that will be drawn on the snapshot
|
||||
"boxes": [
|
||||
{
|
||||
"box": [0.5, 0.5, 0.25, 0.25], // box consists of x, y, width, height which are on a scale between 0 - 1
|
||||
"color": [255, 0, 0], // color of the box, default is red
|
||||
"score": 100 // optional score associated with the box
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "1682970645.13116-1ug7ns",
|
||||
"message": "Successfully created event.",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### `PUT /api/events/<event_id>/end`
|
||||
|
||||
End a specific manual event without a predetermined length.
|
||||
|
@ -28,7 +28,9 @@ from frigate.const import (
|
||||
RECORD_DIR,
|
||||
)
|
||||
from frigate.object_detection import ObjectDetectProcess
|
||||
from frigate.events import EventCleanup, EventProcessor
|
||||
from frigate.events.cleanup import EventCleanup
|
||||
from frigate.events.external import ExternalEventProcessor
|
||||
from frigate.events.maintainer import EventProcessor
|
||||
from frigate.http import create_app
|
||||
from frigate.log import log_process, root_configurer
|
||||
from frigate.models import Event, Recordings, Timeline
|
||||
@ -204,6 +206,11 @@ class FrigateApp:
|
||||
self.config, self.camera_metrics, self.detectors, self.processes
|
||||
)
|
||||
|
||||
def init_external_event_processor(self) -> None:
|
||||
self.external_event_processor = ExternalEventProcessor(
|
||||
self.config, self.event_queue
|
||||
)
|
||||
|
||||
def init_web_server(self) -> None:
|
||||
self.flask_app = create_app(
|
||||
self.config,
|
||||
@ -212,6 +219,7 @@ class FrigateApp:
|
||||
self.detected_frames_processor,
|
||||
self.storage_maintainer,
|
||||
self.onvif_controller,
|
||||
self.external_event_processor,
|
||||
self.plus_api,
|
||||
)
|
||||
|
||||
@ -436,6 +444,7 @@ class FrigateApp:
|
||||
self.start_camera_capture_processes()
|
||||
self.start_storage_maintainer()
|
||||
self.init_stats()
|
||||
self.init_external_event_processor()
|
||||
self.init_web_server()
|
||||
self.start_timeline_processor()
|
||||
self.start_event_processor()
|
||||
|
0
frigate/events/__init__.py
Normal file
0
frigate/events/__init__.py
Normal file
176
frigate/events/cleanup.py
Normal file
176
frigate/events/cleanup.py
Normal file
@ -0,0 +1,176 @@
|
||||
"""Cleanup events based on configured retention."""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from peewee import fn
|
||||
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.const import CLIPS_DIR
|
||||
from frigate.models import Event
|
||||
|
||||
from multiprocessing.synchronize import Event as MpEvent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventCleanup(threading.Thread):
|
||||
def __init__(self, config: FrigateConfig, stop_event: MpEvent):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = "event_cleanup"
|
||||
self.config = config
|
||||
self.stop_event = stop_event
|
||||
self.camera_keys = list(self.config.cameras.keys())
|
||||
|
||||
def expire(self, media_type: str) -> None:
|
||||
# TODO: Refactor media_type to enum
|
||||
## Expire events from unlisted cameras based on the global config
|
||||
if media_type == "clips":
|
||||
retain_config = self.config.record.events.retain
|
||||
file_extension = "mp4"
|
||||
update_params = {"has_clip": False}
|
||||
else:
|
||||
retain_config = self.config.snapshots.retain
|
||||
file_extension = "jpg"
|
||||
update_params = {"has_snapshot": False}
|
||||
|
||||
distinct_labels = (
|
||||
Event.select(Event.label)
|
||||
.where(Event.camera.not_in(self.camera_keys))
|
||||
.distinct()
|
||||
)
|
||||
|
||||
# loop over object types in db
|
||||
for l in distinct_labels:
|
||||
# get expiration time for this label
|
||||
expire_days = retain_config.objects.get(l.label, retain_config.default)
|
||||
expire_after = (
|
||||
datetime.datetime.now() - datetime.timedelta(days=expire_days)
|
||||
).timestamp()
|
||||
# grab all events after specific time
|
||||
expired_events = Event.select().where(
|
||||
Event.camera.not_in(self.camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
# delete the media from disk
|
||||
for event in expired_events:
|
||||
media_name = f"{event.camera}-{event.id}"
|
||||
media_path = Path(
|
||||
f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}"
|
||||
)
|
||||
media_path.unlink(missing_ok=True)
|
||||
if file_extension == "jpg":
|
||||
media_path = Path(
|
||||
f"{os.path.join(CLIPS_DIR, media_name)}-clean.png"
|
||||
)
|
||||
media_path.unlink(missing_ok=True)
|
||||
|
||||
# update the clips attribute for the db entry
|
||||
update_query = Event.update(update_params).where(
|
||||
Event.camera.not_in(self.camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
update_query.execute()
|
||||
|
||||
## Expire events from cameras based on the camera config
|
||||
for name, camera in self.config.cameras.items():
|
||||
if media_type == "clips":
|
||||
retain_config = camera.record.events.retain
|
||||
else:
|
||||
retain_config = camera.snapshots.retain
|
||||
# get distinct objects in database for this camera
|
||||
distinct_labels = (
|
||||
Event.select(Event.label).where(Event.camera == name).distinct()
|
||||
)
|
||||
|
||||
# loop over object types in db
|
||||
for l in distinct_labels:
|
||||
# get expiration time for this label
|
||||
expire_days = retain_config.objects.get(l.label, retain_config.default)
|
||||
expire_after = (
|
||||
datetime.datetime.now() - datetime.timedelta(days=expire_days)
|
||||
).timestamp()
|
||||
# grab all events after specific time
|
||||
expired_events = Event.select().where(
|
||||
Event.camera == name,
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
# delete the grabbed clips from disk
|
||||
for event in expired_events:
|
||||
media_name = f"{event.camera}-{event.id}"
|
||||
media_path = Path(
|
||||
f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}"
|
||||
)
|
||||
media_path.unlink(missing_ok=True)
|
||||
if file_extension == "jpg":
|
||||
media_path = Path(
|
||||
f"{os.path.join(CLIPS_DIR, media_name)}-clean.png"
|
||||
)
|
||||
media_path.unlink(missing_ok=True)
|
||||
# update the clips attribute for the db entry
|
||||
update_query = Event.update(update_params).where(
|
||||
Event.camera == name,
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
update_query.execute()
|
||||
|
||||
def purge_duplicates(self) -> None:
|
||||
duplicate_query = """with grouped_events as (
|
||||
select id,
|
||||
label,
|
||||
camera,
|
||||
has_snapshot,
|
||||
has_clip,
|
||||
row_number() over (
|
||||
partition by label, camera, round(start_time/5,0)*5
|
||||
order by end_time-start_time desc
|
||||
) as copy_number
|
||||
from event
|
||||
)
|
||||
|
||||
select distinct id, camera, has_snapshot, has_clip from grouped_events
|
||||
where copy_number > 1;"""
|
||||
|
||||
duplicate_events = Event.raw(duplicate_query)
|
||||
for event in duplicate_events:
|
||||
logger.debug(f"Removing duplicate: {event.id}")
|
||||
media_name = f"{event.camera}-{event.id}"
|
||||
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
|
||||
media_path.unlink(missing_ok=True)
|
||||
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png")
|
||||
media_path.unlink(missing_ok=True)
|
||||
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
|
||||
media_path.unlink(missing_ok=True)
|
||||
|
||||
(
|
||||
Event.delete()
|
||||
.where(Event.id << [event.id for event in duplicate_events])
|
||||
.execute()
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
# only expire events every 5 minutes
|
||||
while not self.stop_event.wait(300):
|
||||
self.expire("clips")
|
||||
self.expire("snapshots")
|
||||
self.purge_duplicates()
|
||||
|
||||
# drop events from db where has_clip and has_snapshot are false
|
||||
delete_query = Event.delete().where(
|
||||
Event.has_clip == False, Event.has_snapshot == False
|
||||
)
|
||||
delete_query.execute()
|
||||
|
||||
logger.info(f"Exiting event cleanup...")
|
132
frigate/events/external.py
Normal file
132
frigate/events/external.py
Normal file
@ -0,0 +1,132 @@
|
||||
"""Handle external events created by the user."""
|
||||
|
||||
import base64
|
||||
import cv2
|
||||
import datetime
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from multiprocessing.queues import Queue
|
||||
|
||||
from frigate.config import CameraConfig, FrigateConfig
|
||||
from frigate.const import CLIPS_DIR
|
||||
from frigate.events.maintainer import EventTypeEnum
|
||||
from frigate.util import draw_box_with_label
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExternalEventProcessor:
|
||||
def __init__(self, config: FrigateConfig, queue: Queue) -> None:
|
||||
self.config = config
|
||||
self.queue = queue
|
||||
self.default_thumbnail = None
|
||||
|
||||
def create_manual_event(
|
||||
self,
|
||||
camera: str,
|
||||
label: str,
|
||||
sub_label: Optional[str],
|
||||
duration: Optional[int],
|
||||
include_recording: bool,
|
||||
draw: dict[str, any],
|
||||
snapshot_frame: any,
|
||||
) -> str:
|
||||
now = datetime.datetime.now().timestamp()
|
||||
camera_config = self.config.cameras.get(camera)
|
||||
|
||||
# create event id and start frame time
|
||||
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||
event_id = f"{now}-{rand_id}"
|
||||
|
||||
thumbnail = self._write_images(
|
||||
camera_config, label, event_id, draw, snapshot_frame
|
||||
)
|
||||
|
||||
self.queue.put(
|
||||
(
|
||||
EventTypeEnum.api,
|
||||
"new",
|
||||
camera_config,
|
||||
{
|
||||
"id": event_id,
|
||||
"label": label,
|
||||
"sub_label": sub_label,
|
||||
"camera": camera,
|
||||
"start_time": now,
|
||||
"end_time": now + duration if duration is not None else None,
|
||||
"thumbnail": thumbnail,
|
||||
"has_clip": camera_config.record.enabled and include_recording,
|
||||
"has_snapshot": True,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return event_id
|
||||
|
||||
def finish_manual_event(self, event_id: str) -> None:
|
||||
"""Finish external event with indeterminate duration."""
|
||||
now = datetime.datetime.now().timestamp()
|
||||
self.queue.put(
|
||||
(EventTypeEnum.api, "end", None, {"id": event_id, "end_time": now})
|
||||
)
|
||||
|
||||
def _write_images(
|
||||
self,
|
||||
camera_config: CameraConfig,
|
||||
label: str,
|
||||
event_id: str,
|
||||
draw: dict[str, any],
|
||||
img_frame: any,
|
||||
) -> str:
|
||||
# write clean snapshot if enabled
|
||||
if camera_config.snapshots.clean_copy:
|
||||
ret, png = cv2.imencode(".png", img_frame)
|
||||
|
||||
if ret:
|
||||
with open(
|
||||
os.path.join(
|
||||
CLIPS_DIR,
|
||||
f"{camera_config.name}-{event_id}-clean.png",
|
||||
),
|
||||
"wb",
|
||||
) as p:
|
||||
p.write(png.tobytes())
|
||||
|
||||
# write jpg snapshot with optional annotations
|
||||
if draw.get("boxes") and isinstance(draw.get("boxes"), list):
|
||||
for box in draw.get("boxes"):
|
||||
x = box["box"][0] * camera_config.detect.width
|
||||
y = box["box"][1] * camera_config.detect.height
|
||||
width = box["box"][2] * camera_config.detect.width
|
||||
height = box["box"][3] * camera_config.detect.height
|
||||
|
||||
draw_box_with_label(
|
||||
img_frame,
|
||||
x,
|
||||
y,
|
||||
x + width,
|
||||
y + height,
|
||||
label,
|
||||
f"{box.get('score', '-')}% {int(width * height)}",
|
||||
thickness=2,
|
||||
color=box.get("color", (255, 0, 0)),
|
||||
)
|
||||
|
||||
ret, jpg = cv2.imencode(".jpg", img_frame)
|
||||
with open(
|
||||
os.path.join(CLIPS_DIR, f"{camera_config.name}-{event_id}.jpg"),
|
||||
"wb",
|
||||
) as j:
|
||||
j.write(jpg.tobytes())
|
||||
|
||||
# create thumbnail with max height of 175 and save
|
||||
width = int(175 * img_frame.shape[1] / img_frame.shape[0])
|
||||
thumb = cv2.resize(img_frame, dsize=(width, 175), interpolation=cv2.INTER_AREA)
|
||||
ret, jpg = cv2.imencode(".jpg", thumb)
|
||||
return base64.b64encode(jpg.tobytes()).decode("utf-8")
|
@ -1,16 +1,13 @@
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
import threading
|
||||
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from peewee import fn
|
||||
|
||||
from frigate.config import EventsConfig, FrigateConfig
|
||||
from frigate.const import CLIPS_DIR
|
||||
from frigate.models import Event
|
||||
from frigate.types import CameraMetricsTypes
|
||||
from frigate.util import to_relative_box
|
||||
@ -23,7 +20,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventTypeEnum(str, Enum):
|
||||
# api = "api"
|
||||
api = "api"
|
||||
# audio = "audio"
|
||||
tracked_object = "tracked_object"
|
||||
|
||||
@ -97,6 +94,8 @@ class EventProcessor(threading.Thread):
|
||||
continue
|
||||
|
||||
self.handle_object_detection(event_type, camera, event_data)
|
||||
elif source_type == EventTypeEnum.api:
|
||||
self.handle_external_detection(event_type, event_data)
|
||||
|
||||
# set an end_time on events without an end_time before exiting
|
||||
Event.update(end_time=datetime.datetime.now().timestamp()).where(
|
||||
@ -197,160 +196,35 @@ class EventProcessor(threading.Thread):
|
||||
del self.events_in_process[event_data["id"]]
|
||||
self.event_processed_queue.put((event_data["id"], camera))
|
||||
|
||||
def handle_external_detection(self, type: str, event_data: Event):
|
||||
if type == "new":
|
||||
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["thumbnail"],
|
||||
Event.has_clip: event_data["has_clip"],
|
||||
Event.has_snapshot: event_data["has_snapshot"],
|
||||
Event.zones: [],
|
||||
Event.data: {},
|
||||
}
|
||||
elif type == "end":
|
||||
event = {
|
||||
Event.id: event_data["id"],
|
||||
Event.end_time: event_data["end_time"],
|
||||
}
|
||||
|
||||
class EventCleanup(threading.Thread):
|
||||
def __init__(self, config: FrigateConfig, stop_event: MpEvent):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = "event_cleanup"
|
||||
self.config = config
|
||||
self.stop_event = stop_event
|
||||
self.camera_keys = list(self.config.cameras.keys())
|
||||
|
||||
def expire(self, media_type: str) -> None:
|
||||
# TODO: Refactor media_type to enum
|
||||
## Expire events from unlisted cameras based on the global config
|
||||
if media_type == "clips":
|
||||
retain_config = self.config.record.events.retain
|
||||
file_extension = "mp4"
|
||||
update_params = {"has_clip": False}
|
||||
else:
|
||||
retain_config = self.config.snapshots.retain
|
||||
file_extension = "jpg"
|
||||
update_params = {"has_snapshot": False}
|
||||
|
||||
distinct_labels = (
|
||||
Event.select(Event.label)
|
||||
.where(Event.camera.not_in(self.camera_keys))
|
||||
.distinct()
|
||||
)
|
||||
|
||||
# loop over object types in db
|
||||
for l in distinct_labels:
|
||||
# get expiration time for this label
|
||||
expire_days = retain_config.objects.get(l.label, retain_config.default)
|
||||
expire_after = (
|
||||
datetime.datetime.now() - datetime.timedelta(days=expire_days)
|
||||
).timestamp()
|
||||
# grab all events after specific time
|
||||
expired_events = Event.select().where(
|
||||
Event.camera.not_in(self.camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
# delete the media from disk
|
||||
for event in expired_events:
|
||||
media_name = f"{event.camera}-{event.id}"
|
||||
media_path = Path(
|
||||
f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}"
|
||||
try:
|
||||
(
|
||||
Event.insert(event)
|
||||
.on_conflict(
|
||||
conflict_target=[Event.id],
|
||||
update=event,
|
||||
)
|
||||
media_path.unlink(missing_ok=True)
|
||||
if file_extension == "jpg":
|
||||
media_path = Path(
|
||||
f"{os.path.join(CLIPS_DIR, media_name)}-clean.png"
|
||||
)
|
||||
media_path.unlink(missing_ok=True)
|
||||
|
||||
# update the clips attribute for the db entry
|
||||
update_query = Event.update(update_params).where(
|
||||
Event.camera.not_in(self.camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
.execute()
|
||||
)
|
||||
update_query.execute()
|
||||
|
||||
## Expire events from cameras based on the camera config
|
||||
for name, camera in self.config.cameras.items():
|
||||
if media_type == "clips":
|
||||
retain_config = camera.record.events.retain
|
||||
else:
|
||||
retain_config = camera.snapshots.retain
|
||||
# get distinct objects in database for this camera
|
||||
distinct_labels = (
|
||||
Event.select(Event.label).where(Event.camera == name).distinct()
|
||||
)
|
||||
|
||||
# loop over object types in db
|
||||
for l in distinct_labels:
|
||||
# get expiration time for this label
|
||||
expire_days = retain_config.objects.get(l.label, retain_config.default)
|
||||
expire_after = (
|
||||
datetime.datetime.now() - datetime.timedelta(days=expire_days)
|
||||
).timestamp()
|
||||
# grab all events after specific time
|
||||
expired_events = Event.select().where(
|
||||
Event.camera == name,
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
# delete the grabbed clips from disk
|
||||
for event in expired_events:
|
||||
media_name = f"{event.camera}-{event.id}"
|
||||
media_path = Path(
|
||||
f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}"
|
||||
)
|
||||
media_path.unlink(missing_ok=True)
|
||||
if file_extension == "jpg":
|
||||
media_path = Path(
|
||||
f"{os.path.join(CLIPS_DIR, media_name)}-clean.png"
|
||||
)
|
||||
media_path.unlink(missing_ok=True)
|
||||
# update the clips attribute for the db entry
|
||||
update_query = Event.update(update_params).where(
|
||||
Event.camera == name,
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
update_query.execute()
|
||||
|
||||
def purge_duplicates(self) -> None:
|
||||
duplicate_query = """with grouped_events as (
|
||||
select id,
|
||||
label,
|
||||
camera,
|
||||
has_snapshot,
|
||||
has_clip,
|
||||
row_number() over (
|
||||
partition by label, camera, round(start_time/5,0)*5
|
||||
order by end_time-start_time desc
|
||||
) as copy_number
|
||||
from event
|
||||
)
|
||||
|
||||
select distinct id, camera, has_snapshot, has_clip from grouped_events
|
||||
where copy_number > 1;"""
|
||||
|
||||
duplicate_events = Event.raw(duplicate_query)
|
||||
for event in duplicate_events:
|
||||
logger.debug(f"Removing duplicate: {event.id}")
|
||||
media_name = f"{event.camera}-{event.id}"
|
||||
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
|
||||
media_path.unlink(missing_ok=True)
|
||||
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png")
|
||||
media_path.unlink(missing_ok=True)
|
||||
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
|
||||
media_path.unlink(missing_ok=True)
|
||||
|
||||
(
|
||||
Event.delete()
|
||||
.where(Event.id << [event.id for event in duplicate_events])
|
||||
.execute()
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
# only expire events every 5 minutes
|
||||
while not self.stop_event.wait(300):
|
||||
self.expire("clips")
|
||||
self.expire("snapshots")
|
||||
self.purge_duplicates()
|
||||
|
||||
# drop events from db where has_clip and has_snapshot are false
|
||||
delete_query = Event.delete().where(
|
||||
Event.has_clip == False, Event.has_snapshot == False
|
||||
)
|
||||
delete_query.execute()
|
||||
|
||||
logger.info(f"Exiting event cleanup...")
|
||||
except Exception:
|
||||
logger.warning(f"Failed to update manual event: {event_data['id']}")
|
@ -1,8 +1,8 @@
|
||||
import base64
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import copy
|
||||
import glob
|
||||
import logging
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import subprocess as sp
|
||||
@ -34,6 +34,7 @@ from playhouse.shortcuts import model_to_dict
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.const import CLIPS_DIR, MAX_SEGMENT_DURATION, RECORD_DIR
|
||||
from frigate.models import Event, Recordings, Timeline
|
||||
from frigate.events.external import ExternalEventProcessor
|
||||
from frigate.object_processing import TrackedObject
|
||||
from frigate.plus import PlusApi
|
||||
from frigate.ptz import OnvifController
|
||||
@ -60,6 +61,7 @@ def create_app(
|
||||
detected_frames_processor,
|
||||
storage_maintainer: StorageMaintainer,
|
||||
onvif: OnvifController,
|
||||
external_processor: ExternalEventProcessor,
|
||||
plus_api: PlusApi,
|
||||
):
|
||||
app = Flask(__name__)
|
||||
@ -79,6 +81,7 @@ def create_app(
|
||||
app.detected_frames_processor = detected_frames_processor
|
||||
app.storage_maintainer = storage_maintainer
|
||||
app.onvif = onvif
|
||||
app.external_processor = external_processor
|
||||
app.plus_api = plus_api
|
||||
app.camera_error_image = None
|
||||
app.hwaccel_errors = []
|
||||
@ -195,7 +198,7 @@ def send_to_plus(id):
|
||||
return make_response(jsonify({"success": False, "message": message}), 404)
|
||||
|
||||
# events from before the conversion to relative dimensions cant include annotations
|
||||
if any(d > 1 for d in event.data["box"]):
|
||||
if any(d > 1 for d in event.box):
|
||||
include_annotation = None
|
||||
|
||||
if event.end_time is None:
|
||||
@ -251,8 +254,8 @@ def send_to_plus(id):
|
||||
event.save()
|
||||
|
||||
if not include_annotation is None:
|
||||
region = event.data["region"]
|
||||
box = event.data["box"]
|
||||
region = event.region
|
||||
box = event.box
|
||||
|
||||
try:
|
||||
current_app.plus_api.add_annotation(
|
||||
@ -293,7 +296,7 @@ def false_positive(id):
|
||||
return make_response(jsonify({"success": False, "message": message}), 404)
|
||||
|
||||
# events from before the conversion to relative dimensions cant include annotations
|
||||
if any(d > 1 for d in event.data["box"]):
|
||||
if any(d > 1 for d in event.box):
|
||||
message = f"Events prior to 0.13 cannot be submitted as false positives"
|
||||
logger.error(message)
|
||||
return make_response(jsonify({"success": False, "message": message}), 400)
|
||||
@ -310,15 +313,11 @@ def false_positive(id):
|
||||
# need to refetch the event now that it has a plus_id
|
||||
event = Event.get(Event.id == id)
|
||||
|
||||
region = event.data["region"]
|
||||
box = event.data["box"]
|
||||
region = event.region
|
||||
box = event.box
|
||||
|
||||
# provide top score if score is unavailable
|
||||
score = (
|
||||
(event.data["top_score"] if event.data["top_score"] else event.top_score)
|
||||
if event.data["score"] is None
|
||||
else event.data["score"]
|
||||
)
|
||||
score = event.top_score if event.score is None else event.score
|
||||
|
||||
try:
|
||||
current_app.plus_api.add_false_positive(
|
||||
@ -759,7 +758,6 @@ def events():
|
||||
Event.top_score,
|
||||
Event.false_positive,
|
||||
Event.box,
|
||||
Event.data,
|
||||
]
|
||||
|
||||
if camera != "all":
|
||||
@ -848,6 +846,58 @@ def events():
|
||||
return jsonify([model_to_dict(e, exclude=excluded_fields) for e in events])
|
||||
|
||||
|
||||
@bp.route("/events/<camera_name>/<label>/create", methods=["POST"])
|
||||
def create_event(camera_name, label):
|
||||
if not camera_name or not current_app.frigate_config.cameras.get(camera_name):
|
||||
return jsonify(
|
||||
{"success": False, "message": f"{camera_name} is not a valid camera."}, 404
|
||||
)
|
||||
|
||||
if not label:
|
||||
return jsonify({"success": False, "message": f"{label} must be set."}, 404)
|
||||
|
||||
json: dict[str, any] = request.get_json(silent=True) or {}
|
||||
|
||||
try:
|
||||
frame = current_app.detected_frames_processor.get_current_frame(camera_name)
|
||||
|
||||
event_id = current_app.external_processor.create_manual_event(
|
||||
camera_name,
|
||||
label,
|
||||
json.get("sub_label", None),
|
||||
json.get("duration", 30),
|
||||
json.get("include_recording", True),
|
||||
json.get("draw", {}),
|
||||
frame,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"The error is {e}")
|
||||
return jsonify(
|
||||
{"success": False, "message": f"An unknown error occurred: {e}"}, 404
|
||||
)
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Successfully created event.",
|
||||
"event_id": event_id,
|
||||
},
|
||||
200,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/events/<event_id>/end", methods=["PUT"])
|
||||
def end_event(event_id):
|
||||
try:
|
||||
current_app.external_processor.finish_manual_event(event_id)
|
||||
except:
|
||||
return jsonify(
|
||||
{"success": False, "message": f"{event_id} must be set and valid."}, 404
|
||||
)
|
||||
|
||||
return jsonify({"success": True, "message": f"Event successfully ended."}, 200)
|
||||
|
||||
|
||||
@bp.route("/config")
|
||||
def config():
|
||||
config = current_app.frigate_config.dict()
|
||||
@ -866,11 +916,6 @@ def config():
|
||||
|
||||
config["plus"] = {"enabled": current_app.plus_api.is_active()}
|
||||
|
||||
for detector, detector_config in config["detectors"].items():
|
||||
detector_config["model"][
|
||||
"labelmap"
|
||||
] = current_app.frigate_config.model.merged_labelmap
|
||||
|
||||
return jsonify(config)
|
||||
|
||||
|
||||
|
@ -21,7 +21,7 @@ from frigate.config import (
|
||||
FrigateConfig,
|
||||
)
|
||||
from frigate.const import CLIPS_DIR
|
||||
from frigate.events import EventTypeEnum
|
||||
from frigate.events.maintainer import EventTypeEnum
|
||||
from frigate.util import (
|
||||
SharedMemoryFrameManager,
|
||||
calculate_region,
|
||||
|
0
frigate/record/__init__.py
Normal file
0
frigate/record/__init__.py
Normal file
@ -120,6 +120,7 @@ class TestHttp(unittest.TestCase):
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
PlusApi(),
|
||||
)
|
||||
id = "123456.random"
|
||||
@ -155,6 +156,7 @@ class TestHttp(unittest.TestCase):
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
PlusApi(),
|
||||
)
|
||||
id = "123456.random"
|
||||
@ -175,6 +177,7 @@ class TestHttp(unittest.TestCase):
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
PlusApi(),
|
||||
)
|
||||
id = "123456.random"
|
||||
@ -194,6 +197,7 @@ class TestHttp(unittest.TestCase):
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
PlusApi(),
|
||||
)
|
||||
id = "123456.random"
|
||||
@ -215,6 +219,7 @@ class TestHttp(unittest.TestCase):
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
PlusApi(),
|
||||
)
|
||||
id = "123456.random"
|
||||
@ -240,6 +245,7 @@ class TestHttp(unittest.TestCase):
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
PlusApi(),
|
||||
)
|
||||
id = "123456.random"
|
||||
@ -274,6 +280,7 @@ class TestHttp(unittest.TestCase):
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
PlusApi(),
|
||||
)
|
||||
id = "123456.random"
|
||||
@ -298,6 +305,7 @@ class TestHttp(unittest.TestCase):
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
PlusApi(),
|
||||
)
|
||||
|
||||
@ -314,6 +322,7 @@ class TestHttp(unittest.TestCase):
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
PlusApi(),
|
||||
)
|
||||
id = "123456.random"
|
||||
@ -333,6 +342,7 @@ class TestHttp(unittest.TestCase):
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
PlusApi(),
|
||||
)
|
||||
mock_stats.return_value = self.test_stats
|
||||
|
@ -5,7 +5,7 @@ import threading
|
||||
import queue
|
||||
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.events import EventTypeEnum
|
||||
from frigate.events.maintainer import EventTypeEnum
|
||||
from frigate.models import Timeline
|
||||
|
||||
from multiprocessing.queues import Queue
|
||||
|
@ -599,7 +599,9 @@ export default function Events({ path, ...props }) {
|
||||
{event.sub_label
|
||||
? `${event.label.replaceAll('_', ' ')}: ${event.sub_label.replaceAll('_', ' ')}`
|
||||
: event.label.replaceAll('_', ' ')}
|
||||
({((event?.data?.top_score || event.top_score) * 100).toFixed(0)}%)
|
||||
{(event?.data?.top_score || event.top_score || 0) == 0
|
||||
? null
|
||||
: ` (${((event?.data?.top_score || event.top_score) * 100).toFixed(0)}%)`}
|
||||
</div>
|
||||
<div className="text-sm flex">
|
||||
<Clock className="h-5 w-5 mr-2 inline" />
|
||||
|
Loading…
Reference in New Issue
Block a user