Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
Rui Alves 2024-12-07 14:44:06 +00:00
commit 126e3bdcc9
21 changed files with 838 additions and 537 deletions

File diff suppressed because it is too large Load Diff

View File

@ -17,8 +17,8 @@ from fastapi.responses import JSONResponse, PlainTextResponse
from markupsafe import escape from markupsafe import escape
from peewee import operator from peewee import operator
from frigate.api.defs.app_body import AppConfigSetBody from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters
from frigate.api.defs.app_query_parameters import AppTimelineHourlyQueryParameters from frigate.api.defs.request.app_body import AppConfigSetBody
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.const import CONFIG_DIR from frigate.const import CONFIG_DIR

View File

@ -18,7 +18,7 @@ from joserfc import jwt
from peewee import DoesNotExist from peewee import DoesNotExist
from slowapi import Limiter from slowapi import Limiter
from frigate.api.defs.app_body import ( from frigate.api.defs.request.app_body import (
AppPostLoginBody, AppPostLoginBody,
AppPostUsersBody, AppPostUsersBody,
AppPutPasswordBody, AppPutPasswordBody,
@ -85,7 +85,12 @@ def get_remote_addr(request: Request):
return str(ip) return str(ip)
# if there wasn't anything in the route, just return the default # 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: def get_jwt_secret() -> str:

View 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

View File

@ -14,7 +14,16 @@ from fastapi.responses import JSONResponse
from peewee import JOIN, DoesNotExist, fn, operator from peewee import JOIN, DoesNotExist, fn, operator
from playhouse.shortcuts import model_to_dict 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, EventsCreateBody,
EventsDeleteBody, EventsDeleteBody,
EventsDescriptionBody, EventsDescriptionBody,
@ -22,19 +31,15 @@ from frigate.api.defs.events_body import (
EventsSubLabelBody, EventsSubLabelBody,
SubmitPlusBody, SubmitPlusBody,
) )
from frigate.api.defs.events_query_parameters import ( from frigate.api.defs.response.event_response import (
DEFAULT_TIME_RANGE, EventCreateResponse,
EventsQueryParams, EventMultiDeleteResponse,
EventsSearchQueryParams, EventResponse,
EventsSummaryQueryParams, EventUploadPlusResponse,
)
from frigate.api.defs.regenerate_query_parameters import (
RegenerateQueryParameters,
) )
from frigate.api.defs.response.generic_response import GenericResponse
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
from frigate.const import ( from frigate.const import CLIPS_DIR
CLIPS_DIR,
)
from frigate.embeddings import EmbeddingsContext from frigate.embeddings import EmbeddingsContext
from frigate.events.external import ExternalEventProcessor from frigate.events.external import ExternalEventProcessor
from frigate.models import Event, ReviewSegment, Timeline from frigate.models import Event, ReviewSegment, Timeline
@ -46,7 +51,7 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.events]) router = APIRouter(tags=[Tags.events])
@router.get("/events") @router.get("/events", response_model=list[EventResponse])
def events(params: EventsQueryParams = Depends()): def events(params: EventsQueryParams = Depends()):
camera = params.camera camera = params.camera
cameras = params.cameras cameras = params.cameras
@ -265,7 +270,7 @@ def events(params: EventsQueryParams = Depends()):
return JSONResponse(content=list(events)) return JSONResponse(content=list(events))
@router.get("/events/explore") @router.get("/events/explore", response_model=list[EventResponse])
def events_explore(limit: int = 10): def events_explore(limit: int = 10):
# get distinct labels for all events # get distinct labels for all events
distinct_labels = Event.select(Event.label).distinct().order_by(Event.label) distinct_labels = Event.select(Event.label).distinct().order_by(Event.label)
@ -310,7 +315,8 @@ def events_explore(limit: int = 10):
"data": { "data": {
k: v k: v
for k, v in event.data.items() 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], "event_count": label_counts[event.label],
} }
@ -326,7 +332,7 @@ def events_explore(limit: int = 10):
return JSONResponse(content=processed_events) return JSONResponse(content=processed_events)
@router.get("/event_ids") @router.get("/event_ids", response_model=list[EventResponse])
def event_ids(ids: str): def event_ids(ids: str):
ids = ids.split(",") ids = ids.split(",")
@ -647,7 +653,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()):
return JSONResponse(content=[e for e in groups.dicts()]) 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): def event(event_id: str):
try: try:
return model_to_dict(Event.get(Event.id == event_id)) 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) 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): def set_retain(event_id: str):
try: try:
event = Event.get(Event.id == event_id) 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): def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = None):
if not request.app.frigate_config.plus_api.is_active(): if not request.app.frigate_config.plus_api.is_active():
message = "PLUS_API_KEY environment variable is not set" 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): def false_positive(request: Request, event_id: str):
if not request.app.frigate_config.plus_api.is_active(): if not request.app.frigate_config.plus_api.is_active():
message = "PLUS_API_KEY environment variable is not set" 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): def delete_retain(event_id: str):
try: try:
event = Event.get(Event.id == event_id) 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( def set_sub_label(
request: Request, request: Request,
event_id: str, 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( def set_description(
request: Request, request: Request,
event_id: str, 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( def regenerate_description(
request: Request, event_id: str, params: RegenerateQueryParameters = Depends() 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"} 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): def delete_event(request: Request, event_id: str):
result = delete_single_event(event_id, request) result = delete_single_event(event_id, request)
status_code = 200 if result["success"] else 404 status_code = 200 if result["success"] else 404
return JSONResponse(content=result, status_code=status_code) return JSONResponse(content=result, status_code=status_code)
@router.delete("/events/") @router.delete("/events/", response_model=EventMultiDeleteResponse)
def delete_events(request: Request, body: EventsDeleteBody): def delete_events(request: Request, body: EventsDeleteBody):
if not body.event_ids: if not body.event_ids:
return JSONResponse( return JSONResponse(
@ -1097,7 +1103,7 @@ def delete_events(request: Request, body: EventsDeleteBody):
return JSONResponse(content=response, status_code=200) 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( def create_event(
request: Request, request: Request,
camera_name: str, 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): def end_event(request: Request, event_id: str, body: EventsEndBody):
try: try:
end_time = body.end_time or datetime.datetime.now().timestamp() end_time = body.end_time or datetime.datetime.now().timestamp()

View File

@ -9,6 +9,7 @@ import psutil
from fastapi import APIRouter, Request from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from peewee import DoesNotExist 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.request.export_recordings_body import ExportRecordingsBody
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
@ -207,3 +208,14 @@ def export_delete(event_id: str):
), ),
status_code=200, 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,
)

View File

@ -20,7 +20,7 @@ from pathvalidate import sanitize_filename
from peewee import DoesNotExist, fn from peewee import DoesNotExist, fn
from tzlocal import get_localzone_name from tzlocal import get_localzone_name
from frigate.api.defs.media_query_parameters import ( from frigate.api.defs.query.media_query_parameters import (
Extension, Extension,
MediaEventsSnapshotQueryParams, MediaEventsSnapshotQueryParams,
MediaLatestFrameQueryParams, MediaLatestFrameQueryParams,

View File

@ -12,14 +12,14 @@ from fastapi.responses import JSONResponse
from peewee import Case, DoesNotExist, fn, operator from peewee import Case, DoesNotExist, fn, operator
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
from frigate.api.defs.generic_response import GenericResponse from frigate.api.defs.query.review_query_parameters import (
from frigate.api.defs.review_body import ReviewModifyMultipleBody
from frigate.api.defs.review_query_parameters import (
ReviewActivityMotionQueryParams, ReviewActivityMotionQueryParams,
ReviewQueryParams, ReviewQueryParams,
ReviewSummaryQueryParams, 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, ReviewActivityMotionResponse,
ReviewSegmentResponse, ReviewSegmentResponse,
ReviewSummaryResponse, ReviewSummaryResponse,
@ -364,7 +364,7 @@ def delete_reviews(body: ReviewModifyMultipleBody):
ReviewSegment.delete().where(ReviewSegment.id << list_of_ids).execute() ReviewSegment.delete().where(ReviewSegment.id << list_of_ids).execute()
return JSONResponse( return JSONResponse(
content=({"success": True, "message": "Delete reviews"}), status_code=200 content=({"success": True, "message": "Deleted review items."}), status_code=200
) )

View File

@ -525,7 +525,7 @@ class TestHttpReview(BaseTestHttp):
assert response.status_code == 200 assert response.status_code == 200
response_json = response.json() response_json = response.json()
assert response_json["success"] == True 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 # Verify that in DB the review segment was not deleted
review_ids_in_db_after = self._get_reviews([id]) review_ids_in_db_after = self._get_reviews([id])
assert len(review_ids_in_db_after) == 1 assert len(review_ids_in_db_after) == 1
@ -540,7 +540,7 @@ class TestHttpReview(BaseTestHttp):
assert response.status_code == 200 assert response.status_code == 200
response_json = response.json() response_json = response.json()
assert response_json["success"] == True 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 # Verify that in DB the review segment was deleted
review_ids_in_db_after = self._get_reviews([id]) review_ids_in_db_after = self._get_reviews([id])
assert len(review_ids_in_db_after) == 0 assert len(review_ids_in_db_after) == 0
@ -562,7 +562,7 @@ class TestHttpReview(BaseTestHttp):
assert response.status_code == 200 assert response.status_code == 200
response_json = response.json() response_json = response.json()
assert response_json["success"] == True 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 # Verify that in DB all review segments and recordings that were passed were deleted
review_ids_in_db_after = self._get_reviews(ids) review_ids_in_db_after = self._get_reviews(ids)

View File

@ -168,7 +168,7 @@ class TestHttp(unittest.TestCase):
assert event assert event
assert event["id"] == id 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): def test_get_bad_event(self):
app = create_fastapi_app( app = create_fastapi_app(

View File

@ -219,6 +219,8 @@ def draw_box_with_label(
text_width = size[0][0] text_width = size[0][0]
text_height = size[0][1] text_height = size[0][1]
line_height = text_height + size[1] line_height = text_height + size[1]
# get frame height
frame_height = frame.shape[0]
# set the text start position # set the text start position
if position == "ul": if position == "ul":
text_offset_x = x_min text_offset_x = x_min
@ -228,18 +230,23 @@ def draw_box_with_label(
text_offset_y = max(0, y_min - (line_height + 8)) text_offset_y = max(0, y_min - (line_height + 8))
elif position == "bl": elif position == "bl":
text_offset_x = x_min text_offset_x = x_min
text_offset_y = y_max text_offset_y = min(frame_height - line_height, y_max)
elif position == "br": elif position == "br":
text_offset_x = max(0, x_max - (text_width + 8)) text_offset_x = max(0, x_max - (text_width + 8))
text_offset_y = y_max text_offset_y = min(frame_height - line_height, y_max)
# Adjust position if it overlaps with the box or goes out of frame
# Adjust position if it overlaps with the box if position in {"ul", "ur"}:
if position in {"ul", "ur"} and text_offset_y < y_min + thickness: if text_offset_y < y_min + thickness: # Label overlaps with the box
# Move the text below the box if y_min - (line_height + 8) < 0 and y_max + line_height <= frame_height:
text_offset_y = y_max # Not enough space above, and there is space below
elif position in {"bl", "br"} and text_offset_y + line_height > y_max: text_offset_y = y_max
# Move the text above the box elif y_min - (line_height + 8) >= 0:
text_offset_y = max(0, y_min - (line_height + 8)) # 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 # make the coords of the box with a small padding of two pixels
textbox_coords = ( textbox_coords = (