Compare commits

..

12 Commits

Author SHA1 Message Date
Hosted Weblate
1d703978b5
Update translation files
Updated by "Squash Git commits" add-on in Weblate.

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (45 of 45 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (237 of 237 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (40 of 40 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (792 of 792 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (1122 of 1122 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: OverTheHillsAndFarAway <prosjektx@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/nb_NO/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/common
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-replay
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-05-12 22:05:39 +02:00
Hosted Weblate
637641ae43
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (794 of 794 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1122 of 1122 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (792 of 792 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (237 of 237 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (127 of 127 strings)

Co-authored-by: GuoQing Liu <842607283@qq.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/zh_Hans/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/common
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2026-05-12 22:05:39 +02:00
Hosted Weblate
d629fc08fd
Translated using Weblate (French)
Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (French)

Currently translated at 46.6% (21 of 45 strings)

Translated using Weblate (French)

Currently translated at 30.0% (12 of 40 strings)

Co-authored-by: Erwan Cogoluenhes <erwan.cogo@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Le Buzzy <bwinster2@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/fr/
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-replay
2026-05-12 22:05:38 +02:00
Hosted Weblate
052a1e3b16
Translated using Weblate (Spanish)
Currently translated at 100.0% (45 of 45 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (64 of 64 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (237 of 237 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (40 of 40 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (237 of 237 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (101 of 101 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1122 of 1122 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (792 of 792 strings)

Translated using Weblate (Spanish)

Currently translated at 52.5% (31 of 59 strings)

Translated using Weblate (Spanish)

Currently translated at 99.4% (174 of 175 strings)

Translated using Weblate (Spanish)

Currently translated at 23.3% (110 of 471 strings)

Translated using Weblate (Spanish)

Currently translated at 68.8% (31 of 45 strings)

Translated using Weblate (Spanish)

Currently translated at 21.8% (173 of 792 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Spanish)

Currently translated at 62.3% (63 of 101 strings)

Translated using Weblate (Spanish)

Currently translated at 40.6% (35 of 86 strings)

Translated using Weblate (Spanish)

Currently translated at 80.0% (32 of 40 strings)

Translated using Weblate (Spanish)

Currently translated at 67.6% (759 of 1122 strings)

Translated using Weblate (Spanish)

Currently translated at 70.3% (45 of 64 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: jjavin <javiernovoa@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/es/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-replay
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-05-12 22:05:38 +02:00
Hosted Weblate
d880edbe22
Translated using Weblate (Nepali)
Currently translated at 29.7% (14 of 47 strings)

Translated using Weblate (Nepali)

Currently translated at 53.8% (14 of 26 strings)

Translated using Weblate (Nepali)

Currently translated at 16.2% (14 of 86 strings)

Translated using Weblate (Nepali)

Currently translated at 14.0% (14 of 100 strings)

Translated using Weblate (Nepali)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Nepali)

Currently translated at 23.7% (14 of 59 strings)

Translated using Weblate (Nepali)

Currently translated at 56.0% (14 of 25 strings)

Translated using Weblate (Nepali)

Currently translated at 1.1% (13 of 1122 strings)

Translated using Weblate (Nepali)

Currently translated at 32.5% (13 of 40 strings)

Translated using Weblate (Nepali)

Currently translated at 63.6% (14 of 22 strings)

Translated using Weblate (Nepali)

Currently translated at 11.6% (15 of 129 strings)

Translated using Weblate (Nepali)

Currently translated at 18.9% (14 of 74 strings)

Translated using Weblate (Nepali)

Currently translated at 24.1% (14 of 58 strings)

Translated using Weblate (Nepali)

Currently translated at 5.9% (14 of 237 strings)

Translated using Weblate (Nepali)

Currently translated at 3.1% (15 of 471 strings)

Translated using Weblate (Nepali)

Currently translated at 28.5% (14 of 49 strings)

Translated using Weblate (Nepali)

Currently translated at 11.0% (14 of 127 strings)

Translated using Weblate (Nepali)

Currently translated at 2.2% (18 of 792 strings)

Translated using Weblate (Nepali)

Currently translated at 9.6% (14 of 145 strings)

Translated using Weblate (Nepali)

Currently translated at 3.7% (19 of 501 strings)

Translated using Weblate (Nepali)

Currently translated at 20.3% (13 of 64 strings)

Translated using Weblate (Nepali)

Currently translated at 13.8% (14 of 101 strings)

Translated using Weblate (Nepali)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Nepali)

Currently translated at 31.1% (14 of 45 strings)

Translated using Weblate (Nepali)

Currently translated at 8.0% (14 of 175 strings)

Translated using Weblate (Nepali)

Currently translated at 100.0% (6 of 6 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: bijaydewan <bijaydewan@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-recording/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/ne/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Groups
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-auth
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/components-player
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-configeditor
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-recording
Translation: Frigate NVR/views-replay
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-05-12 22:05:38 +02:00
Hosted Weblate
f3eca68067
Translated using Weblate (Dutch)
Currently translated at 63.4% (712 of 1122 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Dutch)

Currently translated at 11.8% (7 of 59 strings)

Translated using Weblate (Dutch)

Currently translated at 20.0% (8 of 40 strings)

Translated using Weblate (Dutch)

Currently translated at 8.8% (4 of 45 strings)

Translated using Weblate (Dutch)

Currently translated at 10.1% (80 of 792 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Dutch)

Currently translated at 93.7% (121 of 129 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: bb61523 <brambini@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/nl/
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Groups
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-replay
Translation: Frigate NVR/views-settings
2026-05-12 22:05:38 +02:00
Hosted Weblate
711e1c6d5f
Translated using Weblate (Catalan)
Currently translated at 100.0% (1122 of 1122 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (237 of 237 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (127 of 127 strings)

Co-authored-by: Gerard Ricart Castells <gerard.ricart@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ca/
Translation: Frigate NVR/common
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-settings
2026-05-12 22:05:38 +02:00
Hosted Weblate
d981cb6a76
Translated using Weblate (Russian)
Currently translated at 92.0% (23 of 25 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (22 of 22 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Max Slotov <max@slotov.dev>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/ru/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/ru/
Translation: Frigate NVR/Config - Groups
Translation: Frigate NVR/Config - Validation
2026-05-12 22:05:38 +02:00
Hosted Weblate
b42cab4c95
Translated using Weblate (Estonian)
Currently translated at 100.0% (237 of 237 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (127 of 127 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/et/
Translation: Frigate NVR/common
Translation: Frigate NVR/objects
2026-05-12 22:05:38 +02:00
Rob Arnold
f1e2240945
Gracefully handle transiently failing exists calls (#23172)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
I have a very repeatable reproduction of an issue where most of my
cameras show a "No frames have been received, check error logs" image in
the UI, but restreaming in HomeAssistant is working flawlessly. The only
errors in the logs I saw were some like this:

`OSError: [Errno 121] Remote I/O error`.

Doing a bit more debugging, it looked like Frigate was failing to create
the thumbnail directory for a camera because it already existed. This
error was a clue as to the class of error. I was surprised to learn that
`os.path.exists` [silently suppresses errors from
os.stat and returns False](https://github.com/python/cpython/blob/main/Lib/genericpath.py#L22).
This makes for a plausible series of events: a transient stat call
fails, so Frigate takes the creation path, which gets upset that the
directory already exists.

I found a few other possible cases to fix but did not make an exhaustive
search. It seems that this `exist_ok` flag is used elsewhere within
Frigate so I thought it would be a good solution.

AI disclosure: I used AI to diagnose my issue and asked it to translate
its init-time patches to the container source into this repo. I verified
that its patches solved the problem I was facing. Its theory fits the
facts - I am using a distributed file system and I saw the error in my
logs. I checked the upstream Python code to verify the error suppression
behavior, and read the corresponding Frigate code. I did not use AI to
author this commit message/PR description; all diction and typos here are my own.
2026-05-12 12:34:46 -06:00
Josh Hawkins
4e90d254ed
Miscellaneous fixes (#23177)
* add optional onClick to EmptyCard

* show EmptyCard in face rec when face library is empty

* add loading indicator

* add description to camera management pane

* Cleanup when use snapshot but can't load snapshot

* Migrate files

* fix birdseye color distortion when configured aspect ratio is unsupported

* Skip processing end for object descriptions

* don't crash if stats is null

* fix genai roles in migration

* frigate+ pane updates

- allow users to select a plus model from the select even when one was not previously loaded
- always show model summary card
- add model filter popover
- add restart button totast

* fix frigate+ pane layout and buttons to match other settings panes

* match button layout in go2rtc settings view

* make audio maintainer respond to dynamic config updates

* check correct zone name in publish state

* fix nested translation extraction for Optional dict and list fields

* mypy

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2026-05-12 10:20:39 -06:00
Nicolas Mowen
c67170aa20
Implement cross-camera safety for indexed media folders (#23164)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* Implement cross-camera safety for indexed media folders

* Cleanup

* Improve robustness
2026-05-11 14:52:18 -05:00
92 changed files with 2069 additions and 450 deletions

View File

@ -26,6 +26,7 @@ from frigate.api.defs.request.app_body import (
AppPutRoleBody,
)
from frigate.api.defs.tags import Tags
from frigate.api.media_auth import check_camera_access, deny_response_for_media_uri
from frigate.config import AuthConfig, NetworkingConfig, ProxyConfig
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
from frigate.models import User
@ -633,6 +634,9 @@ def auth(request: Request):
logger.debug("X-Proxy-Secret header does not match configured secret value")
return fail_response
original_url = request.headers.get("x-original-url")
frigate_config = request.app.frigate_config
# if auth is disabled, just apply the proxy header map and return success
if not auth_config.enabled:
# pass the user header value from the upstream proxy if a mapping is specified
@ -649,6 +653,11 @@ def auth(request: Request):
role = resolve_role(request.headers, proxy_config, config_roles_set)
success_response.headers["remote-role"] = role
deny_status = deny_response_for_media_uri(original_url, role, frigate_config)
if deny_status is not None:
return Response("", status_code=deny_status)
return success_response
# now apply authentication
@ -743,6 +752,11 @@ def auth(request: Request):
success_response.headers["remote-user"] = user
success_response.headers["remote-role"] = role
deny_status = deny_response_for_media_uri(original_url, role, frigate_config)
if deny_status is not None:
return Response("", status_code=deny_status)
return success_response
except Exception as e:
logger.error(f"Error parsing jwt: {e}")
@ -1069,19 +1083,19 @@ async def require_camera_access(
raise HTTPException(status_code=current_user.status_code, detail=detail)
role = current_user["role"]
all_camera_names = set(request.app.frigate_config.cameras.keys())
roles_dict = request.app.frigate_config.auth.roles
allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names)
frigate_config = request.app.frigate_config
# Admin or full access bypasses
if role == "admin" or not roles_dict.get(role):
if check_camera_access(role, camera_name, frigate_config):
return
if camera_name not in allowed_cameras:
raise HTTPException(
status_code=403,
detail=f"Access denied to camera '{camera_name}'. Allowed: {allowed_cameras}",
)
all_camera_names = set(frigate_config.cameras.keys())
allowed_cameras = User.get_allowed_cameras(
role, frigate_config.auth.roles, all_camera_names
)
raise HTTPException(
status_code=403,
detail=f"Access denied to camera '{camera_name}'. Allowed: {allowed_cameras}",
)
def _get_stream_owner_cameras(request: Request, stream_name: str) -> set[str]:

291
frigate/api/media_auth.py Normal file
View File

@ -0,0 +1,291 @@
"""URI-aware authorization for nginx-served static media.
The `/auth` endpoint (used as nginx `auth_request` target) calls into this
module to classify the requested URI from the `X-Original-URL` header and, for
camera-scoped resources, decide whether the current role may access them.
Without this, `auth_request` only verifies the JWT every authenticated user
could read clips, recordings, and exports for *any* camera, bypassing the
per-camera authorization the regular API enforces via `require_camera_access`.
"""
from __future__ import annotations
import logging
import os
from enum import Enum
from typing import Optional
from urllib.parse import unquote, urlparse
from peewee import DoesNotExist
from frigate.config import FrigateConfig
from frigate.const import EXPORT_DIR
from frigate.models import Export, User
logger = logging.getLogger(__name__)
class MediaAuthResolution(str, Enum):
"""Classification of an `X-Original-URL` path for media-auth purposes."""
CAMERA = "camera"
ADMIN_ONLY = "admin_only"
LISTING_MULTI_CAMERA = "listing_multi_camera"
LISTING_NEUTRAL = "listing_neutral"
# Under a recognized media root (/clips, /recordings, /exports) but
# unclassifiable (unknown subtree, no matching DB row, DB error).
# Restricted users are denied; admins/full-access roles are allowed
# (nginx will likely return 404 if the file genuinely doesn't exist).
UNRESOLVED_MEDIA = "unresolved_media"
# Not a media URI at all (e.g. /api/events, /login).
UNKNOWN = "unknown"
def extract_path(original_url: Optional[str]) -> Optional[str]:
"""Return the decoded path component of nginx's `X-Original-URL` header.
nginx forwards the *raw* request URI (with `..` segments intact) via
`$request_uri`. nginx normalizes the path before serving the file, so a
request like `/recordings/.../allowed_cam/../forbidden_cam/file.mp4`
would (1) parse as the allowed camera in our auth check, (2) be served
as the forbidden camera by nginx. To close the bypass we reject any URI
whose path contains `.` or `..` segments outright.
"""
if not original_url:
return None
parsed = urlparse(original_url)
raw_path = parsed.path or original_url
decoded = unquote(raw_path)
if not decoded:
return None
if not decoded.startswith("/"):
decoded = "/" + decoded
segments = decoded.split("/")
if ".." in segments or "." in segments:
return None
return decoded
def resolve_media_uri(
uri: str, frigate_config: Optional[FrigateConfig] = None
) -> tuple[MediaAuthResolution, Optional[str]]:
"""Classify a URI and return the owning camera if applicable.
`frigate_config` is used to disambiguate clip/review filenames whose
camera name contains hyphens by matching against the longest configured
camera-name prefix.
"""
if not uri:
return MediaAuthResolution.UNKNOWN, None
parts = [p for p in uri.split("/") if p]
if not parts:
return MediaAuthResolution.UNKNOWN, None
root = parts[0]
if root == "recordings":
return _resolve_recording(parts)
if root == "clips":
return _resolve_clip(parts, frigate_config)
if root == "exports":
return _resolve_export(parts)
return MediaAuthResolution.UNKNOWN, None
def _resolve_recording(
parts: list[str],
) -> tuple[MediaAuthResolution, Optional[str]]:
# /recordings → neutral
# /recordings/{date} → neutral
# /recordings/{date}/{hour} → multi-camera listing
# /recordings/{date}/{hour}/{cam}/... → camera
if len(parts) <= 2:
return MediaAuthResolution.LISTING_NEUTRAL, None
if len(parts) == 3:
return MediaAuthResolution.LISTING_MULTI_CAMERA, None
return MediaAuthResolution.CAMERA, parts[3]
def _resolve_clip(
parts: list[str], frigate_config: Optional[FrigateConfig]
) -> tuple[MediaAuthResolution, Optional[str]]:
# /clips → multi-camera listing
# /clips/thumbs/{cam}/... → camera
# /clips/previews/{cam}/... → camera
# /clips/review/thumb-{cam}-{review_id}.webp → camera (parsed)
# /clips/faces/... → admin-only
# /clips/genai-requests/... → admin-only
# /clips/preview_restart_cache/... → admin-only
# /clips/{model}/train|dataset/... → admin-only
# /clips/{cam}-{event_id}[-clean].{ext} → camera (parsed)
# other /clips/{subdir}/... → unresolved (deny restricted)
if len(parts) == 1:
return MediaAuthResolution.LISTING_MULTI_CAMERA, None
second = parts[1]
if second in ("thumbs", "previews"):
if len(parts) == 2:
return MediaAuthResolution.LISTING_MULTI_CAMERA, None
return MediaAuthResolution.CAMERA, parts[2]
if second == "review":
if len(parts) == 2:
return MediaAuthResolution.LISTING_MULTI_CAMERA, None
camera = _camera_from_thumb_filename(parts[2], frigate_config)
if camera:
return MediaAuthResolution.CAMERA, camera
return MediaAuthResolution.UNRESOLVED_MEDIA, None
if second in ("faces", "genai-requests", "preview_restart_cache"):
return MediaAuthResolution.ADMIN_ONLY, None
if len(parts) >= 3 and parts[2] in ("train", "dataset"):
return MediaAuthResolution.ADMIN_ONLY, None
if len(parts) == 2:
camera = _camera_from_clip_filename(second, frigate_config)
if camera:
return MediaAuthResolution.CAMERA, camera
return MediaAuthResolution.UNRESOLVED_MEDIA, None
return MediaAuthResolution.UNRESOLVED_MEDIA, None
def _longest_prefix_camera(
stem: str, frigate_config: Optional[FrigateConfig]
) -> Optional[str]:
if frigate_config is None:
return None
for cam in sorted(frigate_config.cameras.keys(), key=len, reverse=True):
if stem.startswith(cam + "-"):
return cam
return None
def _camera_from_clip_filename(
filename: str, frigate_config: Optional[FrigateConfig]
) -> Optional[str]:
"""Match a flat clip filename `{camera}-{event_id}[-clean].{ext}` against
configured camera names. Longest-prefix wins so camera names containing
hyphens (e.g. `front-door`) resolve correctly.
"""
dot = filename.rfind(".")
stem = filename[:dot] if dot > 0 else filename
return _longest_prefix_camera(stem, frigate_config)
def _camera_from_thumb_filename(
filename: str, frigate_config: Optional[FrigateConfig]
) -> Optional[str]:
"""Match a review thumbnail filename `thumb-{camera}-{review_id}.webp`."""
if not filename.startswith("thumb-"):
return None
dot = filename.rfind(".")
stem = filename[len("thumb-") : dot] if dot > 0 else filename[len("thumb-") :]
return _longest_prefix_camera(stem, frigate_config)
def _resolve_export(
parts: list[str],
) -> tuple[MediaAuthResolution, Optional[str]]:
# /exports → multi-camera listing
# /exports/{filename}.mp4 → camera (DB lookup by exact path)
if len(parts) == 1:
return MediaAuthResolution.LISTING_MULTI_CAMERA, None
if len(parts) != 2:
return MediaAuthResolution.UNRESOLVED_MEDIA, None
filename = parts[1]
full_path = os.path.join(EXPORT_DIR, filename)
try:
export = Export.get(Export.video_path == full_path)
return MediaAuthResolution.CAMERA, export.camera
except DoesNotExist:
return MediaAuthResolution.UNRESOLVED_MEDIA, None
except Exception as e:
logger.warning("Export DB lookup failed for %s: %s", filename, e)
return MediaAuthResolution.UNRESOLVED_MEDIA, None
def check_camera_access(role: str, camera: str, frigate_config: FrigateConfig) -> bool:
"""Return True iff `role` may access `camera`.
Mirrors the gating logic in `require_camera_access`: admin and any role
without a non-empty allow-list bypass the check.
"""
if role == "admin":
return True
roles_dict = frigate_config.auth.roles
if not roles_dict.get(role):
return True
all_camera_names = set(frigate_config.cameras.keys())
allowed = User.get_allowed_cameras(role, roles_dict, all_camera_names)
return camera in allowed
def is_role_restricted(role: str, frigate_config: FrigateConfig) -> bool:
"""True if `role` has a non-empty allow-list (i.e. not full-access)."""
if role == "admin":
return False
return bool(frigate_config.auth.roles.get(role))
def deny_response_for_media_uri(
original_url: Optional[str], role: Optional[str], frigate_config: FrigateConfig
) -> Optional[int]:
"""Decide whether the current role should be blocked from `original_url`.
Returns an HTTP status code (403) when access should be denied, or `None`
when the request is allowed.
"""
if not original_url:
return None
path = extract_path(original_url)
# `extract_path` returns None for URIs containing `.` or `..` segments.
# For media-root URIs that's a traversal attempt — deny outright. For
# non-media URIs, pass through (nginx / the backend handle them).
if path is None:
raw = urlparse(original_url).path or original_url
decoded = unquote(raw)
first = decoded.lstrip("/").split("/", 1)[0] if decoded else ""
if first in ("clips", "recordings", "exports"):
return 403
return None
resolution, camera = resolve_media_uri(path, frigate_config)
if resolution == MediaAuthResolution.UNKNOWN:
return None
if not role or role == "admin":
return None
if not is_role_restricted(role, frigate_config):
return None
if resolution == MediaAuthResolution.LISTING_NEUTRAL:
return None
if resolution in (
MediaAuthResolution.LISTING_MULTI_CAMERA,
MediaAuthResolution.ADMIN_ONLY,
MediaAuthResolution.UNRESOLVED_MEDIA,
):
return 403
if resolution == MediaAuthResolution.CAMERA:
if camera and check_camera_access(role, camera, frigate_config):
return None
return 403
return 403

View File

@ -144,7 +144,7 @@ class FrigateApp:
for d in dirs:
if not os.path.exists(d) and not os.path.islink(d):
logger.info(f"Creating directory: {d}")
os.makedirs(d)
os.makedirs(d, exist_ok=True)
else:
logger.debug(f"Skipping directory: {d}")
@ -428,18 +428,11 @@ class FrigateApp:
self.camera_maintainer.start()
def start_audio_processor(self) -> None:
audio_cameras = [
c
for c in self.config.cameras.values()
if c.enabled and c.audio.enabled_in_config
]
if audio_cameras:
self.audio_process = AudioProcessor(
self.config, audio_cameras, self.camera_metrics, self.stop_event
)
self.audio_process.start()
self.processes["audio_detector"] = self.audio_process.pid or 0
self.audio_process = AudioProcessor(
self.config, self.camera_metrics, self.stop_event
)
self.audio_process.start()
self.processes["audio_detector"] = self.audio_process.pid or 0
def start_timeline_processor(self) -> None:
self.timeline_processor = TimelineProcessor(

View File

@ -269,7 +269,9 @@ class ObjectDescriptionProcessor(PostProcessorApi):
if event.has_snapshot and camera_config.objects.genai.use_snapshot:
snapshot_image = self._read_and_crop_snapshot(event)
if not snapshot_image:
self.cleanup_event(event_id)
return
num_thumbnails = len(self.tracked_events.get(event_id, []))

View File

@ -60,7 +60,11 @@ from frigate.data_processing.real_time.license_plate import (
)
from frigate.data_processing.types import DataProcessorMetrics, PostProcessDataEnum
from frigate.db.sqlitevecq import SqliteVecQueueDatabase
from frigate.events.types import EventTypeEnum, RegenerateDescriptionEnum
from frigate.events.types import (
EventStateEnum,
EventTypeEnum,
RegenerateDescriptionEnum,
)
from frigate.genai import GenAIClientManager
from frigate.models import Event, Recordings, ReviewSegment, Trigger
from frigate.types import TrackedObjectUpdateTypesEnum
@ -435,7 +439,7 @@ class EmbeddingMaintainer(threading.Thread):
if update is None:
return
source_type, _, camera, frame_name, data = update
source_type, event_type, camera, frame_name, data = update
logger.debug(
f"Received update - source_type: {source_type}, camera: {camera}, data label: {data.get('label') if data else 'None'}"
@ -485,6 +489,12 @@ class EmbeddingMaintainer(threading.Thread):
for processor in self.post_processors:
if isinstance(processor, ObjectDescriptionProcessor):
# skip end events — _process_finalized handles them via event_end_subscriber.
# processing them here can re-create tracked_events entries after cleanup
# when the event_subscriber queue is backlogged behind event_end_subscriber.
if event_type == EventStateEnum.end:
continue
processor.process_data(
{
"camera": camera,

View File

@ -84,7 +84,6 @@ class AudioProcessor(FrigateProcess):
def __init__(
self,
config: FrigateConfig,
cameras: list[CameraConfig],
camera_metrics: DictProxy,
stop_event: MpEvent,
):
@ -93,12 +92,11 @@ class AudioProcessor(FrigateProcess):
)
self.camera_metrics = camera_metrics
self.cameras = cameras
self.config = config
def run(self) -> None:
self.pre_run_setup(self.config.logger)
audio_threads: list[AudioEventMaintainer] = []
audio_threads: dict[str, AudioEventMaintainer] = {}
threading.current_thread().name = "process:audio_manager"
@ -112,32 +110,56 @@ class AudioProcessor(FrigateProcess):
else:
self.transcription_model_runner = None
if len(self.cameras) == 0:
return
config_subscriber = CameraConfigUpdateSubscriber(
self.config,
self.config.cameras,
[
CameraConfigUpdateEnum.add,
CameraConfigUpdateEnum.audio,
CameraConfigUpdateEnum.ffmpeg,
],
)
for camera in self.cameras:
audio_thread = AudioEventMaintainer(
def spawn_if_needed(camera: CameraConfig) -> None:
name = camera.name
if name is None or name in audio_threads:
return
if not camera.enabled or not camera.audio.enabled:
return
# ffmpeg update may not have arrived yet; wait for next poll
if not any("audio" in i.roles for i in camera.ffmpeg.inputs):
return
thread = AudioEventMaintainer(
camera,
self.config,
self.camera_metrics,
self.transcription_model_runner,
self.stop_event, # type: ignore[arg-type]
)
audio_threads.append(audio_thread)
audio_thread.start()
audio_threads[name] = thread
thread.start()
self.logger.info(f"Audio maintainer started for {name}")
for camera in self.config.cameras.values():
spawn_if_needed(camera)
self.logger.info(f"Audio processor started (pid: {self.pid})")
while not self.stop_event.wait():
pass
# poll for newly added cameras or cameras flipped to audio.enabled at runtime
while not self.stop_event.wait(timeout=1.0):
config_subscriber.check_for_updates()
for camera in self.config.cameras.values():
spawn_if_needed(camera)
for thread in audio_threads:
config_subscriber.stop()
for thread in audio_threads.values():
thread.join(1)
if thread.is_alive():
self.logger.info(f"Waiting for thread {thread.name:s} to exit")
thread.join(10)
for thread in audio_threads:
for thread in audio_threads.values():
if thread.is_alive():
self.logger.warning(f"Thread {thread.name} is still alive")

View File

@ -62,8 +62,10 @@ def get_canvas_shape(width: int, height: int) -> tuple[int, int]:
if round(a_w / a_h, 2) != round(width / height, 2):
canvas_width = int(width // 4 * 4)
canvas_height = int((canvas_width / a_w * a_h) // 4 * 4)
logger.warning(
f"The birdseye resolution is a non-standard aspect ratio, forcing birdseye resolution to {canvas_width} x {canvas_height}"
logger.error(
f"Birdseye resolution {width}x{height} is not a supported aspect ratio "
f"and may cause visual distortion; falling back to {canvas_width}x{canvas_height}. "
f"Set width and height to a supported aspect ratio (16:9, 20:10, 16:6, 32:9, 12:9, 22:15, 9:16, 9:12, 16:3, or 1:1)"
)
return (canvas_width, canvas_height)
@ -796,15 +798,18 @@ class Birdseye:
websocket_server: Any,
) -> None:
self.config = config
canvas_width, canvas_height = get_canvas_shape(
config.birdseye.width, config.birdseye.height
)
self.input: queue.Queue[bytes] = queue.Queue(maxsize=10)
self.converter = FFMpegConverter(
config.ffmpeg,
self.input,
stop_event,
config.birdseye.width,
config.birdseye.height,
config.birdseye.width,
config.birdseye.height,
canvas_width,
canvas_height,
canvas_width,
canvas_height,
config.birdseye.quality,
config.birdseye.restream,
)

View File

@ -610,8 +610,7 @@ class RecordingMaintainer(threading.Thread):
camera,
)
if not os.path.exists(directory):
os.makedirs(directory)
os.makedirs(directory, exist_ok=True)
# file will be in utc due to start_time being in utc
file_name = f"{start_time.strftime('%M.%S.mp4')}"

View File

@ -0,0 +1,381 @@
"""Unit tests for `frigate.api.media_auth`.
Covers URI classification, the role-vs-camera decision matrix, and the export
DB-lookup path. These are pure functions/DB lookups no HTTP stack involved.
"""
import datetime
import logging
import os
import unittest
from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase
from frigate.api.media_auth import (
MediaAuthResolution,
deny_response_for_media_uri,
extract_path,
resolve_media_uri,
)
from frigate.config import FrigateConfig
from frigate.models import Event, Export, Recordings, ReviewSegment
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
_CONFIG = {
"mqtt": {"host": "mqtt"},
"auth": {"roles": {"limited_user": ["front_door"]}},
"cameras": {
"front_door": {
"ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
},
"back_door": {
"ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
},
# Camera name with a hyphen — exercises longest-prefix match.
"back-yard": {
"ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.3:554/video", "roles": ["detect"]}]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
},
},
}
class TestExtractPath(unittest.TestCase):
def test_full_url(self):
self.assertEqual(
extract_path("http://host:8971/clips/front_door-1.jpg"),
"/clips/front_door-1.jpg",
)
def test_strips_query_string(self):
self.assertEqual(
extract_path("http://h/recordings/2026-05-11/14/front_door/00.00.mp4?t=1"),
"/recordings/2026-05-11/14/front_door/00.00.mp4",
)
def test_path_only(self):
self.assertEqual(extract_path("/exports/x.mp4"), "/exports/x.mp4")
def test_percent_decoded(self):
self.assertEqual(
extract_path("http://h/clips/front%20door-1.jpg"),
"/clips/front door-1.jpg",
)
def test_empty(self):
self.assertIsNone(extract_path(None))
self.assertIsNone(extract_path(""))
class TestResolveMediaUri(unittest.TestCase):
def setUp(self):
self.config = FrigateConfig(**_CONFIG)
def _assert(self, uri, resolution, camera=None):
got_resolution, got_camera = resolve_media_uri(uri, self.config)
self.assertEqual(got_resolution, resolution, uri)
self.assertEqual(got_camera, camera, uri)
def test_unknown_paths(self):
self._assert("/api/events", MediaAuthResolution.UNKNOWN)
self._assert("/", MediaAuthResolution.UNKNOWN)
self._assert("", MediaAuthResolution.UNKNOWN)
def test_recordings(self):
self._assert("/recordings/", MediaAuthResolution.LISTING_NEUTRAL)
self._assert("/recordings/2026-05-11/", MediaAuthResolution.LISTING_NEUTRAL)
self._assert(
"/recordings/2026-05-11/14/", MediaAuthResolution.LISTING_MULTI_CAMERA
)
self._assert(
"/recordings/2026-05-11/14/front_door/",
MediaAuthResolution.CAMERA,
camera="front_door",
)
self._assert(
"/recordings/2026-05-11/14/back_door/00.00.mp4",
MediaAuthResolution.CAMERA,
camera="back_door",
)
def test_clip_flat_filename_resolves_camera(self):
self._assert(
"/clips/front_door-1234.jpg",
MediaAuthResolution.CAMERA,
camera="front_door",
)
self._assert(
"/clips/back_door-1234-clean.webp",
MediaAuthResolution.CAMERA,
camera="back_door",
)
def test_clip_filename_with_hyphenated_camera_name(self):
# Camera name "back-yard" itself contains a hyphen; longest-prefix
# match must pick `back-yard`, not the bogus `back` prefix.
self._assert(
"/clips/back-yard-1234.jpg",
MediaAuthResolution.CAMERA,
camera="back-yard",
)
def test_clip_filename_no_matching_camera(self):
# Looks like a media path but couldn't classify — fail closed for
# restricted users (UNRESOLVED_MEDIA), not pass-through.
self._assert(
"/clips/nonexistent-1234.jpg", MediaAuthResolution.UNRESOLVED_MEDIA
)
def test_clip_thumbs(self):
self._assert("/clips/thumbs/", MediaAuthResolution.LISTING_MULTI_CAMERA)
self._assert(
"/clips/thumbs/front_door/",
MediaAuthResolution.CAMERA,
camera="front_door",
)
self._assert(
"/clips/thumbs/back_door/abc.webp",
MediaAuthResolution.CAMERA,
camera="back_door",
)
def test_clip_previews(self):
self._assert("/clips/previews/", MediaAuthResolution.LISTING_MULTI_CAMERA)
self._assert(
"/clips/previews/front_door/",
MediaAuthResolution.CAMERA,
camera="front_door",
)
self._assert(
"/clips/previews/back_door/segment.mp4",
MediaAuthResolution.CAMERA,
camera="back_door",
)
def test_clip_review_thumbs(self):
# Format: /clips/review/thumb-{camera}-{review_id}.webp (frigate/review/maintainer.py).
self._assert(
"/clips/review/thumb-front_door-abc123.webp",
MediaAuthResolution.CAMERA,
camera="front_door",
)
# Hyphenated camera name — longest-prefix match.
self._assert(
"/clips/review/thumb-back-yard-abc123.webp",
MediaAuthResolution.CAMERA,
camera="back-yard",
)
# Unknown camera prefix → unresolved, not allowed for restricted users.
self._assert(
"/clips/review/thumb-unknown-cam-abc123.webp",
MediaAuthResolution.UNRESOLVED_MEDIA,
)
def test_clip_admin_only_subtrees(self):
self._assert("/clips/faces/train/foo.webp", MediaAuthResolution.ADMIN_ONLY)
self._assert("/clips/faces/", MediaAuthResolution.ADMIN_ONLY)
self._assert("/clips/genai-requests/x/0.webp", MediaAuthResolution.ADMIN_ONLY)
self._assert(
"/clips/preview_restart_cache/x.mp4", MediaAuthResolution.ADMIN_ONLY
)
self._assert("/clips/some_model/train/x.jpg", MediaAuthResolution.ADMIN_ONLY)
self._assert("/clips/some_model/dataset/x.jpg", MediaAuthResolution.ADMIN_ONLY)
def test_clip_unknown_subtree_is_unresolved(self):
# Unknown /clips/{x}/{y}/... subtree falls through as unresolved (not
# admin-only) so restricted users get 403 without admins being denied
# access to legitimate but unrecognized resources.
self._assert("/clips/random_dir/foo.jpg", MediaAuthResolution.UNRESOLVED_MEDIA)
def test_clip_top_level_listing(self):
self._assert("/clips/", MediaAuthResolution.LISTING_MULTI_CAMERA)
def test_exports_listing(self):
self._assert("/exports/", MediaAuthResolution.LISTING_MULTI_CAMERA)
class TestExportResolution(unittest.TestCase):
"""Export resolution requires a DB lookup."""
def setUp(self):
migrate_db = SqliteExtDatabase("test.db")
del logging.getLogger("peewee_migrate").handlers[:]
Router(migrate_db).run()
migrate_db.close()
self.db = SqliteQueueDatabase(TEST_DB)
self.db.bind([Event, ReviewSegment, Recordings, Export])
self.config = FrigateConfig(**_CONFIG)
def tearDown(self):
if not self.db.is_closed():
self.db.close()
for f in TEST_DB_CLEANUPS:
try:
os.remove(f)
except OSError:
pass
def _insert_export(self, export_id, camera, filename):
Export.insert(
id=export_id,
camera=camera,
name=f"export-{export_id}",
date=datetime.datetime.now(),
video_path=f"/media/frigate/exports/{filename}",
thumb_path=f"/media/frigate/exports/{filename}.jpg",
in_progress=False,
).execute()
def test_export_resolves_camera(self):
self._insert_export(
"exp1", "back_door", "back_door_20260511_140000-20260511_150000_abc123.mp4"
)
resolution, camera = resolve_media_uri(
"/exports/back_door_20260511_140000-20260511_150000_abc123.mp4",
self.config,
)
self.assertEqual(resolution, MediaAuthResolution.CAMERA)
self.assertEqual(camera, "back_door")
def test_unknown_export_is_unresolved(self):
# No matching row → UNRESOLVED_MEDIA (fail closed for restricted users),
# not UNKNOWN (which would pass-through).
resolution, camera = resolve_media_uri(
"/exports/does_not_exist.mp4", self.config
)
self.assertEqual(resolution, MediaAuthResolution.UNRESOLVED_MEDIA)
self.assertIsNone(camera)
def test_export_anchored_match_not_endswith(self):
# Anchored exact-path equality must NOT match by filename suffix.
# A request like /exports/clip.mp4 must not authorize against a row at
# /media/frigate/exports/back_door_clip.mp4 just because the suffix matches.
self._insert_export("exp_bd", "back_door", "back_door_clip.mp4")
self._insert_export("exp_fd", "front_door", "front_door_clip.mp4")
resolution, _ = resolve_media_uri("/exports/clip.mp4", self.config)
self.assertEqual(resolution, MediaAuthResolution.UNRESOLVED_MEDIA)
class TestDenyResponseForMediaUri(unittest.TestCase):
"""End-to-end decision check used by /auth."""
def setUp(self):
self.config = FrigateConfig(**_CONFIG)
def _deny(self, url, role):
return deny_response_for_media_uri(url, role, self.config)
def test_admin_always_allowed(self):
self.assertIsNone(self._deny("/clips/back_door-1.jpg", "admin"))
self.assertIsNone(self._deny("/clips/", "admin"))
self.assertIsNone(self._deny("/clips/faces/x.webp", "admin"))
self.assertIsNone(
self._deny("/recordings/2026-05-11/14/back_door/00.00.mp4", "admin")
)
def test_unrestricted_role_allowed(self):
# "viewer" role has no entry in roles_dict → full access (matches the
# behavior of require_camera_access).
self.assertIsNone(self._deny("/clips/back_door-1.jpg", "viewer"))
self.assertIsNone(self._deny("/clips/", "viewer"))
def test_restricted_role_allowed_camera(self):
self.assertIsNone(self._deny("/clips/front_door-1.jpg", "limited_user"))
self.assertIsNone(
self._deny("/recordings/2026-05-11/14/front_door/00.00.mp4", "limited_user")
)
self.assertIsNone(
self._deny("/clips/thumbs/front_door/abc.webp", "limited_user")
)
def test_restricted_role_blocked_other_camera(self):
self.assertEqual(self._deny("/clips/back_door-1.jpg", "limited_user"), 403)
self.assertEqual(
self._deny("/recordings/2026-05-11/14/back_door/00.00.mp4", "limited_user"),
403,
)
self.assertEqual(
self._deny("/clips/thumbs/back_door/abc.webp", "limited_user"), 403
)
def test_restricted_role_blocked_admin_only(self):
self.assertEqual(self._deny("/clips/faces/train/foo.webp", "limited_user"), 403)
def test_restricted_role_blocked_multi_camera_listing(self):
self.assertEqual(self._deny("/clips/", "limited_user"), 403)
self.assertEqual(self._deny("/exports/", "limited_user"), 403)
self.assertEqual(self._deny("/recordings/2026-05-11/14/", "limited_user"), 403)
def test_restricted_role_allowed_neutral_listing(self):
self.assertIsNone(self._deny("/recordings/", "limited_user"))
self.assertIsNone(self._deny("/recordings/2026-05-11/", "limited_user"))
def test_non_media_uri_passes_through(self):
self.assertIsNone(self._deny("/api/events", "limited_user"))
self.assertIsNone(self._deny("http://h/login", "limited_user"))
def test_missing_header(self):
self.assertIsNone(self._deny(None, "limited_user"))
self.assertIsNone(self._deny("", "limited_user"))
def test_traversal_in_media_uri_denied_for_all_roles(self):
# Bypass attempt: parts[3] looks like an allowed camera, but the
# normalized path nginx would serve points at a forbidden camera.
# Both restricted and admin should be denied — the URI is malformed
# and we refuse to make an auth decision against it.
traversal_uris = [
"/recordings/2026-05-11/14/front_door/../back_door/00.00.mp4",
"/clips/front_door-1.jpg/../back_door-1.jpg",
"/exports/../recordings/2026-05-11/14/back_door/00.00.mp4",
"/clips/./back_door-1.jpg",
]
for uri in traversal_uris:
self.assertEqual(self._deny(uri, "limited_user"), 403, uri)
self.assertEqual(self._deny(uri, "admin"), 403, uri)
self.assertEqual(self._deny(uri, "viewer"), 403, uri)
def test_traversal_outside_media_passes_through(self):
# `..` in non-media URIs is not our problem; the backend handles it.
self.assertIsNone(self._deny("/api/foo/../bar", "limited_user"))
def test_percent_encoded_traversal_denied(self):
# nginx may decode percent-encoded `%2E%2E` to `..` before serving;
# we must apply the same denial after percent-decoding.
self.assertEqual(
self._deny(
"/recordings/2026-05-11/14/front_door/%2E%2E/back_door/00.mp4",
"limited_user",
),
403,
)
def test_unresolved_media_fails_closed_for_restricted(self):
# Restricted user requesting a media URI we can't classify (no DB row,
# unknown clip prefix, unknown clip subtree) must be denied.
self.assertEqual(self._deny("/clips/nonexistent-1.jpg", "limited_user"), 403)
self.assertEqual(self._deny("/clips/random_dir/foo.jpg", "limited_user"), 403)
self.assertEqual(
self._deny("/clips/review/thumb-unknown_cam-1.webp", "limited_user"),
403,
)
def test_unresolved_media_allowed_for_admin(self):
# Admin and full-access roles are *not* denied on UNRESOLVED_MEDIA —
# nginx returns 404 if the file doesn't exist on disk anyway, and we
# don't want a stale DB to lock out admins.
self.assertIsNone(self._deny("/clips/nonexistent-1.jpg", "admin"))
self.assertIsNone(self._deny("/clips/nonexistent-1.jpg", "viewer"))
if __name__ == "__main__":
unittest.main()

View File

@ -531,8 +531,7 @@ class TrackedObject:
directory = os.path.join(THUMB_DIR, self.camera_config.name)
if not os.path.exists(directory):
os.makedirs(directory)
os.makedirs(directory, exist_ok=True)
thumb_bytes = self.get_thumbnail("webp")

View File

@ -492,7 +492,7 @@ def migrate_018_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]
genai = new_config.get("genai")
if genai and genai.get("provider"):
genai["roles"] = ["embeddings", "vision", "tools"]
genai["roles"] = ["embeddings", "descriptions", "chat"]
new_config["genai"] = {"default": genai}
# Remove deprecated sync_recordings from global record config

View File

@ -150,29 +150,51 @@ def extract_translations_from_schema(
# Handle anyOf cases
elif "anyOf" in field_schema:
for item in field_schema["anyOf"]:
nested = None
if item.get("type") == "null":
continue
if "properties" in item:
nested = extract_translations_from_schema(item, defs=defs)
elif "$ref" in item:
ref_path = item["$ref"]
if ref_path.startswith("#/$defs/"):
ref_name = ref_path.split("/")[-1]
if ref_name in defs:
nested = extract_translations_from_schema(
defs[ref_name], defs=defs
)
elif (
"additionalProperties" in item
and isinstance(item["additionalProperties"], dict)
and "$ref" in item["additionalProperties"]
):
ref_path = item["additionalProperties"]["$ref"]
if ref_path.startswith("#/$defs/"):
ref_name = ref_path.split("/")[-1]
if ref_name in defs:
nested = extract_translations_from_schema(
defs[ref_name], defs=defs
)
elif (
"items" in item
and isinstance(item["items"], dict)
and ("$ref" in item["items"])
):
ref_path = item["items"]["$ref"]
if ref_path.startswith("#/$defs/"):
ref_name = ref_path.split("/")[-1]
if ref_name in defs:
nested = extract_translations_from_schema(
defs[ref_name], defs=defs
)
if nested:
nested_without_root = {
k: v
for k, v in nested.items()
if k not in ("label", "description")
}
field_translations.update(nested_without_root)
elif "$ref" in item:
ref_path = item["$ref"]
if ref_path.startswith("#/$defs/"):
ref_name = ref_path.split("/")[-1]
if ref_name in defs:
ref_schema = defs[ref_name]
nested = extract_translations_from_schema(
ref_schema, defs=defs
)
nested_without_root = {
k: v
for k, v in nested.items()
if k not in ("label", "description")
}
field_translations.update(nested_without_root)
if field_translations:
translations[field_name] = field_translations

View File

@ -49,7 +49,8 @@
"gl": "Galego (Gallec)",
"id": "Bahasa Indonesia (Indonesi)",
"ur": "اردو (Urdú)",
"hr": "Hrvatski (croat)"
"hr": "Hrvatski (croat)",
"bs": "Bosanski (Bosni)"
},
"system": "Sistema",
"systemMetrics": "Mètriques del sistema",

View File

@ -121,5 +121,9 @@
"royal_mail": "Royal Mail",
"school_bus": "Bus escolar",
"skunk": "Mofeta",
"kangaroo": "Cangur"
"kangaroo": "Cangur",
"baby": "Nadó",
"baby_stroller": "Cotxet",
"rickshaw": "Ricksaw",
"Rodent": "Rosegador"
}

View File

@ -1661,7 +1661,9 @@
"options": {
"embeddings": "Incrustació",
"vision": "Visió",
"tools": "Eines"
"tools": "Eines",
"descriptions": "Descripcions",
"chat": "Xat"
}
},
"semanticSearchModel": {
@ -1886,5 +1888,55 @@
"semanticSearch": {
"jinav2SmallModelSize": "La mida 'petita' amb el model Jina V2 té un alt cost de RAM i d'inferència. Es recomana el model 'gran' amb una GPU discreta."
}
},
"modelSize": {
"large": "Gran",
"small": "Petit"
},
"birdseye": {
"trackingMode": {
"objects": "Objectes",
"motion": "Moviment",
"continuous": "Continu"
}
},
"snapshot": {
"retainMode": {
"all": "Tots",
"motion": "Moviment",
"active_objects": "Objectes Actius"
}
},
"ui": {
"timeFormat": {
"browser": "Visor",
"12hour": "12 hores",
"24hour": "24 hores"
},
"TimeOrDateStyle": {
"full": "Complet",
"long": "Llarg",
"medium": "Mitjà",
"short": "Curt"
},
"unitSystem": {
"metric": "Métric",
"imperial": "Imperial"
}
},
"review": {
"imageSource": {
"recordings": "Gravacions",
"previews": "Previsualitzacions"
}
},
"logger": {
"logLevel": {
"debug": "Depurar",
"info": "Informació",
"warning": "Avís",
"error": "Error",
"critical": "Crític"
}
}
}

View File

@ -33,7 +33,11 @@
},
"filters": {
"label": "Audio filters",
"description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives."
"description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives.",
"threshold": {
"label": "Minimum audio confidence",
"description": "Minimum confidence threshold for the audio event to be counted."
}
},
"enabled_in_config": {
"label": "Original audio state",

View File

@ -559,7 +559,11 @@
},
"filters": {
"label": "Audio filters",
"description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives."
"description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives.",
"threshold": {
"label": "Minimum audio confidence",
"description": "Minimum confidence threshold for the audio event to be counted."
}
},
"enabled_in_config": {
"label": "Original audio state",

View File

@ -32,7 +32,11 @@
"title": "Recent Recognitions",
"titleShort": "Recent",
"aria": "Select recent recognitions",
"empty": "There are no recent face recognition attempts"
"empty": "There are no recent face recognition attempts",
"emptyNoLibrary": {
"title": "Upload a face",
"description": "You must add at least one face to the library for face recognition to function."
}
},
"deleteFaceLibrary": {
"title": "Delete Name",

View File

@ -446,6 +446,7 @@
},
"cameraManagement": {
"title": "Manage Cameras",
"description": "Add, edit, and delete cameras, control which cameras are enabled, and configure per-profile and camera type overrides. To configure streams, detection, motion, and other camera-specific settings, choose the specific section under Camera Configuration.",
"addCamera": "Add New Camera",
"deleteCamera": "Delete Camera",
"deleteCameraDialog": {
@ -1127,8 +1128,16 @@
"cameras": "Cameras",
"loading": "Loading model information…",
"error": "Failed to load model information",
"noModelLoaded": "No Frigate+ model is currently loaded.",
"availableModels": "Available Models",
"loadingAvailableModels": "Loading available models…",
"selectModel": "Select a model",
"noModelsAvailable": "No models available",
"filter": {
"ariaLabel": "Filter models by type",
"baseModels": "Base Models",
"fineTunedModels": "Fine-tuned Models"
},
"modelSelect": "Your available models on Frigate+ can be selected here. Note that only models compatible with your current detector configuration can be selected."
},
"unsavedChanges": "Unsaved Frigate+ settings changes",
@ -1744,4 +1753,4 @@
"jinav2SmallModelSize": "The 'small' size with the Jina V2 model has high RAM and inference cost. The 'large' model with a discrete GPU is recommended."
}
}
}
}

View File

@ -155,7 +155,7 @@
"id": "Bahasa Indonesia (Indonesio)",
"ur": "اردو (Urdu)",
"hr": "Hrvatski (Croata)",
"bs": "Añadir transmisión"
"bs": "Bosanski (Bosnio)"
},
"appearance": "Apariencia",
"darkMode": {
@ -198,9 +198,9 @@
"faceLibrary": "Biblioteca de rostros",
"classification": "Clasificación",
"profiles": "Perfiles",
"actions": "Añadir rol",
"features": "Añadir preajuste",
"chat": "Añadir rostro"
"actions": "Acciones",
"features": "Funciones",
"chat": "Chat"
},
"unit": {
"speed": {
@ -262,13 +262,13 @@
"undo": "Deshacer",
"copiedToClipboard": "Copiado al portapapeles",
"modified": "Modificado",
"overridden": "Guardar y reiniciar",
"resetToGlobal": "Restablecer todo",
"resetToDefault": "Añadir máscara",
"saveAll": "Añadir zona",
"savingAll": "Guardando todo.…",
"undoAll": "Añadir ajuste de movimiento",
"retry": "Añadir grupo de cámaras"
"overridden": "Sobrescrito",
"resetToGlobal": "Restablecer a global",
"resetToDefault": "Restablecer valores predeterminados",
"saveAll": "Guardar todo",
"savingAll": "Guardando todo…",
"undoAll": "Deshacer todo",
"retry": "Reintentar"
},
"toast": {
"save": {
@ -277,7 +277,7 @@
"title": "No se pudieron guardar los cambios de configuración: {{errorMessage}}"
},
"title": "Guardar",
"success": "Los cambios de configuración se han guardado correctamente."
"success": "Cambios de configuración guardados correctamente."
},
"copyUrlToClipboard": "URL copiada al portapapeles."
},
@ -332,6 +332,6 @@
"optional": "Opcional",
"internalID": "La ID interna que usa Frigate en la configuración y en la base de datos"
},
"no_items": "Añadir archivo",
"validation_errors": "Añadir usuario"
"no_items": "No hay elementos",
"validation_errors": "Errores de validación"
}

View File

@ -30,5 +30,17 @@
"no_similar_objects_found": "No se encontraron objetos similares.",
"semantic_search_required": "La búsqueda semántica debe estar activada para encontrar objetos similares.",
"send": "Enviar",
"suggested_requests": "Prueba preguntando:"
"suggested_requests": "Prueba preguntando:",
"starting_requests": {
"show_recent_events": "Mostrar eventos recientes",
"show_camera_status": "Mostrar estado de la cámara",
"recap": "¿Qué ha pasado mientras estaba fuera?",
"watch_camera": "Vigilar una cámara en busca de actividad"
},
"starting_requests_prompts": {
"show_recent_events": "Muéstrame los eventos recientes de la última hora",
"show_camera_status": "¿Cuál es el estado actual de mis cámaras?",
"recap": "¿Qué ha pasado mientras estaba fuera?",
"watch_camera": "Vigila la puerta principal y avísame si aparece alguien"
}
}

View File

@ -68,5 +68,28 @@
"select_all": "Todas",
"normalActivity": "Normal",
"needsReview": "Necesita revisión",
"securityConcern": "Aviso de seguridad"
"securityConcern": "Aviso de seguridad",
"motionSearch": {
"menuItem": "Búsqueda de movimiento",
"openMenu": "Opciones de cámara"
},
"motionPreviews": {
"menuItem": "Ver vistas previas de movimiento",
"title": "Vistas previas de movimiento: {{camera}}",
"mobileSettingsTitle": "Ajustes de vistas previas de movimiento",
"mobileSettingsDesc": "Ajusta la velocidad de reproducción y el atenuado, y elige una fecha para revisar clips solo de movimiento.",
"dim": "Atenuar",
"dimAria": "Ajustar intensidad de atenuado",
"dimDesc": "Aumenta el atenuado para mejorar la visibilidad de las áreas con movimiento.",
"speed": "Velocidad",
"speedAria": "Seleccionar velocidad de reproducción de las vistas previas",
"speedDesc": "Elige la velocidad a la que se reproducen los clips de vista previa.",
"back": "Atrás",
"empty": "No hay vistas previas disponibles",
"noPreview": "Vista previa no disponible",
"seekAria": "Mover el reproductor de {{camera}} a {{time}}",
"filter": "Filtrar",
"filterDesc": "Selecciona áreas para mostrar solo clips con movimiento en esas regiones.",
"filterClear": "Limpiar"
}
}

View File

@ -51,5 +51,78 @@
"descKeepExports": "Las exportaciones seguirán disponibles como exportaciones sin categoría.",
"descDeleteExports": "Todas las exportaciones de este caso se eliminarán de forma permanente.",
"deleteExports": "Eliminar también las exportaciones"
},
"caseCard": {
"emptyCase": "Aún no hay exportaciones"
},
"jobCard": {
"defaultName": "Exportación de {{camera}}",
"queued": "En cola",
"running": "En ejecución",
"preparing": "Preparando",
"copying": "Copiando",
"encoding": "Codificando",
"encodingRetry": "Codificando (reintento)",
"finalizing": "Finalizando"
},
"caseView": {
"noDescription": "Sin descripción",
"createdAt": "Creado {{value}}",
"exportCount_one": "1 exportación",
"exportCount_other": "{{count}} exportaciones",
"cameraCount_one": "1 cámara",
"cameraCount_other": "{{count}} cámaras",
"showMore": "Mostrar más",
"showLess": "Mostrar menos",
"emptyTitle": "Este caso está vacío",
"emptyDescription": "Añade exportaciones existentes sin categorizar para mantener el caso organizado.",
"emptyDescriptionNoExports": "Todavía no hay exportaciones sin categorizar disponibles para añadir."
},
"caseEditor": {
"createTitle": "Crear caso",
"editTitle": "Editar caso",
"namePlaceholder": "Nombre del caso",
"descriptionPlaceholder": "Añade notas o contexto para este caso"
},
"addExportDialog": {
"title": "Añadir exportación a {{caseName}}",
"searchPlaceholder": "Buscar exportaciones sin categorizar",
"empty": "Ninguna exportación sin categorizar coincide con esta búsqueda.",
"addButton_one": "Añadir 1 exportación",
"addButton_other": "Añadir {{count}} exportaciones",
"adding": "Añadiendo..."
},
"selected_one": "{{count}} seleccionados",
"selected_other": "{{count}} seleccionados",
"bulkActions": {
"addToCase": "Añadir al caso",
"moveToCase": "Mover al caso",
"removeFromCase": "Eliminar del caso",
"delete": "Eliminar",
"deleteNow": "Eliminar ahora"
},
"bulkDelete": {
"title": "Eliminar exportaciones",
"desc_one": "¿Seguro que quieres eliminar {{count}} exportación?",
"desc_other": "¿Seguro que quieres eliminar {{count}} exportaciones?"
},
"bulkRemoveFromCase": {
"title": "Eliminar del caso",
"desc_one": "¿Eliminar {{count}} exportación de este caso?",
"desc_other": "¿Eliminar {{count}} exportaciones de este caso?",
"descKeepExports": "Las exportaciones se moverán a sin categorizar.",
"descDeleteExports": "Las exportaciones se eliminarán permanentemente.",
"deleteExports": "Eliminar exportaciones en su lugar"
},
"bulkToast": {
"success": {
"delete": "Exportaciones eliminadas correctamente",
"reassign": "Asignación de caso actualizada correctamente",
"remove": "Exportaciones eliminadas del caso correctamente"
},
"error": {
"deleteFailed": "No se pudieron eliminar las exportaciones: {{errorMessage}}",
"reassignFailed": "No se pudo actualizar la asignación del caso: {{errorMessage}}"
}
}
}

View File

@ -40,6 +40,38 @@
"end": "Hora de finalización"
},
"settings": {
"title": "Ajustes de búsqueda"
"title": "Ajustes de búsqueda",
"parallelMode": "Modo paralelo",
"parallelModeDesc": "Analiza varios segmentos de grabación al mismo tiempo (más rápido, pero consume significativamente más CPU)",
"threshold": "Umbral de sensibilidad",
"thresholdDesc": "Los valores más bajos detectan cambios más pequeños (1-255)",
"minArea": "Área mínima de cambio",
"minAreaDesc": "Porcentaje mínimo de la región de interés que debe cambiar para considerarse significativo",
"frameSkip": "Salto de fotogramas",
"frameSkipDesc": "Procesa cada N fotogramas. Establécelo según la tasa de FPS de tu cámara para procesar un fotograma por segundo (p. ej., 5 para una cámara de 5 FPS, 30 para una cámara de 30 FPS). Los valores más altos serán más rápidos, pero pueden omitir eventos de movimiento breves.",
"maxResults": "Resultados máximos",
"maxResultsDesc": "Detener después de esta cantidad de marcas de tiempo coincidentes"
},
"errors": {
"noCamera": "Selecciona una cámara",
"noROI": "Dibuja una región de interés",
"noTimeRange": "Selecciona un rango de tiempo",
"invalidTimeRange": "La hora de fin debe ser posterior a la hora de inicio",
"searchFailed": "La búsqueda falló: {{message}}",
"polygonTooSmall": "El polígono debe tener al menos 3 puntos",
"unknown": "Error desconocido"
},
"changePercentage": "{{percentage}}% cambiado",
"metrics": {
"title": "Métricas de búsqueda",
"segmentsScanned": "Segmentos analizados",
"segmentsProcessed": "Procesado",
"segmentsSkippedInactive": "Omitido (sin actividad)",
"segmentsSkippedHeatmap": "Omitido (sin superposición de ROI)",
"fallbackFullRange": "Análisis completo de respaldo",
"framesDecoded": "Fotogramas decodificados",
"wallTime": "Tiempo de búsqueda",
"segmentErrors": "Errores de segmento",
"seconds": "{{seconds}} s"
}
}

View File

@ -38,6 +38,22 @@
},
"sourceCamera": "Cámara de origen",
"replayCamera": "Cámara de reproducción",
"initializingReplay": "Inicializando reproducción de depuración…"
"initializingReplay": "Inicializando reproducción de depuración…",
"stoppingReplay": "Deteniendo repetición de depuración...",
"stopReplay": "Detener repetición",
"confirmStop": {
"title": "¿Detener repetición de depuración?",
"description": "Esto detendrá la sesión y eliminará todos los datos temporales. ¿Estás seguro?",
"confirm": "Detener repetición",
"cancel": "Cancelar"
},
"activity": "Actividad",
"objects": "Lista de objetos",
"audioDetections": "Detecciones de audio",
"noActivity": "No se detectó actividad",
"activeTracking": "Seguimiento activo",
"noActiveTracking": "No hay seguimiento activo",
"configuration": "Configuración",
"configurationDesc": "Ajusta con precisión la detección de movimiento y los ajustes de seguimiento de objetos para la cámara de repetición de depuración. No se guardará ningún cambio en el archivo de configuración de Frigate."
}
}

View File

@ -121,5 +121,9 @@
"royal_mail": "Poste du Royaume Uni",
"school_bus": "Bus scolaire",
"skunk": "Mouffette",
"kangaroo": "Kangourou"
"kangaroo": "Kangourou",
"baby": "Bébé",
"baby_stroller": "Poussette",
"rickshaw": "Pousse-pousse",
"Rodent": "Rongeur"
}

View File

@ -3,6 +3,31 @@
"description": "Rejouer les enregistrement de la camera, à but de débogage. La liste d'objets montre un résumé avec retard des objets détectés; et l'onglet Messages montre le flux des messages internes à Frigate liés à la vidéo rejouée.",
"websocket_messages": "Messages",
"dialog": {
"title": "Démarrer le Rejeu-Debogage"
"title": "Démarrer le Rejeu-Debogage",
"timeRange": "Intervalle",
"preset": {
"1m": "Dernière minute",
"5m": "5 dernières minutes",
"timeline": "Depuis la chronologie",
"custom": "Personnalisé"
},
"startButton": "Démarrer le revisionnage",
"selectFromTimeline": "Sélectionner",
"starting": "Démarrage du revisionnage...",
"startLabel": "Démarrer",
"endLabel": "Fin",
"toast": {
"error": "Echec du démarrage du revisionnage de déboggage : {{error}}",
"alreadyActive": "Une session de revisionnage est déjà active",
"stopError": "Echec de l'arrêt du revisionnage de déboggage : {{error}}",
"goToReplay": "Vers le revisionnage"
}
},
"page": {
"noSession": "Aucune session de revisionnage de déboggage active",
"noSessionDesc": "Démarrer un revisionnage de déboggage depuis l'Historique en cliquant sur le boutons Actions dans la barre d'outils et choisir Revisionnage de déboggage.",
"goToRecordings": "Vers l'historique",
"preparingClip": "Préparation du clip…",
"preparingClipDesc": "Frigate est encore en train de recoller les enregistrements pour l'intervalle de temps sélectionnée. Cela peut prendre une minute pour les plus longues intervalles."
}
}

View File

@ -6,5 +6,16 @@
"bellow": "तलतिर",
"motorcycle": "मोटरसाइकल",
"whoop": "हुप (Whoop)",
"whispering": "सानो बोल्दै"
"whispering": "सानो बोल्दै",
"babbling": "बडबडाउँदै",
"bus": "बस",
"laughter": "हाँसो",
"train": "रेल",
"snicker": "स्निकर",
"boat": "डुङ्गा",
"crying": "रुँदै",
"singing": "गाउँदै",
"choir": "गायन यन्त्र",
"yodeling": "योडेलिङ",
"chant": "मन्त्र"
}

View File

@ -3,6 +3,16 @@
"untilForRestart": "फ्रिगेट पुनः सुरु नभएसम्म।",
"untilRestart": "पुन: सुरु नभएसम्म",
"never": "कहिल्यै होइन",
"ago": "{{timeAgo}} अघि"
"ago": "{{timeAgo}} अघि",
"untilForTime": "{{time}} सम्म",
"justNow": "भर्खरै",
"today": "आज",
"yesterday": "हिजो",
"last7": "पछिल्लो ७ दिन",
"last14": "पछिल्लो १४ दिन",
"last30": "पछिल्लो ३० दिन",
"thisWeek": "यो हप्ता",
"lastWeek": "गत हप्ता",
"thisMonth": "यो महिना"
}
}

View File

@ -5,7 +5,12 @@
"login": "लगइन",
"firstTimeLogin": "पहिलो पटक लग इन गर्ने प्रयास गर्दै हुनुहुन्छ? प्रमाणपत्रहरू फ्रिगेट लगहरूमा छापिएका हुन्छन्।",
"errors": {
"usernameRequired": "प्रयोगकर्ता नाम आवश्यक छ"
"usernameRequired": "प्रयोगकर्ता नाम आवश्यक छ",
"passwordRequired": "पासवर्ड आवश्यक छ",
"rateLimit": "दर सीमा नाघ्यो। पछि फेरि प्रयास गर्नुहोस्।",
"loginFailed": "लगइन असफल भयो",
"unknownError": "अज्ञात त्रुटि। लगहरू जाँच गर्नुहोस्",
"webUnknownError": "अज्ञात त्रुटि। कन्सोल लगहरू जाँच गर्नुहोस्।"
}
}
}

View File

@ -6,8 +6,23 @@
"delete": {
"label": "क्यामेरा समूह मेटाउनुहोस्",
"confirm": {
"title": "मेटाउने पुष्टि गर्नुहोस्"
"title": "मेटाउने पुष्टि गर्नुहोस्",
"desc": "के तपाईं क्यामेरा समूह <em>{{name}}</em> मेटाउन निश्चित हुनुहुन्छ?"
}
},
"name": {
"label": "नाम",
"placeholder": "नाम प्रविष्ट गर्नुहोस्…",
"errorMessage": {
"mustLeastCharacters": "क्यामेरा समूहको नाम कम्तिमा २ वर्णको हुनुपर्छ।",
"exists": "क्यामेरा समूहको नाम पहिले नै अवस्थित छ।",
"nameMustNotPeriod": "क्यामेरा समूहको नाममा पूर्णविराम हुनुहुँदैन।",
"invalid": "क्यामेरा समूहको नाम अमान्य छ।"
}
},
"cameras": {
"label": "क्यामेराहरू",
"desc": "यस समूहको लागि क्यामेराहरू चयन गर्नुहोस्।"
}
}
}

