mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-14 15:15:22 +03:00
Convert events endpoints to FastAPI
This commit is contained in:
parent
cc259736da
commit
1b601b8c35
@ -21,7 +21,6 @@ from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
from frigate.api.auth import AuthBp, get_jwt_secret, limiter
|
||||
from frigate.api.defs.tags import Tags
|
||||
from frigate.api.event import EventBp
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.const import CONFIG_DIR
|
||||
from frigate.embeddings import EmbeddingsContext
|
||||
@ -43,7 +42,6 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
bp = Blueprint("frigate", __name__)
|
||||
bp.register_blueprint(EventBp)
|
||||
bp.register_blueprint(AuthBp)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
52
frigate/api/defs/events_query_parameters.py
Normal file
52
frigate/api/defs/events_query_parameters.py
Normal 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
|
||||
@ -9,3 +9,4 @@ class Tags(Enum):
|
||||
notifications = "Notifications"
|
||||
review = "Review"
|
||||
export = "Export"
|
||||
events = "Events"
|
||||
|
||||
@ -11,17 +11,20 @@ from urllib.parse import unquote
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from flask import (
|
||||
Blueprint,
|
||||
current_app,
|
||||
jsonify,
|
||||
make_response,
|
||||
request,
|
||||
)
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.params import Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from peewee import JOIN, DoesNotExist, fn, operator
|
||||
from PIL import Image
|
||||
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 (
|
||||
CLIPS_DIR,
|
||||
)
|
||||
@ -33,57 +36,55 @@ from frigate.util.builtin import get_tz_modifiers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EventBp = Blueprint("events", __name__)
|
||||
|
||||
DEFAULT_TIME_RANGE = "00:00,24:00"
|
||||
router = APIRouter(tags=[Tags.events])
|
||||
|
||||
|
||||
@EventBp.route("/events")
|
||||
def events():
|
||||
camera = request.args.get("camera", "all")
|
||||
cameras = request.args.get("cameras", "all")
|
||||
@router.get("/events")
|
||||
def events(params: EventsQueryParams = Depends()):
|
||||
camera = params.camera
|
||||
cameras = params.cameras
|
||||
|
||||
# handle old camera arg
|
||||
if cameras == "all" and camera != "all":
|
||||
cameras = camera
|
||||
|
||||
label = unquote(request.args.get("label", "all"))
|
||||
labels = request.args.get("labels", "all")
|
||||
label = unquote(params.label)
|
||||
labels = params.labels
|
||||
|
||||
# handle old label arg
|
||||
if labels == "all" and label != "all":
|
||||
labels = label
|
||||
|
||||
sub_label = request.args.get("sub_label", "all")
|
||||
sub_labels = request.args.get("sub_labels", "all")
|
||||
sub_label = params.sub_label
|
||||
sub_labels = params.sub_labels
|
||||
|
||||
# handle old sub_label arg
|
||||
if sub_labels == "all" and sub_label != "all":
|
||||
sub_labels = sub_label
|
||||
|
||||
zone = request.args.get("zone", "all")
|
||||
zones = request.args.get("zones", "all")
|
||||
zone = params.zone
|
||||
zones = params.zones
|
||||
|
||||
# handle old label arg
|
||||
if zones == "all" and zone != "all":
|
||||
zones = zone
|
||||
|
||||
limit = request.args.get("limit", 100)
|
||||
after = request.args.get("after", type=float)
|
||||
before = request.args.get("before", type=float)
|
||||
time_range = request.args.get("time_range", DEFAULT_TIME_RANGE)
|
||||
has_clip = request.args.get("has_clip", type=int)
|
||||
has_snapshot = request.args.get("has_snapshot", type=int)
|
||||
in_progress = request.args.get("in_progress", type=int)
|
||||
include_thumbnails = request.args.get("include_thumbnails", default=1, type=int)
|
||||
favorites = request.args.get("favorites", type=int)
|
||||
min_score = request.args.get("min_score", type=float)
|
||||
max_score = request.args.get("max_score", type=float)
|
||||
is_submitted = request.args.get("is_submitted", type=int)
|
||||
min_length = request.args.get("min_length", type=float)
|
||||
max_length = request.args.get("max_length", type=float)
|
||||
limit = params.limit
|
||||
after = params.after
|
||||
before = params.before
|
||||
time_range = params.time_range
|
||||
has_clip = params.has_clip
|
||||
has_snapshot = params.has_snapshot
|
||||
in_progress = params.in_progress
|
||||
include_thumbnails = params.include_thumbnails
|
||||
favorites = params.favorites
|
||||
min_score = params.min_score
|
||||
max_score = params.max_score
|
||||
is_submitted = params.is_submitted
|
||||
min_length = params.min_length
|
||||
max_length = params.max_length
|
||||
|
||||
sort = request.args.get("sort", type=str)
|
||||
sort = params.sort
|
||||
|
||||
clauses = []
|
||||
|
||||
@ -163,7 +164,7 @@ def events():
|
||||
|
||||
if time_range != DEFAULT_TIME_RANGE:
|
||||
# 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)
|
||||
|
||||
times = time_range.split(",")
|
||||
@ -248,13 +249,11 @@ def events():
|
||||
.iterator()
|
||||
)
|
||||
|
||||
return jsonify(list(events))
|
||||
return JSONResponse(content=list(events))
|
||||
|
||||
|
||||
@EventBp.route("/events/explore")
|
||||
def events_explore():
|
||||
limit = request.args.get("limit", 10, type=int)
|
||||
|
||||
@router.get("/events/explore")
|
||||
def events_explore(limit: int = 10):
|
||||
subquery = Event.select(
|
||||
Event.id,
|
||||
Event.camera,
|
||||
@ -316,66 +315,65 @@ def events_explore():
|
||||
for event in events
|
||||
]
|
||||
|
||||
return jsonify(processed_events)
|
||||
return JSONResponse(content=processed_events)
|
||||
|
||||
|
||||
@EventBp.route("/event_ids")
|
||||
def event_ids():
|
||||
idString = request.args.get("ids")
|
||||
ids = idString.split(",")
|
||||
@router.get("/event_ids")
|
||||
def event_ids(ids: str):
|
||||
ids = ids.split(",")
|
||||
|
||||
if not ids:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Valid list of ids must be sent"}),
|
||||
400,
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Valid list of ids must be sent"}),
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
try:
|
||||
events = Event.select().where(Event.id << ids).dicts().iterator()
|
||||
return jsonify(list(events))
|
||||
return JSONResponse(list(events))
|
||||
except Exception:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Events not found"}), 400
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Events not found"}), status_code=400
|
||||
)
|
||||
|
||||
|
||||
@EventBp.route("/events/search")
|
||||
def events_search():
|
||||
query = request.args.get("query", type=str)
|
||||
search_type = request.args.get("search_type", "thumbnail,description", type=str)
|
||||
include_thumbnails = request.args.get("include_thumbnails", default=1, type=int)
|
||||
limit = request.args.get("limit", 50, type=int)
|
||||
@router.get("/events/search")
|
||||
def events_search(request: Request, params: EventsSearchQueryParams = Depends()):
|
||||
query = params.query
|
||||
search_type = params.search_type
|
||||
include_thumbnails = params.include_thumbnails
|
||||
limit = params.limit
|
||||
|
||||
# Filters
|
||||
cameras = request.args.get("cameras", "all", type=str)
|
||||
labels = request.args.get("labels", "all", type=str)
|
||||
zones = request.args.get("zones", "all", type=str)
|
||||
after = request.args.get("after", type=float)
|
||||
before = request.args.get("before", type=float)
|
||||
cameras = params.cameras
|
||||
labels = params.labels
|
||||
zones = params.zones
|
||||
after = params.after
|
||||
before = params.before
|
||||
|
||||
if not query:
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": "A search query must be supplied",
|
||||
}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
if not current_app.frigate_config.semantic_search.enabled:
|
||||
return make_response(
|
||||
jsonify(
|
||||
if not request.app.frigate_config.semantic_search.enabled:
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Semantic search is not enabled",
|
||||
}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
context: EmbeddingsContext = current_app.embeddings
|
||||
context: EmbeddingsContext = request.app.embeddings
|
||||
|
||||
selected_columns = [
|
||||
Event.id,
|
||||
@ -434,14 +432,14 @@ def events_search():
|
||||
try:
|
||||
search_event: Event = Event.get(Event.id == query)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Event not found",
|
||||
}
|
||||
),
|
||||
404,
|
||||
status_code=404,
|
||||
)
|
||||
thumbnail = base64.b64decode(search_event.thumbnail)
|
||||
img = np.array(Image.open(io.BytesIO(thumbnail)).convert("RGB"))
|
||||
@ -501,7 +499,7 @@ def events_search():
|
||||
}
|
||||
|
||||
if not results:
|
||||
return jsonify([])
|
||||
return JSONResponse(content=[])
|
||||
|
||||
# Get the event data
|
||||
events = (
|
||||
@ -534,15 +532,15 @@ def events_search():
|
||||
]
|
||||
events = sorted(events, key=lambda x: x["search_distance"])[:limit]
|
||||
|
||||
return jsonify(events)
|
||||
return JSONResponse(content=events)
|
||||
|
||||
|
||||
@EventBp.route("/events/summary")
|
||||
def events_summary():
|
||||
tz_name = request.args.get("timezone", default="utc", type=str)
|
||||
@router.get("/events/summary")
|
||||
def events_summary(params: EventsSummaryQueryParams = Depends()):
|
||||
tz_name = params.timezone
|
||||
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name)
|
||||
has_clip = request.args.get("has_clip", type=int)
|
||||
has_snapshot = request.args.get("has_snapshot", type=int)
|
||||
has_clip = params.has_clip
|
||||
has_snapshot = params.has_snapshot
|
||||
|
||||
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",))
|
||||
def event(id):
|
||||
@router.get("/events/{event_id}")
|
||||
def event(event_id: str):
|
||||
try:
|
||||
return model_to_dict(Event.get(Event.id == id))
|
||||
return model_to_dict(Event.get(Event.id == event_id))
|
||||
except DoesNotExist:
|
||||
return "Event not found", 404
|
||||
|
||||
|
||||
@EventBp.route("/events/<id>/retain", methods=("POST",))
|
||||
def set_retain(id):
|
||||
@router.post("/events/{event_id}/retain")
|
||||
def set_retain(event_id: str):
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Event " + event_id + " not found"}),
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
event.retain_indefinitely = True
|
||||
event.save()
|
||||
|
||||
return make_response(
|
||||
jsonify({"success": True, "message": "Event " + id + " retained"}), 200
|
||||
return JSONResponse(
|
||||
content=({"success": True, "message": "Event " + event_id + " retained"}),
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
@EventBp.route("/events/<id>/plus", methods=("POST",))
|
||||
def send_to_plus(id):
|
||||
if not current_app.plus_api.is_active():
|
||||
@router.post("/events/{event_id}/plus")
|
||||
def send_to_plus(request: Request, event_id: str):
|
||||
if not request.app.plus_api.is_active():
|
||||
message = "PLUS_API_KEY environment variable is not set"
|
||||
logger.error(message)
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": message,
|
||||
}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
include_annotation = (
|
||||
@ -627,11 +627,13 @@ def send_to_plus(id):
|
||||
)
|
||||
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
message = f"Event {id} not found"
|
||||
message = f"Event {event_id} not found"
|
||||
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
|
||||
if event.data.get("box") is None:
|
||||
@ -639,20 +641,22 @@ def send_to_plus(id):
|
||||
|
||||
if event.end_time is None:
|
||||
logger.error(f"Unable to load clean png for in-progress event: {event.id}")
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Unable to load clean png for in-progress event",
|
||||
}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
if event.plus_id:
|
||||
message = "Already submitted to plus"
|
||||
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
|
||||
try:
|
||||
@ -660,29 +664,29 @@ def send_to_plus(id):
|
||||
image = cv2.imread(os.path.join(CLIPS_DIR, filename))
|
||||
except Exception:
|
||||
logger.error(f"Unable to load clean png for event: {event.id}")
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{"success": False, "message": "Unable to load clean png for event"}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
if image is None or image.size == 0:
|
||||
logger.error(f"Unable to load clean png for event: {event.id}")
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{"success": False, "message": "Unable to load clean png for event"}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
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:
|
||||
logger.exception(ex)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Error uploading image"}),
|
||||
400,
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Error uploading image"}),
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
# store image id in the database
|
||||
@ -693,7 +697,7 @@ def send_to_plus(id):
|
||||
box = event.data["box"]
|
||||
|
||||
try:
|
||||
current_app.plus_api.add_annotation(
|
||||
request.app.plus_api.add_annotation(
|
||||
event.plus_id,
|
||||
box,
|
||||
event.label,
|
||||
@ -701,59 +705,67 @@ def send_to_plus(id):
|
||||
except ValueError:
|
||||
message = "Error uploading annotation, unsupported label provided."
|
||||
logger.error(message)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": message}),
|
||||
400,
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": message}),
|
||||
status_code=400,
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Error uploading annotation"}),
|
||||
400,
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Error uploading annotation"}),
|
||||
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",))
|
||||
def false_positive(id):
|
||||
if not current_app.plus_api.is_active():
|
||||
@router.put("/events/{event_id}/false_positive")
|
||||
def false_positive(request: Request, event_id: str):
|
||||
if not request.app.plus_api.is_active():
|
||||
message = "PLUS_API_KEY environment variable is not set"
|
||||
logger.error(message)
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": message,
|
||||
}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
message = f"Event {id} not found"
|
||||
message = f"Event {event_id} not found"
|
||||
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
|
||||
if event.data.get("box") is None:
|
||||
message = "Events prior to 0.13 cannot be submitted as false positives"
|
||||
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:
|
||||
message = "False positive already submitted to Frigate+"
|
||||
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:
|
||||
plus_response = send_to_plus(id)
|
||||
plus_response = send_to_plus(event_id)
|
||||
if plus_response.status_code != 200:
|
||||
return plus_response
|
||||
# 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"]
|
||||
box = event.data["box"]
|
||||
@ -766,7 +778,7 @@ def false_positive(id):
|
||||
)
|
||||
|
||||
try:
|
||||
current_app.plus_api.add_false_positive(
|
||||
request.app.plus_api.add_false_positive(
|
||||
event.plus_id,
|
||||
region,
|
||||
box,
|
||||
@ -779,92 +791,101 @@ def false_positive(id):
|
||||
except ValueError:
|
||||
message = "Error uploading false positive, unsupported label provided."
|
||||
logger.error(message)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": message}),
|
||||
400,
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": message}),
|
||||
status_code=400,
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Error uploading false positive"}),
|
||||
400,
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Error uploading false positive"}),
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
event.false_positive = True
|
||||
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",))
|
||||
def delete_retain(id):
|
||||
@router.delete("/events/{event_id}/retain")
|
||||
def delete_retain(event_id: str):
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Event " + event_id + " not found"}),
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
event.retain_indefinitely = False
|
||||
event.save()
|
||||
|
||||
return make_response(
|
||||
jsonify({"success": True, "message": "Event " + id + " un-retained"}), 200
|
||||
return JSONResponse(
|
||||
content=({"success": True, "message": "Event " + event_id + " un-retained"}),
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
@EventBp.route("/events/<id>/sub_label", methods=("POST",))
|
||||
def set_sub_label(id):
|
||||
@router.post("/events/{event_id}/sub_label")
|
||||
def set_sub_label(
|
||||
request: Request,
|
||||
event_id: str,
|
||||
body: dict = None,
|
||||
):
|
||||
try:
|
||||
event: Event = Event.get(Event.id == id)
|
||||
event: Event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
return JSONResponse(
|
||||
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_score = json.get("subLabelScore")
|
||||
|
||||
if new_sub_label is None:
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": "A sub label must be supplied",
|
||||
}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
if new_sub_label and len(new_sub_label) > 100:
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": new_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):
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": new_score
|
||||
+ " does not fit within the expected bounds 0 <= score <= 1.0",
|
||||
}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
if not event.end_time:
|
||||
# update tracked object
|
||||
tracked_obj: TrackedObject = (
|
||||
current_app.detected_frames_processor.camera_states[
|
||||
request.app.detected_frames_processor.camera_states[
|
||||
event.camera
|
||||
].tracked_objects.get(event.id)
|
||||
)
|
||||
@ -875,7 +896,7 @@ def set_sub_label(id):
|
||||
# update timeline items
|
||||
Timeline.update(
|
||||
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
|
||||
|
||||
@ -885,70 +906,79 @@ def set_sub_label(id):
|
||||
event.data = data
|
||||
|
||||
event.save()
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"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",))
|
||||
def set_description(id):
|
||||
@router.post("/events/{event_id}/description")
|
||||
def set_description(
|
||||
request: Request,
|
||||
event_id: str,
|
||||
body: dict = None,
|
||||
):
|
||||
try:
|
||||
event: Event = Event.get(Event.id == id)
|
||||
event: Event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
return JSONResponse(
|
||||
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")
|
||||
|
||||
if new_description is None or len(new_description) == 0:
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": "description cannot be empty",
|
||||
}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
event.data["description"] = new_description
|
||||
event.save()
|
||||
|
||||
# If semantic search is enabled, update the index
|
||||
if current_app.frigate_config.semantic_search.enabled:
|
||||
context: EmbeddingsContext = current_app.embeddings
|
||||
if request.app.frigate_config.semantic_search.enabled:
|
||||
context: EmbeddingsContext = request.app.embeddings
|
||||
context.embeddings.description.upsert(
|
||||
documents=[new_description],
|
||||
metadatas=[get_metadata(event)],
|
||||
ids=[id],
|
||||
ids=[event_id],
|
||||
)
|
||||
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"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",))
|
||||
def delete_event(id):
|
||||
@router.delete("/events/{event_id}")
|
||||
def delete_event(request: Request, event_id: str):
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Event " + event_id + " not found"}),
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
media_name = f"{event.camera}-{event.id}"
|
||||
@ -962,40 +992,48 @@ def delete_event(id):
|
||||
media.unlink(missing_ok=True)
|
||||
|
||||
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 current_app.frigate_config.semantic_search.enabled:
|
||||
context: EmbeddingsContext = current_app.embeddings
|
||||
context.embeddings.thumbnail.delete(ids=[id])
|
||||
context.embeddings.description.delete(ids=[id])
|
||||
return make_response(
|
||||
jsonify({"success": True, "message": "Event " + id + " deleted"}), 200
|
||||
if request.app.frigate_config.semantic_search.enabled:
|
||||
context: EmbeddingsContext = request.app.embeddings
|
||||
context.embeddings.thumbnail.delete(ids=[event_id])
|
||||
context.embeddings.description.delete(ids=[event_id])
|
||||
return JSONResponse(
|
||||
content=({"success": True, "message": "Event " + event_id + " deleted"}),
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
@EventBp.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 make_response(
|
||||
jsonify(
|
||||
@router.post("/events/{camera_name}/{label}/create")
|
||||
def create_event(
|
||||
request: Request,
|
||||
camera_name: str,
|
||||
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."}
|
||||
),
|
||||
404,
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
if not label:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": f"{label} must be set."}), 404
|
||||
return JSONResponse(
|
||||
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:
|
||||
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,
|
||||
label,
|
||||
# TODO: Create body model
|
||||
json.get("source_type", "api"),
|
||||
json.get("sub_label", None),
|
||||
json.get("score", 0),
|
||||
@ -1006,38 +1044,39 @@ def create_event(camera_name, label):
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "An unknown error occurred"}),
|
||||
500,
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "An unknown error occurred"}),
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Successfully created event.",
|
||||
"event_id": event_id,
|
||||
}
|
||||
),
|
||||
200,
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
@EventBp.route("/events/<event_id>/end", methods=["PUT"])
|
||||
def end_event(event_id):
|
||||
json: dict[str, any] = request.get_json(silent=True) or {}
|
||||
@router.put("/events/{event_id}/end")
|
||||
def end_event(request: Request, event_id: str, body: dict):
|
||||
json: dict[str, any] = body or {}
|
||||
|
||||
try:
|
||||
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:
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{"success": False, "message": f"{event_id} must be set and valid."}
|
||||
),
|
||||
404,
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
return make_response(
|
||||
jsonify({"success": True, "message": "Event successfully ended."}), 200
|
||||
return JSONResponse(
|
||||
content=({"success": True, "message": "Event successfully ended."}),
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from frigate.api import app as main_app
|
||||
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.ptz.onvif import OnvifController
|
||||
from frigate.stats.emitter import StatsEmitter
|
||||
@ -14,11 +17,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def create_fastapi_app(
|
||||
frigate_config,
|
||||
embeddings: Optional[EmbeddingsContext],
|
||||
detected_frames_processor,
|
||||
storage_maintainer: StorageMaintainer,
|
||||
onvif: OnvifController,
|
||||
plus_api: PlusApi,
|
||||
stats_emitter: StatsEmitter,
|
||||
external_processor: ExternalEventProcessor,
|
||||
):
|
||||
logger.info("Starting FastAPI app")
|
||||
app = FastAPI(
|
||||
@ -34,11 +39,13 @@ def create_fastapi_app(
|
||||
app.include_router(export.router)
|
||||
# App Properties
|
||||
app.frigate_config = frigate_config
|
||||
app.embeddings = embeddings
|
||||
app.detected_frames_processor = detected_frames_processor
|
||||
app.storage_maintainer = storage_maintainer
|
||||
app.camera_error_image = None
|
||||
app.onvif = onvif
|
||||
app.plus_api = plus_api
|
||||
app.stats_emitter = stats_emitter
|
||||
app.external_processor = external_processor
|
||||
|
||||
return app
|
||||
|
||||
@ -529,7 +529,7 @@ def motion_activity(params: ReviewActivityMotionQueryParams = Depends()):
|
||||
# change types for output
|
||||
df.index = df.index.astype(int) // (10**9)
|
||||
normalized = df.reset_index().to_dict("records")
|
||||
return jsonify(normalized)
|
||||
return JSONResponse(content=normalized)
|
||||
|
||||
|
||||
@router.get("/review/activity/audio")
|
||||
@ -590,4 +590,4 @@ def audio_activity(params: ReviewActivityMotionQueryParams = Depends()):
|
||||
# change types for output
|
||||
df.index = df.index.astype(int) // (10**9)
|
||||
normalized = df.reset_index().to_dict("records")
|
||||
return jsonify(normalized)
|
||||
return JSONResponse(content=normalized)
|
||||
|
||||
@ -402,11 +402,13 @@ class FrigateApp:
|
||||
|
||||
self.fastapi_app = create_fastapi_app(
|
||||
self.config,
|
||||
self.embeddings,
|
||||
self.detected_frames_processor,
|
||||
self.storage_maintainer,
|
||||
self.onvif_controller,
|
||||
self.plus_api,
|
||||
self.stats_emitter,
|
||||
self.external_event_processor,
|
||||
)
|
||||
|
||||
def init_onvif(self) -> None:
|
||||
|
||||
@ -369,6 +369,7 @@ class TestHttp(unittest.TestCase):
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
PlusApi(),
|
||||
None,
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user