mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-16 18:16:44 +03:00
Miscellaneous Fixes (#21289)
* Exclude yolov9 license plate from migraphx runner * clarify auth endpoint return in openapi schema * Clarify ROCm enrichments * fix object mask creation * Consider audio activity when deciding if recording segments should be kept due to motion * ensure python defs match openapi spec for auth endpoints * Fix check for audio activity to keep a segemnt * fix calendar popover modal bug on export dialog --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
parent
e1545a8db8
commit
fa16539429
@ -13,7 +13,7 @@ Object detection and enrichments (like Semantic Search, Face Recognition, and Li
|
|||||||
|
|
||||||
- **AMD**
|
- **AMD**
|
||||||
|
|
||||||
- ROCm will automatically be detected and used for enrichments in the `-rocm` Frigate image.
|
- ROCm support in the `-rocm` Frigate image is automatically detected for enrichments, but only some enrichment models are available due to ROCm's focus on LLMs and limited stability with certain neural network models. Frigate disables models that perform poorly or are unstable to ensure reliable operation, so only compatible enrichments may be active.
|
||||||
|
|
||||||
- **Intel**
|
- **Intel**
|
||||||
|
|
||||||
|
|||||||
25
docs/static/frigate-api.yaml
vendored
25
docs/static/frigate-api.yaml
vendored
@ -17,20 +17,25 @@ paths:
|
|||||||
summary: Authenticate request
|
summary: Authenticate request
|
||||||
description: |-
|
description: |-
|
||||||
Authenticates the current request based on proxy headers or JWT token.
|
Authenticates the current request based on proxy headers or JWT token.
|
||||||
Returns user role and permissions for camera access.
|
|
||||||
This endpoint verifies authentication credentials and manages JWT token refresh.
|
This endpoint verifies authentication credentials and manages JWT token refresh.
|
||||||
|
On success, no JSON body is returned; authentication state is communicated via response headers and cookies.
|
||||||
operationId: auth_auth_get
|
operationId: auth_auth_get
|
||||||
responses:
|
responses:
|
||||||
"200":
|
|
||||||
description: Successful Response
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema: {}
|
|
||||||
"202":
|
"202":
|
||||||
description: Authentication Accepted
|
description: Authentication Accepted (no response body, different headers depending on auth method)
|
||||||
content:
|
headers:
|
||||||
application/json:
|
remote-user:
|
||||||
schema: {}
|
description: Authenticated username or "anonymous" in proxy-only mode
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
remote-role:
|
||||||
|
description: Resolved role (e.g., admin, viewer, or custom)
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
Set-Cookie:
|
||||||
|
description: May include refreshed JWT cookie ("frigate-token") when applicable
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
"401":
|
"401":
|
||||||
description: Authentication Failed
|
description: Authentication Failed
|
||||||
/profile:
|
/profile:
|
||||||
|
|||||||
@ -553,7 +553,32 @@ def resolve_role(
|
|||||||
"/auth",
|
"/auth",
|
||||||
dependencies=[Depends(allow_public())],
|
dependencies=[Depends(allow_public())],
|
||||||
summary="Authenticate request",
|
summary="Authenticate request",
|
||||||
description="Authenticates the current request based on proxy headers or JWT token. Returns user role and permissions for camera access.",
|
description=(
|
||||||
|
"Authenticates the current request based on proxy headers or JWT token. "
|
||||||
|
"This endpoint verifies authentication credentials and manages JWT token refresh. "
|
||||||
|
"On success, no JSON body is returned; authentication state is communicated via response headers and cookies."
|
||||||
|
),
|
||||||
|
status_code=202,
|
||||||
|
responses={
|
||||||
|
202: {
|
||||||
|
"description": "Authentication Accepted (no response body)",
|
||||||
|
"headers": {
|
||||||
|
"remote-user": {
|
||||||
|
"description": 'Authenticated username or "anonymous" in proxy-only mode',
|
||||||
|
"schema": {"type": "string"},
|
||||||
|
},
|
||||||
|
"remote-role": {
|
||||||
|
"description": "Resolved role (e.g., admin, viewer, or custom)",
|
||||||
|
"schema": {"type": "string"},
|
||||||
|
},
|
||||||
|
"Set-Cookie": {
|
||||||
|
"description": "May include refreshed JWT cookie when applicable",
|
||||||
|
"schema": {"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
401: {"description": "Authentication Failed"},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
def auth(request: Request):
|
def auth(request: Request):
|
||||||
auth_config: AuthConfig = request.app.frigate_config.auth
|
auth_config: AuthConfig = request.app.frigate_config.auth
|
||||||
@ -698,7 +723,7 @@ def auth(request: Request):
|
|||||||
"/profile",
|
"/profile",
|
||||||
dependencies=[Depends(allow_any_authenticated())],
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
summary="Get user profile",
|
summary="Get user profile",
|
||||||
description="Returns the current authenticated user's profile including username, role, and allowed cameras.",
|
description="Returns the current authenticated user's profile including username, role, and allowed cameras. This endpoint requires authentication and returns information about the user's permissions.",
|
||||||
)
|
)
|
||||||
def profile(request: Request):
|
def profile(request: Request):
|
||||||
username = request.headers.get("remote-user", "anonymous")
|
username = request.headers.get("remote-user", "anonymous")
|
||||||
@ -717,7 +742,7 @@ def profile(request: Request):
|
|||||||
"/logout",
|
"/logout",
|
||||||
dependencies=[Depends(allow_public())],
|
dependencies=[Depends(allow_public())],
|
||||||
summary="Logout user",
|
summary="Logout user",
|
||||||
description="Logs out the current user by clearing the session cookie.",
|
description="Logs out the current user by clearing the session cookie. After logout, subsequent requests will require re-authentication.",
|
||||||
)
|
)
|
||||||
def logout(request: Request):
|
def logout(request: Request):
|
||||||
auth_config: AuthConfig = request.app.frigate_config.auth
|
auth_config: AuthConfig = request.app.frigate_config.auth
|
||||||
@ -733,7 +758,7 @@ limiter = Limiter(key_func=get_remote_addr)
|
|||||||
"/login",
|
"/login",
|
||||||
dependencies=[Depends(allow_public())],
|
dependencies=[Depends(allow_public())],
|
||||||
summary="Login with credentials",
|
summary="Login with credentials",
|
||||||
description="Authenticates a user with username and password. Returns a JWT token as a secure HTTP-only cookie that can be used for subsequent API requests. The token can also be retrieved and used as a Bearer token in the Authorization header.",
|
description='Authenticates a user with username and password. Returns a JWT token as a secure HTTP-only cookie that can be used for subsequent API requests. The JWT token can also be retrieved from the response and used as a Bearer token in the Authorization header.\n\nExample using Bearer token:\n```\ncurl -H "Authorization: Bearer <token_value>" https://frigate_ip:8971/api/profile\n```',
|
||||||
)
|
)
|
||||||
@limiter.limit(limit_value=rateLimiter.get_limit)
|
@limiter.limit(limit_value=rateLimiter.get_limit)
|
||||||
def login(request: Request, body: AppPostLoginBody):
|
def login(request: Request, body: AppPostLoginBody):
|
||||||
@ -776,7 +801,7 @@ def login(request: Request, body: AppPostLoginBody):
|
|||||||
"/users",
|
"/users",
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Get all users",
|
summary="Get all users",
|
||||||
description="Returns a list of all users with their usernames and roles. Requires admin role.",
|
description="Returns a list of all users with their usernames and roles. Requires admin role. Each user object contains the username and assigned role.",
|
||||||
)
|
)
|
||||||
def get_users():
|
def get_users():
|
||||||
exports = (
|
exports = (
|
||||||
@ -789,7 +814,7 @@ def get_users():
|
|||||||
"/users",
|
"/users",
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Create new user",
|
summary="Create new user",
|
||||||
description="Creates a new user with the specified username, password, and role. Requires admin role. Password must meet strength requirements.",
|
description='Creates a new user with the specified username, password, and role. Requires admin role. Password must meet strength requirements: minimum 8 characters, at least one uppercase letter, at least one digit, and at least one special character (!@#$%^&*(),.?":{} |<>).',
|
||||||
)
|
)
|
||||||
def create_user(
|
def create_user(
|
||||||
request: Request,
|
request: Request,
|
||||||
@ -823,7 +848,7 @@ def create_user(
|
|||||||
"/users/{username}",
|
"/users/{username}",
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Delete user",
|
summary="Delete user",
|
||||||
description="Deletes a user by username. The built-in admin user cannot be deleted. Requires admin role.",
|
description="Deletes a user by username. The built-in admin user cannot be deleted. Requires admin role. Returns success message or error if user not found.",
|
||||||
)
|
)
|
||||||
def delete_user(request: Request, username: str):
|
def delete_user(request: Request, username: str):
|
||||||
# Prevent deletion of the built-in admin user
|
# Prevent deletion of the built-in admin user
|
||||||
@ -840,7 +865,7 @@ def delete_user(request: Request, username: str):
|
|||||||
"/users/{username}/password",
|
"/users/{username}/password",
|
||||||
dependencies=[Depends(allow_any_authenticated())],
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
summary="Update user password",
|
summary="Update user password",
|
||||||
description="Updates a user's password. Users can only change their own password unless they have admin role. Requires the current password to verify identity. Password must meet strength requirements (minimum 8 characters, uppercase letter, digit, and special character).",
|
description="Updates a user's password. Users can only change their own password unless they have admin role. Requires the current password to verify identity for non-admin users. Password must meet strength requirements: minimum 8 characters, at least one uppercase letter, at least one digit, and at least one special character (!@#$%^&*(),.?\":{} |<>). If user changes their own password, a new JWT cookie is automatically issued.",
|
||||||
)
|
)
|
||||||
async def update_password(
|
async def update_password(
|
||||||
request: Request,
|
request: Request,
|
||||||
@ -926,7 +951,7 @@ async def update_password(
|
|||||||
"/users/{username}/role",
|
"/users/{username}/role",
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Update user role",
|
summary="Update user role",
|
||||||
description="Updates a user's role. The built-in admin user's role cannot be modified. Requires admin role.",
|
description="Updates a user's role. The built-in admin user's role cannot be modified. Requires admin role. Valid roles are defined in the configuration.",
|
||||||
)
|
)
|
||||||
async def update_role(
|
async def update_role(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@ -131,6 +131,7 @@ class ONNXModelRunner(BaseModelRunner):
|
|||||||
|
|
||||||
return model_type in [
|
return model_type in [
|
||||||
EnrichmentModelTypeEnum.paddleocr.value,
|
EnrichmentModelTypeEnum.paddleocr.value,
|
||||||
|
EnrichmentModelTypeEnum.yolov9_license_plate.value,
|
||||||
EnrichmentModelTypeEnum.jina_v1.value,
|
EnrichmentModelTypeEnum.jina_v1.value,
|
||||||
EnrichmentModelTypeEnum.jina_v2.value,
|
EnrichmentModelTypeEnum.jina_v2.value,
|
||||||
EnrichmentModelTypeEnum.facenet.value,
|
EnrichmentModelTypeEnum.facenet.value,
|
||||||
|
|||||||
@ -119,6 +119,7 @@ class RecordingCleanup(threading.Thread):
|
|||||||
Recordings.path,
|
Recordings.path,
|
||||||
Recordings.objects,
|
Recordings.objects,
|
||||||
Recordings.motion,
|
Recordings.motion,
|
||||||
|
Recordings.dBFS,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(Recordings.camera == config.name)
|
(Recordings.camera == config.name)
|
||||||
@ -126,6 +127,7 @@ class RecordingCleanup(threading.Thread):
|
|||||||
(
|
(
|
||||||
(Recordings.end_time < continuous_expire_date)
|
(Recordings.end_time < continuous_expire_date)
|
||||||
& (Recordings.motion == 0)
|
& (Recordings.motion == 0)
|
||||||
|
& (Recordings.dBFS == 0)
|
||||||
)
|
)
|
||||||
| (Recordings.end_time < motion_expire_date)
|
| (Recordings.end_time < motion_expire_date)
|
||||||
)
|
)
|
||||||
@ -185,6 +187,7 @@ class RecordingCleanup(threading.Thread):
|
|||||||
mode == RetainModeEnum.motion
|
mode == RetainModeEnum.motion
|
||||||
and recording.motion == 0
|
and recording.motion == 0
|
||||||
and recording.objects == 0
|
and recording.objects == 0
|
||||||
|
and recording.dBFS == 0
|
||||||
)
|
)
|
||||||
or (mode == RetainModeEnum.active_objects and recording.objects == 0)
|
or (mode == RetainModeEnum.active_objects and recording.objects == 0)
|
||||||
):
|
):
|
||||||
|
|||||||
@ -67,7 +67,7 @@ class SegmentInfo:
|
|||||||
if (
|
if (
|
||||||
not keep
|
not keep
|
||||||
and retain_mode == RetainModeEnum.motion
|
and retain_mode == RetainModeEnum.motion
|
||||||
and (self.motion_count > 0 or self.average_dBFS > 0)
|
and (self.motion_count > 0 or self.average_dBFS != 0)
|
||||||
):
|
):
|
||||||
keep = True
|
keep = True
|
||||||
|
|
||||||
|
|||||||
@ -440,6 +440,7 @@ function CustomTimeSelector({
|
|||||||
<FaCalendarAlt />
|
<FaCalendarAlt />
|
||||||
<div className="flex flex-wrap items-center">
|
<div className="flex flex-wrap items-center">
|
||||||
<Popover
|
<Popover
|
||||||
|
modal={false}
|
||||||
open={startOpen}
|
open={startOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@ -461,7 +462,10 @@ function CustomTimeSelector({
|
|||||||
{formattedStart}
|
{formattedStart}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="flex flex-col items-center">
|
<PopoverContent
|
||||||
|
disablePortal={isDesktop}
|
||||||
|
className="flex flex-col items-center"
|
||||||
|
>
|
||||||
<TimezoneAwareCalendar
|
<TimezoneAwareCalendar
|
||||||
timezone={config?.ui.timezone}
|
timezone={config?.ui.timezone}
|
||||||
selectedDay={new Date(startTime * 1000)}
|
selectedDay={new Date(startTime * 1000)}
|
||||||
@ -506,6 +510,7 @@ function CustomTimeSelector({
|
|||||||
</Popover>
|
</Popover>
|
||||||
<FaArrowRight className="size-4 text-primary" />
|
<FaArrowRight className="size-4 text-primary" />
|
||||||
<Popover
|
<Popover
|
||||||
|
modal={false}
|
||||||
open={endOpen}
|
open={endOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@ -527,7 +532,10 @@ function CustomTimeSelector({
|
|||||||
{formattedEnd}
|
{formattedEnd}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="flex flex-col items-center">
|
<PopoverContent
|
||||||
|
disablePortal={isDesktop}
|
||||||
|
className="flex flex-col items-center"
|
||||||
|
>
|
||||||
<TimezoneAwareCalendar
|
<TimezoneAwareCalendar
|
||||||
timezone={config?.ui.timezone}
|
timezone={config?.ui.timezone}
|
||||||
selectedDay={new Date(endTime * 1000)}
|
selectedDay={new Date(endTime * 1000)}
|
||||||
@ -545,7 +553,7 @@ function CustomTimeSelector({
|
|||||||
<SelectSeparator className="bg-secondary" />
|
<SelectSeparator className="bg-secondary" />
|
||||||
<input
|
<input
|
||||||
className="text-md mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
className="text-md mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
id="startTime"
|
id="endTime"
|
||||||
type="time"
|
type="time"
|
||||||
value={endClock}
|
value={endClock}
|
||||||
step={isIOS ? "60" : "1"}
|
step={isIOS ? "60" : "1"}
|
||||||
|
|||||||
@ -178,6 +178,19 @@ export default function ObjectMaskEditPane({
|
|||||||
filteredMask.splice(index, 0, coordinates);
|
filteredMask.splice(index, 0, coordinates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prevent duplicating global masks under specific object filters
|
||||||
|
if (!globalMask) {
|
||||||
|
const globalObjectMasksArray = Array.isArray(cameraConfig.objects.mask)
|
||||||
|
? cameraConfig.objects.mask
|
||||||
|
: cameraConfig.objects.mask
|
||||||
|
? [cameraConfig.objects.mask]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
filteredMask = filteredMask.filter(
|
||||||
|
(mask) => !globalObjectMasksArray.includes(mask),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
queryString = filteredMask
|
queryString = filteredMask
|
||||||
.map((pointsArray) => {
|
.map((pointsArray) => {
|
||||||
const coordinates = flattenPoints(parseCoordinates(pointsArray)).join(
|
const coordinates = flattenPoints(parseCoordinates(pointsArray)).join(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user