View File

@ -5,7 +5,30 @@
"button": "पुनः सुरु",
"restarting": {
"title": "फ्रिगेट पुन: सुरु हुँदैछ",
"content": "यो पृष्ठ {{countdown}} सेकेन्डमा पुन: लोड हुनेछ।"
"content": "यो पृष्ठ {{countdown}} सेकेन्डमा पुन: लोड हुनेछ।",
"button": "अहिले नै जबरजस्ती पुन: लोड गर्नुहोस्"
}
},
"explore": {
"plus": {
"submitToPlus": {
"label": "फ्रिगेट+ मा पेश गर्नुहोस्",
"desc": "तपाईंले बेवास्ता गर्न चाहनुभएको स्थानहरूमा रहेका वस्तुहरू गलत सकारात्मक होइनन्। तिनीहरूलाई गलत सकारात्मकको रूपमा पेश गर्नाले मोडेल भ्रमित हुनेछ।"
},
"review": {
"question": {
"label": "फ्रिगेट प्लसको लागि यो लेबल पुष्टि गर्नुहोस्",
"ask_a": "के यो वस्तु <code>{{label}}</code> हो?",
"ask_an": "के यो वस्तु <code>{{label}}</code> हो?",
"ask_full": "के यो वस्तु <code>{{untranslatedLabel}}</code> ({{translatedLabel}}) हो?"
},
"state": {
"submitted": "पेश गरियो"
}
}
},
"video": {
"viewInHistory": "इतिहासमा हेर्नुहोस्"
}
}
}

