mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
FEAT: Ability to set sub labels for specific events (#2949)
* Add sub label to model and set / delete funs * Add migrations for sub label * Tweaks to API and model * Show sublabel if available * Cleanups * Update docs * Show person in UI title * Fix typo and don't fail on no json * Transfer sub labels for in progress events * Remove sublabel reset * Remove person only check * Make default null * Update docs and formatting * Make default null * Make nullable in migration * Undo null * Update model to accept null * Update migration to accept null * Don't set to default values * Remove redundant defaults and update http logic * Only need a single route * Enforce 20 character limit in http * Update docs to mention 20 character limit * Cleanup * Separate insert and update to make sure updated values are retained when event ends * Use insert instead of replace * Remove redundant if and have should_update_db include clip or snapshot requirement.
This commit is contained in:
parent
0abd0627df
commit
b1cc64d4fa
@ -186,13 +186,24 @@ Sets retain to true for the event id.
|
|||||||
|
|
||||||
Sets retain to false for the event id (event may be deleted quickly after removing).
|
Sets retain to false for the event id (event may be deleted quickly after removing).
|
||||||
|
|
||||||
|
### `POST /api/events/<id>/sub_label`
|
||||||
|
|
||||||
|
Set a sub label for an event. For example to update `person` -> `person's name` if they were recognized with facial recognition.
|
||||||
|
Sub labels must be 20 characters or shorter.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"subLabel": "some_string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### `GET /api/events/<id>/thumbnail.jpg`
|
### `GET /api/events/<id>/thumbnail.jpg`
|
||||||
|
|
||||||
Returns a thumbnail for the event id optimized for notifications. Works while the event is in progress and after completion. Passing `?format=android` will convert the thumbnail to 2:1 aspect ratio.
|
Returns a thumbnail for the event id optimized for notifications. Works while the event is in progress and after completion. Passing `?format=android` will convert the thumbnail to 2:1 aspect ratio.
|
||||||
|
|
||||||
### `GET /api/<camera_name>/<label>/thumbnail.jpg`
|
### `GET /api/<camera_name>/<label>/thumbnail.jpg`
|
||||||
|
|
||||||
Returns the thumbnail from the latest event for the given camera and label combo. Using `any` as the label will return the latest thumbnail regardless of type.
|
Returns the thumbnail from the latest event for the given camera and label combo. Using `any` as the label will return the latest thumbnail regardless of type.
|
||||||
|
|
||||||
### `GET /api/events/<id>/clip.mp4`
|
### `GET /api/events/<id>/clip.mp4`
|
||||||
|
|
||||||
|
@ -14,14 +14,22 @@ from frigate.models import Event
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def should_insert_db(prev_event, current_event):
|
||||||
|
"""If current event has new clip or snapshot."""
|
||||||
|
return (
|
||||||
|
(not prev_event["has_clip"] and not prev_event["has_snapshot"])
|
||||||
|
and (current_event["has_clip"] or current_event["has_snapshot"])
|
||||||
|
)
|
||||||
|
|
||||||
def should_update_db(prev_event, current_event):
|
def should_update_db(prev_event, current_event):
|
||||||
|
"""If current_event has updated fields and (clip or snapshot)."""
|
||||||
return (
|
return (
|
||||||
prev_event["top_score"] != current_event["top_score"]
|
(current_event["has_clip"] or current_event["has_snapshot"])
|
||||||
or prev_event["entered_zones"] != current_event["entered_zones"]
|
and (prev_event["top_score"] != current_event["top_score"]
|
||||||
or prev_event["thumbnail"] != current_event["thumbnail"]
|
or prev_event["entered_zones"] != current_event["entered_zones"]
|
||||||
or prev_event["has_clip"] != current_event["has_clip"]
|
or prev_event["thumbnail"] != current_event["thumbnail"]
|
||||||
or prev_event["has_snapshot"] != current_event["has_snapshot"]
|
or prev_event["has_clip"] != current_event["has_clip"]
|
||||||
|
or prev_event["has_snapshot"] != current_event["has_snapshot"])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -58,33 +66,52 @@ class EventProcessor(threading.Thread):
|
|||||||
if event_type == "start":
|
if event_type == "start":
|
||||||
self.events_in_process[event_data["id"]] = event_data
|
self.events_in_process[event_data["id"]] = event_data
|
||||||
|
|
||||||
|
elif event_type == "update" and should_insert_db(
|
||||||
|
self.events_in_process[event_data["id"]], event_data
|
||||||
|
):
|
||||||
|
self.events_in_process[event_data["id"]] = event_data
|
||||||
|
# TODO: this will generate a lot of db activity possibly
|
||||||
|
Event.insert(
|
||||||
|
id=event_data["id"],
|
||||||
|
label=event_data["label"],
|
||||||
|
camera=camera,
|
||||||
|
start_time=event_data["start_time"] - event_config.pre_capture,
|
||||||
|
end_time=None,
|
||||||
|
top_score=event_data["top_score"],
|
||||||
|
false_positive=event_data["false_positive"],
|
||||||
|
zones=list(event_data["entered_zones"]),
|
||||||
|
thumbnail=event_data["thumbnail"],
|
||||||
|
region=event_data["region"],
|
||||||
|
box=event_data["box"],
|
||||||
|
area=event_data["area"],
|
||||||
|
has_clip=event_data["has_clip"],
|
||||||
|
has_snapshot=event_data["has_snapshot"],
|
||||||
|
).execute()
|
||||||
|
|
||||||
elif event_type == "update" and should_update_db(
|
elif event_type == "update" and should_update_db(
|
||||||
self.events_in_process[event_data["id"]], event_data
|
self.events_in_process[event_data["id"]], event_data
|
||||||
):
|
):
|
||||||
self.events_in_process[event_data["id"]] = event_data
|
self.events_in_process[event_data["id"]] = event_data
|
||||||
# TODO: this will generate a lot of db activity possibly
|
# TODO: this will generate a lot of db activity possibly
|
||||||
if event_data["has_clip"] or event_data["has_snapshot"]:
|
Event.update(
|
||||||
Event.replace(
|
label=event_data["label"],
|
||||||
id=event_data["id"],
|
camera=camera,
|
||||||
label=event_data["label"],
|
start_time=event_data["start_time"] - event_config.pre_capture,
|
||||||
camera=camera,
|
end_time=None,
|
||||||
start_time=event_data["start_time"] - event_config.pre_capture,
|
top_score=event_data["top_score"],
|
||||||
end_time=None,
|
false_positive=event_data["false_positive"],
|
||||||
top_score=event_data["top_score"],
|
zones=list(event_data["entered_zones"]),
|
||||||
false_positive=event_data["false_positive"],
|
thumbnail=event_data["thumbnail"],
|
||||||
zones=list(event_data["entered_zones"]),
|
region=event_data["region"],
|
||||||
thumbnail=event_data["thumbnail"],
|
box=event_data["box"],
|
||||||
region=event_data["region"],
|
area=event_data["area"],
|
||||||
box=event_data["box"],
|
has_clip=event_data["has_clip"],
|
||||||
area=event_data["area"],
|
has_snapshot=event_data["has_snapshot"],
|
||||||
has_clip=event_data["has_clip"],
|
).where(Event.id == event_data["id"]).execute()
|
||||||
has_snapshot=event_data["has_snapshot"],
|
|
||||||
).execute()
|
|
||||||
|
|
||||||
elif event_type == "end":
|
elif event_type == "end":
|
||||||
if event_data["has_clip"] or event_data["has_snapshot"]:
|
if event_data["has_clip"] or event_data["has_snapshot"]:
|
||||||
Event.replace(
|
Event.update(
|
||||||
id=event_data["id"],
|
|
||||||
label=event_data["label"],
|
label=event_data["label"],
|
||||||
camera=camera,
|
camera=camera,
|
||||||
start_time=event_data["start_time"] - event_config.pre_capture,
|
start_time=event_data["start_time"] - event_config.pre_capture,
|
||||||
@ -98,7 +125,7 @@ class EventProcessor(threading.Thread):
|
|||||||
area=event_data["area"],
|
area=event_data["area"],
|
||||||
has_clip=event_data["has_clip"],
|
has_clip=event_data["has_clip"],
|
||||||
has_snapshot=event_data["has_snapshot"],
|
has_snapshot=event_data["has_snapshot"],
|
||||||
).execute()
|
).where(Event.id == event_data["id"]).execute()
|
||||||
|
|
||||||
del self.events_in_process[event_data["id"]]
|
del self.events_in_process[event_data["id"]]
|
||||||
self.event_processed_queue.put((event_data["id"], camera))
|
self.event_processed_queue.put((event_data["id"], camera))
|
||||||
|
@ -126,14 +126,14 @@ def set_retain(id):
|
|||||||
event = Event.get(Event.id == id)
|
event = Event.get(Event.id == id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"success": False, "message": "Event" + id + " not found"}), 404
|
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||||
)
|
)
|
||||||
|
|
||||||
event.retain_indefinitely = True
|
event.retain_indefinitely = True
|
||||||
event.save()
|
event.save()
|
||||||
|
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"success": True, "message": "Event" + id + " retained"}), 200
|
jsonify({"success": True, "message": "Event " + id + " retained"}), 200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -143,16 +143,42 @@ def delete_retain(id):
|
|||||||
event = Event.get(Event.id == id)
|
event = Event.get(Event.id == id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"success": False, "message": "Event" + id + " not found"}), 404
|
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||||
)
|
)
|
||||||
|
|
||||||
event.retain_indefinitely = False
|
event.retain_indefinitely = False
|
||||||
event.save()
|
event.save()
|
||||||
|
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"success": True, "message": "Event" + id + " un-retained"}), 200
|
jsonify({"success": True, "message": "Event " + id + " un-retained"}), 200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@bp.route("/events/<id>/sub_label", methods=("POST",))
|
||||||
|
def set_sub_label(id):
|
||||||
|
try:
|
||||||
|
event = Event.get(Event.id == id)
|
||||||
|
except DoesNotExist:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.json:
|
||||||
|
new_sub_label = request.json.get("subLabel")
|
||||||
|
else:
|
||||||
|
new_sub_label = None
|
||||||
|
|
||||||
|
|
||||||
|
if new_sub_label and len(new_sub_label) > 20:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": new_sub_label + " exceeds the 20 character limit for sub_label"}), 400
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
event.sub_label = new_sub_label
|
||||||
|
event.save()
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": True, "message": "Event " + id + " sub label set to " + new_sub_label}), 200
|
||||||
|
)
|
||||||
|
|
||||||
@bp.route("/events/<id>", methods=("DELETE",))
|
@bp.route("/events/<id>", methods=("DELETE",))
|
||||||
def delete_event(id):
|
def delete_event(id):
|
||||||
@ -160,7 +186,7 @@ def delete_event(id):
|
|||||||
event = Event.get(Event.id == id)
|
event = Event.get(Event.id == id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"success": False, "message": "Event" + id + " not found"}), 404
|
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||||
)
|
)
|
||||||
|
|
||||||
media_name = f"{event.camera}-{event.id}"
|
media_name = f"{event.camera}-{event.id}"
|
||||||
@ -175,7 +201,7 @@ def delete_event(id):
|
|||||||
|
|
||||||
event.delete_instance()
|
event.delete_instance()
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"success": True, "message": "Event" + id + " deleted"}), 200
|
jsonify({"success": True, "message": "Event " + id + " deleted"}), 200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ from playhouse.sqlite_ext import *
|
|||||||
class Event(Model):
|
class Event(Model):
|
||||||
id = CharField(null=False, primary_key=True, max_length=30)
|
id = CharField(null=False, primary_key=True, max_length=30)
|
||||||
label = CharField(index=True, max_length=20)
|
label = CharField(index=True, max_length=20)
|
||||||
|
sub_label = CharField(max_length=20, null=True)
|
||||||
camera = CharField(index=True, max_length=20)
|
camera = CharField(index=True, max_length=20)
|
||||||
start_time = DateTimeField()
|
start_time = DateTimeField()
|
||||||
end_time = DateTimeField()
|
end_time = DateTimeField()
|
||||||
|
46
migrations/008_add_sub_label.py
Normal file
46
migrations/008_add_sub_label.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""Peewee migrations -- 008_add_sub_label.py.
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
import peewee as pw
|
||||||
|
from playhouse.sqlite_ext import *
|
||||||
|
from decimal import ROUND_HALF_EVEN
|
||||||
|
from frigate.models import Event
|
||||||
|
|
||||||
|
try:
|
||||||
|
import playhouse.postgres_ext as pw_pext
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
SQL = pw.SQL
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator, database, fake=False, **kwargs):
|
||||||
|
migrator.add_fields(
|
||||||
|
Event,
|
||||||
|
sub_label=pw.CharField(max_length=20, null=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator, database, fake=False, **kwargs):
|
||||||
|
migrator.remove_fields(Event, ["sub_label"])
|
@ -315,7 +315,7 @@ export default function Events({ path, ...props }) {
|
|||||||
<div className="m-2 flex grow">
|
<div className="m-2 flex grow">
|
||||||
<div className="flex flex-col grow">
|
<div className="flex flex-col grow">
|
||||||
<div className="capitalize text-lg font-bold">
|
<div className="capitalize text-lg font-bold">
|
||||||
{event.label} ({(event.top_score * 100).toFixed(0)}%)
|
{event.sub_label ? `${event.label}: ${event.sub_label}` : event.label} ({(event.top_score * 100).toFixed(0)}%)
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
{new Date(event.start_time * 1000).toLocaleDateString()}{' '}
|
{new Date(event.start_time * 1000).toLocaleDateString()}{' '}
|
||||||
|
Loading…
Reference in New Issue
Block a user