Convert events endpoints to FastAPI

This commit is contained in:
Rui Alves 2024-09-16 10:44:03 +01:00
parent cc259736da
commit 1b601b8c35
8 changed files with 330 additions and 230 deletions

View File

@ -21,7 +21,6 @@ from werkzeug.middleware.proxy_fix import ProxyFix
from frigate.api.auth import AuthBp, get_jwt_secret, limiter from frigate.api.auth import AuthBp, get_jwt_secret, limiter
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
from frigate.api.event import EventBp
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.const import CONFIG_DIR from frigate.const import CONFIG_DIR
from frigate.embeddings import EmbeddingsContext from frigate.embeddings import EmbeddingsContext
@ -43,7 +42,6 @@ logger = logging.getLogger(__name__)
bp = Blueprint("frigate", __name__) bp = Blueprint("frigate", __name__)
bp.register_blueprint(EventBp)
bp.register_blueprint(AuthBp) bp.register_blueprint(AuthBp)
router = APIRouter() router = APIRouter()

View File

@ -0,0 +1,52 @@
from typing import Optional
from pydantic import BaseModel
DEFAULT_TIME_RANGE = "00:00,24:00"
class EventsQueryParams(BaseModel):
camera: Optional[str] = "all"
cameras: Optional[str] = "all"
label: Optional[str] = "all"
labels: Optional[str] = "all"
sub_label: Optional[str] = "all"
sub_labels: Optional[str] = "all"
zone: Optional[str] = "all"
zones: Optional[str] = "all"
limit: Optional[int] = 100
after: Optional[float] = None
before: Optional[float] = None
time_range: Optional[str] = DEFAULT_TIME_RANGE
has_clip: Optional[int] = None
has_snapshot: Optional[int] = None
in_progress: Optional[int] = None
include_thumbnails: Optional[int] = 1
favorites: Optional[int] = None
min_score: Optional[float] = None
max_score: Optional[float] = None
is_submitted: Optional[int] = None
min_length: Optional[float] = None
max_length: Optional[float] = None
sort: Optional[str] = None
timezone: Optional[str] = "utc"
class EventsSearchQueryParams(BaseModel):
query: Optional[str] = None
search_type: Optional[str] = "thumbnail,description"
include_thumbnails: Optional[int] = 1
limit: Optional[int] = 50
cameras: Optional[str] = "all"
labels: Optional[str] = "all"
zones: Optional[str] = "all"
after: Optional[float] = None
before: Optional[float] = None
timezone: Optional[str] = "utc"
class EventsSummaryQueryParams(BaseModel):
timezone: Optional[str] = "utc"
has_clip: Optional[int] = None
has_snapshot: Optional[int] = None

View File

@ -9,3 +9,4 @@ class Tags(Enum):
notifications = "Notifications" notifications = "Notifications"
review = "Review" review = "Review"
export = "Export" export = "Export"
events = "Events"

View File