View File

@ -7,5 +7,24 @@
},
"count_one": "{{count}} कक्षा",
"count_other": "{{count}} कक्षाहरू"
},
"labels": {
"label": "लेबलहरू",
"all": {
"title": "सबै लेबलहरू",
"short": "लेबलहरू"
},
"count_one": "{{count}} लेबल",
"count_other": "{{count}} लेबलहरू"
},
"zones": {
"label": "क्षेत्रहरू",
"all": {
"title": "सबै क्षेत्रहरू",
"short": "क्षेत्रहरू"
}
},
"dates": {
"selectPreset": "प्रिसेट चयन गर्नुहोस्…"
}
}

View File

@ -5,5 +5,22 @@
"title": "यो फ्रेम Frigate+ मा बुझाउने हो?",
"submit": "पेश गर्नुहोस्",
"previewError": "स्न्यापसट पूर्वावलोकन लोड गर्न सकिएन। रेकर्डिङ यस समयमा उपलब्ध नहुन सक्छ।"
},
"noRecordingsFoundForThisTime": "यस समयको लागि कुनै रेकर्डिङ फेला परेन",
"livePlayerRequiredIOSVersion": "यस लाइभ स्ट्रिम प्रकारको लागि iOS १७.१ वा सोभन्दा माथिको संस्करण आवश्यक छ।",
"streamOffline": {
"title": "अफलाइन स्ट्रिम गर्नुहोस्",
"desc": "{{cameraName}} <code>detect</code> स्ट्रिममा कुनै पनि फ्रेमहरू प्राप्त भएका छैनन्, त्रुटि लगहरू जाँच गर्नुहोस्"
},
"cameraDisabled": "क्यामेरा असक्षम पारिएको छ",
"stats": {
"streamType": {
"title": "स्ट्रिम प्रकार:",
"short": "प्रकार"
},
"bandwidth": {
"title": "ब्यान्डविथ:",
"short": "ब्यान्डविथ"
}
}
}

