Skip to content

Commit

Permalink
FEAT: Ability to set sub labels for specific events (#2949)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
NickM-27 authored Mar 17, 2022
1 parent 0abd062 commit b1cc64d
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 33 deletions.
13 changes: 12 additions & 1 deletion docs/docs/integrations/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

### `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`

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`

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`

Expand Down
77 changes: 52 additions & 25 deletions frigate/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,22 @@

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):
"""If current_event has updated fields and (clip or snapshot)."""
return (
prev_event["top_score"] != current_event["top_score"]
or prev_event["entered_zones"] != current_event["entered_zones"]
or prev_event["thumbnail"] != current_event["thumbnail"]
or prev_event["has_clip"] != current_event["has_clip"]
or prev_event["has_snapshot"] != current_event["has_snapshot"]
(current_event["has_clip"] or current_event["has_snapshot"])
and (prev_event["top_score"] != current_event["top_score"]
or prev_event["entered_zones"] != current_event["entered_zones"]
or prev_event["thumbnail"] != current_event["thumbnail"]
or prev_event["has_clip"] != current_event["has_clip"]
or prev_event["has_snapshot"] != current_event["has_snapshot"])
)


Expand Down Expand Up @@ -58,33 +66,52 @@ def run(self):
if event_type == "start":
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(
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
if event_data["has_clip"] or event_data["has_snapshot"]:
Event.replace(
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()
Event.update(
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"],
).where(Event.id == event_data["id"]).execute()

elif event_type == "end":
if event_data["has_clip"] or event_data["has_snapshot"]:
Event.replace(
id=event_data["id"],
Event.update(
label=event_data["label"],
camera=camera,
start_time=event_data["start_time"] - event_config.pre_capture,
Expand All @@ -98,7 +125,7 @@ def run(self):
area=event_data["area"],
has_clip=event_data["has_clip"],
has_snapshot=event_data["has_snapshot"],
).execute()
).where(Event.id == event_data["id"]).execute()

del self.events_in_process[event_data["id"]]
self.event_processed_queue.put((event_data["id"], camera))
Expand Down
38 changes: 32 additions & 6 deletions frigate/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,14 @@ def set_retain(id):
event = Event.get(Event.id == id)
except DoesNotExist:
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.save()

return make_response(
jsonify({"success": True, "message": "Event" + id + " retained"}), 200
jsonify({"success": True, "message": "Event " + id + " retained"}), 200
)


Expand All @@ -143,24 +143,50 @@ def delete_retain(id):
event = Event.get(Event.id == id)
except DoesNotExist:
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.save()

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",))
def delete_event(id):
try:
event = Event.get(Event.id == id)
except DoesNotExist:
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}"
Expand All @@ -175,7 +201,7 @@ def delete_event(id):

event.delete_instance()
return make_response(
jsonify({"success": True, "message": "Event" + id + " deleted"}), 200
jsonify({"success": True, "message": "Event " + id + " deleted"}), 200
)


Expand Down
1 change: 1 addition & 0 deletions frigate/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
class Event(Model):
id = CharField(null=False, primary_key=True, max_length=30)
label = CharField(index=True, max_length=20)
sub_label = CharField(max_length=20, null=True)
camera = CharField(index=True, max_length=20)
start_time = DateTimeField()
end_time = DateTimeField()
Expand Down
46 changes: 46 additions & 0 deletions migrations/008_add_sub_label.py
Original file line number Diff line number Diff line change
@@ -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"])
2 changes: 1 addition & 1 deletion web/src/routes/Events.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export default function Events({ path, ...props }) {
<div className="m-2 flex grow">
<div className="flex flex-col grow">
<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 className="text-sm">
{new Date(event.start_time * 1000).toLocaleDateString()}{' '}
Expand Down

0 comments on commit b1cc64d

Please sign in to comment.