mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-14 23:25:25 +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.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()
|
||||||
|
|||||||
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"
|
notifications = "Notifications"
|
||||||
review = "Review"
|
review = "Review"
|
||||||
export = "Export"
|
export = "Export"
|
||||||
|
events = "Events"
|
||||||
|
|||||||
@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -369,6 +369,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
PlusApi(),
|
PlusApi(),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user