View File

@ -7,5 +7,27 @@
"friendly_name": {
"label": "मैत्रीपूर्ण नाम",
"description": "फ्रिगेट UI मा प्रयोग गरिएको क्यामेरा मैत्री नाम"
},
"enabled": {
"label": "सक्षम पारिएको",
"description": "सक्षम पारिएको"
},
"audio": {
"label": "अडियो पत्ता लगाउने सुविधा",
"description": "यस क्यामेराको लागि अडियो-आधारित घटना पत्ता लगाउने सेटिङहरू।",
"enabled": {
"label": "अडियो पत्ता लगाउने सुविधा सक्षम पार्नुहोस्",
"description": "यस क्यामेराको लागि अडियो घटना पत्ता लगाउने सुविधा सक्षम वा असक्षम पार्नुहोस्।"
},
"max_not_heard": {
"label": "समयसीमा समाप्त गर्नुहोस्",
"description": "अडियो घटना समाप्त हुनुभन्दा पहिले कन्फिगर गरिएको अडियो प्रकार बिना सेकेन्डको मात्रा।"
},
"min_volume": {
"label": "न्यूनतम भोल्युम"
}
},
"zones": {
"label": "क्षेत्रहरू"
}
}

View File

@ -1 +1,42 @@
{}
{
"version": {
"label": "हालको कन्फिगरेसन संस्करण",
"description": "माइग्रेसन वा ढाँचा परिवर्तनहरू पत्ता लगाउन मद्दत गर्न सक्रिय कन्फिगरेसनको संख्यात्मक वा स्ट्रिङ संस्करण।"
},
"safe_mode": {
"label": "सुरक्षित मोड",
"description": "सक्षम हुँदा, समस्या निवारणको लागि कम सुविधाहरूको साथ सुरक्षित मोडमा फ्रिगेट सुरु गर्नुहोस्।"
},
"environment_vars": {
"label": "वातावरणीय चरहरू",
"description": "होम असिस्टेन्ट ओएसमा फ्रिगेट प्रक्रियाको लागि सेट गर्नुपर्ने वातावरण चरहरूको कुञ्जी/मान जोडीहरू। गैर-HAOS प्रयोगकर्ताहरूले यसको सट्टा डकर वातावरण चर कन्फिगरेसन प्रयोग गर्नुपर्छ।"
},
"logger": {
"label": "लगिङ",
"description": "पूर्वनिर्धारित लग शब्दावली र प्रति-घटक लग स्तर ओभरराइडहरू नियन्त्रण गर्दछ।",
"default": {
"label": "लगिङ स्तर",
"description": "पूर्वनिर्धारित विश्वव्यापी लग शब्दावली (डिबग, जानकारी, चेतावनी, त्रुटि)।"
},
"logs": {
"label": "प्रति-प्रक्रिया लग स्तर",
"description": "विशिष्ट मोड्युलहरूको लागि शब्दावली बढाउन वा घटाउन प्रति-घटक लग स्तर ओभरराइड हुन्छ।"
}
},
"audio": {
"label": "अडियो पत्ता लगाउने सुविधा",
"enabled": {
"label": "अडियो पत्ता लगाउने सुविधा सक्षम पार्नुहोस्"
},
"max_not_heard": {
"label": "समयसीमा समाप्त गर्नुहोस्",
"description": "अडियो घटना समाप्त हुनुभन्दा पहिले कन्फिगर गरिएको अडियो प्रकार बिना सेकेन्डको मात्रा।"
},
"min_volume": {
"label": "न्यूनतम भोल्युम"
}
},
"auth": {
"label": "प्रमाणीकरण"
}
}