@ -11,17 +11,20 @@ from urllib.parse import unquote
import cv2 import cv2
import numpy as np import numpy as np
from flask import ( from fastapi import APIRouter, Request
Blueprint, from fastapi.params import Depends
current_app, from fastapi.responses import JSONResponse
jsonify,
make_response,
request,
)
from peewee import JOIN, DoesNotExist, fn, operator from peewee import JOIN, DoesNotExist, fn, operator
from PIL import Image from PIL import Image
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
from frigate.api.defs.events_query_parameters import (
DEFAULT_TIME_RANGE,
EventsQueryParams,
EventsSearchQueryParams,
EventsSummaryQueryParams,
)
from frigate.api.defs.tags import Tags
from frigate.const import ( from frigate.const import (
CLIPS_DIR, CLIPS_DIR,
) )
@ -33,57 +36,55 @@ from frigate.util.builtin import get_tz_modifiers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
EventBp = Blueprint("events", __name__) router = APIRouter(tags=[Tags.events])
DEFAULT_TIME_RANGE = "00:00,24:00"
@EventBp.route("/events") @router.get("/events")
def events(): def events(params: EventsQueryParams = Depends()):
camera = request.args.get("camera", "all") camera = params.camera
cameras = request.args.get("cameras", "all") cameras = params.cameras
# handle old camera arg # handle old camera arg
if cameras == "all" and camera != "all": if cameras == "all" and camera != "all":
cameras = camera cameras = camera
label = unquote(request.args.get("label", "all")) label = unquote(params.label)
labels = request.args.get("labels", "all") labels = params.labels
# handle old label arg # handle old label arg
if labels == "all" and label != "all": if labels == "all" and label != "all":
labels = label labels = label
sub_label = request.args.get("sub_label", "all") sub_label = params.sub_label
sub_labels = request.args.get("sub_labels", "all") sub_labels = params.sub_labels
# handle old sub_label arg # handle old sub_label arg
if sub_labels == "all" and sub_label != "all": if sub_labels == "all" and sub_label != "all":
sub_labels = sub_label sub_labels = sub_label
zone = request.args.get("zone", "all") zone = params.zone
zones = request.args.get("zones", "all") zones = params.zones
# handle old label arg # handle old label arg
if zones == "all" and zone != "all": if zones == "all" and zone != "all":
zones = zone zones = zone
limit = request.args.get("limit", 100) limit = params.limit
after = request.args.get("after", type=float) after = params.after
before = request.args.get("before", type=float) before = params.before
time_range = request.args.get("time_range", DEFAULT_TIME_RANGE) time_range = params.time_range
has_clip = request.args.get("has_clip", type=int) has_clip = params.has_clip
has_snapshot = request.args.get("has_snapshot", type=int) has_snapshot = params.has_snapshot
in_progress = request.args.get("in_progress", type=int) in_progress = params.in_progress
include_thumbnails = request.args.get("include_thumbnails", default=1, type=int) include_thumbnails = params.include_thumbnails
favorites = request.args.get("favorites", type=int) favorites = params.favorites
min_score = request.args.get("min_score", type=float) min_score = params.min_score
max_score = request.args.get("max_score", type=float) max_score = params.max_score
is_submitted = request.args.get("is_submitted", type=int) is_submitted = params.is_submitted
min_length = request.args.get("min_length", type=float) min_length = params.min_length
max_length = request.args.get("max_length", type=float) max_length = params.max_length
sort = request.args.get("sort", type=str) sort = params.sort
clauses = [] clauses = []
@ -163,7 +164,7 @@ def events():
if time_range != DEFAULT_TIME_RANGE: if time_range != DEFAULT_TIME_RANGE:
# get timezone arg to ensure browser times are used # get timezone arg to ensure browser times are used
tz_name = request.args.get("timezone", default="utc", type=str) tz_name = params.timezone
hour_modifier, minute_modifier, _ = get_tz_modifiers(tz_name) hour_modifier, minute_modifier, _ = get_tz_modifiers(tz_name)
times = time_range.split(",") times = time_range.split(",")
@ -248,13 +249,11 @@ def events():
.iterator() .iterator()
) )
return jsonify(list(events)) return JSONResponse(content=list(events))
@EventBp.route("/events/explore") @router.get("/events/explore")
def events_explore(): def events_explore(limit: int = 10):
limit = request.args.get("limit", 10, type=int)
subquery = Event.select( subquery = Event.select(
Event.id, Event.id,
Event.camera, Event.camera,
@ -316,66 +315,65 @@ def events_explore():
for event in events for event in events
] ]
return jsonify(processed_events) return JSONResponse(content=processed_events)
@EventBp.route("/event_ids") @router.get("/event_ids")
def event_ids(): def event_ids(ids: str):
idString = request.args.get("ids") ids = ids.split(",")
ids = idString.split(",")
if not ids: if not ids:
return make_response( return JSONResponse(
jsonify({"success": False, "message": "Valid list of ids must be sent"}), content=({"success": False, "message": "Valid list of ids must be sent"}),
400, status_code=400,
) )
try: try:
events = Event.select().where(Event.id << ids).dicts().iterator() events = Event.select().where(Event.id << ids).dicts().iterator()
return jsonify(list(events)) return JSONResponse(list(events))
except Exception: except Exception:
return make_response( return JSONResponse(
jsonify({"success": False, "message": "Events not found"}), 400 content=({"success": False, "message": "Events not found"}), status_code=400
) )
@EventBp.route("/events/search") @router.get("/events/search")
def events_search(): def events_search(request: Request, params: EventsSearchQueryParams = Depends()):
query = request.args.get("query", type=str) query = params.query
search_type = request.args.get("search_type", "thumbnail,description", type=str) search_type = params.search_type
include_thumbnails = request.args.get("include_thumbnails", default=1, type=int) include_thumbnails = params.include_thumbnails
limit = request.args.get("limit", 50, type=int) limit = params.limit
# Filters # Filters
cameras = request.args.get("cameras", "all", type=str) cameras = params.cameras
labels = request.args.get("labels", "all", type=str) labels = params.labels
zones = request.args.get("zones", "all", type=str) zones = params.zones
after = request.args.get("after", type=float) after = params.after
before = request.args.get("before", type=float) before = params.before
if not query: if not query:
return make_response( return JSONResponse(
jsonify( content=(
{ {
"success": False, "success": False,
"message": "A search query must be supplied", "message": "A search query must be supplied",
} }
), ),
400, status_code=400,
) )
if not current_app.frigate_config.semantic_search.enabled: if not request.app.frigate_config.semantic_search.enabled:
return make_response( return JSONResponse(
jsonify( content=(
{ {
"success": False, "success": False,
"message": "Semantic search is not enabled", "message": "Semantic search is not enabled",
} }
), ),
400, status_code=400,
) )
context: EmbeddingsContext = current_app.embeddings context: EmbeddingsContext = request.app.embeddings
selected_columns = [ selected_columns = [
Event.id, Event.id,
@ -434,14 +432,14 @@ def events_search():
try: try:
search_event: Event = Event.get(Event.id == query) search_event: Event = Event.get(Event.id == query)
except DoesNotExist: except DoesNotExist:
return make_response( return JSONResponse(
jsonify( content=(
{ {
"success": False, "success": False,
"message": "Event not found", "message": "Event not found",
} }
), ),
404, status_code=404,
) )
thumbnail = base64.b64decode(search_event.thumbnail) thumbnail = base64.b64decode(search_event.thumbnail)
img = np.array(Image.open(io.BytesIO(thumbnail)).convert("RGB")) img = np.array(Image.open(io.BytesIO(thumbnail)).convert("RGB"))
@ -501,7 +499,7 @@ def events_search():
} }
if not results: if not results:
return jsonify([]) return JSONResponse(content=[])
# Get the event data # Get the event data
events = ( events = (
@ -534,15 +532,15 @@ def events_search():
] ]
events = sorted(events, key=lambda x: x["search_distance"])[:limit] events = sorted(events, key=lambda x: x["search_distance"])[:limit]
return jsonify(events) return JSONResponse(content=events)
@EventBp.route("/events/summary") @router.get("/events/summary")
def events_summary(): def events_summary(params: EventsSummaryQueryParams = Depends()):
tz_name = request.args.get("timezone", default="utc", type=str) tz_name = params.timezone
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name) hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name)
has_clip = request.args.get("has_clip", type=int) has_clip = params.has_clip
has_snapshot = request.args.get("has_snapshot", type=int) has_snapshot = params.has_snapshot
clauses = [] clauses = []
@ -579,47 +577,49 @@ def events_summary():
) )
) )
return jsonify([e for e in groups.dicts()]) return JSONResponse(content=[e for e in groups.dicts()])
@EventBp.route("/events/<id>", methods=("GET",)) @router.get("/events/{event_id}")
def event(id): def event(event_id: str):
try: try:
return model_to_dict(Event.get(Event.id == id)) return model_to_dict(Event.get(Event.id == event_id))
except DoesNotExist: except DoesNotExist:
return "Event not found", 404 return "Event not found", 404
@EventBp.route("/events/<id>/retain", methods=("POST",)) @router.post("/events/{event_id}/retain")
def set_retain(id): def set_retain(event_id: str):
try: try:
event = Event.get(Event.id == id) event = Event.get(Event.id == event_id)
except DoesNotExist: except DoesNotExist:
return make_response( return JSONResponse(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404 content=({"success": False, "message": "Event " + event_id + " not found"}),
status_code=404,
) )
event.retain_indefinitely = True event.retain_indefinitely = True
event.save() event.save()
return make_response( return JSONResponse(
jsonify({"success": True, "message": "Event " + id + " retained"}), 200 content=({"success": True, "message": "Event " + event_id + " retained"}),
status_code=200,
) )
@EventBp.route("/events/<id>/plus", methods=("POST",)) @router.post("/events/{event_id}/plus")
def send_to_plus(id): def send_to_plus(request: Request, event_id: str):
if not current_app.plus_api.is_active(): if not request.app.plus_api.is_active():
message = "PLUS_API_KEY environment variable is not set" message = "PLUS_API_KEY environment variable is not set"
logger.error(message) logger.error(message)
return make_response( return JSONResponse(
jsonify( content=(
{ {
"success": False, "success": False,
"message": message, "message": message,
} }
), ),
400, status_code=400,
) )
include_annotation = ( include_annotation = (
@ -627,11 +627,13 @@ def send_to_plus(id):
) )
try: try:
event = Event.get(Event.id == id) event = Event.get(Event.id == event_id)
except DoesNotExist: except DoesNotExist:
message = f"Event {id} not found" message = f"Event {event_id} not found"
logger.error(message) logger.error(message)
return make_response(jsonify({"success": False, "message": message}), 404) return JSONResponse(
content=({"success": False, "message": message}), status_code=404
)
# events from before the conversion to relative dimensions cant include annotations # events from before the conversion to relative dimensions cant include annotations
if event.data.get("box") is None: if event.data.get("box") is None:
@ -639,20 +641,22 @@ def send_to_plus(id):
if event.end_time is None: if event.end_time is None:
logger.error(f"Unable to load clean png for in-progress event: {event.id}") logger.error(f"Unable to load clean png for in-progress event: {event.id}")
return make_response( return JSONResponse(
jsonify( content=(
{ {
"success": False, "success": False,
"message": "Unable to load clean png for in-progress event", "message": "Unable to load clean png for in-progress event",
} }
), ),
400, status_code=400,
) )
if event.plus_id: if event.plus_id:
message = "Already submitted to plus" message = "Already submitted to plus"
logger.error(message) logger.error(message)
return make_response(jsonify({"success": False, "message": message}), 400) return JSONResponse(
content=({"success": False, "message": message}), status_code=400
)
# load clean.png # load clean.png
try: try:
@ -660,29 +664,29 @@ def send_to_plus(id):
image = cv2.imread(os.path.join(CLIPS_DIR, filename)) image = cv2.imread(os.path.join(CLIPS_DIR, filename))
except Exception: except Exception:
logger.error(f"Unable to load clean png for event: {event.id}") logger.error(f"Unable to load clean png for event: {event.id}")
return make_response( return JSONResponse(
jsonify( content=(
{"success": False, "message": "Unable to load clean png for event"} {"success": False, "message": "Unable to load clean png for event"}
), ),
400, status_code=400,
) )
if image is None or image.size == 0: if image is None or image.size == 0:
logger.error(f"Unable to load clean png for event: {event.id}") logger.error(f"Unable to load clean png for event: {event.id}")
return make_response( return JSONResponse(
jsonify( content=(
{"success": False, "message": "Unable to load clean png for event"} {"success": False, "message": "Unable to load clean png for event"}
), ),
400, status_code=400,
) )
try: try:
plus_id = current_app.plus_api.upload_image(image, event.camera) plus_id = request.app.plus_api.upload_image(image, event.camera)
except Exception as ex: except Exception as ex:
logger.exception(ex) logger.exception(ex)
return make_response( return JSONResponse(
jsonify({"success": False, "message": "Error uploading image"}), content=({"success": False, "message": "Error uploading image"}),
400, status_code=400,
) )
# store image id in the database # store image id in the database
@ -693,7 +697,7 @@ def send_to_plus(id):
box = event.data["box"] box = event.data["box"]
try: try:
current_app.plus_api.add_annotation( request.app.plus_api.add_annotation(
event.plus_id, event.plus_id,
box, box,
event.label, event.label,
@ -701,59 +705,67 @@ def send_to_plus(id):
except ValueError: except ValueError:
message = "Error uploading annotation, unsupported label provided." message = "Error uploading annotation, unsupported label provided."
logger.error(message) logger.error(message)
return make_response( return JSONResponse(
jsonify({"success": False, "message": message}), content=({"success": False, "message": message}),
400, status_code=400,
) )
except Exception as ex: except Exception as ex:
logger.exception(ex) logger.exception(ex)
return make_response( return JSONResponse(
jsonify({"success": False, "message": "Error uploading annotation"}), content=({"success": False, "message": "Error uploading annotation"}),
400, status_code=400,
) )
return make_response(jsonify({"success": True, "plus_id": plus_id}), 200) return JSONResponse(
content=({"success": True, "plus_id": plus_id}), status_code=200
)
@EventBp.route("/events/<id>/false_positive", methods=("PUT",)) @router.put("/events/{event_id}/false_positive")
def false_positive(id): def false_positive(request: Request, event_id: str):
if not current_app.plus_api.is_active(): if not request.app.plus_api.is_active():
message = "PLUS_API_KEY environment variable is not set" message = "PLUS_API_KEY environment variable is not set"
logger.error(message) logger.error(message)
return make_response( return JSONResponse(
jsonify( content=(
{ {
"success": False, "success": False,
"message": message, "message": message,
} }
), ),
400, status_code=400,
) )
try: try:
event = Event.get(Event.id == id) event = Event.get(Event.id == event_id)
except DoesNotExist: except DoesNotExist:
message = f"Event {id} not found" message = f"Event {event_id} not found"
logger.error(message) logger.error(message)
return make_response(jsonify({"success": False, "message": message}), 404) return JSONResponse(
content=({"success": False, "message": message}), status_code=404
)
# events from before the conversion to relative dimensions cant include annotations # events from before the conversion to relative dimensions cant include annotations
if event.data.get("box") is None: if event.data.get("box") is None:
message = "Events prior to 0.13 cannot be submitted as false positives" message = "Events prior to 0.13 cannot be submitted as false positives"
logger.error(message) logger.error(message)
return make_response(jsonify({"success": False, "message": message}), 400) return JSONResponse(
content=({"success": False, "message": message}), status_code=400
)
if event.false_positive: if event.false_positive:
message = "False positive already submitted to Frigate+" message = "False positive already submitted to Frigate+"
logger.error(message) logger.error(message)
return make_response(jsonify({"success": False, "message": message}), 400) return JSONResponse(
content=({"success": False, "message": message}), status_code=400
)
if not event.plus_id: if not event.plus_id:
plus_response = send_to_plus(id) plus_response = send_to_plus(event_id)
if plus_response.status_code != 200: if plus_response.status_code != 200:
return plus_response return plus_response
# need to refetch the event now that it has a plus_id # need to refetch the event now that it has a plus_id
event = Event.get(Event.id == id) event = Event.get(Event.id == event_id)
region = event.data["region"] region = event.data["region"]
box = event.data["box"] box = event.data["box"]
@ -766,7 +778,7 @@ def false_positive(id):
) )
try: try:
current_app.plus_api.add_false_positive( request.app.plus_api.add_false_positive(
event.plus_id, event.plus_id,
region, region,
box, box,
@ -779,92 +791,101 @@ def false_positive(id):
except ValueError: except ValueError:
message = "Error uploading false positive, unsupported label provided." message = "Error uploading false positive, unsupported label provided."
logger.error(message) logger.error(message)
return make_response( return JSONResponse(
jsonify({"success": False, "message": message}), content=({"success": False, "message": message}),
400, status_code=400,
) )
except Exception as ex: except Exception as ex:
logger.exception(ex) logger.exception(ex)
return make_response( return JSONResponse(
jsonify({"success": False, "message": "Error uploading false positive"}), content=({"success": False, "message": "Error uploading false positive"}),
400, status_code=400,
) )
event.false_positive = True event.false_positive = True
event.save() event.save()
return make_response(jsonify({"success": True, "plus_id": event.plus_id}), 200) return JSONResponse(
content=({"success": True, "plus_id": event.plus_id}), status_code=200
)
@EventBp.route("/events/<id>/retain", methods=("DELETE",)) @router.delete("/events/{event_id}/retain")
def delete_retain(id): def delete_retain(event_id: str):
try: try:
event = Event.get(Event.id == id) event = Event.get(Event.id == event_id)
except DoesNotExist: except DoesNotExist:
return make_response( return JSONResponse(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404 content=({"success": False, "message": "Event " + event_id + " not found"}),
status_code=404,
) )
event.retain_indefinitely = False event.retain_indefinitely = False
event.save() event.save()
return make_response( return JSONResponse(
jsonify({"success": True, "message": "Event " + id + " un-retained"}), 200 content=({"success": True, "message": "Event " + event_id + " un-retained"}),
status_code=200,
) )
@EventBp.route("/events/<id>/sub_label", methods=("POST",)) @router.post("/events/{event_id}/sub_label")
def set_sub_label(id): def set_sub_label(
request: Request,
event_id: str,
body: dict = None,
):
try: try:
event: Event = Event.get(Event.id == id) event: Event = Event.get(Event.id == event_id)
except DoesNotExist: except DoesNotExist:
return make_response( return JSONResponse(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404 content=({"success": False, "message": "Event " + event_id + " not found"}),
status_code=404,
) )
json: dict[str, any] = request.get_json(silent=True) or {} json: dict[str, any] = body or {}
new_sub_label = json.get("subLabel") new_sub_label = json.get("subLabel")
new_score = json.get("subLabelScore") new_score = json.get("subLabelScore")
if new_sub_label is None: if new_sub_label is None:
return make_response( return JSONResponse(
jsonify( content=(
{ {
"success": False, "success": False,
"message": "A sub label must be supplied", "message": "A sub label must be supplied",
} }
), ),
400, status_code=400,
) )
if new_sub_label and len(new_sub_label) > 100: if new_sub_label and len(new_sub_label) > 100:
return make_response( return JSONResponse(
jsonify( content=(
{ {
"success": False, "success": False,
"message": new_sub_label "message": new_sub_label
+ " exceeds the 100 character limit for sub_label", + " exceeds the 100 character limit for sub_label",
} }
), ),
400, status_code=400,
) )
if new_score is not None and (new_score > 1.0 or new_score < 0): if new_score is not None and (new_score > 1.0 or new_score < 0):
return make_response( return JSONResponse(
jsonify( content=(
{ {
"success": False, "success": False,
"message": new_score "message": new_score
+ " does not fit within the expected bounds 0 <= score <= 1.0", + " does not fit within the expected bounds 0 <= score <= 1.0",
} }
), ),
400, status_code=400,
) )
if not event.end_time: if not event.end_time:
# update tracked object # update tracked object
tracked_obj: TrackedObject = ( tracked_obj: TrackedObject = (
current_app.detected_frames_processor.camera_states[ request.app.detected_frames_processor.camera_states[
event.camera event.camera
].tracked_objects.get(event.id) ].tracked_objects.get(event.id)
) )
@ -875,7 +896,7 @@ def set_sub_label(id):
# update timeline items # update timeline items
Timeline.update( Timeline.update(
data=Timeline.data.update({"sub_label": (new_sub_label, new_score)}) data=Timeline.data.update({"sub_label": (new_sub_label, new_score)})
).where(Timeline.source_id == id).execute() ).where(Timeline.source_id == event_id).execute()
event.sub_label = new_sub_label event.sub_label = new_sub_label
@ -885,70 +906,79 @@ def set_sub_label(id):
event.data = data event.data = data
event.save() event.save()
return make_response( return JSONResponse(
jsonify( content=(
{ {
"success": True, "success": True,
"message": "Event " + id + " sub label set to " + new_sub_label, "message": "Event " + event_id + " sub label set to " + new_sub_label,
} }
), ),
200, status_code=200,
) )
@EventBp.route("/events/<id>/description", methods=("POST",)) @router.post("/events/{event_id}/description")
def set_description(id): def set_description(
request: Request,
event_id: str,
body: dict = None,
):
try: try:
event: Event = Event.get(Event.id == id) event: Event = Event.get(Event.id == event_id)
except DoesNotExist: except DoesNotExist:
return make_response( return JSONResponse(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404 content=({"success": False, "message": "Event " + event_id + " not found"}),
status_code=404,
) )
json: dict[str, any] = request.get_json(silent=True) or {} json: dict[str, any] = body or {}
new_description = json.get("description") new_description = json.get("description")
if new_description is None or len(new_description) == 0: if new_description is None or len(new_description) == 0:
return make_response( return JSONResponse(
jsonify( content=(
{ {
"success": False, "success": False,
"message": "description cannot be empty", "message": "description cannot be empty",
} }
), ),
400, status_code=400,
) )
event.data["description"] = new_description event.data["description"] = new_description
event.save() event.save()
# If semantic search is enabled, update the index # If semantic search is enabled, update the index
if current_app.frigate_config.semantic_search.enabled: if request.app.frigate_config.semantic_search.enabled:
context: EmbeddingsContext = current_app.embeddings context: EmbeddingsContext = request.app.embeddings
context.embeddings.description.upsert( context.embeddings.description.upsert(
documents=[new_description], documents=[new_description],
metadatas=[get_metadata(event)], metadatas=[get_metadata(event)],
ids=[id], ids=[event_id],
) )
return make_response( return JSONResponse(
jsonify( content=(
{ {
"success": True, "success": True,
"message": "Event " + id + " description set to " + new_description, "message": "Event "
+ event_id
+ " description set to "
+ new_description,
} }
), ),
200, status_code=200,
) )
@EventBp.route("/events/<id>", methods=("DELETE",)) @router.delete("/events/{event_id}")
def delete_event(id): def delete_event(request: Request, event_id: str):
try: try:
event = Event.get(Event.id == id) event = Event.get(Event.id == event_id)
except DoesNotExist: except DoesNotExist:
return make_response( return JSONResponse(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404 content=({"success": False, "message": "Event " + event_id + " not found"}),
status_code=404,
) )
media_name = f"{event.camera}-{event.id}" media_name = f"{event.camera}-{event.id}"
@ -962,40 +992,48 @@ def delete_event(id):
media.unlink(missing_ok=True) media.unlink(missing_ok=True)
event.delete_instance() event.delete_instance()
Timeline.delete().where(Timeline.source_id == id).execute() Timeline.delete().where(Timeline.source_id == event_id).execute()
# If semantic search is enabled, update the index # If semantic search is enabled, update the index
if current_app.frigate_config.semantic_search.enabled: if request.app.frigate_config.semantic_search.enabled:
context: EmbeddingsContext = current_app.embeddings context: EmbeddingsContext = request.app.embeddings
context.embeddings.thumbnail.delete(ids=[id]) context.embeddings.thumbnail.delete(ids=[event_id])
context.embeddings.description.delete(ids=[id]) context.embeddings.description.delete(ids=[event_id])
return make_response( return JSONResponse(
jsonify({"success": True, "message": "Event " + id + " deleted"}), 200 content=({"success": True, "message": "Event " + event_id + " deleted"}),
status_code=200,
) )
@EventBp.route("/events/<camera_name>/<label>/create", methods=["POST"]) @router.post("/events/{camera_name}/{label}/create")
def create_event(camera_name, label): def create_event(
if not camera_name or not current_app.frigate_config.cameras.get(camera_name): request: Request,
return make_response( camera_name: str,
jsonify( label: str,
body: dict = None,
):
if not camera_name or not request.app.frigate_config.cameras.get(camera_name):
return JSONResponse(
content=(
{"success": False, "message": f"{camera_name} is not a valid camera."} {"success": False, "message": f"{camera_name} is not a valid camera."}
), ),
404, status_code=404,
) )
if not label: if not label:
return make_response( return JSONResponse(
jsonify({"success": False, "message": f"{label} must be set."}), 404 content=({"success": False, "message": f"{label} must be set."}),
status_code=404,
) )
json: dict[str, any] = request.get_json(silent=True) or {} json: dict[str, any] = body or {}
try: try:
frame = current_app.detected_frames_processor.get_current_frame(camera_name) frame = request.app.detected_frames_processor.get_current_frame(camera_name)
event_id = current_app.external_processor.create_manual_event( event_id = request.app.external_processor.create_manual_event(
camera_name, camera_name,
label, label,
# TODO: Create body model
json.get("source_type", "api"), json.get("source_type", "api"),
json.get("sub_label", None), json.get("sub_label", None),
json.get("score", 0), json.get("score", 0),
@ -1006,38 +1044,39 @@ def create_event(camera_name, label):
) )
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
return make_response( return JSONResponse(
jsonify({"success": False, "message": "An unknown error occurred"}), content=({"success": False, "message": "An unknown error occurred"}),
500, status_code=500,
) )
return make_response( return JSONResponse(
jsonify( content=(
{ {
"success": True, "success": True,
"message": "Successfully created event.", "message": "Successfully created event.",
"event_id": event_id, "event_id": event_id,
} }
), ),
200, status_code=200,
) )
@EventBp.route("/events/<event_id>/end", methods=["PUT"]) @router.put("/events/{event_id}/end")
def end_event(event_id): def end_event(request: Request, event_id: str, body: dict):
json: dict[str, any] = request.get_json(silent=True) or {} json: dict[str, any] = body or {}
try: try:
end_time = json.get("end_time", datetime.now().timestamp()) end_time = json.get("end_time", datetime.now().timestamp())
current_app.external_processor.finish_manual_event(event_id, end_time) request.app.external_processor.finish_manual_event(event_id, end_time)
except Exception: except Exception:
return make_response( return JSONResponse(
jsonify( content=(
{"success": False, "message": f"{event_id} must be set and valid."} {"success": False, "message": f"{event_id} must be set and valid."}
), ),
404, status_code=404,
) )
return make_response( return JSONResponse(
jsonify({"success": True, "message": "Event successfully ended."}), 200 content=({"success": True, "message": "Event successfully ended."}),
status_code=200,
) )

View File

@ -1,9 +1,12 @@
import logging import logging
from typing import Optional
from fastapi import FastAPI from fastapi import FastAPI
from frigate.api import app as main_app from frigate.api import app as main_app
from frigate.api import export, media, notification, preview, review from frigate.api import export, media, notification, preview, review
from frigate.embeddings import EmbeddingsContext
from frigate.events.external import ExternalEventProcessor
from frigate.plus import PlusApi from frigate.plus import PlusApi
from frigate.ptz.onvif import OnvifController from frigate.ptz.onvif import OnvifController
from frigate.stats.emitter import StatsEmitter from frigate.stats.emitter import StatsEmitter
@ -14,11 +17,13 @@ logger = logging.getLogger(__name__)
def create_fastapi_app( def create_fastapi_app(
frigate_config, frigate_config,
embeddings: Optional[EmbeddingsContext],
detected_frames_processor, detected_frames_processor,
storage_maintainer: StorageMaintainer, storage_maintainer: StorageMaintainer,
onvif: OnvifController, onvif: OnvifController,
plus_api: PlusApi, plus_api: PlusApi,
stats_emitter: StatsEmitter, stats_emitter: StatsEmitter,
external_processor: ExternalEventProcessor,
): ):
logger.info("Starting FastAPI app") logger.info("Starting FastAPI app")
app = FastAPI( app = FastAPI(
@ -34,11 +39,13 @@ def create_fastapi_app(
app.include_router(export.router) app.include_router(export.router)
# App Properties # App Properties
app.frigate_config = frigate_config app.frigate_config = frigate_config
app.embeddings = embeddings
app.detected_frames_processor = detected_frames_processor app.detected_frames_processor = detected_frames_processor
app.storage_maintainer = storage_maintainer app.storage_maintainer = storage_maintainer
app.camera_error_image = None app.camera_error_image = None
app.onvif = onvif app.onvif = onvif
app.plus_api = plus_api app.plus_api = plus_api
app.stats_emitter = stats_emitter app.stats_emitter = stats_emitter
app.external_processor = external_processor
return app return app

View File

@ -529,7 +529,7 @@ def motion_activity(params: ReviewActivityMotionQueryParams = Depends()):
# change types for output # change types for output
df.index = df.index.astype(int) // (10**9) df.index = df.index.astype(int) // (10**9)
normalized = df.reset_index().to_dict("records") normalized = df.reset_index().to_dict("records")
return jsonify(normalized) return JSONResponse(content=normalized)
@router.get("/review/activity/audio") @router.get("/review/activity/audio")
@ -590,4 +590,4 @@ def audio_activity(params: ReviewActivityMotionQueryParams = Depends()):
# change types for output # change types for output
df.index = df.index.astype(int) // (10**9) df.index = df.index.astype(int) // (10**9)
normalized = df.reset_index().to_dict("records") normalized = df.reset_index().to_dict("records")
return jsonify(normalized) return JSONResponse(content=normalized)

View File

@ -402,11 +402,13 @@ class FrigateApp:
self.fastapi_app = create_fastapi_app( self.fastapi_app = create_fastapi_app(
self.config, self.config,
self.embeddings,
self.detected_frames_processor, self.detected_frames_processor,
self.storage_maintainer, self.storage_maintainer,
self.onvif_controller, self.onvif_controller,
self.plus_api, self.plus_api,
self.stats_emitter, self.stats_emitter,
self.external_event_processor,
) )
def init_onvif(self) -> None: def init_onvif(self) -> None:

View File

@ -369,6 +369,7 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
None,
PlusApi(), PlusApi(),
None, None,
) )