mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-07-30 13:48:07 +02:00
Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
126e3bdcc9
1199
docs/static/frigate-api.yaml
vendored
1199
docs/static/frigate-api.yaml
vendored
File diff suppressed because it is too large
Load Diff
@ -17,8 +17,8 @@ from fastapi.responses import JSONResponse, PlainTextResponse
|
||||
from markupsafe import escape
|
||||
from peewee import operator
|
||||
|
||||
from frigate.api.defs.app_body import AppConfigSetBody
|
||||
from frigate.api.defs.app_query_parameters import AppTimelineHourlyQueryParameters
|
||||
from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters
|
||||
from frigate.api.defs.request.app_body import AppConfigSetBody
|
||||
from frigate.api.defs.tags import Tags
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.const import CONFIG_DIR
|
||||
|
@ -18,7 +18,7 @@ from joserfc import jwt
|
||||
from peewee import DoesNotExist
|
||||
from slowapi import Limiter
|
||||
|
||||
from frigate.api.defs.app_body import (
|
||||
from frigate.api.defs.request.app_body import (
|
||||
AppPostLoginBody,
|
||||
AppPostUsersBody,
|
||||
AppPutPasswordBody,
|
||||
@ -85,7 +85,12 @@ def get_remote_addr(request: Request):
|
||||
return str(ip)
|
||||
|
||||
# if there wasn't anything in the route, just return the default
|
||||
return request.remote_addr or "127.0.0.1"
|
||||
remote_addr = None
|
||||
|
||||
if hasattr(request, "remote_addr"):
|
||||
remote_addr = request.remote_addr
|
||||
|
||||
return remote_addr or "127.0.0.1"
|
||||
|
||||
|
||||
def get_jwt_secret() -> str:
|
||||
|
42
frigate/api/defs/response/event_response.py
Normal file
42
frigate/api/defs/response/event_response.py
Normal file
@ -0,0 +1,42 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class EventResponse(BaseModel):
|
||||
id: str
|
||||
label: str
|
||||
sub_label: Optional[str]
|
||||
camera: str
|
||||
start_time: float
|
||||
end_time: Optional[float]
|
||||
false_positive: Optional[bool]
|
||||
zones: list[str]
|
||||
thumbnail: str
|
||||
has_clip: bool
|
||||
has_snapshot: bool
|
||||
retain_indefinitely: bool
|
||||
plus_id: Optional[str]
|
||||
model_hash: Optional[str]
|
||||
detector_type: Optional[str]
|
||||
model_type: Optional[str]
|
||||
data: dict[str, Any]
|
||||
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
|
||||
|
||||
class EventCreateResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
event_id: str
|
||||
|
||||
|
||||
class EventMultiDeleteResponse(BaseModel):
|
||||
success: bool
|
||||
deleted_events: list[str]
|
||||
not_found_events: list[str]
|
||||
|
||||
|
||||
class EventUploadPlusResponse(BaseModel):
|
||||
success: bool
|
||||
plus_id: str
|
@ -14,7 +14,16 @@ from fastapi.responses import JSONResponse
|
||||
from peewee import JOIN, DoesNotExist, fn, operator
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from frigate.api.defs.events_body import (
|
||||
from frigate.api.defs.query.events_query_parameters import (
|
||||
DEFAULT_TIME_RANGE,
|
||||
EventsQueryParams,
|
||||
EventsSearchQueryParams,
|
||||
EventsSummaryQueryParams,
|
||||
)
|
||||
from frigate.api.defs.query.regenerate_query_parameters import (
|
||||
RegenerateQueryParameters,
|
||||
)
|
||||
from frigate.api.defs.request.events_body import (
|
||||
EventsCreateBody,
|
||||
EventsDeleteBody,
|
||||
EventsDescriptionBody,
|
||||
@ -22,19 +31,15 @@ from frigate.api.defs.events_body import (
|
||||
EventsSubLabelBody,
|
||||
SubmitPlusBody,
|
||||
)
|
||||
from frigate.api.defs.events_query_parameters import (
|
||||
DEFAULT_TIME_RANGE,
|
||||
EventsQueryParams,
|
||||
EventsSearchQueryParams,
|
||||
EventsSummaryQueryParams,
|
||||
)
|
||||
from frigate.api.defs.regenerate_query_parameters import (
|
||||
RegenerateQueryParameters,
|
||||
from frigate.api.defs.response.event_response import (
|
||||
EventCreateResponse,
|
||||
EventMultiDeleteResponse,
|
||||
EventResponse,
|
||||
EventUploadPlusResponse,
|
||||
)
|
||||
from frigate.api.defs.response.generic_response import GenericResponse
|
||||
from frigate.api.defs.tags import Tags
|
||||
from frigate.const import (
|
||||
CLIPS_DIR,
|
||||
)
|
||||
from frigate.const import CLIPS_DIR
|
||||
from frigate.embeddings import EmbeddingsContext
|
||||
from frigate.events.external import ExternalEventProcessor
|
||||
from frigate.models import Event, ReviewSegment, Timeline
|
||||
@ -46,7 +51,7 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=[Tags.events])
|
||||
|
||||
|
||||
@router.get("/events")
|
||||
@router.get("/events", response_model=list[EventResponse])
|
||||
def events(params: EventsQueryParams = Depends()):
|
||||
camera = params.camera
|
||||
cameras = params.cameras
|
||||
@ -265,7 +270,7 @@ def events(params: EventsQueryParams = Depends()):
|
||||
return JSONResponse(content=list(events))
|
||||
|
||||
|
||||
@router.get("/events/explore")
|
||||
@router.get("/events/explore", response_model=list[EventResponse])
|
||||
def events_explore(limit: int = 10):
|
||||
# get distinct labels for all events
|
||||
distinct_labels = Event.select(Event.label).distinct().order_by(Event.label)
|
||||
@ -310,7 +315,8 @@ def events_explore(limit: int = 10):
|
||||
"data": {
|
||||
k: v
|
||||
for k, v in event.data.items()
|
||||
if k in ["type", "score", "top_score", "description"]
|
||||
if k
|
||||
in ["type", "score", "top_score", "description", "sub_label_score"]
|
||||
},
|
||||
"event_count": label_counts[event.label],
|
||||
}
|
||||
@ -326,7 +332,7 @@ def events_explore(limit: int = 10):
|
||||
return JSONResponse(content=processed_events)
|
||||
|
||||
|
||||
@router.get("/event_ids")
|
||||
@router.get("/event_ids", response_model=list[EventResponse])
|
||||
def event_ids(ids: str):
|
||||
ids = ids.split(",")
|
||||
|
||||
@ -647,7 +653,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()):
|
||||
return JSONResponse(content=[e for e in groups.dicts()])
|
||||
|
||||
|
||||
@router.get("/events/{event_id}")
|
||||
@router.get("/events/{event_id}", response_model=EventResponse)
|
||||
def event(event_id: str):
|
||||
try:
|
||||
return model_to_dict(Event.get(Event.id == event_id))
|
||||
@ -655,7 +661,7 @@ def event(event_id: str):
|
||||
return JSONResponse(content="Event not found", status_code=404)
|
||||
|
||||
|
||||
@router.post("/events/{event_id}/retain")
|
||||
@router.post("/events/{event_id}/retain", response_model=GenericResponse)
|
||||
def set_retain(event_id: str):
|
||||
try:
|
||||
event = Event.get(Event.id == event_id)
|
||||
@ -674,7 +680,7 @@ def set_retain(event_id: str):
|
||||
)
|
||||
|
||||
|
||||
@router.post("/events/{event_id}/plus")
|
||||
@router.post("/events/{event_id}/plus", response_model=EventUploadPlusResponse)
|
||||
def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = None):
|
||||
if not request.app.frigate_config.plus_api.is_active():
|
||||
message = "PLUS_API_KEY environment variable is not set"
|
||||
@ -786,7 +792,7 @@ def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = None):
|
||||
)
|
||||
|
||||
|
||||
@router.put("/events/{event_id}/false_positive")
|
||||
@router.put("/events/{event_id}/false_positive", response_model=EventUploadPlusResponse)
|
||||
def false_positive(request: Request, event_id: str):
|
||||
if not request.app.frigate_config.plus_api.is_active():
|
||||
message = "PLUS_API_KEY environment variable is not set"
|
||||
@ -875,7 +881,7 @@ def false_positive(request: Request, event_id: str):
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/events/{event_id}/retain")
|
||||
@router.delete("/events/{event_id}/retain", response_model=GenericResponse)
|
||||
def delete_retain(event_id: str):
|
||||
try:
|
||||
event = Event.get(Event.id == event_id)
|
||||
@ -894,7 +900,7 @@ def delete_retain(event_id: str):
|
||||
)
|
||||
|
||||
|
||||
@router.post("/events/{event_id}/sub_label")
|
||||
@router.post("/events/{event_id}/sub_label", response_model=GenericResponse)
|
||||
def set_sub_label(
|
||||
request: Request,
|
||||
event_id: str,
|
||||
@ -946,7 +952,7 @@ def set_sub_label(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/events/{event_id}/description")
|
||||
@router.post("/events/{event_id}/description", response_model=GenericResponse)
|
||||
def set_description(
|
||||
request: Request,
|
||||
event_id: str,
|
||||
@ -993,7 +999,7 @@ def set_description(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/events/{event_id}/description/regenerate")
|
||||
@router.put("/events/{event_id}/description/regenerate", response_model=GenericResponse)
|
||||
def regenerate_description(
|
||||
request: Request, event_id: str, params: RegenerateQueryParameters = Depends()
|
||||
):
|
||||
@ -1064,14 +1070,14 @@ def delete_single_event(event_id: str, request: Request) -> dict:
|
||||
return {"success": True, "message": f"Event {event_id} deleted"}
|
||||
|
||||
|
||||
@router.delete("/events/{event_id}")
|
||||
@router.delete("/events/{event_id}", response_model=GenericResponse)
|
||||
def delete_event(request: Request, event_id: str):
|
||||
result = delete_single_event(event_id, request)
|
||||
status_code = 200 if result["success"] else 404
|
||||
return JSONResponse(content=result, status_code=status_code)
|
||||
|
||||
|
||||
@router.delete("/events/")
|
||||
@router.delete("/events/", response_model=EventMultiDeleteResponse)
|
||||
def delete_events(request: Request, body: EventsDeleteBody):
|
||||
if not body.event_ids:
|
||||
return JSONResponse(
|
||||
@ -1097,7 +1103,7 @@ def delete_events(request: Request, body: EventsDeleteBody):
|
||||
return JSONResponse(content=response, status_code=200)
|
||||
|
||||
|
||||
@router.post("/events/{camera_name}/{label}/create")
|
||||
@router.post("/events/{camera_name}/{label}/create", response_model=EventCreateResponse)
|
||||
def create_event(
|
||||
request: Request,
|
||||
camera_name: str,
|
||||
@ -1153,7 +1159,7 @@ def create_event(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/events/{event_id}/end")
|
||||
@router.put("/events/{event_id}/end", response_model=GenericResponse)
|
||||
def end_event(request: Request, event_id: str, body: EventsEndBody):
|
||||
try:
|
||||
end_time = body.end_time or datetime.datetime.now().timestamp()
|
||||
|
@ -9,6 +9,7 @@ import psutil
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from peewee import DoesNotExist
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from frigate.api.defs.request.export_recordings_body import ExportRecordingsBody
|
||||
from frigate.api.defs.tags import Tags
|
||||
@ -207,3 +208,14 @@ def export_delete(event_id: str):
|
||||
),
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/exports/{export_id}")
|
||||
def get_export(export_id: str):
|
||||
try:
|
||||
return JSONResponse(content=model_to_dict(Export.get(Export.id == export_id)))
|
||||
except DoesNotExist:
|
||||
return JSONResponse(
|
||||
content={"success": False, "message": "Export not found"},
|
||||
status_code=404,
|
||||
)
|
||||
|
@ -20,7 +20,7 @@ from pathvalidate import sanitize_filename
|
||||
from peewee import DoesNotExist, fn
|
||||
from tzlocal import get_localzone_name
|
||||
|
||||
from frigate.api.defs.media_query_parameters import (
|
||||
from frigate.api.defs.query.media_query_parameters import (
|
||||
Extension,
|
||||
MediaEventsSnapshotQueryParams,
|
||||
MediaLatestFrameQueryParams,
|
||||
|
@ -12,14 +12,14 @@ from fastapi.responses import JSONResponse
|
||||
from peewee import Case, DoesNotExist, fn, operator
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from frigate.api.defs.generic_response import GenericResponse
|
||||
from frigate.api.defs.review_body import ReviewModifyMultipleBody
|
||||
from frigate.api.defs.review_query_parameters import (
|
||||
from frigate.api.defs.query.review_query_parameters import (
|
||||
ReviewActivityMotionQueryParams,
|
||||
ReviewQueryParams,
|
||||
ReviewSummaryQueryParams,
|
||||
)
|
||||
from frigate.api.defs.review_responses import (
|
||||
from frigate.api.defs.request.review_body import ReviewModifyMultipleBody
|
||||
from frigate.api.defs.response.generic_response import GenericResponse
|
||||
from frigate.api.defs.response.review_response import (
|
||||
ReviewActivityMotionResponse,
|
||||
ReviewSegmentResponse,
|
||||
ReviewSummaryResponse,
|
||||
@ -364,7 +364,7 @@ def delete_reviews(body: ReviewModifyMultipleBody):
|
||||
ReviewSegment.delete().where(ReviewSegment.id << list_of_ids).execute()
|
||||
|
||||
return JSONResponse(
|
||||
content=({"success": True, "message": "Delete reviews"}), status_code=200
|
||||
content=({"success": True, "message": "Deleted review items."}), status_code=200
|
||||
)
|
||||
|
||||
|
||||
|
@ -525,7 +525,7 @@ class TestHttpReview(BaseTestHttp):
|
||||
assert response.status_code == 200
|
||||
response_json = response.json()
|
||||
assert response_json["success"] == True
|
||||
assert response_json["message"] == "Delete reviews"
|
||||
assert response_json["message"] == "Deleted review items."
|
||||
# Verify that in DB the review segment was not deleted
|
||||
review_ids_in_db_after = self._get_reviews([id])
|
||||
assert len(review_ids_in_db_after) == 1
|
||||
@ -540,7 +540,7 @@ class TestHttpReview(BaseTestHttp):
|
||||
assert response.status_code == 200
|
||||
response_json = response.json()
|
||||
assert response_json["success"] == True
|
||||
assert response_json["message"] == "Delete reviews"
|
||||
assert response_json["message"] == "Deleted review items."
|
||||
# Verify that in DB the review segment was deleted
|
||||
review_ids_in_db_after = self._get_reviews([id])
|
||||
assert len(review_ids_in_db_after) == 0
|
||||
@ -562,7 +562,7 @@ class TestHttpReview(BaseTestHttp):
|
||||
assert response.status_code == 200
|
||||
response_json = response.json()
|
||||
assert response_json["success"] == True
|
||||
assert response_json["message"] == "Delete reviews"
|
||||
assert response_json["message"] == "Deleted review items."
|
||||
|
||||
# Verify that in DB all review segments and recordings that were passed were deleted
|
||||
review_ids_in_db_after = self._get_reviews(ids)
|
||||
|
@ -168,7 +168,7 @@ class TestHttp(unittest.TestCase):
|
||||
|
||||
assert event
|
||||
assert event["id"] == id
|
||||
assert event == model_to_dict(Event.get(Event.id == id))
|
||||
assert event["id"] == model_to_dict(Event.get(Event.id == id))["id"]
|
||||
|
||||
def test_get_bad_event(self):
|
||||
app = create_fastapi_app(
|
||||
|
@ -219,6 +219,8 @@ def draw_box_with_label(
|
||||
text_width = size[0][0]
|
||||
text_height = size[0][1]
|
||||
line_height = text_height + size[1]
|
||||
# get frame height
|
||||
frame_height = frame.shape[0]
|
||||
# set the text start position
|
||||
if position == "ul":
|
||||
text_offset_x = x_min
|
||||
@ -228,17 +230,22 @@ def draw_box_with_label(
|
||||
text_offset_y = max(0, y_min - (line_height + 8))
|
||||
elif position == "bl":
|
||||
text_offset_x = x_min
|
||||
text_offset_y = y_max
|
||||
text_offset_y = min(frame_height - line_height, y_max)
|
||||
elif position == "br":
|
||||
text_offset_x = max(0, x_max - (text_width + 8))
|
||||
text_offset_y = min(frame_height - line_height, y_max)
|
||||
# Adjust position if it overlaps with the box or goes out of frame
|
||||
if position in {"ul", "ur"}:
|
||||
if text_offset_y < y_min + thickness: # Label overlaps with the box
|
||||
if y_min - (line_height + 8) < 0 and y_max + line_height <= frame_height:
|
||||
# Not enough space above, and there is space below
|
||||
text_offset_y = y_max
|
||||
|
||||
# Adjust position if it overlaps with the box
|
||||
if position in {"ul", "ur"} and text_offset_y < y_min + thickness:
|
||||
# Move the text below the box
|
||||
text_offset_y = y_max
|
||||
elif position in {"bl", "br"} and text_offset_y + line_height > y_max:
|
||||
# Move the text above the box
|
||||
elif y_min - (line_height + 8) >= 0:
|
||||
# Enough space above, keep the label at the top
|
||||
text_offset_y = max(0, y_min - (line_height + 8))
|
||||
elif position in {"bl", "br"}:
|
||||
if text_offset_y + line_height > frame_height:
|
||||
# If there's not enough space below, try above the box
|
||||
text_offset_y = max(0, y_min - (line_height + 8))
|
||||
|
||||
# make the coords of the box with a small padding of two pixels
|
||||
|
Loading…
Reference in New Issue
Block a user