View File

@ -12,6 +12,33 @@
"timestamp_style": {
"global": {
"appearance": "विश्वव्यापी उपस्थिति"
},
"cameras": {
"appearance": "उपस्थिति"
}
},
"motion": {
"global": {
"sensitivity": "विश्वव्यापी संवेदनशीलता",
"algorithm": "विश्वव्यापी एल्गोरिथम"
},
"cameras": {
"sensitivity": "संवेदनशीलता",
"algorithm": "एल्गोरिथ्म"
}
},
"snapshots": {
"global": {
"display": "विश्वव्यापी प्रदर्शन"
},
"cameras": {
"display": "प्रदर्शन"
}
},
"detect": {
"global": {
"resolution": "विश्वव्यापी रिजोल्युसन",
"tracking": "विश्वव्यापी ट्र्याकिङ"
}
}
}

View File

@ -3,5 +3,14 @@
"maximum": "बढीमा हुनुपर्छ {{limit}}",
"exclusiveMinimum": "{{limit}} भन्दा बढी हुनुपर्छ",
"exclusiveMaximum": ".{{limit}} भन्दा कम हुनुपर्छ",
"minLength": "कम्तिमा {{limit}} वर्ण(हरू) हुनुपर्छ।"
"minLength": "कम्तिमा {{limit}} वर्ण(हरू) हुनुपर्छ।",
"maxLength": "बढीमा {{limit}} वर्ण(हरू) हुनु पर्छ",
"minItems": "कम्तिमा {{limit}} वस्तुहरू हुनुपर्छ",
"maxItems": "बढीमा {{limit}} वस्तुहरू हुनुपर्छ",
"pattern": "अमान्य ढाँचा",
"required": "यो क्षेत्र आवश्यक छ",
"type": "अमान्य मान प्रकार",
"enum": "अनुमति दिइएको मानहरू मध्ये एक हुनुपर्छ",
"const": "मान अपेक्षित स्थिरांकसँग मेल खाँदैन",
"uniqueItems": "सबै वस्तुहरू अद्वितीय हुनुपर्छ"
}

View File

@ -3,5 +3,14 @@
"bicycle": "साइकल",
"car": "कार",
"motorcycle": "मोटरसाइकल",
"airplane": "हवाइजहाज"
"airplane": "हवाइजहाज",
"bus": "बस",
"train": "रेल",
"boat": "डुङ्गा",
"traffic_light": "ट्राफिक लाइट",
"fire_hydrant": "आगो निभाउने यन्त्र",
"street_sign": "सडक चिन्ह",
"stop_sign": "रोक चिन्ह",
"parking_meter": "पार्किङ मिटर",
"bench": "बेन्च"
}

View File

@ -3,5 +3,13 @@
"title": "फ्रिगेट च्याट",
"subtitle": "क्यामेरा व्यवस्थापन र अन्तर्दृष्टिको लागि तपाईंको एआई सहायक",
"placeholder": "सोध्नुहोस्...",
"error": "केही गडबड भयो। कृपया फेरि प्रयास गर्नुहोस्।"
"error": "केही गडबड भयो। कृपया फेरि प्रयास गर्नुहोस्।",
"processing": "प्रशोधन गर्दै...",
"toolsUsed": "प्रयोग गरिएको: {{tools}}",
"showTools": "उपकरणहरू देखाउनुहोस् ({{count}})",
"hideTools": "उपकरणहरू लुकाउनुहोस्",
"call": "कल गर्नुहोस्",
"result": "नतिजा",
"arguments": "तर्कहरू:",
"response": "प्रतिक्रिया:"
}

View File

@ -10,6 +10,16 @@
},
"button": {
"deleteClassificationAttempts": "वर्गीकरण छविहरू मेटाउनुहोस्",
"renameCategory": "वर्गको नाम बदल्नुहोस्"
"renameCategory": "वर्गको नाम बदल्नुहोस्",
"deleteCategory": "कक्षा मेटाउनुहोस्",
"deleteImages": "छविहरू मेटाउनुहोस्",
"trainModel": "रेल मोडेल",
"addClassification": "वर्गीकरण थप्नुहोस्",
"deleteModels": "मोडेलहरू मेटाउनुहोस्",
"editModel": "मोडेल सम्पादन गर्नुहोस्"
},
"tooltip": {
"trainingInProgress": "मोडेल हाल प्रशिक्षणमा छिन्",
"noNewImages": "तालिम दिनको लागि कुनै नयाँ तस्बिरहरू छैनन्। पहिले डेटासेटमा थप तस्बिरहरू वर्गीकृत गर्नुहोस्।"
}
}

View File

@ -3,5 +3,16 @@
"configEditor": "कन्फिग सम्पादक",
"safeConfigEditor": "कन्फिग सम्पादक (सुरक्षित मोड)",
"safeModeDescription": "कन्फिग प्रमाणीकरण त्रुटिको कारणले फ्रिगेट सुरक्षित मोडमा छ।",
"copyConfig": "कन्फिग प्रतिलिपि गर्नुहोस्"
"copyConfig": "कन्फिग प्रतिलिपि गर्नुहोस्",
"saveAndRestart": "बचत गर्नुहोस् र पुन: सुरु गर्नुहोस्",
"saveOnly": "बचत मात्र",
"confirm": "बचत नगरी बाहिर निस्कने हो?",
"toast": {
"success": {
"copyToClipboard": "कन्फिगरेसन क्लिपबोर्डमा प्रतिलिपि गरियो।"
},
"error": {
"savingError": "कन्फिगरेसन बचत गर्दा त्रुटि भयो"
}
}
}

View File

@ -5,5 +5,19 @@
"label": "गति",
"only": "गति मात्र"
},
"allCameras": "सबै क्यामेराहरू"
"allCameras": "सबै क्यामेराहरू",
"empty": {
"alert": "समीक्षा गर्न कुनै अलर्टहरू छैनन्",
"detection": "समीक्षा गर्न कुनै पनि पत्ता लगाइएको छैन",
"motion": "गतिसम्बन्धी कुनै डेटा फेला परेन",
"recordingsDisabled": {
"title": "रेकर्डिङहरू सक्षम पारिएको हुनुपर्छ",
"description": "क्यामेराको लागि रेकर्डिङ सक्षम पारिएको बेला मात्र समीक्षा वस्तुहरू सिर्जना गर्न सकिन्छ।"
}
},
"timeline": {
"label": "समयरेखा",
"aria": "टाइमलाइन चयन गर्नुहोस्"
},
"zoomIn": "जुम इन गर्नुहोस्"
}

View File

@ -1,5 +1,28 @@
{
"details": {
"timestamp": "टाइमस्ट्याम्प"
},
"documentTitle": "अन्वेषण गर्नुहोस् - फ्रिगेट",
"generativeAI": "जेनेरेटिभ एआई",
"exploreMore": "थप {{label}} वस्तुहरू अन्वेषण गर्नुहोस्",
"exploreIsUnavailable": {
"title": "अन्वेषण उपलब्ध छैन",
"embeddingsReindexing": {
"context": "ट्र्याक गरिएका वस्तु इम्बेडिङहरूले पुन: अनुक्रमणिका समाप्त गरेपछि अन्वेषण प्रयोग गर्न सकिन्छ।",
"startingUp": "सुरु गर्दै…",
"estimatedTime": "अनुमानित बाँकी समय:",
"finishingShortly": "चाँडै नै समाप्त हुँदैछ",
"step": {
"thumbnailsEmbedded": "इम्बेड गरिएका थम्बनेलहरू: ",
"descriptionsEmbedded": "इम्बेड गरिएका विवरणहरू: ",
"trackedObjectsProcessed": "ट्र्याक गरिएका वस्तुहरू प्रशोधन गरियो: "
}
},
"downloadingModels": {
"context": "फ्रिगेटले सिमान्टिक खोज सुविधालाई समर्थन गर्न आवश्यक इम्बेडिङ मोडेलहरू डाउनलोड गर्दैछ। तपाईंको नेटवर्क जडानको गतिमा निर्भर गर्दै यसले धेरै मिनेट लिन सक्छ।",
"setup": {
"visionModel": "भिजन मोडेल"
}
}
}
}

View File

@ -4,5 +4,21 @@
"headings": {
"cases": "केसहरू",
"uncategorizedExports": "वर्गीकृत नगरिएका निर्यातहरू"
},
"documentTitle": "निर्यात - फ्रिगेट",
"deleteExport": {
"label": "निर्यात मेटाउनुहोस्",
"desc": "के तपाईं {{exportName}} मेटाउन चाहनुहुन्छ?"
},
"editExport": {
"title": "निर्यातको नाम बदल्नुहोस्",
"desc": "यो निर्यातको लागि नयाँ नाम प्रविष्ट गर्नुहोस्।",
"saveExport": "निर्यात बचत गर्नुहोस्"
},
"tooltip": {
"shareExport": "निर्यात सेयर गर्नुहोस्",
"downloadVideo": "भिडियो डाउनलोड गर्नुहोस्",
"editName": "नाम सम्पादन गर्नुहोस्",
"deleteExport": "निर्यात मेटाउनुहोस्"
}
}

View File

@ -7,6 +7,20 @@
},
"details": {
"unknown": "अज्ञात",
"timestamp": "टाइमस्ट्याम्प"
"timestamp": "टाइमस्ट्याम्प",
"scoreInfo": "स्कोर भनेको सबै अनुहारको स्कोरको भारित औसत हो, जुन प्रत्येक छविमा अनुहारको आकारद्वारा भारित हुन्छ।"
},
"documentTitle": "फेस लाइब्रेरी - फ्रिगेट",
"uploadFaceImage": {
"title": "अनुहारको छवि अपलोड गर्नुहोस्",
"desc": "अनुहारहरू स्क्यान गर्न र {{pageToggle}} को लागि समावेश गर्न एउटा छवि अपलोड गर्नुहोस्"
},
"collections": "सङ्ग्रहहरू",
"createFaceLibrary": {
"new": "नयाँ अनुहार सिर्जना गर्नुहोस्",
"nextSteps": "बलियो जग निर्माण गर्न:<li>प्रत्येक पत्ता लागेको व्यक्तिको लागि छविहरू चयन गर्न र तालिम दिन हालसालैको पहिचान ट्याब प्रयोग गर्नुहोस्।</li><li>उत्तम परिणामहरूको लागि सिधा-अन छविहरूमा ध्यान केन्द्रित गर्नुहोस्; कोणमा अनुहारहरू खिच्ने तालिम छविहरूबाट बच्नुहोस्।</li></ul>"
},
"steps": {
"faceName": "अनुहारको नाम प्रविष्ट गर्नुहोस्"
}
}

View File

@ -7,5 +7,28 @@
"twoWayTalk": {
"enable": "दुईतर्फी कुराकानी सक्षम पार्नुहोस्",
"disable": "दुईतर्फी कुराकानी असक्षम पार्नुहोस्"
},
"cameraAudio": {
"enable": "क्यामेरा अडियो सक्षम पार्नुहोस्",
"disable": "क्यामेरा अडियो असक्षम पार्नुहोस्"
},
"ptz": {
"move": {
"clickMove": {
"label": "क्यामेरालाई केन्द्रमा राख्न फ्रेममा क्लिक गर्नुहोस्",
"enable": "सार्न क्लिक गर्नुहोस् सक्षम पार्नुहोस्",
"enableWithZoom": "सार्न क्लिक गर्नुहोस् / जुम गर्न तान्नुहोस् सक्षम गर्नुहोस्",
"disable": "सार्न क्लिक गर्ने सुविधा असक्षम पार्नुहोस्"
},
"left": {
"label": "PTZ क्यामेरालाई बायाँतिर सार्नुहोस्"
},
"up": {
"label": "PTZ क्यामेरा माथि सार्नुहोस्"
},
"down": {
"label": "PTZ क्यामेरा तल सार्नुहोस्"
}
}
}
}

View File

@ -3,5 +3,15 @@
"title": "गति खोज",
"description": "रुचिको क्षेत्र परिभाषित गर्न बहुभुज कोर्नुहोस्, र त्यो क्षेत्र भित्र गति परिवर्तनहरू खोज्नको लागि समय दायरा निर्दिष्ट गर्नुहोस्।",
"selectCamera": "गति खोज लोड हुँदैछ",
"startSearch": "खोज सुरु गर्नुहोस्"
"startSearch": "खोज सुरु गर्नुहोस्",
"searchStarted": "खोजी सुरु भयो",
"searchCancelled": "खोज रद्द गरियो",
"cancelSearch": "रद्द गर्नुहोस्",
"searching": "खोजी भइरहेको छ।",
"searchComplete": "खोज पूरा भयो",
"noResultsYet": "चयन गरिएको क्षेत्रमा चाल परिवर्तनहरू फेला पार्न खोज चलाउनुहोस्",
"noChangesFound": "चयन गरिएको क्षेत्रमा कुनै पिक्सेल परिवर्तनहरू फेला परेनन्",
"changesFound_one": "{{count}} गति परिवर्तन फेला पर्यो",
"changesFound_other": "{{count}} गति परिवर्तनहरू फेला परे",
"framesProcessed": "{{count}} फ्रेमहरू प्रशोधन गरियो"
}

View File

@ -5,7 +5,8 @@
"filters": "फिल्टरहरू",
"toast": {
"error": {
"noValidTimeSelected": "कुनै मान्य समय दायरा चयन गरिएको छैन"
"noValidTimeSelected": "कुनै मान्य समय दायरा चयन गरिएको छैन",
"endTimeMustAfterStartTime": "अन्त्य समय सुरु समय पछि हुनुपर्छ"
}
}
}

View File

@ -5,6 +5,16 @@
"dialog": {
"title": "डिबग रिप्ले सुरु गर्नुहोस्",
"description": "वस्तु पत्ता लगाउने र ट्र्याकिङ समस्याहरू डिबग गर्न ऐतिहासिक फुटेज लुप गर्ने अस्थायी रिप्ले क्यामेरा सिर्जना गर्नुहोस्। रिप्ले क्यामेरामा स्रोत क्यामेरा जस्तै पत्ता लगाउने कन्फिगरेसन हुनेछ। सुरु गर्न समय दायरा छनौट गर्नुहोस्।",
"camera": "स्रोत क्यामेरा"
"camera": "स्रोत क्यामेरा",
"timeRange": "समय दायरा",
"preset": {
"1m": "अन्तिम १ मिनेट",
"5m": "अन्तिम ५ मिनेट",
"timeline": "टाइमलाइनबाट",
"custom": "अनुकूलन"
},
"startButton": "रिप्ले सुरु गर्नुहोस्",
"selectFromTimeline": "चयन गर्नुहोस्",
"starting": "रिप्ले सुरु गर्दै..."
}
}

View File

@ -4,6 +4,19 @@
"searchFor": "खोज्नुहोस् {{inputValue}}",
"button": {
"clear": "खोज खाली गर्नुहोस्",
"save": "खोज बचत गर्नुहोस्"
"save": "खोज बचत गर्नुहोस्",
"delete": "सुरक्षित गरिएको खोज मेटाउनुहोस्",
"filterInformation": "फिल्टर जानकारी",
"filterActive": "फिल्टरहरू सक्रिय छन्"
},
"trackedObjectId": "ट्र्याक गरिएको वस्तु ID",
"filter": {
"label": {
"cameras": "क्यामेराहरू",
"labels": "लेबलहरू",
"zones": "क्षेत्रहरू",
"sub_labels": "उप लेबलहरू",
"attributes": "विशेषताहरू"
}
}
}

View File

@ -4,6 +4,14 @@
"authentication": "प्रमाणीकरण सेटिङहरू - फ्रिगेट",
"cameraManagement": "क्यामेराहरू व्यवस्थापन गर्नुहोस् - फ्रिगेट",
"cameraReview": "क्यामेरा समीक्षा सेटिङहरू - फ्रिगेट",
"enrichments": "संवर्धन सेटिङहरू - फ्रिगेट"
"enrichments": "संवर्धन सेटिङहरू - फ्रिगेट",
"masksAndZones": "मास्क र जोन सम्पादक - फ्रिगेट",
"motionTuner": "मोशन ट्युनर - फ्रिगेट",
"object": "डिबग - फ्रिगेट",
"general": "UI सेटिङहरू - फ्रिगेट",
"globalConfig": "विश्वव्यापी कन्फिगरेसन - फ्रिगेट",
"cameraConfig": "क्यामेरा कन्फिगरेसन - फ्रिगेट",
"frigatePlus": "फ्रिगेट+ सेटिङहरू - फ्रिगेट",
"notifications": "सूचना सेटिङहरू - फ्रिगेट"
}
}

View File

@ -6,7 +6,19 @@
"enrichments": "संवर्धन तथ्याङ्क - फ्रिगेट",
"logs": {
"frigate": "फ्रिगेट लगहरू - फ्रिगेट",
"go2rtc": "Go2RTC लगहरू - फ्रिगेट"
"go2rtc": "Go2RTC लगहरू - फ्रिगेट",
"nginx": "Nginx लगहरू - फ्रिगेट",
"websocket": "सन्देश लगहरू - फ्रिगेट"
}
},
"title": "प्रणाली",
"metrics": "प्रणाली मेट्रिक्स",
"logs": {
"websocket": {
"label": "सन्देशहरू",
"pause": "पज गर्नुहोस्",
"resume": "पुनःसुरु गर्नुहोस्",
"clear": "खाली गर्नुहोस्"
}
}
}

View File

@ -35,6 +35,9 @@
"live_enabled": {
"label": "Live transcriptie",
"description": "Live streamingtranscriptie van audio inschakelen tijdens ontvangst."
},
"enabled": {
"label": "Zet audio transcriptie aan"
}
},
"birdseye": {

View File

@ -49,7 +49,7 @@
},
"timestamp_style": {
"global": {
"appearance": "Globaal voorkomen"
"appearance": "Algemeen uiterlijk"
},
"cameras": {
"appearance": "Voorkomen"

View File

@ -1,6 +1,6 @@
{
"minimum": "Minimale waarde van {{limit}} vereist",
"maximum": "Mag niet meer dan {{limit}} bedragen.",
"maximum": "Mag niet meer dan {{limit}} bedragen",
"exclusiveMinimum": "Waarde moet groter zijn dan {{limit}}",
"exclusiveMaximum": "Moet minder zijn dan {{limit}}",
"minLength": "Moet minstens {{limit}} karakters zijn",

View File

@ -1 +1,10 @@
{}
{
"documentTitle": "Chat - Frigate",
"placeholder": "Stel een vraag...",
"error": "Er is iets misgegaan. Probeer opnieuw.",
"processing": "Verwerken...",
"toolsUsed": "Gebruikt: {{tools}}",
"hideTools": "Gereedschap verbergen",
"call": "Rinkel",
"title": "Frigate Chat"
}

View File

@ -12,10 +12,10 @@
},
"toast": {
"success": {
"deletedCategory_one": "Verwijderde klasse",
"deletedCategory_other": "Verwijderde klassen",
"deletedImage_one": "Verwijderde afbeelding",
"deletedImage_other": "Verwijderde afbeeldingen",
"deletedCategory_one": "Verwijderd {{count}} klasse",
"deletedCategory_other": "Verwijderde {{count}} klassen",
"deletedImage_one": "Verwijderde {{count}} afbeelding",
"deletedImage_other": "Verwijderde {{count}} afbeeldingen",
"categorizedImage": "Succesvol geclassificeerde afbeelding",
"trainedModel": "Succesvol getraind model.",
"trainingModel": "Modeltraining succesvol gestart.",

View File

@ -1 +1,9 @@
{}
{
"startSearch": "Zoeken Starten",
"searchStarted": "Zoekopdracht gestart",
"searchCancelled": "Zoekopdracht geannuleerd",
"cancelSearch": "Annuleer",
"searching": "Zoekopdracht bezig.",
"searchComplete": "Zoekopdracht voltooid",
"title": "Beweging Zoeken"
}

View File

@ -1 +1,10 @@
{}
{
"websocket_messages": "Berichten",
"dialog": {
"camera": "Bron Camera",
"preset": {
"1m": "Laatste 1 Minuut",
"5m": "Laatste 5 Minuten"
}
}
}

View File

@ -468,7 +468,7 @@
},
"restart_required": "Herstart vereist (maskers/zones gewijzigd)",
"motionMaskLabel": "Bewegingsmasker {{number}}",
"objectMaskLabel": "Objectmasker {{number}} ({{label}})"
"objectMaskLabel": "Objectmasker {{number}}"
},
"motionDetectionTuner": {
"title": "Bewegingsdetectie-afsteller",
@ -504,7 +504,7 @@
"desc": "Toon objectkaders rond gevolgde objecten",
"colors": {
"label": "Kleuren van objectkaders",
"info": "<li>Bij het opstarten wordt er een andere kleur toegewezen aan elk objectlabel.</li> <li>Een dunne donkerblauwe lijn geeft aan dat het object op dit moment niet wordt gedetecteerd.</li> <li>Een dunne grijze lijn geeft aan dat het object als stilstaand wordt herkend.</li> <li>Een dikke lijn geeft aan dat het object het doelwit is van automatische tracking (indien ingeschakeld).</li>"
"info": "<li>Bij het opstarten wordt er een andere kleur toegewezen aan elk objectlabel</li> <li>Een dunne donkerblauwe lijn geeft aan dat het object op dit moment niet wordt gedetecteerd</li> <li>Een dunne grijze lijn geeft aan dat het object als stilstaand wordt herkend</li> <li>Een dikke lijn geeft aan dat het object het doelwit is van automatische tracking (indien ingeschakeld)</li>"
}
},
"timestamp": {

View File

@ -1,7 +1,8 @@
{
"audio": {
"global": {
"sensitivity": "Общая чувствительность"
"sensitivity": "Общая чувствительность",
"detection": "Общее обнаружение"
},
"cameras": {
"detection": "Обнаружение",

View File

@ -27,5 +27,6 @@
"detectRequired": "Как минимум один входной поток должен быть назначен роли 'detect'.",
"hwaccelDetectOnly": "Только входной поток с ролью detect может настраивать аппаратное ускорение."
}
}
},
"minimum": "Должно быть минимум {{limit}}"
}

View File

@ -221,7 +221,8 @@
"gl": "加利西亚语 (Galego)",
"id": "印度尼西亚语 (Bahasa Indonesia)",
"ur": "乌尔都语 (اردو)",
"hr": "克罗地亚语 (Hrvatski)"
"hr": "克罗地亚语 (Hrvatski)",
"bs": "波斯尼亚语Bosanski"
},
"appearance": "外观",
"darkMode": {

View File

@ -37,7 +37,11 @@
},
"filters": {
"label": "音频过滤器",
"description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。"
"description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。",
"threshold": {
"label": "最低音频置信度",
"description": "设置音频事件所需的最低置信度阈值。"
}
},
"enabled_in_config": {
"label": "原始音频状态",
@ -68,7 +72,7 @@
},
"mode": {
"label": "追踪模式",
"description": "在鸟瞰视图中包含摄像头的模式'objects'(目标)、'motion'(动作)或 'continuous'(持续)。"
"description": "在鸟瞰视图中包含摄像头的模式有:“基于目标”、“基于画面变动”或“连续”。"
},
"order": {
"label": "排序位置",
@ -603,7 +607,7 @@
},
"image_source": {
"label": "核查图像来源",
"description": "发送给生成式 AI 的画面来源('preview' 或 'recordings''recordings' 使用更高质量的画面帧,但会消耗更多的 token。"
"description": "发送给生成式 AI 的画面来源(“预览” 或 “录制”);“录制”将使用更高质量的画面帧,但会消耗更多的 token。"
},
"additional_concerns": {
"label": "额外关注事项",
@ -723,7 +727,7 @@
"description": "摄像头特定语义搜索触发器的操作和匹配条件。",
"friendly_name": {
"label": "友好名称",
"description": "在 UI 中为此触发器显示的可选友好名称。"
"description": "可选友好名称,用于在界面上为触发器显示此名称。"
},
"enabled": {
"label": "开启此触发器",
@ -852,7 +856,7 @@
"description": "用于在页面中排序摄像头的顺序(只会影响默认仪表板和列表);数值越大则在越后面。"
},
"dashboard": {
"label": "在 UI 中显示",
"label": "在页面中显示",
"description": "切换此摄像头在 Frigate 页面的所有位置是否可见。禁用此项将需要手动编辑配置才能在页面中再次查看此摄像头。"
}
},
@ -873,7 +877,7 @@
"description": "区域允许您定义帧的特定区域,以便确定目标是否在特定区域内。",
"friendly_name": {
"label": "区域名称",
"description": "区域的友好名称,显示在 Frigate UI 中。如果未设置,将使用区域名称的格式化版本。"
"description": "区域的友好名称,显示在 Frigate 页面中。如果未设置,将使用区域名称的格式化版本。"
},
"enabled": {
"label": "开启",

View File

@ -48,7 +48,11 @@
},
"filters": {
"label": "音频过滤器",
"description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。"
"description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。",
"threshold": {
"label": "最低音频置信度",
"description": "设置音频事件所需的最低置信度阈值。"
}
},
"enabled_in_config": {
"label": "原始音频状态",
@ -136,7 +140,7 @@
},
"mode": {
"label": "追踪模式",
"description": "在鸟瞰视图中包含摄像头的模式'objects'(目标)、'motion'(动作)或 'continuous'(持续)。"
"description": "在鸟瞰视图中包含摄像头的模式有:“基于目标”、“基于画面变动”或“连续”。"
},
"order": {
"label": "排序位置",
@ -252,7 +256,7 @@
"description": "所有摄像头的人脸检测和识别设置;可按摄像头覆盖。",
"model_size": {
"label": "模型大小",
"description": "用于人脸嵌入的模型大小(small/large);较大的可能需要 GPU。"
"description": "用于人脸嵌入的模型大小(小型/大型);较大的可能需要 GPU。"
},
"unknown_score": {
"label": "未知分数阈值",
@ -544,19 +548,19 @@
"description": "用户界面偏好设置,如时区、时间/日期格式和单位。",
"timezone": {
"label": "时区",
"description": "UI 中显示的可选时区(如果未设置,则默认为浏览器本地时间)。"
"description": "可选时区,用于整个界面展示时间(如果未设置,则默认为浏览器本地时间的时区)。"
},
"time_format": {
"label": "时间格式",
"description": "UI 中使用的时间格式browser、12hour 或 24hour)。"
"description": "页面中将使用的时间格式浏览器、12小时制 或 24小时制)。"
},
"date_style": {
"label": "日期样式",
"description": "UI 中使用的日期样式full、long、medium、short)。"
"description": "页面中将使用的日期样式(完整、长、中等、短)。"
},
"time_style": {
"label": "时间样式",
"description": "UI 中使用的时间样式full、long、medium、short)。"
"description": "页面中将使用的时间样式(完整、长、中等、短)。"
},
"unit_system": {
"label": "单位系统",
@ -1756,7 +1760,7 @@
},
"review": {
"label": "核查",
"description": "控制 UI 和存储使用的警报、检测和 GenAI 核查摘要的设置。",
"description": "控制界面与存储所使用的警报、检测和生成式 AI 核查总结的相关设置。",
"alerts": {
"label": "警报配置",
"description": "哪些追踪目标生成警报以及如何保留警报的设置。",
@ -1822,7 +1826,7 @@
},
"image_source": {
"label": "核查图像来源",
"description": "发送给生成式 AI 的画面来源('preview' 或 'recordings''recordings' 使用更高质量的画面帧,但会消耗更多的 token。"
"description": "发送给生成式 AI 的画面来源(“预览” 或 “录制”);“录制”将使用更高质量的画面帧,但会消耗更多的 token。"
},
"additional_concerns": {
"label": "额外关注事项",
@ -2015,7 +2019,7 @@
},
"model_size": {
"label": "模型大小",
"description": "选择模型大小;'small' 在 CPU 上运行,'large' 通常需要 GPU。"
"description": "选择模型大小;“小型”模型一般在 CPU 上运行,而“大型”模型通常需要 GPU。"
},
"device": {
"label": "设备",
@ -2026,7 +2030,7 @@
"description": "摄像头特定语义搜索触发器的操作和匹配条件。",
"friendly_name": {
"label": "友好名称",
"description": "在 UI 中为此触发器显示的可选友好名称。"
"description": "可选友好名称,用于在界面上为触发器显示此名称。"
},
"enabled": {
"label": "开启此触发器",
@ -2059,7 +2063,7 @@
},
"model_size": {
"label": "模型大小",
"description": "用于文本检测/识别的模型大小,大多数用户应使用 'small',只有'small'模型支持中文。"
"description": "用于文本检测/识别的模型大小,大多数用户应使用“小型”模型,而且只有“小型”模型支持中文车牌。"
},
"detection_threshold": {
"label": "检测阈值",
@ -2172,7 +2176,7 @@
"description": "用于在页面中排序摄像头的顺序(只会影响默认仪表板和列表);数值越大则在越后面。"
},
"dashboard": {
"label": "在 UI 中显示",
"label": "在页面中显示",
"description": "切换此摄像头在 Frigate 页面中是否可见。禁用后需要手动编辑配置才能再次在页面中查看此摄像头。"
}
},

View File

@ -121,5 +121,9 @@
"royal_mail": "英国皇家邮政",
"school_bus": "校车",
"skunk": "臭鼬",
"kangaroo": "袋鼠"
"kangaroo": "袋鼠",
"baby": "婴儿",
"baby_stroller": "婴儿车",
"rickshaw": "三轮车",
"Rodent": "啮齿动物"
}

View File

@ -30,7 +30,11 @@
"title": "近期识别记录",
"aria": "选择近期识别记录",
"empty": "近期未检测到人脸识别操作",
"titleShort": "近期"
"titleShort": "近期",
"emptyNoLibrary": {
"title": "更新人脸",
"description": "你必须向库中添加至少一张人脸,人脸识别功能才能正常工作。"
}
},
"selectItem": "选择 {{item}}",
"selectFace": "选择人脸",

View File

@ -52,7 +52,7 @@
"systemTls": "TLS加密链接",
"systemAuthentication": "验证",
"systemNetworking": "网络",
"systemProxy": "代理",
"systemProxy": "反向代理",
"systemUi": "界面",
"systemLogging": "日志",
"systemEnvironmentVariables": "环境变量",
@ -1659,7 +1659,9 @@
"options": {
"embeddings": "嵌入Embedding",
"vision": "视觉Vision",
"tools": "工具Tools"
"tools": "工具Tools",
"descriptions": "描述生成",
"chat": "聊天对话"
}
},
"semanticSearchModel": {
@ -1929,5 +1931,55 @@
"semanticSearch": {
"jinav2SmallModelSize": "Jina V2 的大型模型版本内存占用与推理开销较高,建议搭配独立显卡使用大型模型。"
}
},
"birdseye": {
"trackingMode": {
"objects": "基于目标",
"motion": "基于画面变动",
"continuous": "连续"
}
},
"snapshot": {
"retainMode": {
"all": "所有",
"motion": "画面变动",
"active_objects": "活动目标"
}
},
"ui": {
"timeFormat": {
"browser": "基于浏览器",
"12hour": "12 小时制",
"24hour": "24 小时制"
},
"TimeOrDateStyle": {
"full": "完整",
"long": "长",
"medium": "中等",
"short": "段"
},
"unitSystem": {
"metric": "公制单位",
"imperial": "英制单位"
}
},
"review": {
"imageSource": {
"recordings": "录制文件",
"previews": "预览"
}
},
"logger": {
"logLevel": {
"debug": "调试",
"info": "信息",
"warning": "警告",
"error": "错误",
"critical": "关键"
}
},
"modelSize": {
"small": "小型",
"large": "大型"
}
}

View File

@ -12,6 +12,7 @@ type EmptyCardProps = {
description?: string;
buttonText?: string;
link?: string;
onClick?: () => void;
};
export function EmptyCard({
className,
@ -21,6 +22,7 @@ export function EmptyCard({
description,
buttonText,
link,
onClick,
}: EmptyCardProps) {
let TitleComponent;
@ -39,11 +41,16 @@ export function EmptyCard({
{description}
</div>
)}
{buttonText?.length && (
<Button size="sm" variant="select">
<Link to={link ?? "#"}>{buttonText}</Link>
</Button>
)}
{buttonText?.length &&
(onClick ? (
<Button size="sm" variant="select" onClick={onClick}>
{buttonText}
</Button>
) : (
<Button size="sm" variant="select">
<Link to={link ?? "#"}>{buttonText}</Link>
</Button>
))}
</div>
);
}

View File

@ -528,7 +528,7 @@ export default function ZoneEditPane({
);
updateConfig();
// Only publish WS state for base config when zone has a name
if (!editingProfile && zoneName) {
if (!editingProfile && polygon?.name) {
sendZoneState(enabled ? "ON" : "OFF");
}
} else {

View File

@ -1,5 +1,6 @@
import AddFaceIcon from "@/components/icons/AddFaceIcon";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { EmptyCard } from "@/components/card/EmptyCard";
import CreateFaceWizardDialog from "@/components/overlay/detail/FaceCreateWizardDialog";
import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog";
import UploadImageDialog from "@/components/overlay/dialog/UploadImageDialog";
@ -473,7 +474,9 @@ export default function FaceLibrary() {
attemptImages={trainImages}
faceNames={faces}
selectedFaces={selectedFaces}
isLoading={faceData === undefined}
onClickFaces={onClickFaces}
onAddFace={() => setAddFace(true)}
onRefresh={refreshFaces}
/>
) : (
@ -691,7 +694,9 @@ type TrainingGridProps = {
attemptImages: string[];
faceNames: string[];
selectedFaces: string[];
isLoading: boolean;
onClickFaces: (images: string[], ctrl: boolean) => void;
onAddFace: () => void;
onRefresh: (
data?:
| FaceLibraryData
@ -708,7 +713,9 @@ function TrainingGrid({
attemptImages,
faceNames,
selectedFaces,
isLoading,
onClickFaces,
onAddFace,
onRefresh,
}: TrainingGridProps) {
const { t } = useTranslation(["views/faceLibrary"]);
@ -762,6 +769,25 @@ function TrainingGrid({
]);
if (attemptImages.length == 0) {
if (isLoading) {
return (
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 items-center text-center" />
);
}
if (faceNames.length == 0) {
return (
<EmptyCard
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 items-center text-center"
icon={<AddFaceIcon className="size-16" />}
title={t("train.emptyNoLibrary.title")}
description={t("train.emptyNoLibrary.description")}
buttonText={t("button.addFace")}
onClick={onAddFace}
/>
);
}
return (
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
<LuFolderCheck className="size-16" />

View File

@ -122,9 +122,12 @@ export default function CameraManagementView({
<div className="scrollbar-container flex-1 overflow-y-auto pb-2">
{viewMode === "settings" ? (
<>
<Heading as="h4" className="mb-6">
<Heading as="h4" className="mb-2">
{t("cameraManagement.title")}
</Heading>
<p className="mb-6 max-w-5xl text-sm text-muted-foreground">
{t("cameraManagement.description")}
</p>
<div className="w-full max-w-5xl space-y-6">
<div className="flex gap-2">

View File

@ -1,5 +1,5 @@
import Heading from "@/components/ui/heading";
import { useCallback, useContext, useEffect, useState } from "react";
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { Toaster } from "@/components/ui/sonner";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { toast } from "sonner";
@ -10,7 +10,7 @@ import { CheckCircle2, XCircle } from "lucide-react";
import { Trans, useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Link } from "react-router-dom";
import { LuExternalLink } from "react-icons/lu";
import { LuExternalLink, LuFilter } from "react-icons/lu";
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
import {
Select,
@ -19,6 +19,14 @@ import {
SelectItem,
SelectTrigger,
} from "@/components/ui/select";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
import {
@ -26,6 +34,8 @@ import {
SplitCardRow,
} from "@/components/card/SettingsGroupCard";
import FrigatePlusCurrentModelSummary from "@/views/settings/components/FrigatePlusCurrentModelSummary";
import { useRestart } from "@/api/ws";
import RestartDialog from "@/components/overlay/dialog/RestartDialog";
type FrigatePlusModel = {
id: string;
@ -58,6 +68,8 @@ export default function FrigatePlusSettingsView({
useSWR<FrigateConfig>("config");
const [changedValue, setChangedValue] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
const { send: sendRestart } = useRestart();
const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!;
@ -76,7 +88,7 @@ export default function FrigatePlusSettingsView({
},
);
const { data: availableModels = {} } = useSWR<
const { data: availableModels = {}, isLoading: isLoadingModels } = useSWR<
Record<string, FrigatePlusModel>
>("/plus/models", {
fallbackData: {},
@ -92,6 +104,19 @@ export default function FrigatePlusSettingsView({
},
});
const [showBaseModels, setShowBaseModels] = useState(true);
const [showFineTunedModels, setShowFineTunedModels] = useState(true);
const filteredModelEntries = useMemo(
() =>
Object.entries(availableModels || {}).filter(([, model]) =>
model.isBaseModel ? showBaseModels : showFineTunedModels,
),
[availableModels, showBaseModels, showFineTunedModels],
);
const isFilterActive = !showBaseModels || !showFineTunedModels;
useEffect(() => {
if (config) {
if (frigatePlusSettings?.model.id == undefined) {
@ -128,47 +153,60 @@ export default function FrigatePlusSettingsView({
const saveToConfig = useCallback(async () => {
setIsLoading(true);
axios
.put(`config/set?model.path=plus://${frigatePlusSettings.model.id}`, {
try {
// Clear the existing model section so only the new path remains
await axios.put("config/set", {
requires_restart: 0,
})
.then((res) => {
if (res.status === 200) {
toast.success(t("frigatePlus.toast.success"), {
position: "top-center",
});
setChangedValue(false);
updateConfig();
} else {
toast.error(
t("frigatePlus.toast.error", { errorMessage: res.statusText }),
{
position: "top-center",
},
);
}
})
.catch((error) => {
const errorMessage =
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
config_data: { model: null },
});
const res = await axios.put("config/set", {
requires_restart: 0,
config_data: {
model: { path: `plus://${frigatePlusSettings.model.id}` },
},
});
if (res.status === 200) {
toast.success(t("frigatePlus.toast.success"), {
position: "top-center",
action: (
<a onClick={() => setRestartDialogOpen(true)}>
<Button>
{t("restart.button", { ns: "components/dialog" })}
</Button>
</a>
),
});
setChangedValue(false);
updateConfig();
} else {
toast.error(
t("toast.save.error.title", { errorMessage, ns: "common" }),
t("frigatePlus.toast.error", { errorMessage: res.statusText }),
{
position: "top-center",
},
);
})
.finally(() => {
addMessage(
"plus_restart",
t("frigatePlus.restart_required"),
undefined,
"plus_restart",
);
setIsLoading(false);
}
} catch (error) {
const err = error as {
response?: { data?: { message?: string; detail?: string } };
};
const errorMessage =
err.response?.data?.message ||
err.response?.data?.detail ||
"Unknown error";
toast.error(t("toast.save.error.title", { errorMessage, ns: "common" }), {
position: "top-center",
});
} finally {
addMessage(
"plus_restart",
t("frigatePlus.restart_required"),
undefined,
"plus_restart",
);
setIsLoading(false);
}
}, [updateConfig, addMessage, frigatePlusSettings, t]);
const onCancel = useCallback(() => {
@ -201,274 +239,340 @@ export default function FrigatePlusSettingsView({
}
return (
<div className="flex size-full flex-col md:flex-row">
<div className="flex size-full flex-col md:pr-2">
<Toaster position="top-center" closeButton={true} />
<div className="mt-2 flex h-full w-full flex-col">
<div className="scrollbar-container flex-1 overflow-y-auto">
<div className="w-full max-w-5xl space-y-6">
<div className="flex flex-col gap-0">
<Heading as="h4" className="mb-2">
{t("frigatePlus.title")}
</Heading>
<div className="w-full max-w-5xl space-y-6 pt-2">
<div className="flex flex-col gap-0">
<Heading as="h4" className="mb-2">
{t("frigatePlus.title")}
</Heading>
<p className="text-sm text-muted-foreground">
{t("frigatePlus.description")}
</p>
</div>
<p className="text-sm text-muted-foreground">
{t("frigatePlus.description")}
</p>
</div>
<div className="space-y-6">
<SettingsGroupCard title={t("frigatePlus.cardTitles.api")}>
<SplitCardRow
label={t("frigatePlus.apiKey.title")}
description={
<>
<p>{t("frigatePlus.apiKey.desc")}</p>
{!config?.model.plus && (
<div className="mt-2 flex items-center text-primary-variant">
<Link
to="https://frigate.video/plus"
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("frigatePlus.apiKey.plusLink")}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
)}
</>
}
content={
<div className="flex items-center gap-2">
{config?.plus?.enabled ? (
<CheckCircle2 className="h-5 w-5 text-green-500" />
) : (
<XCircle className="h-5 w-5 text-red-500" />
)}
<span className="text-sm">
{config?.plus?.enabled
? t("frigatePlus.apiKey.validated")
: t("frigatePlus.apiKey.notValidated")}
</span>
</div>
}
/>
</SettingsGroupCard>
{config?.model.plus && (
<FrigatePlusCurrentModelSummary plusModel={config.model.plus} />
)}
{config?.model.plus && (
<SettingsGroupCard
title={t("frigatePlus.cardTitles.otherModels")}
>
<SplitCardRow
label={t("frigatePlus.modelInfo.availableModels")}
description={
<Trans ns="views/settings">
frigatePlus.modelInfo.modelSelect
</Trans>
}
content={
<Select
value={frigatePlusSettings.model.id}
onValueChange={(value) =>
handleFrigatePlusConfigChange({
model: { id: value as string },
})
}
<div className="space-y-6">
<SettingsGroupCard title={t("frigatePlus.cardTitles.api")}>
<SplitCardRow
label={t("frigatePlus.apiKey.title")}
description={
<>
<p>{t("frigatePlus.apiKey.desc")}</p>
{!config?.model.plus && (
<div className="mt-2 flex items-center text-primary-variant">
<Link
to="https://frigate.video/plus"
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("frigatePlus.apiKey.plusLink")}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
)}
</>
}
content={
<div className="flex items-center gap-2">
{config?.plus?.enabled ? (
<CheckCircle2 className="h-5 w-5 text-green-500" />
) : (
<XCircle className="h-5 w-5 text-red-500" />
)}
<span className="text-sm">
{config?.plus?.enabled
? t("frigatePlus.apiKey.validated")
: t("frigatePlus.apiKey.notValidated")}
</span>
</div>
}
/>
</SettingsGroupCard>
{config?.plus?.enabled && (
<FrigatePlusCurrentModelSummary plusModel={config.model.plus} />
)}
{config?.plus?.enabled && (
<SettingsGroupCard title={t("frigatePlus.cardTitles.otherModels")}>
<SplitCardRow
label={t("frigatePlus.modelInfo.availableModels")}
description={
<Trans ns="views/settings">
frigatePlus.modelInfo.modelSelect
</Trans>
}
content={
<div className="flex w-full items-center gap-2">
<Select
value={frigatePlusSettings.model.id}
onValueChange={(value) =>
handleFrigatePlusConfigChange({
model: { id: value as string },
})
}
>
<SelectTrigger className="w-full">
{frigatePlusSettings.model.id &&
availableModels?.[frigatePlusSettings.model.id] ? (
<SelectTrigger className="w-full">
{new Date(
availableModels?.[frigatePlusSettings.model.id]
? new Date(
availableModels[
frigatePlusSettings.model.id
].trainDate,
).toLocaleString() +
" " +
availableModels[frigatePlusSettings.model.id]
.baseModel +
" (" +
(availableModels[frigatePlusSettings.model.id]
.isBaseModel
? t(
"frigatePlus.modelInfo.plusModelType.baseModel",
" " +
availableModels[frigatePlusSettings.model.id]
.baseModel +
" (" +
(availableModels[frigatePlusSettings.model.id]
.isBaseModel
? t(
"frigatePlus.modelInfo.plusModelType.baseModel",
)
: t(
"frigatePlus.modelInfo.plusModelType.userModel",
)) +
") " +
availableModels[frigatePlusSettings.model.id].name +
" (" +
availableModels[frigatePlusSettings.model.id]
.width +
"x" +
availableModels[frigatePlusSettings.model.id]
.height +
")"
: isLoadingModels
? t("frigatePlus.modelInfo.loadingAvailableModels")
: t("frigatePlus.modelInfo.selectModel")}
</SelectTrigger>
<SelectContent>
<SelectGroup>
{filteredModelEntries.length === 0 ? (
<div className="px-4 py-3 text-center text-sm text-muted-foreground">
{t("frigatePlus.modelInfo.noModelsAvailable")}
</div>
) : (
filteredModelEntries.map(([id, model]) => (
<SelectItem
key={id}
className="cursor-pointer"
value={id}
disabled={
!model.supportedDetectors.includes(
Object.values(config.detectors)[0].type,
)
: t(
"frigatePlus.modelInfo.plusModelType.userModel",
)) +
") " +
availableModels[frigatePlusSettings.model.id]
.name +
" (" +
availableModels[frigatePlusSettings.model.id]
.width +
"x" +
availableModels[frigatePlusSettings.model.id]
.height +
")"}
</SelectTrigger>
) : (
<SelectTrigger className="w-full">
{t("frigatePlus.modelInfo.loadingAvailableModels")}
</SelectTrigger>
)}
<SelectContent>
<SelectGroup>
{Object.entries(availableModels || {}).map(
([id, model]) => (
<SelectItem
key={id}
className="cursor-pointer"
value={id}
disabled={
!model.supportedDetectors.includes(
Object.values(config.detectors)[0].type,
)
}
>
{new Date(model.trainDate).toLocaleString()}{" "}
<div>
{model.baseModel} {" ("}
{model.isBaseModel
? t(
"frigatePlus.modelInfo.plusModelType.baseModel",
)
: t(
"frigatePlus.modelInfo.plusModelType.userModel",
)}
{")"}
</div>
<div>
{model.name} (
{model.width + "x" + model.height})
</div>
<div>
{t(
"frigatePlus.modelInfo.supportedDetectors",
)}
: {model.supportedDetectors.join(", ")}
</div>
<div className="text-xs text-muted-foreground">
{id}
</div>
</SelectItem>
),
)}
</SelectGroup>
</SelectContent>
</Select>
}
/>
</SettingsGroupCard>
)}
<SettingsGroupCard
title={t("frigatePlus.cardTitles.configuration")}
>
<SplitCardRow
label={t("frigatePlus.snapshotConfig.title")}
description={
<>
<p>
<Trans ns="views/settings">
frigatePlus.snapshotConfig.desc
</Trans>
</p>
<div className="mt-2 flex items-center text-primary-variant">
<Link
to={getLocaleDocUrl("plus/faq")}
target="_blank"
rel="noopener noreferrer"
className="inline"
}
>
{new Date(model.trainDate).toLocaleString()}{" "}
<div>
{model.baseModel} {" ("}
{model.isBaseModel
? t(
"frigatePlus.modelInfo.plusModelType.baseModel",
)
: t(
"frigatePlus.modelInfo.plusModelType.userModel",
)}
{")"}
</div>
<div>
{model.name} (
{model.width + "x" + model.height})
</div>
<div>
{t(
"frigatePlus.modelInfo.supportedDetectors",
)}
: {model.supportedDetectors.join(", ")}
</div>
<div className="text-xs text-muted-foreground">
{id}
</div>
</SelectItem>
))
)}
</SelectGroup>
</SelectContent>
</Select>
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className="focus:outline-none"
aria-label={t(
"frigatePlus.modelInfo.filter.ariaLabel",
)}
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
</>
}
content={
<div className="space-y-3">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-secondary">
<th className="px-4 py-2 text-left">
{t("frigatePlus.snapshotConfig.table.camera")}
</th>
<th className="px-4 py-2 text-center">
{t(
"frigatePlus.snapshotConfig.table.snapshots",
)}
</th>
</tr>
</thead>
<tbody>
{Object.entries(config.cameras).map(
([name, camera]) => (
<tr
key={name}
className="border-b border-secondary"
>
<td className="px-4 py-2">
<CameraNameLabel camera={name} />
</td>
<td className="px-4 py-2 text-center">
{camera.snapshots.enabled ? (
<CheckCircle2 className="mx-auto size-5 text-green-500" />
) : (
<XCircle className="mx-auto size-5 text-danger" />
)}
</td>
</tr>
),
<LuFilter
className={cn(
"size-4",
isFilterActive
? "text-selected"
: "text-secondary-foreground",
)}
</tbody>
</table>
</div>
</div>
}
/>
</SettingsGroupCard>
</div>
</div>
</div>
/>
</button>
</PopoverTrigger>
<PopoverContent align="end" className="w-56">
<div className="space-y-3">
<div className="text-sm text-primary-variant">
{t("frigatePlus.modelInfo.filter.ariaLabel")}
</div>
<div className="flex items-center justify-between">
<Label
htmlFor="filterBaseModels"
className="cursor-pointer text-primary"
>
{t("frigatePlus.modelInfo.filter.baseModels")}
</Label>
<Switch
id="filterBaseModels"
checked={showBaseModels}
onCheckedChange={setShowBaseModels}
/>
</div>
<div className="flex items-center justify-between">
<Label
htmlFor="filterFineTunedModels"
className="cursor-pointer text-primary"
>
{t(
"frigatePlus.modelInfo.filter.fineTunedModels",
)}
</Label>
<Switch
id="filterFineTunedModels"
checked={showFineTunedModels}
onCheckedChange={setShowFineTunedModels}
/>
</div>
</div>
</PopoverContent>
</Popover>
</div>
}
/>
</SettingsGroupCard>
)}
<div className="sticky bottom-0 z-50 w-full border-t border-secondary bg-background pb-5 pt-0 md:pr-2">
<div className="flex flex-col items-center gap-4 pt-2 md:flex-row md:justify-end">
<div className="flex w-full items-center gap-2 md:w-auto">
<Button
className="flex min-w-36 flex-1 gap-2"
variant="outline"
aria-label={t("button.reset", { ns: "common" })}
onClick={onCancel}
>
{t("button.reset", { ns: "common" })}
</Button>
<Button
variant="select"
disabled={!changedValue || isLoading}
className="flex min-w-36 flex-1 gap-2"
aria-label={t("button.save", { ns: "common" })}
onClick={saveToConfig}
>
{isLoading ? (
<>
<ActivityIndicator className="h-4 w-4" />
{t("button.saving", { ns: "common" })}
</>
) : (
t("button.save", { ns: "common" })
)}
</Button>
<SettingsGroupCard title={t("frigatePlus.cardTitles.configuration")}>
<SplitCardRow
label={t("frigatePlus.snapshotConfig.title")}
description={
<>
<p>
<Trans ns="views/settings">
frigatePlus.snapshotConfig.desc
</Trans>
</p>
<div className="mt-2 flex items-center text-primary-variant">
<Link
to={getLocaleDocUrl("plus/faq")}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
</>
}
content={
<div className="space-y-3">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-secondary">
<th className="px-4 py-2 text-left">
{t("frigatePlus.snapshotConfig.table.camera")}
</th>
<th className="px-4 py-2 text-center">
{t("frigatePlus.snapshotConfig.table.snapshots")}
</th>
</tr>
</thead>
<tbody>
{Object.entries(config.cameras).map(
([name, camera]) => (
<tr
key={name}
className="border-b border-secondary"
>
<td className="px-4 py-2">
<CameraNameLabel camera={name} />
</td>
<td className="px-4 py-2 text-center">
{camera.snapshots.enabled ? (
<CheckCircle2 className="mx-auto size-5 text-green-500" />
) : (
<XCircle className="mx-auto size-5 text-danger" />
)}
</td>
</tr>
),
)}
</tbody>
</table>
</div>
</div>
}
/>
</SettingsGroupCard>
</div>
</div>
<div className="sticky bottom-0 z-50 mt-6 w-full border-t border-secondary bg-background pt-0">
<div
className={cn(
"flex flex-col items-center gap-4 pt-2 md:flex-row",
changedValue ? "justify-between" : "justify-end",
)}
>
{changedValue && (
<div className="flex items-center gap-2">
<span className="text-sm text-unsaved">
{t("unsavedChanges")}
</span>
</div>
)}
<div className="flex w-full flex-col gap-2 sm:flex-row sm:items-center md:w-auto">
{changedValue && (
<Button
onClick={onCancel}
variant="outline"
disabled={isLoading}
className="flex min-w-36 flex-1 gap-2"
>
{t("button.undo", { ns: "common" })}
</Button>
)}
<Button
onClick={saveToConfig}
variant="select"
disabled={!changedValue || isLoading}
className="flex min-w-36 flex-1 gap-2"
>
{isLoading ? (
<>
<ActivityIndicator className="h-4 w-4" />
{t("button.saving", { ns: "common" })}
</>
) : (
t("button.save", { ns: "common" })
)}
</Button>
</div>
</div>
</div>
<RestartDialog
isOpen={restartDialogOpen}
onClose={() => setRestartDialogOpen(false)}
onRestart={() => sendRestart("restart")}
/>
</div>
);
}

View File

@ -385,7 +385,7 @@ export default function Go2RtcStreamsSettingsView({
</span>
</div>
)}
<div className="flex w-full items-center gap-2 md:w-auto">
<div className="flex w-full flex-col gap-2 sm:flex-row sm:items-center md:w-auto">
{hasChanges && (
<Button
onClick={onReset}

View File

@ -16,14 +16,11 @@ export default function FrigatePlusCurrentModelSummary({
return (
<SettingsGroupCard title={t("frigatePlus.cardTitles.currentModel")}>
{plusModel === undefined && (
{!plusModel && (
<p className="text-muted-foreground">
{t("frigatePlus.modelInfo.loading")}
{t("frigatePlus.modelInfo.noModelLoaded")}
</p>
)}
{plusModel === null && (
<p className="text-danger">{t("frigatePlus.modelInfo.error")}</p>
)}
{plusModel && (
<div className="space-y-6">
<SplitCardRow

View File

@ -450,7 +450,7 @@ export default function GeneralMetrics({
series[key] = { name: key, data: [] };
}
if (stats.temp !== undefined) {
if (stats?.temp !== undefined) {
hasValidGpu = true;
series[key].data.push({ x: statsIdx + 1, y: stats.temp });
}
@ -562,7 +562,7 @@ export default function GeneralMetrics({
series[key] = { name: key, data: [] };
}
if (stats.temp !== undefined) {
if (stats?.temp !== undefined) {
hasValidNpu = true;
series[key].data.push({ x: statsIdx + 1, y: stats.temp });
}