Compare commits

...

44 Commits

Author SHA1 Message Date
dependabot[bot]
ebfd2a1332
Merge 6902de1d64 into 03f4f76b72 2026-05-20 07:25:27 +00:00
Hosted Weblate
03f4f76b72 Translated using Weblate (Norwegian Bokmål)
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
Currently translated at 100.0% (1141 of 1141 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (794 of 794 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (127 of 127 strings)

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-facelibrary/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-facelibrary
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-19 15:22:50 -05:00
Hosted Weblate
6ffb9f2c9e Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1162 of 1162 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1150 of 1150 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1141 of 1141 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 98.8% (1128 of 1141 strings)

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: Edward Zhang <hsrzq@126.com>
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-chat/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-chat
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2026-05-19 15:22:50 -05:00
Hosted Weblate
b470258d95 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (101 of 101 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (794 of 794 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (64 of 64 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (45 of 45 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (237 of 237 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (237 of 237 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (1150 of 1150 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (473 of 473 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jamie HUANG <114514020@live.asia.edu.tw>
Co-authored-by: fascinate722 <fascinate722@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/zh_Hant/
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-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-player
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-classificationmodel
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-replay
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-05-19 15:22:50 -05:00
Hosted Weblate
fac11286f5 Translated using Weblate (Urdu)
Currently translated at 4.0% (1 of 25 strings)

Translated using Weblate (Urdu)

Currently translated at 4.5% (1 of 22 strings)

Translated using Weblate (Urdu)

Currently translated at 0.1% (1 of 794 strings)

Translated using Weblate (Urdu)

Currently translated at 0.2% (1 of 473 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Muhammad Arsalan Siddiqui <mailofarsalan@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ur/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ur/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/ur/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/ur/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Groups
Translation: Frigate NVR/Config - Validation
2026-05-19 15:22:50 -05:00
Hosted Weblate
50f7f11f0b 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-19 15:22:50 -05:00
Hosted Weblate
f96127c264 Translated using Weblate (Spanish)
Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1171 of 1171 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1150 of 1150 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1141 of 1141 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1137 of 1137 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1129 of 1129 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (794 of 794 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (129 of 129 strings)

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/objects/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/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/objects
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-classificationmodel
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-replay
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-05-19 15:22:50 -05:00
Hosted Weblate
2ae415be6b 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-19 15:22:50 -05:00
Hosted Weblate
59faa4e088 Translated using Weblate (Dutch)
Currently translated at 100.0% (794 of 794 strings)

Translated using Weblate (Dutch)

Currently translated at 83.0% (49 of 59 strings)

Translated using Weblate (Dutch)

Currently translated at 83.9% (397 of 473 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (1150 of 1150 strings)

Translated using Weblate (Dutch)

Currently translated at 15.2% (72 of 473 strings)

Translated using Weblate (Dutch)

Currently translated at 30.0% (15 of 50 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (45 of 45 strings)

Translated using Weblate (Dutch)

Currently translated at 10.2% (81 of 794 strings)

Translated using Weblate (Dutch)

Currently translated at 59.3% (35 of 59 strings)

Translated using Weblate (Dutch)

Currently translated at 35.0% (14 of 40 strings)

Translated using Weblate (Dutch)

Currently translated at 23.7% (14 of 59 strings)

Translated using Weblate (Dutch)

Currently translated at 24.4% (11 of 45 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (26 of 26 strings)

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: Bart Smeding <bartsmeding@gmail.com>
Co-authored-by: Björn Vanneste <info@nidhhoggr.net>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hosted Weblate user 151476 <marijndekker3@gmail.com>
Co-authored-by: bb61523 <brambini@gmail.com>
Co-authored-by: soosterwaal <sebastiaan@bg-engineering.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/nl/
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 - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Groups
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/components-player
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-19 15:22:50 -05:00
Hosted Weblate
3df7c22f4d Translated using Weblate (Italian)
Currently translated at 27.7% (220 of 794 strings)

Translated using Weblate (Italian)

Currently translated at 24.9% (118 of 473 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (237 of 237 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (64 of 64 strings)

Translated using Weblate (Italian)

Currently translated at 24.8% (197 of 794 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (1141 of 1141 strings)

Translated using Weblate (Italian)

Currently translated at 20.0% (95 of 473 strings)

Translated using Weblate (Italian)

Currently translated at 77.3% (882 of 1141 strings)

Co-authored-by: Gringo <ita.translations@tiscali.it>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/it/
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-events
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-05-19 15:22:50 -05:00
Hosted Weblate
f4cbbe806d Translated using Weblate (Polish)
Currently translated at 91.9% (218 of 237 strings)

Translated using Weblate (Polish)

Currently translated at 63.5% (731 of 1150 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J P <jpoloczek24@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/pl/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-settings
2026-05-19 15:22:50 -05:00
Hosted Weblate
cfb1420660 Translated using Weblate (Portuguese)
Currently translated at 100.0% (2 of 2 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: ssantos <ssantos@web.de>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-input/pt/
Translation: Frigate NVR/components-input
2026-05-19 15:22:50 -05:00
Hosted Weblate
161f56b5d4 Translated using Weblate (Catalan)
Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1171 of 1171 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1162 of 1162 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1151 of 1151 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1150 of 1150 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1141 of 1141 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (794 of 794 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1141 of 1141 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1137 of 1137 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1129 of 1129 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (794 of 794 strings)

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: Eduardo Pastor Fernández <123eduardoneko123@gmail.com>
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/audio/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ca/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2026-05-19 15:22:50 -05:00
Hosted Weblate
5ddf8bc1b0 Translated using Weblate (Romanian)
Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (1141 of 1141 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (794 of 794 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (1129 of 1129 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (237 of 237 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (127 of 127 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: lukasig <lukasig@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ro/
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-19 15:22:50 -05:00
Hosted Weblate
dc2c48f6d7 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-19 15:22:50 -05:00
Hosted Weblate
6e5d55ff64 Translated using Weblate (Estonian)
Currently translated at 100.0% (127 of 127 strings)

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-19 15:22:50 -05:00
Hosted Weblate
d439b09f90 Translated using Weblate (German)
Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (German)

Currently translated at 100.0% (794 of 794 strings)

Translated using Weblate (German)

Currently translated at 100.0% (1141 of 1141 strings)

Translated using Weblate (German)

Currently translated at 100.0% (237 of 237 strings)

Translated using Weblate (German)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (German)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (German)

Currently translated at 100.0% (50 of 50 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Sebastian Sie <sebastian.neuplanitz@googlemail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/de/
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-facelibrary
Translation: Frigate NVR/views-settings
2026-05-19 15:22:50 -05:00
Hosted Weblate
3f7768a48f Translated using Weblate (Thai)
Currently translated at 31.8% (7 of 22 strings)

Translated using Weblate (Thai)

Currently translated at 22.8% (40 of 175 strings)

Translated using Weblate (Thai)

Currently translated at 15.2% (9 of 59 strings)

Translated using Weblate (Thai)

Currently translated at 15.5% (7 of 45 strings)

Translated using Weblate (Thai)

Currently translated at 0.5% (4 of 794 strings)

Translated using Weblate (Thai)

Currently translated at 16.0% (4 of 25 strings)

Translated using Weblate (Thai)

Currently translated at 16.0% (8 of 50 strings)

Translated using Weblate (Thai)

Currently translated at 13.5% (8 of 59 strings)

Translated using Weblate (Thai)

Currently translated at 37.5% (24 of 64 strings)

Translated using Weblate (Thai)

Currently translated at 21.7% (38 of 175 strings)

Translated using Weblate (Thai)

Currently translated at 12.0% (3 of 25 strings)

Translated using Weblate (Thai)

Currently translated at 11.6% (10 of 86 strings)

Translated using Weblate (Thai)

Currently translated at 14.0% (7 of 50 strings)

Translated using Weblate (Thai)

Currently translated at 13.3% (6 of 45 strings)

Translated using Weblate (Thai)

Currently translated at 7.8% (90 of 1141 strings)

Translated using Weblate (Thai)

Currently translated at 0.3% (3 of 794 strings)

Translated using Weblate (Thai)

Currently translated at 4.6% (6 of 129 strings)

Translated using Weblate (Thai)

Currently translated at 1.2% (6 of 473 strings)

Translated using Weblate (Thai)

Currently translated at 77.6% (184 of 237 strings)

Translated using Weblate (Thai)

Currently translated at 9.6% (14 of 145 strings)

Translated using Weblate (Thai)

Currently translated at 27.2% (6 of 22 strings)

Translated using Weblate (Thai)

Currently translated at 8.0% (2 of 25 strings)

Translated using Weblate (Thai)

Currently translated at 11.8% (7 of 59 strings)

Translated using Weblate (Thai)

Currently translated at 8.8% (4 of 45 strings)

Translated using Weblate (Thai)

Currently translated at 10.4% (9 of 86 strings)

Translated using Weblate (Thai)

Currently translated at 16.0% (16 of 100 strings)

Translated using Weblate (Thai)

Currently translated at 21.1% (37 of 175 strings)

Translated using Weblate (Thai)

Currently translated at 15.0% (6 of 40 strings)

Translated using Weblate (Thai)

Currently translated at 45.0% (27 of 60 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ton Zabretooth <zabretooth@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/th/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/th/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Groups
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/common
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-classificationmodel
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-replay
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-05-19 15:22:50 -05:00
Josh Hawkins
7881bea60f
Filter outbound websocket broadcasts by per-recipient camera access (#23256)
* filter outbound ws broadcasts by per-recipient camera access

* fan out config updates to comms

* tests

* mypy

* allow viewers to use jobstate

* update agent instructions

* remove vitest
2026-05-19 14:51:16 -05:00
Nicolas Mowen
b0b00fe1d0
GenAI Refactor (#23253)
* Ensure runtime options are passed

* Add attribute info to prompt when configured

* Move GenAI plugins to dedicated directory

* Migrate prompts to dedicated folder

* Move chat prompts to prompts

* Implement reasoning traces in the UI

* Cleanup

* Make azure a subclass of openai

* Implement reasoning for other providers

* mypy

* Cleanup
2026-05-19 13:03:57 -05:00
Josh Hawkins
b1de5e2290
Add attributes to UI filters list (#23250)
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
* preserve user-set min_score on attribute filters instead of bumping any 0.5 value

use model_fields_set to distinguish "user explicitly set min_score" from "Pydantic applied the generic FilterConfig default of 0.5"

* add config test for attributes

* fix attributes frontend type

* add expanded hidden field context

* extend schema modification

* special case for attributes

* i18n for attributes

* handle dedicated lpr mode

* strip unrendered FilterConfig fields from attribute filter form data to fix validation errors
2026-05-19 08:31:50 -06:00
Josh Hawkins
4fdc107987
Improve go2rtc pane in Settings (#23251)
* improve layout and handling of multiple ffmpeg args in go2rtc pane

* add e2e tests

* fix spacing
2026-05-19 08:30:04 -06:00
GuoQing Liu
a83809de54
fix: fix chat request params miss runtime_options (#23247)
* fix: fix chat request params miss runtime_options

* fix: mypy
2026-05-19 06:29:28 -06:00
Josh Hawkins
43d97acd21
Miscellaneous fixes (#23238)
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
* start audio transcription post processor when enabled on any camera

* Fetch embed key whenever an error occurs in case the llama server was restarted

* mypy

* add tooltips for colored dots in settings menu

* add ability to reorder cameras from management pane

* add ability to reorder birdseye

* add reordering save text to camera management view

* Include NPU in latency performance hint

* Implement turbo for NPU on object detection

* hide order fields

* drop auto-derived field paths from camera value when unset globally

* use correct field type for export hwaccel args

* add debug replay to detail actions menu

* clarify debug replay in docs

* guard get_current_frame_time against missing camera state

* Implement debug reply from export

* Refactor debug replay to use sources for dynamic playback

* Mypy

* fix debug export replay source timestamp handling

* skip replay cameras in stats immediately

* broadcast debug replay state over ws and buffer pre-OPEN sends

- push debug replay session state over the job_state ws topic so the status bar reacts instantly to start/stop without polling
- fix child-effect-before-parent-effect race in WsProvider that silently dropped initial snapshot requests on cold load

* fix debug replay test hang

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2026-05-18 22:52:40 -05:00
Josh Hawkins
d968f00500
Settings UI fixes (#23237)
Some checks are pending
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* detector UI fixes

- derive detector and model from memo rather than using two drain useeffects
- sanitize save payload through sanitizeSectionData to prevent yaml validation issues

* increase display duration for restart required toasts

* mimic logic in detector section for save all button

also, increase toast duration for restart required toasts

* fixes and tweaks

- use section hidden fields for sanitization instead of duplicating code
- use parent hooks so save all, pending data, and the status dots work correctly
2026-05-18 13:22:54 -06:00
Josh Hawkins
620923c27e
clear both detector and model together (#23232)
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
2026-05-18 08:49:25 -05:00
Josh Hawkins
32daf6f494
Miscellaneous fixes (#23217)
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
* fix hardcoded leading-slash hrefs to respect FRIGATE_BASE_PATH

* update docs for default detector
2026-05-17 14:40:33 -06:00
Josh Hawkins
7413ce08d4
Merge detector and model in settings UI (#23216)
* add embedded mode to BaseSection so parents can host the save action

* add optional action slot to current Frigate+ model summary

* add w-full to action slot flex wrapper for explicit width contract

* i18n

* merged detectors and model settings view

* fix document title

* Embed detector form in merged settings view

* add detection model card with tabs and custom model embed

* add Frigate+ model selector with filter popover to merged page

* Add mismatch banner and gate save on detector and model compatibility

* Wire atomic save, restart toast, and undo on detectors and model page

* Clear child pending data on undo

* route merged detectors and model view in settings

* trim Frigate+ page to account-only and remove old detection model view

* basic e2e

* Fix unsaved-changes guard, custom path leak, and post-failure cache resync

* Rename to Detectors and model, float Modified badge, use ConfigMessageBanner for mismatch

* Hide Plus/Custom tabs when Frigate+ is not enabled

* Detect active Plus model via model.plus.id instead of path prefix

* Sync state back to snapshot when child form un-modifies and remount on undo

* Always require restart on save since model changes also need one

* Wrap Frigate+ model selector in SplitCardRow with label and description

* rename tab

* update docs

* sync top-level model with default detector's resolved model

when the user doesn't define a top-level `model:` block, `FrigateConfig.model` stayed at pydantic field defaults (320×320, /labelmap.txt) while the per-detector model picked up `DEFAULT_MODEL` for openvino on cpu (300×300, coco_91cl_bkgr.txt introduced in #23127), causing `RemoteObjectDetector` to fail with "buffer is too small for requested array" because the SHM was sized from the per-detector model but mapped using the top-level one. After the detector loop, copy the first detector's resolved model up to `self.model` so both sides agree on dimensions and labelmap

* revert to cpu detector by default

use openvino cpu for new configs only

* add defaults
2026-05-17 11:54:21 -06:00
Nicolas Mowen
b712e1fbd9
Implement semantic query for chat (#23206)
Some checks failed
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
2026-05-15 14:32:53 -05:00
Josh Hawkins
c6eadfebb8
Miscellaneous fixes (#23201)
* sync filter entries with track and listen labels

- Auto-populate `audio.filters` from `audio.listen` instead of the full audio labelmap, matching how `objects.filters` is keyed by `track` (no longer need to populate the full audio labelmap, which was added in #22630)
- Synthesize the matching filter entries in the settings form on load so each track/listen label shows its collapsible after a profile is selected, since the backend's auto-populate only runs at config init

* translate main label for lifecycle description with attribute

* reject restricted go2rtc stream sources when added via api

* add env var check function
2026-05-15 10:06:38 -05:00
Nicolas Mowen
d9c1ea908d
Chat improvements (#23195)
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
* Support token streaming stats

* Propogate streaming token stats to chat calls

* Show token stats for each image

* Add settings to handle token stats and other options

* i18n

* Use select

* Improve mobile layout and spacing
2026-05-14 12:05:38 -05:00
Nicolas Mowen
78fc472026
Improve Intel Stats (#23190)
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 per intel-gpu stats collection

* Improve device naming

* Improve GPU vendor handling

* Cleanup
2026-05-13 15:12:48 -06:00
Aaron Daubman
c8cfb9400a
Fix multi-GPU OpenVINO detection for enrichments (#23188)
On multi-GPU systems, OpenVINO enumerates devices as "GPU.0", "GPU.1",
etc. rather than a single "GPU". The exact string match in
is_openvino_gpu_npu_available() fails to recognize these suffixed device
names, causing enrichments (face recognition, semantic search) to
silently fall back to CPU-only inference via ONNXModelRunner instead of
using OpenVINOModelRunner on GPU.

Switch from exact match to prefix match so both single-GPU ("GPU") and
multi-GPU ("GPU.0", "GPU.1") device names are correctly detected, along
with any future suffixed variants for NPU and other accelerators.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 11:22:55 -06:00
Josh Hawkins
ca75f06456
Miscellaneous fixes (#23186)
* improve scroll handling for non-modal DropdownMenu in classification and face selection dialogs

* clean up

* fix incorrect key capitalization

* fix profile array overrides not replacing base arrays

don't use lodash merge(), it does positional merging and an empty source array doesn't override the destination, and shorter arrays leak destination elements through.

backend is unaffected, so the saved config and actual backend functionality was right

* only show audio debug tab when audio is enabled in config

* move apple_compatibility out of advanced

* remove retry_interval from UI

99% of users should never be changing this

* hide switch in optionalfieldwidget if editing a profile

* add override badges for cameras and profiles

collect shared functions into the config util and separate hooks

* Use new models endpoint info to determine modalities

* clarify language

* fix linter

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2026-05-13 11:04:11 -05:00
Josh Hawkins
bd1fc1cc72
API access improvements (#23183)
* restrict viewer access to logs, labels, and go2rtc stream list

* filter stats data for non admins

* track creator on vlm watch jobs and scope view/cancel to admin or creator

* add shortcut for admins in /stats
2026-05-13 10:40:29 -05:00
YDKK
e20fc521b1
fix: fix ReviewTimeline ZoomIn/Out tooltip text (#23184) 2026-05-13 10:28:20 -05:00
GuoQing Liu
19ec6fa245
fix: fix i18n (#23174)
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
* fix: fix embedding time locale

* fix: fix setting i18n

* fix: fix lpr setting item i18n

* fix: fix code
2026-05-13 07:38:33 -05: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
Puma7
e9432d55e8
log(masks): include camera name in invalid-coordinates error (#23156)
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
get_relative_coordinates() previously logged
"Not applying mask due to invalid coordinates. X,Y is outside ..."
without naming the camera, so on a multi-camera setup the user had
to guess which one to fix.

Add an optional camera_name kwarg with default "" (no behavior
change for existing callers). The global object-mask path in
FrigateConfig.validate_config passes camera_name=camera_config.name
since it already has it in scope, so legacy configs with absolute
pixel coordinates now get an actionable log line:

  Not applying mask due to invalid coordinates for camera back.
  9000,9000 is outside of the detection resolution 800x400.
  Use the editor in the UI to correct the mask.

Existing wording is preserved verbatim except for the inserted
" for camera <name>" segment. Runtime behavior is unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-10 15:21:44 -05:00
Weblate (bot)
1f154a0205
Translations update from Hosted Weblate (#23089)
* Added translation using Weblate (Galician)

Added translation using Weblate (Galician)

Added translation using Weblate (Galician)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Turkish)

Added translation using Weblate (Turkish)

Added translation using Weblate (Turkish)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Latvian)

Added translation using Weblate (Latvian)

Added translation using Weblate (Latvian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Lithuanian)

Added translation using Weblate (Lithuanian)

Added translation using Weblate (Lithuanian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Thai)

Added translation using Weblate (Thai)

Added translation using Weblate (Thai)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Portuguese (Brazil))

Added translation using Weblate (Portuguese (Brazil))

Added translation using Weblate (Portuguese (Brazil))

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Translated using Weblate (German)

Currently translated at 100.0% (1098 of 1098 strings)

Translated using Weblate (German)

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (German)

Currently translated at 100.0% (792 of 792 strings)

Translated using Weblate (German)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (German)

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (German)

Currently translated at 100.0% (45 of 45 strings)

Translated using Weblate (German)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (German)

Currently translated at 100.0% (40 of 40 strings)

Translated using Weblate (German)

Currently translated at 100.0% (101 of 101 strings)

Translated using Weblate (German)

Currently translated at 100.0% (236 of 236 strings)

Translated using Weblate (German)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (German)

Currently translated at 100.0% (792 of 792 strings)

Translated using Weblate (German)

Currently translated at 100.0% (64 of 64 strings)

Translated using Weblate (German)

Currently translated at 100.0% (1086 of 1086 strings)

Translated using Weblate (German)

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (German)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (German)

Currently translated at 100.0% (145 of 145 strings)

Added translation using Weblate (German)

Added translation using Weblate (German)

Added translation using Weblate (German)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Sebastian Sie <sebastian.neuplanitz@googlemail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/de/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-player
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
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

* Added translation using Weblate (Danish)

Added translation using Weblate (Danish)

Added translation using Weblate (Danish)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Translated using Weblate (Greek)

Currently translated at 4.5% (1 of 22 strings)

Translated using Weblate (Greek)

Currently translated at 4.0% (1 of 25 strings)

Translated using Weblate (Greek)

Currently translated at 0.2% (1 of 471 strings)

Added translation using Weblate (Greek)

Added translation using Weblate (Greek)

Added translation using Weblate (Greek)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Vasilis Ieropoulos <kirav96@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/el/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/el/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/el/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Groups
Translation: Frigate NVR/Config - Validation

* Translated using Weblate (Estonian)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Estonian)

Currently translated at 24.4% (11 of 45 strings)

Added translation using Weblate (Estonian)

Added translation using Weblate (Estonian)

Translated using Weblate (Estonian)

Currently translated at 17.5% (7 of 40 strings)

Added translation using Weblate (Estonian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/et/
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-replay

* Added translation using Weblate (Russian)

Added translation using Weblate (Russian)

Added translation using Weblate (Russian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Translated using Weblate (Romanian)

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (1098 of 1098 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (792 of 792 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (1092 of 1092 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (792 of 792 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (1086 of 1086 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (40 of 40 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (45 of 45 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (59 of 59 strings)

Added translation using Weblate (Romanian)

Added translation using Weblate (Romanian)

Added translation using Weblate (Romanian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: lukasig <lukasig@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/ro/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
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

* Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Ukrainian)

Added translation using Weblate (Ukrainian)

Added translation using Weblate (Ukrainian)

Translated using Weblate (Ukrainian)

Currently translated at 1.4% (7 of 469 strings)

Co-authored-by: A T <andrey.timchenko@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/uk/
Translation: Frigate NVR/Config - Cameras

* Translated using Weblate (Bosnian)

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (1092 of 1092 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (792 of 792 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (101 of 101 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (1086 of 1086 strings)

Translated using Weblate (Bosnian)

Currently translated at 99.7% (790 of 792 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (236 of 236 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (45 of 45 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (790 of 790 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (1081 of 1081 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (174 of 174 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (6 of 6 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (469 of 469 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (123 of 123 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (49 of 49 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (101 of 101 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (40 of 40 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (99 of 99 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (64 of 64 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Bosnian)

Currently translated at 21.1% (26 of 123 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Bosnian)

Currently translated at 4.6% (3 of 64 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (47 of 47 strings)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Translated using Weblate (Bosnian)

Currently translated at 4.0% (2 of 49 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (236 of 236 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (236 of 236 strings)

Translated using Weblate (Bosnian)

Currently translated at 50.0% (3 of 6 strings)

Translated using Weblate (Bosnian)

Currently translated at 0.5% (3 of 501 strings)

Translated using Weblate (Bosnian)

Currently translated at 0.2% (3 of 1081 strings)

Translated using Weblate (Bosnian)

Currently translated at 1.6% (2 of 123 strings)

Translated using Weblate (Bosnian)

Currently translated at 10.0% (1 of 10 strings)

Translated using Weblate (Bosnian)

Currently translated at 9.0% (2 of 22 strings)

Translated using Weblate (Bosnian)

Currently translated at 3.4% (2 of 58 strings)

Translated using Weblate (Bosnian)

Currently translated at 2.0% (1 of 49 strings)

Translated using Weblate (Bosnian)

Currently translated at 8.0% (2 of 25 strings)

Translated using Weblate (Bosnian)

Currently translated at 0.8% (1 of 123 strings)

Translated using Weblate (Bosnian)

Currently translated at 0.3% (2 of 501 strings)

Translated using Weblate (Bosnian)

Currently translated at 20.0% (2 of 10 strings)

Translated using Weblate (Bosnian)

Currently translated at 4.2% (2 of 47 strings)

Translated using Weblate (Bosnian)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Bosnian)

Currently translated at 2.0% (2 of 99 strings)

Translated using Weblate (Bosnian)

Currently translated at 1.9% (2 of 101 strings)

Translated using Weblate (Bosnian)

Currently translated at 0.2% (1 of 469 strings)

Translated using Weblate (Bosnian)

Currently translated at 2.3% (2 of 86 strings)

Translated using Weblate (Bosnian)

Currently translated at 0.1% (1 of 790 strings)

Translated using Weblate (Bosnian)

Currently translated at 5.0% (2 of 40 strings)

Translated using Weblate (Bosnian)

Currently translated at 50.0% (1 of 2 strings)

Translated using Weblate (Bosnian)

Currently translated at 0.8% (2 of 236 strings)

Translated using Weblate (Bosnian)

Currently translated at 3.1% (2 of 64 strings)

Translated using Weblate (Bosnian)

Currently translated at 1.3% (2 of 145 strings)

Translated using Weblate (Bosnian)

Currently translated at 1.3% (1 of 74 strings)

Translated using Weblate (Bosnian)

Currently translated at 1.1% (2 of 174 strings)

Translated using Weblate (Bosnian)

Currently translated at 33.3% (2 of 6 strings)

Translated using Weblate (Bosnian)

Currently translated at 7.6% (2 of 26 strings)

Translated using Weblate (Bosnian)

Currently translated at 0.1% (1 of 1081 strings)

Translated using Weblate (Bosnian)

Currently translated at 1.5% (2 of 129 strings)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Bosnian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: jasoisjaso <jaso.bih@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-icons/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-input/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-recording/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/bs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/bs/
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-icons
Translation: Frigate NVR/components-input
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

* Translated using Weblate (Japanese)

Currently translated at 62.8% (690 of 1098 strings)

Translated using Weblate (Japanese)

Currently translated at 82.5% (33 of 40 strings)

Translated using Weblate (Japanese)

Currently translated at 73.4% (47 of 64 strings)

Translated using Weblate (Japanese)

Currently translated at 54.2% (32 of 59 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Japanese)

Currently translated at 9.7% (46 of 471 strings)

Translated using Weblate (Japanese)

Currently translated at 9.9% (79 of 792 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (45 of 45 strings)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Translated using Weblate (Japanese)

Currently translated at 81.1% (82 of 101 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: alpha <etc@alpha-line.org>
Co-authored-by: 塩野拓夢 <salt.field1210@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ja/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-replay
Translation: Frigate NVR/views-settings

* Translated using Weblate (Catalan)

Currently translated at 100.0% (1098 of 1098 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (792 of 792 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1092 of 1092 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1092 of 1092 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (792 of 792 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1086 of 1086 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (45 of 45 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (40 of 40 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (40 of 40 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (45 of 45 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (45 of 45 strings)

Added translation using Weblate (Catalan)

Added translation using Weblate (Catalan)

Added translation using Weblate (Catalan)

Translated using Weblate (Catalan)

Currently translated at 100.0% (236 of 236 strings)

Co-authored-by: Eduardo Pastor Fernández <123eduardoneko123@gmail.com>
Co-authored-by: Gerard Ricart Castells <gerard.ricart@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: anton garcias <isaga.percompartir@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/ca/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/common
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

* Added translation using Weblate (Czech)

Added translation using Weblate (Czech)

Added translation using Weblate (Czech)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Portuguese)

Added translation using Weblate (Portuguese)

Added translation using Weblate (Portuguese)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Vietnamese)

Added translation using Weblate (Vietnamese)

Added translation using Weblate (Vietnamese)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Added translation using Weblate (Icelandic)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Armenian)

Added translation using Weblate (Armenian)

Added translation using Weblate (Armenian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Croatian)

Added translation using Weblate (Croatian)

Added translation using Weblate (Croatian)

Translated using Weblate (Croatian)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Croatian)

Currently translated at 91.5% (216 of 236 strings)

Translated using Weblate (Croatian)

Currently translated at 95.9% (118 of 123 strings)

Translated using Weblate (Croatian)

Currently translated at 98.9% (98 of 99 strings)

Translated using Weblate (Croatian)

Currently translated at 0.2% (1 of 469 strings)

Translated using Weblate (Croatian)

Currently translated at 4.0% (1 of 25 strings)

Translated using Weblate (Croatian)

Currently translated at 0.1% (1 of 790 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: jasoisjaso <jaso.bih@gmail.com>
Co-authored-by: stipe-jurkovic <sjurko00@fesb.hr>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/hr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/hr/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Groups
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-live

* Translated using Weblate (Hungarian)

Currently translated at 76.4% (133 of 174 strings)

Translated using Weblate (Hungarian)

Currently translated at 40.0% (16 of 40 strings)

Translated using Weblate (Hungarian)

Currently translated at 37.7% (17 of 45 strings)

Translated using Weblate (Hungarian)

Currently translated at 32.2% (19 of 59 strings)

Translated using Weblate (Hungarian)

Currently translated at 92.7% (219 of 236 strings)

Translated using Weblate (Hungarian)

Currently translated at 39.5% (428 of 1081 strings)

Added translation using Weblate (Hungarian)

Added translation using Weblate (Hungarian)

Added translation using Weblate (Hungarian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: ZsiGiT <zsigit@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/hu/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-replay
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system

* Added translation using Weblate (Hindi)

Added translation using Weblate (Hindi)

Added translation using Weblate (Hindi)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Hebrew)

Added translation using Weblate (Hebrew)

Added translation using Weblate (Hebrew)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Malayalam)

Added translation using Weblate (Malayalam)

Added translation using Weblate (Malayalam)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Translated using Weblate (Polish)

Currently translated at 59.7% (656 of 1098 strings)

Translated using Weblate (Polish)

Currently translated at 93.0% (120 of 129 strings)

Translated using Weblate (Polish)

Currently translated at 5.0% (2 of 40 strings)

Translated using Weblate (Polish)

Currently translated at 4.4% (2 of 45 strings)

Translated using Weblate (Polish)

Currently translated at 77.9% (46 of 59 strings)

Translated using Weblate (Polish)

Currently translated at 2.2% (1 of 45 strings)

Added translation using Weblate (Polish)

Added translation using Weblate (Polish)

Added translation using Weblate (Polish)

Co-authored-by: Dawid Kędzierski <dawidk612@wp.pl>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: K0RR <k0rr@users.noreply.hosted.weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Wojciech Niziński <niziak-weblate@spox.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/pl/
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

* Translated using Weblate (Italian)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Italian)

Currently translated at 24.1% (191 of 792 strings)

Translated using Weblate (Italian)

Currently translated at 76.8% (844 of 1098 strings)

Translated using Weblate (Italian)

Currently translated at 70.7% (777 of 1098 strings)

Translated using Weblate (Italian)

Currently translated at 18.6% (88 of 471 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (45 of 45 strings)

Translated using Weblate (Italian)

Currently translated at 23.9% (190 of 792 strings)

Translated using Weblate (Italian)

Currently translated at 70.3% (772 of 1098 strings)

Translated using Weblate (Italian)

Currently translated at 18.4% (87 of 471 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Italian)

Currently translated at 21.9% (174 of 792 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Italian)

Currently translated at 67.8% (745 of 1098 strings)

Translated using Weblate (Italian)

Currently translated at 16.1% (76 of 471 strings)

Translated using Weblate (Italian)

Currently translated at 67.4% (741 of 1098 strings)

Translated using Weblate (Italian)

Currently translated at 21.3% (169 of 792 strings)

Translated using Weblate (Italian)

Currently translated at 68.2% (741 of 1086 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (236 of 236 strings)

Translated using Weblate (Italian)

Currently translated at 16.1% (76 of 471 strings)

Translated using Weblate (Italian)

Currently translated at 21.3% (169 of 792 strings)

Translated using Weblate (Italian)

Currently translated at 11.1% (88 of 792 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (64 of 64 strings)

Translated using Weblate (Italian)

Currently translated at 66.1% (718 of 1086 strings)

Translated using Weblate (Italian)

Currently translated at 10.6% (50 of 471 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (40 of 40 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (236 of 236 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (45 of 45 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Italian)

Currently translated at 10.3% (82 of 792 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (236 of 236 strings)

Added translation using Weblate (Italian)

Added translation using Weblate (Italian)

Added translation using Weblate (Italian)

Co-authored-by: Gringo <ita.translations@tiscali.it>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/it/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
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

* Translated using Weblate (Arabic)

Currently translated at 4.0% (1 of 25 strings)

Translated using Weblate (Arabic)

Currently translated at 13.8% (14 of 101 strings)

Translated using Weblate (Arabic)

Currently translated at 0.1% (1 of 792 strings)

Translated using Weblate (Arabic)

Currently translated at 9.0% (2 of 22 strings)

Translated using Weblate (Arabic)

Currently translated at 2.5% (1 of 40 strings)

Translated using Weblate (Arabic)

Currently translated at 0.0% (0 of 59 strings)

Translated using Weblate (Arabic)

Currently translated at 0.4% (2 of 471 strings)

Added translation using Weblate (Arabic)

Added translation using Weblate (Arabic)

Added translation using Weblate (Arabic)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: محمد الخوالده <belalalkohawaldeh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/ar/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ar/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ar/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/ar/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/ar/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/ar/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/ar/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Groups
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-motionSearch

* Translated using Weblate (Indonesian)

Currently translated at 4.5% (1 of 22 strings)

Translated using Weblate (Indonesian)

Currently translated at 2.5% (1 of 40 strings)

Translated using Weblate (Indonesian)

Currently translated at 11.4% (27 of 236 strings)

Translated using Weblate (Indonesian)

Currently translated at 50.3% (65 of 129 strings)

Translated using Weblate (Indonesian)

Currently translated at 0.1% (1 of 792 strings)

Translated using Weblate (Indonesian)

Currently translated at 92.3% (24 of 26 strings)

Translated using Weblate (Indonesian)

Currently translated at 4.0% (1 of 25 strings)

Translated using Weblate (Indonesian)

Currently translated at 2.7% (30 of 1092 strings)

Translated using Weblate (Indonesian)

Currently translated at 13.9% (12 of 86 strings)

Translated using Weblate (Indonesian)

Currently translated at 0.2% (1 of 471 strings)

Translated using Weblate (Indonesian)

Currently translated at 19.1% (96 of 501 strings)

Translated using Weblate (Indonesian)

Currently translated at 25.2% (25 of 99 strings)

Added translation using Weblate (Indonesian)

Added translation using Weblate (Indonesian)

Added translation using Weblate (Indonesian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: R Setiawan <setiawan.kerjaan@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/id/
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-player
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings

* Added translation using Weblate (Dutch)

Added translation using Weblate (Dutch)

Added translation using Weblate (Dutch)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Translated using Weblate (Nepali)

Currently translated at 10.2% (5 of 49 strings)

Translated using Weblate (Nepali)

Currently translated at 1.0% (5 of 471 strings)

Translated using Weblate (Nepali)

Currently translated at 12.5% (5 of 40 strings)

Translated using Weblate (Nepali)

Currently translated at 5.4% (7 of 129 strings)

Translated using Weblate (Nepali)

Currently translated at 4.0% (5 of 123 strings)

Translated using Weblate (Nepali)

Currently translated at 50.0% (5 of 10 strings)

Translated using Weblate (Nepali)

Currently translated at 22.7% (5 of 22 strings)

Translated using Weblate (Nepali)

Currently translated at 7.8% (5 of 64 strings)

Translated using Weblate (Nepali)

Currently translated at 83.3% (5 of 6 strings)

Translated using Weblate (Nepali)

Currently translated at 19.2% (5 of 26 strings)

Translated using Weblate (Nepali)

Currently translated at 13.3% (6 of 45 strings)

Translated using Weblate (Nepali)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Nepali)

Currently translated at 8.4% (5 of 59 strings)

Translated using Weblate (Nepali)

Currently translated at 50.0% (5 of 10 strings)

Translated using Weblate (Nepali)

Currently translated at 6.7% (5 of 74 strings)

Translated using Weblate (Nepali)

Currently translated at 0.6% (1 of 145 strings)

Translated using Weblate (Nepali)

Currently translated at 4.6% (4 of 86 strings)

Translated using Weblate (Nepali)

Currently translated at 3.4% (6 of 174 strings)

Translated using Weblate (Nepali)

Currently translated at 10.3% (6 of 58 strings)

Translated using Weblate (Nepali)

Currently translated at 4.9% (5 of 101 strings)

Translated using Weblate (Nepali)

Currently translated at 0.4% (5 of 1086 strings)

Translated using Weblate (Nepali)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Nepali)

Currently translated at 1.5% (8 of 501 strings)

Translated using Weblate (Nepali)

Currently translated at 5.0% (5 of 99 strings)

Translated using Weblate (Nepali)

Currently translated at 1.6% (4 of 236 strings)

Translated using Weblate (Nepali)

Currently translated at 20.0% (5 of 25 strings)

Translated using Weblate (Nepali)

Currently translated at 10.6% (5 of 47 strings)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

Added translation using Weblate (Nepali)

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-icons/ne/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-input/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-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 - 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-icons
Translation: Frigate NVR/components-input
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

* Added translation using Weblate (Spanish)

Added translation using Weblate (Spanish)

Added translation using Weblate (Spanish)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Translated using Weblate (French)

Currently translated at 27.5% (11 of 40 strings)

Translated using Weblate (French)

Currently translated at 45.7% (27 of 59 strings)

Translated using Weblate (French)

Currently translated at 5.4% (43 of 792 strings)

Translated using Weblate (French)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (French)

Currently translated at 80.1% (81 of 101 strings)

Translated using Weblate (French)

Currently translated at 33.7% (159 of 471 strings)

Translated using Weblate (French)

Currently translated at 4.4% (2 of 45 strings)

Translated using Weblate (French)

Currently translated at 5.0% (3 of 59 strings)

Translated using Weblate (French)

Currently translated at 5.0% (2 of 40 strings)

Added translation using Weblate (French)

Added translation using Weblate (French)

Added translation using Weblate (French)

Translated using Weblate (French)

Currently translated at 77.4% (837 of 1081 strings)

Translated using Weblate (French)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (French)

Currently translated at 100.0% (236 of 236 strings)

Translated using Weblate (French)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (French)

Currently translated at 100.0% (123 of 123 strings)

Translated using Weblate (French)

Currently translated at 100.0% (174 of 174 strings)

Translated using Weblate (French)

Currently translated at 33.9% (159 of 469 strings)

Translated using Weblate (French)

Currently translated at 5.4% (43 of 790 strings)

Translated using Weblate (French)

Currently translated at 80.1% (81 of 101 strings)

Translated using Weblate (French)

Currently translated at 100.0% (145 of 145 strings)

Co-authored-by: Benoit St-Martin <benoitstmartin@gmail.com>
Co-authored-by: Buzz Android <romain.laurent.lejeune@outlook.fr>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jérémy MRPX <jeremy.marpaux@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: aztazt <007@free.fr>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/fr/
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-explore/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/fr/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-replay
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system

* Added translation using Weblate (Swedish)

Added translation using Weblate (Swedish)

Added translation using Weblate (Swedish)

Translated using Weblate (Swedish)

Currently translated at 4.5% (1 of 22 strings)

Translated using Weblate (Swedish)

Currently translated at 0.4% (2 of 469 strings)

Translated using Weblate (Swedish)

Currently translated at 4.0% (1 of 25 strings)

Translated using Weblate (Swedish)

Currently translated at 0.1% (1 of 790 strings)

Co-authored-by: Fredrik B <fredrik@brannvall.nu>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/sv/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Groups
Translation: Frigate NVR/Config - Validation

* Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Finnish)

Added translation using Weblate (Finnish)

Added translation using Weblate (Finnish)

Translated using Weblate (Finnish)

Currently translated at 50.8% (120 of 236 strings)

Translated using Weblate (Finnish)

Currently translated at 39.0% (25 of 64 strings)

Translated using Weblate (Finnish)

Currently translated at 19.9% (216 of 1081 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: MarkA <marka@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/fi/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/fi/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/fi/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-settings

* Added translation using Weblate (Serbian)

Added translation using Weblate (Serbian)

Added translation using Weblate (Serbian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Albanian)

Added translation using Weblate (Albanian)

Added translation using Weblate (Albanian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Korean)

Added translation using Weblate (Korean)

Added translation using Weblate (Korean)

Translated using Weblate (Korean)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Korean)

Currently translated at 6.1% (29 of 469 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Korean)

Currently translated at 12.2% (97 of 790 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (236 of 236 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: John <john@akfn.net>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ko/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/ko/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/ko/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ko/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ko/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-player

* Added translation using Weblate (Kannada)

Added translation using Weblate (Kannada)

Added translation using Weblate (Kannada)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Slovak)

Added translation using Weblate (Slovak)

Added translation using Weblate (Slovak)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Slovenian)

Added translation using Weblate (Slovenian)

Added translation using Weblate (Slovenian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Translated using Weblate (Urdu)

Currently translated at 80.0% (8 of 10 strings)

Translated using Weblate (Urdu)

Currently translated at 10.1% (51 of 501 strings)

Translated using Weblate (Urdu)

Currently translated at 6.8% (4 of 58 strings)

Translated using Weblate (Urdu)

Currently translated at 0.7% (1 of 129 strings)

Added translation using Weblate (Urdu)

Added translation using Weblate (Urdu)

Added translation using Weblate (Urdu)

Co-authored-by: David Ghum <dghum2024@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/ur/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/ur/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/ur/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ur/
Translation: Frigate NVR/audio
Translation: Frigate NVR/components-auth
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-facelibrary

* Added translation using Weblate (Uzbek)

Added translation using Weblate (Uzbek)

Added translation using Weblate (Uzbek)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Added translation using Weblate (Chinese (Traditional Han script))

Added translation using Weblate (Chinese (Traditional Han script))

Added translation using Weblate (Chinese (Traditional Han script))

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1098 of 1098 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% (471 of 471 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1092 of 1092 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (45 of 45 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (40 of 40 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% (59 of 59 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1086 of 1086 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 42.2% (19 of 45 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 50.0% (20 of 40 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 33.8% (20 of 59 strings)

Added translation using Weblate (Chinese (Simplified Han script))

Added translation using Weblate (Chinese (Simplified Han script))

Added translation using Weblate (Chinese (Simplified Han script))

Co-authored-by: GuoQing Liu <842607283@qq.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
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/views-chat/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/zh_Hans/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
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

* Added translation using Weblate (Norwegian Bokmål)

Added translation using Weblate (Norwegian Bokmål)

Added translation using Weblate (Norwegian Bokmål)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (101 of 101 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: OverTheHillsAndFarAway <prosjektx@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/nb_NO/
Translation: Frigate NVR/components-dialog

* Added translation using Weblate (Cantonese (Traditional Han script))

Added translation using Weblate (Cantonese (Traditional Han script))

Added translation using Weblate (Cantonese (Traditional Han script))

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>

---------

Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Sebastian Sie <sebastian.neuplanitz@googlemail.com>
Co-authored-by: Vasilis Ieropoulos <kirav96@gmail.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: lukasig <lukasig@hotmail.com>
Co-authored-by: A T <andrey.timchenko@gmail.com>
Co-authored-by: jasoisjaso <jaso.bih@gmail.com>
Co-authored-by: alpha <etc@alpha-line.org>
Co-authored-by: 塩野拓夢 <salt.field1210@gmail.com>
Co-authored-by: Eduardo Pastor Fernández <123eduardoneko123@gmail.com>
Co-authored-by: Gerard Ricart Castells <gerard.ricart@gmail.com>
Co-authored-by: anton garcias <isaga.percompartir@gmail.com>
Co-authored-by: stipe-jurkovic <sjurko00@fesb.hr>
Co-authored-by: ZsiGiT <zsigit@gmail.com>
Co-authored-by: Dawid Kędzierski <dawidk612@wp.pl>
Co-authored-by: K0RR <k0rr@users.noreply.hosted.weblate.org>
Co-authored-by: Wojciech Niziński <niziak-weblate@spox.org>
Co-authored-by: Gringo <ita.translations@tiscali.it>
Co-authored-by: محمد الخوالده <belalalkohawaldeh@gmail.com>
Co-authored-by: R Setiawan <setiawan.kerjaan@gmail.com>
Co-authored-by: bijaydewan <bijaydewan@gmail.com>
Co-authored-by: Benoit St-Martin <benoitstmartin@gmail.com>
Co-authored-by: Buzz Android <romain.laurent.lejeune@outlook.fr>
Co-authored-by: Jérémy MRPX <jeremy.marpaux@gmail.com>
Co-authored-by: aztazt <007@free.fr>
Co-authored-by: Fredrik B <fredrik@brannvall.nu>
Co-authored-by: MarkA <marka@users.noreply.hosted.weblate.org>
Co-authored-by: John <john@akfn.net>
Co-authored-by: David Ghum <dghum2024@gmail.com>
Co-authored-by: GuoQing Liu <842607283@qq.com>
Co-authored-by: OverTheHillsAndFarAway <prosjektx@users.noreply.hosted.weblate.org>
2026-05-10 12:38:27 -05:00
dependabot[bot]
fb68e95725
Bump fast-uri from 3.1.0 to 3.1.2 in /docs (#23139)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.0 to 3.1.2.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.2)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-10 12:12:44 -05:00
dependabot[bot]
6902de1d64
Bump tailwind-scrollbar from 3.1.0 to 4.0.2 in /web
Bumps [tailwind-scrollbar](https://github.com/adoxography/tailwind-scrollbar) from 3.1.0 to 4.0.2.
- [Release notes](https://github.com/adoxography/tailwind-scrollbar/releases)
- [Commits](https://github.com/adoxography/tailwind-scrollbar/compare/v3.1.0...v4.0.2)

---
updated-dependencies:
- dependency-name: tailwind-scrollbar
  dependency-version: 4.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-01 13:18:09 +00:00
522 changed files with 33366 additions and 3545 deletions

View File

@ -162,7 +162,6 @@ When reviewing code, do NOT comment on:
- **Linting**: ESLint (see `web/.eslintrc.cjs`)
- **Formatting**: Prettier with Tailwind CSS plugin
- **Type Safety**: TypeScript strict mode enabled
- **Testing**: Vitest for unit tests
### Component Patterns
@ -233,6 +232,9 @@ ruff format frigate/
# Run linter
ruff check frigate/
# Type check
python3 -u -m mypy --config-file frigate/mypy.ini frigate
```
### Frontend (from web/ directory)
@ -252,6 +254,38 @@ npm run lint:fix
# Format code
npm run prettier:write
# E2E: first-time setup
npm install
npx playwright install chromium
# E2E: build the app and run all tests
npm run e2e:build && npm run e2e
# E2E: interactive UI for debugging
npm run e2e:ui
# E2E: run a specific spec
npx playwright test --config e2e/playwright.config.ts e2e/specs/live.spec.ts
# E2E: filter by name, or run only desktop/mobile
npx playwright test --config e2e/playwright.config.ts --grep="severity tab"
npx playwright test --config e2e/playwright.config.ts --project=desktop
# E2E: regenerate mock data after backend model changes (from repo root)
PYTHONPATH=. python3 web/e2e/fixtures/mock-data/generate-mock-data.py
# Regenerate config translations from Pydantic models — outputs to
# web/public/locales/en/config/{global,cameras}.json. NEVER edit those
# JSON files by hand; change the Pydantic field title/description and
# re-run this script. (from repo root)
python3 generate_config_translations.py
# Extract i18n keys from source into the locale files after adding
# new t() calls. Use the :ci variant to verify the locale files are
# in sync with source (fails if extraction would change anything).
npm run i18n:extract
npm run i18n:extract:ci
```
### Docker Development
@ -371,6 +405,10 @@ except ValueError:
)
```
## WebSocket Broadcasts
Outbound WebSocket broadcasts go through a per-recipient classifier in `frigate/comms/ws.py` that enforces camera-level access. **The classifier is fail-closed: any topic it doesn't recognize is dropped for every client.** New outbound topics must be classified there or they'll silently disappear.
## Project-Specific Conventions
### Configuration Files

View File

@ -3,7 +3,6 @@
import json
import os
import sys
from pathlib import Path
from typing import Any
from ruamel.yaml import YAML
@ -18,37 +17,12 @@ from frigate.const import (
)
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
from frigate.util.config import find_config_file
from frigate.util.services import is_restricted_go2rtc_source
sys.path.remove("/opt/frigate")
yaml = YAML()
# Check if arbitrary exec sources are allowed (defaults to False for security)
allow_arbitrary_exec = None
if "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.environ:
allow_arbitrary_exec = os.environ.get("GO2RTC_ALLOW_ARBITRARY_EXEC")
elif (
os.path.isdir("/run/secrets")
and os.access("/run/secrets", os.R_OK)
and "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.listdir("/run/secrets")
):
allow_arbitrary_exec = (
Path(os.path.join("/run/secrets", "GO2RTC_ALLOW_ARBITRARY_EXEC"))
.read_text()
.strip()
)
# check for the add-on options file
elif os.path.isfile("/data/options.json"):
with open("/data/options.json") as f:
raw_options = f.read()
options = json.loads(raw_options)
allow_arbitrary_exec = options.get("go2rtc_allow_arbitrary_exec")
ALLOW_ARBITRARY_EXEC = allow_arbitrary_exec is not None and str(
allow_arbitrary_exec
).lower() in ("true", "1", "yes")
config_file = find_config_file()
try:
@ -128,18 +102,13 @@ if LIBAVFORMAT_VERSION_MAJOR < 59:
go2rtc_config["ffmpeg"]["rtsp"] = rtsp_args
def is_restricted_source(stream_source: str) -> bool:
"""Check if a stream source is restricted (echo, expr, or exec)."""
return stream_source.strip().startswith(("echo:", "expr:", "exec:"))
for name in list(go2rtc_config.get("streams", {})):
stream = go2rtc_config["streams"][name]
if isinstance(stream, str):
try:
formatted_stream = substitute_frigate_vars(stream)
if not ALLOW_ARBITRARY_EXEC and is_restricted_source(formatted_stream):
if is_restricted_go2rtc_source(formatted_stream):
print(
f"[ERROR] Stream '{name}' uses a restricted source (echo/expr/exec) which is disabled by default for security. "
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
@ -158,7 +127,7 @@ for name in list(go2rtc_config.get("streams", {})):
for i, stream_item in enumerate(stream):
try:
formatted_stream = substitute_frigate_vars(stream_item)
if not ALLOW_ARBITRARY_EXEC and is_restricted_source(formatted_stream):
if is_restricted_go2rtc_source(formatted_stream):
print(
f"[ERROR] Stream '{name}' item {i + 1} uses a restricted source (echo/expr/exec) which is disabled by default for security. "
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."

View File

@ -172,7 +172,7 @@ Custom models may also require different input tensor formats. The colorspace co
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detection model" /> to configure the model path, dimensions, and input format.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and open the **Custom Model** tab to configure the model path, dimensions, and input format.
| Field | Description |
| --------------------------------------------- | ------------------------------------ |

View File

@ -110,7 +110,7 @@ Here are some common starter configuration examples. These can be configured thr
1. Navigate to <NavPath path="Settings > System > MQTT" /> and configure the MQTT connection to your Home Assistant Mosquitto broker
2. Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `Raspberry Pi (H.264)`
3. Navigate to <NavPath path="Settings > System > Detector hardware" /> and add a detector with **Type** `EdgeTPU` and **Device** `usb`
3. Navigate to <NavPath path="Settings > System > Detectors and model" /> and add a detector with **Type** `EdgeTPU` and **Device** `usb`
4. Navigate to <NavPath path="Settings > Global configuration > Recording" /> and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion`
5. Navigate to <NavPath path="Settings > Global configuration > Snapshots" /> and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30`
6. Navigate to <NavPath path="Settings > Camera configuration > Management" /> and add your camera with the appropriate RTSP stream URL
@ -189,7 +189,7 @@ cameras:
1. Navigate to <NavPath path="Settings > System > MQTT" /> and set **Enable MQTT** to off
2. Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`
3. Navigate to <NavPath path="Settings > System > Detector hardware" /> and add a detector with **Type** `EdgeTPU` and **Device** `usb`
3. Navigate to <NavPath path="Settings > System > Detectors and model" /> and add a detector with **Type** `EdgeTPU` and **Device** `usb`
4. Navigate to <NavPath path="Settings > Global configuration > Recording" /> and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion`
5. Navigate to <NavPath path="Settings > Global configuration > Snapshots" /> and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30`
6. Navigate to <NavPath path="Settings > Camera configuration > Management" /> and add your camera with the appropriate RTSP stream URL
@ -266,8 +266,8 @@ cameras:
1. Navigate to <NavPath path="Settings > System > MQTT" /> and configure the connection to your MQTT broker
2. Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`
3. Navigate to <NavPath path="Settings > System > Detector hardware" /> and add a detector with **Type** `openvino` and **Device** `AUTO`
4. Navigate to <NavPath path="Settings > System > Detection model" /> and configure the OpenVINO model path and settings
3. Navigate to <NavPath path="Settings > System > Detectors and model" /> and add a detector with **Type** `openvino` and **Device** `AUTO`
4. On the same page, in the **Custom Model** tab, configure the OpenVINO model path and settings
5. Navigate to <NavPath path="Settings > Global configuration > Recording" /> and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion`
6. Navigate to <NavPath path="Settings > Global configuration > Snapshots" /> and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30`
7. Navigate to <NavPath path="Settings > Camera configuration > Management" /> and add your camera with the appropriate RTSP stream URL

View File

@ -72,7 +72,7 @@ This does not affect using hardware for accelerating other tasks such as [semant
# Officially Supported Detectors
Frigate provides a number of builtin detector types. By default, Frigate will use a single OpenVINO detector running on the CPU. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras.
Frigate provides a number of builtin detector types. By default, Frigate will use a single CPU detector. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras.
## Edge TPU Detector
@ -91,7 +91,7 @@ See [common Edge TPU troubleshooting steps](/troubleshooting/edgetpu) if the Edg
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`.
</TabItem>
<TabItem value="yaml">
@ -111,7 +111,7 @@ detectors:
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors, specifying `usb:0` and `usb:1` as the device for each.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors, specifying `usb:0` and `usb:1` as the device for each.
</TabItem>
<TabItem value="yaml">
@ -136,7 +136,7 @@ _warning: may have [compatibility issues](https://github.com/blakeblackshear/fri
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **EdgeTPU** from the detector type dropdown and click **Add**, then leave the device field empty.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **EdgeTPU** from the detector type dropdown and click **Add**, then leave the device field empty.
</TabItem>
<TabItem value="yaml">
@ -156,7 +156,7 @@ detectors:
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `pci`.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `pci`.
</TabItem>
<TabItem value="yaml">
@ -176,7 +176,7 @@ detectors:
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors, specifying `pci:0` and `pci:1` as the device for each.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors, specifying `pci:0` and `pci:1` as the device for each.
</TabItem>
<TabItem value="yaml">
@ -199,7 +199,7 @@ detectors:
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors with different device types (e.g., `usb` and `pci`).
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors with different device types (e.g., `usb` and `pci`).
</TabItem>
<TabItem value="yaml">
@ -246,7 +246,7 @@ After placing the downloaded files for the tflite model and labels in your confi
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure the model settings:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`. Then on the same page, in the **Custom Model** tab, configure the model settings:
| Field | Value |
| ---------------------------------------- | ----------------------------------------------------------------- |
@ -309,7 +309,7 @@ Use this configuration for YOLO-based models. When no custom model path or URL i
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure the model settings:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then on the same page, in the **Custom Model** tab, configure the model settings:
| Field | Value |
| ---------------------------------------- | ----------------------- |
@ -365,7 +365,7 @@ For SSD-based models, provide either a model path or URL to your compiled SSD mo
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure the model settings:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then on the same page, in the **Custom Model** tab, configure the model settings:
| Field | Value |
| --------------------------------------- | ------ |
@ -410,7 +410,7 @@ The Hailo detector supports all YOLO models compiled for Hailo hardware that inc
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure the model settings to match your custom model dimensions and format.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then on the same page, in the **Custom Model** tab, configure the model settings to match your custom model dimensions and format.
</TabItem>
<TabItem value="yaml">
@ -465,7 +465,7 @@ When using many cameras one detector may not be enough to keep up. Multiple dete
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **OpenVINO** from the detector type dropdown and click **Add** to add multiple detectors, each targeting `GPU` or `NPU`.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **OpenVINO** from the detector type dropdown and click **Add** to add multiple detectors, each targeting `GPU` or `NPU`.
</TabItem>
<TabItem value="yaml">
@ -508,7 +508,7 @@ Use the model configuration shown below when using the OpenVINO detector with th
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | ------------------------------------------ |
@ -558,7 +558,7 @@ After placing the downloaded onnx model in your config folder, use the following
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | ------------------------------------------------- |
@ -620,7 +620,7 @@ After placing the downloaded onnx model in your config folder, use the following
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | -------------------------------------------------------- |
@ -676,7 +676,7 @@ After placing the downloaded onnx model in your `config/model_cache` folder, use
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| --------------------------------------- | --------------------------------- |
@ -728,7 +728,7 @@ After placing the downloaded onnx model in your config/model_cache folder, use t
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `CPU`. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `CPU`. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | ---------------------------------- |
@ -807,7 +807,7 @@ Using the detector config below will connect to the client:
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`.
</TabItem>
<TabItem value="yaml">
@ -841,7 +841,7 @@ When Frigate is started with the following config it will connect to the detecto
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | -------------------------------------------------------- |
@ -1002,7 +1002,7 @@ When using many cameras one detector may not be enough to keep up. Multiple dete
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **ONNX** from the detector type dropdown and click **Add** to add multiple detectors.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **ONNX** from the detector type dropdown and click **Add** to add multiple detectors.
</TabItem>
<TabItem value="yaml">
@ -1050,7 +1050,7 @@ After placing the downloaded onnx model in your config folder, use the following
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | ------------------------------------------------- |
@ -1109,7 +1109,7 @@ After placing the downloaded onnx model in your config folder, use the following
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | -------------------------------------------------------- |
@ -1158,7 +1158,7 @@ After placing the downloaded onnx model in your config folder, use the following
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | -------------------------------------------------------- |
@ -1207,7 +1207,7 @@ After placing the downloaded onnx model in your `config/model_cache` folder, use
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| --------------------------------------- | --------------------------------- |
@ -1252,7 +1252,7 @@ After placing the downloaded onnx model in your `config/model_cache` folder, use
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | ------------------------------------------- |
@ -1328,7 +1328,7 @@ A TensorFlow Lite model is provided in the container at `/cpu_model.tflite` and
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **CPU** from the detector type dropdown and click **Add**. Configure the number of threads and click **Add** again to add additional CPU detectors as needed (one per camera is recommended).
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **CPU** from the detector type dropdown and click **Add**. Configure the number of threads and click **Add** again to add additional CPU detectors as needed (one per camera is recommended).
</TabItem>
<TabItem value="yaml">
@ -1364,7 +1364,7 @@ To integrate CodeProject.AI into Frigate, configure the detector as follows:
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **DeepStack** from the detector type dropdown and click **Add**. Set the API URL to point to your CodeProject.AI server (e.g., `http://<your_codeproject_ai_server_ip>:<port>/v1/vision/detection`).
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **DeepStack** from the detector type dropdown and click **Add**. Set the API URL to point to your CodeProject.AI server (e.g., `http://<your_codeproject_ai_server_ip>:<port>/v1/vision/detection`).
</TabItem>
<TabItem value="yaml">
@ -1403,7 +1403,7 @@ To configure the MemryX detector, use the following example configuration:
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`.
</TabItem>
<TabItem value="yaml">
@ -1423,7 +1423,7 @@ detectors:
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **MemryX** from the detector type dropdown and click **Add** to add multiple detectors, specifying `PCIe:0`, `PCIe:1`, `PCIe:2`, etc. as the device for each.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **MemryX** from the detector type dropdown and click **Add** to add multiple detectors, specifying `PCIe:0`, `PCIe:1`, `PCIe:2`, etc. as the device for each.
</TabItem>
<TabItem value="yaml">
@ -1467,7 +1467,7 @@ Below is the recommended configuration for using the **YOLO-NAS** (small) model
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | ------------------------------------------------- |
@ -1515,7 +1515,7 @@ Below is the recommended configuration for using the **YOLOv9** (small) model wi
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | ------------------------------------------------- |
@ -1562,7 +1562,7 @@ Below is the recommended configuration for using the **YOLOX** (small) model wit
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | ----------------------- |
@ -1609,7 +1609,7 @@ Below is the recommended configuration for using the **SSDLite MobileNet v2** mo
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | ----------------------- |
@ -1768,7 +1768,7 @@ Use the config below to work with generated TRT models:
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **TensorRT** from the detector type dropdown and click **Add**, then set the device to `0` (the default GPU index). Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **TensorRT** from the detector type dropdown and click **Add**, then set the device to `0` (the default GPU index). Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | ------------------------------------------------------------ |
@ -1825,7 +1825,7 @@ Use the model configuration shown below when using the synaptics detector with t
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **Synaptics** from the detector type dropdown and click **Add**. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **Synaptics** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | ---------------------------- |
@ -1879,7 +1879,7 @@ When using many cameras one detector may not be enough to keep up. Multiple dete
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **RKNN** from the detector type dropdown and click **Add** to add multiple detectors, each with `num_cores` set to `0` for automatic selection.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **RKNN** from the detector type dropdown and click **Add** to add multiple detectors, each with `num_cores` set to `0` for automatic selection.
</TabItem>
<TabItem value="yaml">
@ -1921,7 +1921,7 @@ This `config.yml` shows all relevant options to configure the detector and expla
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **RKNN** from the detector type dropdown and click **Add**. Set `num_cores` to `0` for automatic selection (increase for better performance on multicore NPUs, e.g., set to `3` on rk3588).
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **RKNN** from the detector type dropdown and click **Add**. Set `num_cores` to `0` for automatic selection (increase for better performance on multicore NPUs, e.g., set to `3` on rk3588).
</TabItem>
<TabItem value="yaml">
@ -1958,7 +1958,7 @@ The inference time was determined on a rk3588 with 3 NPU cores.
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | ----------------------------------------------------------------------- |
@ -2004,7 +2004,7 @@ The pre-trained YOLO-NAS weights from DeciAI are subject to their license and ca
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | -------------------------------------------------- |
@ -2044,7 +2044,7 @@ model: # required
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | ---------------------------------------------- |
@ -2138,7 +2138,7 @@ Once completed, configure the detector as follows:
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to your AI server (e.g., service name, container name, or `host:port`), the zoo to `degirum/public`, and provide your authentication token if needed.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to your AI server (e.g., service name, container name, or `host:port`), the zoo to `degirum/public`, and provide your authentication token if needed.
</TabItem>
<TabItem value="yaml">
@ -2181,7 +2181,7 @@ It is also possible to eliminate the need for an AI server and run the hardware
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to `@local`, the zoo to `degirum/public`, and provide your authentication token.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to `@local`, the zoo to `degirum/public`, and provide your authentication token.
</TabItem>
<TabItem value="yaml">
@ -2218,7 +2218,7 @@ If you do not possess whatever hardware you want to run, there's also the option
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to `@cloud`, the zoo to `degirum/public`, and provide your authentication token.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to `@cloud`, the zoo to `degirum/public`, and provide your authentication token.
</TabItem>
<TabItem value="yaml">
@ -2274,7 +2274,7 @@ Use the model configuration shown below when using the axengine detector with th
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and select **AXEngine NPU** from the detector type dropdown and click **Add**. Then navigate to <NavPath path="Settings > System > Detection model" /> and configure:
Navigate to <NavPath path="Settings > System > Detectors and model" /> and select **AXEngine NPU** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
| Field | Value |
| ---------------------------------------- | ----------------------- |

View File

@ -204,8 +204,8 @@ You need to refer to **Configure hardware acceleration** above to enable the con
<ConfigTabs>
<TabItem value="ui">
1. Navigate to <NavPath path="Settings > System > Detector hardware" /> and add a detector with **Type** `OpenVINO` and **Device** `GPU`
2. Navigate to <NavPath path="Settings > System > Detection model" /> and configure the model settings for OpenVINO:
1. Navigate to <NavPath path="Settings > System > Detectors and model" /> and add a detector with **Type** `OpenVINO` and **Device** `GPU`
2. On the same page, in the **Custom Model** tab, configure the model settings for OpenVINO:
| Field | Value |
| ---------------------------------------- | ------------------------------------------ |
@ -273,7 +273,7 @@ services:
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Detector hardware" /> and add a detector with **Type** `EdgeTPU` and **Device** `usb`.
Navigate to <NavPath path="Settings > System > Detectors and model" /> and add a detector with **Type** `EdgeTPU` and **Device** `usb`.
</TabItem>
<TabItem value="yaml">

View File

@ -3,6 +3,8 @@ id: plus
title: Frigate+
---
import NavPath from "@site/src/components/NavPath";
For more information about how to use Frigate+ to improve your model, see the [Frigate+ docs](/plus/).
:::info
@ -57,7 +59,7 @@ You can view all of your submitted images at [https://plus.frigate.video](https:
Once you have [requested your first model](../plus/first_model.md) and gotten your own model ID, it can be used with a special model path. No other information needs to be configured for Frigate+ models because it fetches the remaining config from Frigate+ automatically.
You can either choose the new model from the Frigate+ pane in the Settings page of the Frigate UI, or manually set the model at the root level in your config:
You can either choose the new model from the <NavPath path="Settings > System > Detectors and model" /> pane in the Frigate UI (the **Frigate+ Model** tab), or manually set the model at the root level in your config:
```yaml
detectors: ...

View File

@ -37,6 +37,8 @@ The per-clip variation is typically quite low and is mostly an artifact of keyfr
Debug Replay lets you re-run Frigate's detection pipeline against a section of recorded video without manually configuring a dummy camera. It automatically extracts the recording, creates a temporary camera with the same detection settings as the original, and loops the clip through the pipeline so you can observe detections in real time.
Debug Replay isn't intended to be a one-stop pane for all Frigate diagnostics or a comprehensive debugging environment for every Frigate feature. It merely makes it easier to spin up a "dummy camera" and perform some common adjustments in real-time. You'll still need to use the normal tools (logs, an MQTT client, etc) to debug your feature.
### When to use
- Reproducing a detection or tracking issue from a specific time range

View File

@ -10971,9 +10971,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"funding": [
{
"type": "github",

View File

@ -63,8 +63,8 @@ SYSTEM_NAV: dict[str, tuple[str, str]] = {
"environment_vars": ("System", "Environment variables"),
"telemetry": ("System", "Telemetry"),
"birdseye": ("System", "Birdseye"),
"detectors": ("System", "Detector hardware"),
"model": ("System", "Detection model"),
"detectors": ("System", "Detectors and model"),
"model": ("System", "Detectors and model"),
}
# All known top-level config section keys

View File

@ -96,11 +96,46 @@ def version():
@router.get("/stats", dependencies=[Depends(allow_any_authenticated())])
def stats(request: Request):
return JSONResponse(content=request.app.stats_emitter.get_latest_stats())
def stats(
request: Request,
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
):
stats_data = request.app.stats_emitter.get_latest_stats()
# Admins see the full snapshot
if request.headers.get("remote-role") == "admin":
return JSONResponse(content=stats_data)
allowed_set = set(allowed_cameras)
# Shallow-copy so we don't mutate the cached stats history entry.
filtered = {**stats_data}
cameras = stats_data.get("cameras")
if cameras is not None:
filtered["cameras"] = {
name: data for name, data in cameras.items() if name in allowed_set
}
bandwidth = stats_data.get("bandwidth_usages")
if bandwidth is not None:
filtered["bandwidth_usages"] = {
name: data for name, data in bandwidth.items() if name in allowed_set
}
# cmdline can leak camera URLs/paths; strip but keep cpu/mem so
# client-side problem heuristics still work.
cpu_usages = stats_data.get("cpu_usages")
if cpu_usages is not None:
filtered["cpu_usages"] = {
pid: {k: v for k, v in usage.items() if k != "cmdline"}
for pid, usage in cpu_usages.items()
}
return JSONResponse(content=filtered)
@router.get("/stats/history", dependencies=[Depends(allow_any_authenticated())])
@router.get("/stats/history", dependencies=[Depends(require_role(["admin"]))])
def stats_history(request: Request, keys: str = None):
if keys:
keys = keys.split(",")
@ -739,6 +774,8 @@ def config_set(request: Request, body: AppConfigSetBody):
if request.app.dispatcher is not None:
request.app.dispatcher.config = config
for comm in request.app.dispatcher.comms:
comm.config = config
if body.update_topic:
if body.update_topic.startswith("config/cameras/"):
@ -835,7 +872,7 @@ def nvinfo():
@router.get(
"/logs/{service}",
tags=[Tags.logs],
dependencies=[Depends(allow_any_authenticated())],
dependencies=[Depends(require_role(["admin"]))],
)
async def logs(
service: str = Path(enum=["frigate", "nginx", "go2rtc"]),
@ -1040,12 +1077,27 @@ def get_media_sync_status(job_id: str):
@router.get("/labels", dependencies=[Depends(allow_any_authenticated())])
def get_labels(camera: str = ""):
def get_labels(
camera: str = "",
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
):
try:
if camera:
if camera not in allowed_cameras:
return JSONResponse(
content={
"success": False,
"message": f"Access denied to camera '{camera}'",
},
status_code=403,
)
events = Event.select(Event.label).where(Event.camera == camera).distinct()
else:
events = Event.select(Event.label).distinct()
events = (
Event.select(Event.label)
.where(Event.camera << allowed_cameras)
.distinct()
)
except Exception as e:
logger.error(e)
return JSONResponse(
@ -1058,9 +1110,16 @@ def get_labels(camera: str = ""):
@router.get("/sub_labels", dependencies=[Depends(allow_any_authenticated())])
def get_sub_labels(split_joined: Optional[int] = None):
def get_sub_labels(
split_joined: Optional[int] = None,
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
):
try:
events = Event.select(Event.sub_label).distinct()
events = (
Event.select(Event.sub_label)
.where(Event.camera << allowed_cameras)
.distinct()
)
except Exception:
return JSONResponse(
content=({"success": False, "message": "Failed to get sub_labels"}),

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]:

View File

@ -19,7 +19,9 @@ from zeep.exceptions import Fault, TransportError
from zeep.transports import AsyncTransport
from frigate.api.auth import (
_get_stream_owner_cameras,
allow_any_authenticated,
get_current_user,
require_go2rtc_stream_access,
require_role,
)
@ -31,11 +33,12 @@ from frigate.config.camera.updater import (
CameraConfigUpdateTopic,
)
from frigate.config.env import substitute_frigate_vars
from frigate.models import User
from frigate.util.builtin import clean_camera_user_pass
from frigate.util.camera_cleanup import cleanup_camera_db, cleanup_camera_files
from frigate.util.config import find_config_file
from frigate.util.image import run_ffmpeg_snapshot
from frigate.util.services import ffprobe_stream
from frigate.util.services import ffprobe_stream, is_restricted_go2rtc_source
logger = logging.getLogger(__name__)
@ -66,7 +69,7 @@ def _is_valid_host(host: str) -> bool:
@router.get("/go2rtc/streams", dependencies=[Depends(allow_any_authenticated())])
def go2rtc_streams():
async def go2rtc_streams(request: Request):
r = requests.get("http://127.0.0.1:1984/api/streams")
if not r.ok:
logger.error("Failed to fetch streams from go2rtc")
@ -75,6 +78,24 @@ def go2rtc_streams():
status_code=500,
)
stream_data = r.json()
# Roles with an explicit camera list see only streams owned by an allowed
# camera. Admin and full-access roles (no list / empty list) see all streams.
current_user = await get_current_user(request)
if not isinstance(current_user, JSONResponse):
role = current_user["role"]
roles_dict = request.app.frigate_config.auth.roles
if role != "admin" and roles_dict.get(role):
all_camera_names = set(request.app.frigate_config.cameras.keys())
allowed_cameras = set(
User.get_allowed_cameras(role, roles_dict, all_camera_names)
)
stream_data = {
name: data
for name, data in stream_data.items()
if _get_stream_owner_cameras(request, name) & allowed_cameras
}
for data in stream_data.values():
for producer in data.get("producers") or []:
producer["url"] = clean_camera_user_pass(producer.get("url", ""))
@ -126,9 +147,24 @@ def go2rtc_add_stream(request: Request, stream_name: str, src: str = ""):
params = {"name": stream_name}
if src:
try:
params["src"] = substitute_frigate_vars(src)
resolved_src = substitute_frigate_vars(src)
except KeyError:
params["src"] = src
resolved_src = src
if is_restricted_go2rtc_source(resolved_src):
logger.warning(
"Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)",
stream_name,
)
return JSONResponse(
content={
"success": False,
"message": "Restricted stream source type",
},
status_code=400,
)
params["src"] = resolved_src
r = requests.put(
"http://127.0.0.1:1984/api/streams",
@ -966,7 +1002,6 @@ async def onvif_probe(
probe = ffprobe_stream(
request.app.frigate_config.ffmpeg, test_uri, detailed=False
)
print(probe)
ok = probe is not None and getattr(probe, "returncode", 1) == 0
tested_candidates.append(
{

View File

@ -10,7 +10,7 @@ from functools import reduce
from typing import Any, Dict, List, Optional
import cv2
from fastapi import APIRouter, Body, Depends, Request
from fastapi import APIRouter, Body, Depends, HTTPException, Request
from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import BaseModel
@ -35,9 +35,13 @@ from frigate.api.defs.response.chat_response import (
ToolCall,
)
from frigate.api.defs.tags import Tags
from frigate.api.event import events
from frigate.api.event import _build_attribute_filter_clause, events
from frigate.config import FrigateConfig
from frigate.config.ui import UnitSystemEnum
from frigate.genai.prompts import (
build_chat_system_prompt,
get_attribute_classifications,
get_tool_definitions,
)
from frigate.genai.utils import build_assistant_message_for_conversation
from frigate.jobs.vlm_watch import (
get_vlm_watch_job,
@ -68,338 +72,21 @@ class VLMMonitorRequest(BaseModel):
zones: List[str] = []
def get_tool_definitions() -> List[Dict[str, Any]]:
"""
Get OpenAI-compatible tool definitions for Frigate.
Returns a list of tool definitions that can be used with OpenAI-compatible
function calling APIs.
"""
return [
{
"type": "function",
"function": {
"name": "search_objects",
"description": (
"Search the historical record of detected objects in Frigate. "
"Use this ONLY for questions about the PAST — e.g. 'did anyone come by today?', "
"'when was the last car?', 'show me detections from yesterday'. "
"Do NOT use this for monitoring or alerting requests about future events — "
"use start_camera_watch instead for those. "
"An 'object' in Frigate represents a tracked detection (e.g., a person, package, car). "
"When the user asks about a specific name (person, delivery company, animal, etc.), "
"filter by sub_label only and do not set label."
),
"parameters": {
"type": "object",
"properties": {
"camera": {
"type": "string",
"description": "Camera name to filter by (optional).",
},
"label": {
"type": "string",
"description": "Object label to filter by (e.g., 'person', 'package', 'car').",
},
"sub_label": {
"type": "string",
"description": "Name of a person, delivery company, animal, etc. When filtering by a specific name, use only sub_label; do not set label.",
},
"after": {
"type": "string",
"description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').",
},
"before": {
"type": "string",
"description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').",
},
"zones": {
"type": "array",
"items": {"type": "string"},
"description": "List of zone names to filter by.",
},
"limit": {
"type": "integer",
"description": "Maximum number of objects to return (default: 25).",
"default": 25,
},
},
},
"required": [],
},
},
{
"type": "function",
"function": {
"name": "find_similar_objects",
"description": (
"Find tracked objects that are visually and semantically similar "
"to a specific past event. Use this when the user references a "
"particular object they have seen and wants to find other "
"sightings of the same or similar one ('that green car', 'the "
"person in the red jacket', 'the package that was delivered'). "
"Prefer this over search_objects whenever the user's intent is "
"'find more like this specific one.' Use search_objects first "
"only if you need to locate the anchor event. Requires semantic "
"search to be enabled."
),
"parameters": {
"type": "object",
"properties": {
"event_id": {
"type": "string",
"description": "The id of the anchor event to find similar objects to.",
},
"after": {
"type": "string",
"description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').",
},
"before": {
"type": "string",
"description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').",
},
"cameras": {
"type": "array",
"items": {"type": "string"},
"description": "Optional list of cameras to restrict to. Defaults to all.",
},
"labels": {
"type": "array",
"items": {"type": "string"},
"description": "Optional list of labels to restrict to. Defaults to the anchor event's label.",
},
"sub_labels": {
"type": "array",
"items": {"type": "string"},
"description": "Optional list of sub_labels (names) to restrict to.",
},
"zones": {
"type": "array",
"items": {"type": "string"},
"description": "Optional list of zones. An event matches if any of its zones overlap.",
},
"similarity_mode": {
"type": "string",
"enum": ["visual", "semantic", "fused"],
"description": "Which similarity signal(s) to use. 'fused' (default) combines visual and semantic.",
"default": "fused",
},
"min_score": {
"type": "number",
"description": "Drop matches with a similarity score below this threshold (0.0-1.0).",
},
"limit": {
"type": "integer",
"description": "Maximum number of matches to return (default: 10).",
"default": 10,
},
},
"required": ["event_id"],
},
},
},
{
"type": "function",
"function": {
"name": "set_camera_state",
"description": (
"Change a camera's feature state (e.g., turn detection on/off, enable/disable recordings). "
"Use camera='*' to apply to all cameras at once. "
"Only call this tool when the user explicitly asks to change a camera setting. "
"Requires admin privileges."
),
"parameters": {
"type": "object",
"properties": {
"camera": {
"type": "string",
"description": "Camera name to target, or '*' to target all cameras.",
},
"feature": {
"type": "string",
"enum": [
"detect",
"record",
"snapshots",
"audio",
"motion",
"enabled",
"birdseye",
"birdseye_mode",
"improve_contrast",
"ptz_autotracker",
"motion_contour_area",
"motion_threshold",
"notifications",
"audio_transcription",
"review_alerts",
"review_detections",
"object_descriptions",
"review_descriptions",
"profile",
],
"description": (
"The feature to change. Most features accept ON or OFF. "
"birdseye_mode accepts CONTINUOUS, MOTION, or OBJECTS. "
"motion_contour_area and motion_threshold accept a number. "
"profile accepts a profile name or 'none' to deactivate (requires camera='*')."
),
},
"value": {
"type": "string",
"description": "The value to set. ON or OFF for toggles, a number for thresholds, a profile name or 'none' for profile.",
},
},
"required": ["camera", "feature", "value"],
},
},
},
{
"type": "function",
"function": {
"name": "get_live_context",
"description": (
"Get the current live image and detection information for a camera: objects being tracked, "
"zones, timestamps. Use this to understand what is visible in the live view. "
"Call this when answering questions about what is happening right now on a specific camera."
),
"parameters": {
"type": "object",
"properties": {
"camera": {
"type": "string",
"description": "Camera name to get live context for.",
},
},
"required": ["camera"],
},
},
},
{
"type": "function",
"function": {
"name": "start_camera_watch",
"description": (
"Start a continuous VLM watch job that monitors a camera and sends a notification "
"when a specified condition is met. Use this when the user wants to be alerted about "
"a future event, e.g. 'tell me when guests arrive' or 'notify me when the package is picked up'. "
"Only one watch job can run at a time. Returns a job ID."
),
"parameters": {
"type": "object",
"properties": {
"camera": {
"type": "string",
"description": "Camera ID to monitor.",
},
"condition": {
"type": "string",
"description": (
"Natural-language description of the condition to watch for, "
"e.g. 'a person arrives at the front door'."
),
},
"max_duration_minutes": {
"type": "integer",
"description": "Maximum time to watch before giving up (minutes, default 60).",
"default": 60,
},
"labels": {
"type": "array",
"items": {"type": "string"},
"description": "Object labels that should trigger a VLM check (e.g. ['person', 'car']). If omitted, any detection on the camera triggers a check.",
},
"zones": {
"type": "array",
"items": {"type": "string"},
"description": "Zone names to filter by. If specified, only detections in these zones trigger a VLM check.",
},
},
"required": ["camera", "condition"],
},
},
},
{
"type": "function",
"function": {
"name": "stop_camera_watch",
"description": (
"Cancel the currently running VLM watch job. Use this when the user wants to "
"stop a previously started watch, e.g. 'stop watching the front door'."
),
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "get_profile_status",
"description": (
"Get the current profile status including the active profile and "
"timestamps of when each profile was last activated. Use this to "
"determine time periods for recap requests — e.g. when the user asks "
"'what happened while I was away?', call this first to find the relevant "
"time window based on profile activation history."
),
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "get_recap",
"description": (
"Get a recap of all activity (alerts and detections) for a given time period. "
"Use this after calling get_profile_status to retrieve what happened during "
"a specific window — e.g. 'what happened while I was away?'. Returns a "
"chronological list of activity with camera, objects, zones, and GenAI-generated "
"descriptions when available. Summarize the results for the user."
),
"parameters": {
"type": "object",
"properties": {
"after": {
"type": "string",
"description": "Start of the time period in ISO 8601 format (e.g. '2025-03-15T08:00:00').",
},
"before": {
"type": "string",
"description": "End of the time period in ISO 8601 format (e.g. '2025-03-15T17:00:00').",
},
"cameras": {
"type": "string",
"description": "Comma-separated camera IDs to include, or 'all' for all cameras. Default is 'all'.",
},
"severity": {
"type": "string",
"enum": ["alert", "detection"],
"description": "Filter by severity level. Omit to include both alerts and detections.",
},
},
"required": ["after", "before"],
},
},
},
]
@router.get(
"/chat/tools",
dependencies=[Depends(allow_any_authenticated())],
summary="Get available tools",
description="Returns OpenAI-compatible tool definitions for function calling.",
)
def get_tools() -> JSONResponse:
def get_tools(request: Request) -> JSONResponse:
"""Get list of available tools for LLM function calling."""
tools = get_tool_definitions()
config = request.app.frigate_config
semantic_search_enabled = bool(getattr(config.semantic_search, "enabled", False))
attribute_classifications = get_attribute_classifications(config)
tools = get_tool_definitions(
semantic_search_enabled=semantic_search_enabled,
attribute_classifications=attribute_classifications,
)
return JSONResponse(content={"tools": tools})
@ -432,16 +119,29 @@ def _resolve_zones(
async def _execute_search_objects(
request: Request,
arguments: Dict[str, Any],
allowed_cameras: List[str],
config: FrigateConfig,
) -> JSONResponse:
"""
Execute the search_objects tool.
This searches for detected objects (events) in Frigate using the same
logic as the events API endpoint.
Routes to the semantic path when the LLM supplied a `semantic_query`
and semantic search is enabled; otherwise delegates to the standard
events API logic.
"""
config = request.app.frigate_config
semantic_query = arguments.get("semantic_query")
if isinstance(semantic_query, str):
semantic_query = semantic_query.strip() or None
else:
semantic_query = None
if semantic_query and getattr(config.semantic_search, "enabled", False):
return await _execute_search_objects_semantic(
request, arguments, allowed_cameras, semantic_query
)
# Parse after/before as server local time; convert to Unix timestamp
after = arguments.get("after")
before = arguments.get("before")
@ -477,11 +177,14 @@ async def _execute_search_objects(
elif zones is None:
zones = "all"
attribute = arguments.get("attribute")
# Build query parameters compatible with EventsQueryParams
query_params = EventsQueryParams(
cameras=arguments.get("camera", "all"),
labels=arguments.get("label", "all"),
sub_labels=arguments.get("sub_label", "all"), # case-insensitive on the backend
attributes=attribute if attribute else "all",
zones=zones,
zone=zones,
after=after,
@ -508,6 +211,124 @@ async def _execute_search_objects(
)
async def _execute_search_objects_semantic(
request: Request,
arguments: Dict[str, Any],
allowed_cameras: List[str],
semantic_query: str,
) -> JSONResponse:
"""Search objects via fused thumbnail + description embeddings.
Runs both visual and description vec searches against `semantic_query`,
intersects the candidates with the structured filters (camera, label,
sub_label, zones, time window) the LLM supplied, and ranks the survivors
by fused similarity. Mirrors the candidate-then-filter pattern used by
find_similar_objects since sqlite-vec's IN filter is unreliable.
"""
from peewee import fn
config = request.app.frigate_config
context = request.app.embeddings
if context is None:
logger.warning(
"semantic_query supplied but embeddings context is unavailable; "
"returning empty results."
)
return JSONResponse(content=[])
after = parse_iso_to_timestamp(arguments.get("after"))
before = parse_iso_to_timestamp(arguments.get("before"))
camera_arg = arguments.get("camera")
if camera_arg and camera_arg != "all":
if camera_arg not in allowed_cameras:
return JSONResponse(content=[])
cameras = [camera_arg]
else:
cameras = list(allowed_cameras) if allowed_cameras else []
if not cameras:
return JSONResponse(content=[])
label = arguments.get("label")
sub_label = arguments.get("sub_label")
attribute = arguments.get("attribute")
zones = arguments.get("zones")
if isinstance(zones, list) and zones:
zones = _resolve_zones(zones, config, cameras)
else:
zones = None
limit = int(arguments.get("limit", 25))
limit = max(1, min(limit, 100))
visual_distances: Dict[str, float] = {}
description_distances: Dict[str, float] = {}
try:
rows = context.search_thumbnail(semantic_query)
visual_distances = {row[0]: row[1] for row in rows}
except Exception:
logger.exception(
"search_thumbnail failed for semantic_query: %s", semantic_query
)
try:
rows = context.search_description(semantic_query)
description_distances = {row[0]: row[1] for row in rows}
except Exception:
logger.exception(
"search_description failed for semantic_query: %s", semantic_query
)
vec_ids = set(visual_distances) | set(description_distances)
if not vec_ids:
return JSONResponse(content=[])
clauses = [Event.id.in_(list(vec_ids)), Event.camera.in_(cameras)]
if after is not None:
clauses.append(Event.start_time >= after)
if before is not None:
clauses.append(Event.start_time <= before)
if label:
clauses.append(Event.label == label)
if sub_label:
# case-insensitive match to mirror events() behavior
clauses.append(fn.LOWER(Event.sub_label.cast("text")) == sub_label.lower())
if attribute:
attribute_clause = _build_attribute_filter_clause(attribute)
if attribute_clause is not None:
clauses.append(attribute_clause)
if zones:
zone_clauses = [Event.zones.cast("text") % f'*"{zone}"*' for zone in zones]
clauses.append(reduce(operator.or_, zone_clauses))
eligible = {e.id: e for e in Event.select().where(reduce(operator.and_, clauses))}
scored: List[tuple[str, float]] = []
for eid in eligible:
v_score = (
distance_to_score(visual_distances[eid], context.thumb_stats)
if eid in visual_distances
else None
)
d_score = (
distance_to_score(description_distances[eid], context.desc_stats)
if eid in description_distances
else None
)
fused = fuse_scores(v_score, d_score)
if fused is None:
continue
scored.append((eid, fused))
scored.sort(key=lambda pair: pair[1], reverse=True)
scored = scored[:limit]
results = [hydrate_event(eligible[eid], score=score) for eid, score in scored]
return JSONResponse(content=results)
async def _execute_find_similar_objects(
request: Request,
arguments: Dict[str, Any],
@ -696,9 +517,7 @@ async def execute_tool(
logger.debug(f"Executing tool: {tool_name} with arguments: {arguments}")
if tool_name == "search_objects":
return await _execute_search_objects(
arguments, allowed_cameras, request.app.frigate_config
)
return await _execute_search_objects(request, arguments, allowed_cameras)
if tool_name == "find_similar_objects":
result = await _execute_find_similar_objects(
@ -878,9 +697,7 @@ async def _execute_tool_internal(
This is used by the chat completion endpoint to execute tools.
"""
if tool_name == "search_objects":
response = await _execute_search_objects(
arguments, allowed_cameras, request.app.frigate_config
)
response = await _execute_search_objects(request, arguments, allowed_cameras)
try:
if hasattr(response, "body"):
body_str = response.body.decode("utf-8")
@ -1293,64 +1110,21 @@ async def chat_completion(
status_code=400,
)
tools = get_tool_definitions()
config = request.app.frigate_config
semantic_search_enabled = bool(getattr(config.semantic_search, "enabled", False))
attribute_classifications = get_attribute_classifications(config)
tools = get_tool_definitions(
semantic_search_enabled=semantic_search_enabled,
attribute_classifications=attribute_classifications,
)
conversation = []
current_datetime = datetime.now()
current_date_str = current_datetime.strftime("%Y-%m-%d")
current_time_str = current_datetime.strftime("%I:%M:%S %p")
cameras_info = []
config = request.app.frigate_config
has_speed_zone = False
for camera_id in allowed_cameras:
if camera_id not in config.cameras:
continue
camera_config = config.cameras[camera_id]
friendly_name = (
camera_config.friendly_name
if camera_config.friendly_name
else camera_id.replace("_", " ").title()
)
zone_names = list(camera_config.zones.keys())
if not has_speed_zone:
has_speed_zone = any(
zone.distances for zone in camera_config.zones.values()
)
if zone_names:
cameras_info.append(
f" - {friendly_name} (ID: {camera_id}, zones: {', '.join(zone_names)})"
)
else:
cameras_info.append(f" - {friendly_name} (ID: {camera_id})")
cameras_section = ""
if cameras_info:
cameras_section = (
"\n\nAvailable cameras:\n"
+ "\n".join(cameras_info)
+ "\n\nWhen users refer to cameras by their friendly name (e.g., 'Back Deck Camera'), use the corresponding camera ID (e.g., 'back_deck_cam') in tool calls."
)
speed_units_section = ""
if has_speed_zone:
speed_unit = (
"mph" if config.ui.unit_system == UnitSystemEnum.imperial else "km/h"
)
speed_units_section = f"\n\nReport object speeds to the user in {speed_unit}."
system_prompt = f"""You are a helpful assistant for Frigate, a security camera NVR system. You help users answer questions about their cameras, detected objects, and events.
Current server local date and time: {current_date_str} at {current_time_str}
Do not start your response with phrases like "I will check...", "Let me see...", or "Let me look...". Answer directly.
Always present times to the user in the server's local timezone. When tool results include start_time_local and end_time_local, use those exact strings when listing or describing detection times—do not convert or invent timestamps. Do not use UTC or ISO format with Z for the user-facing answer unless the tool result only provides Unix timestamps without local time fields.
When users ask about "today", "yesterday", "this week", etc., use the current date above as reference.
When searching for objects or events, use ISO 8601 format for dates (e.g., {current_date_str}T00:00:00Z for the start of today).
Always be accurate with time calculations based on the current date provided.
When a user refers to a specific object they have seen or describe with identifying details ("that green car", "the person in the red jacket", "a package left today"), prefer the find_similar_objects tool over search_objects. Use search_objects first only to locate the anchor event, then pass its id to find_similar_objects. For generic queries like "show me all cars today", keep using search_objects. If a user message begins with [attached_event:<id>], treat that event id as the anchor for any similarity or "tell me more" request in the same message and call find_similar_objects with that id.{cameras_section}{speed_units_section}"""
system_prompt = build_chat_system_prompt(
config=config,
allowed_cameras=allowed_cameras,
semantic_search_enabled=semantic_search_enabled,
attribute_classifications=attribute_classifications,
)
conversation.append(
{
@ -1411,6 +1185,18 @@ When a user refers to a specific object they have seen or describe with identify
)
+ b"\n"
)
elif kind == "reasoning_delta":
yield (
json.dumps({"type": "reasoning", "delta": value}).encode(
"utf-8"
)
+ b"\n"
)
elif kind == "stats":
yield (
json.dumps({"type": "stats", **value}).encode("utf-8")
+ b"\n"
)
elif kind == "message":
msg = value
if msg.get("finish_reason") == "error":
@ -1506,6 +1292,7 @@ When a user refers to a specific object they have seen or describe with identify
final_content = response.get("content") or ""
if body.stream:
final_reasoning = response.get("reasoning")
async def stream_body() -> Any:
if tool_calls:
@ -1520,6 +1307,15 @@ When a user refers to a specific object they have seen or describe with identify
).encode("utf-8")
+ b"\n"
)
# Emit the full reasoning trace up front when the
# underlying client did not stream it
if final_reasoning:
yield (
json.dumps(
{"type": "reasoning", "delta": final_reasoning}
).encode("utf-8")
+ b"\n"
)
# Stream content in word-sized chunks for smooth UX
for part in chunk_content(final_content):
yield (
@ -1540,6 +1336,7 @@ When a user refers to a specific object they have seen or describe with identify
message=ChatMessageResponse(
role="assistant",
content=final_content,
reasoning=response.get("reasoning"),
tool_calls=None,
),
finish_reason=response.get("finish_reason", "stop"),
@ -1641,6 +1438,7 @@ async def start_vlm_monitor(
dispatcher=request.app.dispatcher,
labels=body.labels,
zones=body.zones,
username=request.headers.get("remote-user", ""),
)
except RuntimeError as e:
logger.error("Failed to start VLM watch job: %s", e, exc_info=True)
@ -1661,10 +1459,22 @@ async def start_vlm_monitor(
summary="Get current VLM watch job",
description="Returns the current (or most recently completed) VLM watch job.",
)
async def get_vlm_monitor() -> JSONResponse:
async def get_vlm_monitor(request: Request) -> JSONResponse:
job = get_vlm_watch_job()
if job is None:
return JSONResponse(content={"active": False}, status_code=200)
role = request.headers.get("remote-role", "viewer")
username = request.headers.get("remote-user", "")
# Admin and the job's creator always see the job. Other users only see it
# if they have access to the camera being watched; otherwise hide it.
if role != "admin" and username != job.username:
try:
await require_camera_access(job.camera, request=request)
except HTTPException:
return JSONResponse(content={"active": False}, status_code=200)
return JSONResponse(content={"active": True, **job.to_dict()}, status_code=200)
@ -1674,7 +1484,27 @@ async def get_vlm_monitor() -> JSONResponse:
summary="Cancel the current VLM watch job",
description="Cancels the running watch job if one exists.",
)
async def cancel_vlm_monitor() -> JSONResponse:
async def cancel_vlm_monitor(request: Request) -> JSONResponse:
job = get_vlm_watch_job()
if job is None:
return JSONResponse(
content={"success": False, "message": "No active watch job to cancel."},
status_code=404,
)
role = request.headers.get("remote-role", "viewer")
username = request.headers.get("remote-user", "")
# Admin can cancel any job; other users can only cancel jobs they started.
if role != "admin" and username != job.username:
return JSONResponse(
content={
"success": False,
"message": "Not authorized to cancel this watch job.",
},
status_code=403,
)
cancelled = stop_vlm_watch_job()
if not cancelled:
return JSONResponse(

View File

@ -6,11 +6,18 @@ from datetime import datetime
from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse
from peewee import DoesNotExist
from pydantic import BaseModel, Field
from frigate.api.auth import require_role
from frigate.api.defs.tags import Tags
from frigate.jobs.debug_replay import start_debug_replay_job
from frigate.jobs.debug_replay import (
ExportDebugReplaySource,
RecordingDebugReplaySource,
start_debug_replay_job,
)
from frigate.models import Export
from frigate.util.services import get_video_properties
logger = logging.getLogger(__name__)
@ -25,6 +32,12 @@ class DebugReplayStartBody(BaseModel):
end_time: float = Field(title="End timestamp")
class DebugReplayStartFromExportBody(BaseModel):
"""Request body for starting a debug replay session from an export."""
export_id: str = Field(title="Export id")
class DebugReplayStartResponse(BaseModel):
"""Response for starting a debug replay session."""
@ -73,13 +86,95 @@ class DebugReplayStopResponse(BaseModel):
async def start_debug_replay(request: Request, body: DebugReplayStartBody):
"""Start a debug replay session asynchronously."""
replay_manager = request.app.replay_manager
source = RecordingDebugReplaySource(
source_camera=body.camera,
start_ts=body.start_time,
end_ts=body.end_time,
)
try:
job_id = await asyncio.to_thread(
start_debug_replay_job,
source_camera=body.camera,
start_ts=body.start_time,
end_ts=body.end_time,
source=source,
frigate_config=request.app.frigate_config,
config_publisher=request.app.config_publisher,
replay_manager=replay_manager,
)
except RuntimeError:
return JSONResponse(
content={
"success": False,
"message": "A replay session is already active",
},
status_code=409,
)
except ValueError:
logger.exception("Rejected debug replay start request")
return JSONResponse(
content={
"success": False,
"message": "Invalid debug replay parameters",
},
status_code=400,
)
return JSONResponse(
content={
"success": True,
"replay_camera": replay_manager.replay_camera_name,
"job_id": job_id,
},
status_code=202,
)
@router.post(
"/debug_replay/start_from_export",
response_model=DebugReplayStartResponse,
status_code=202,
responses={
400: {"description": "Invalid export, time range, or no recordings"},
404: {"description": "Export not found"},
409: {"description": "A replay session is already active"},
},
dependencies=[Depends(require_role(["admin"]))],
summary="Start debug replay from an export",
description="Start a debug replay session covering an existing export's "
"time range. The end time is derived from the export's video duration.",
)
async def start_debug_replay_from_export(
request: Request, body: DebugReplayStartFromExportBody
):
"""Start a debug replay session from an existing export."""
try:
export: Export = Export.get(Export.id == body.export_id)
except DoesNotExist:
return JSONResponse(
content={"success": False, "message": "Export not found"},
status_code=404,
)
properties = await get_video_properties(
request.app.frigate_config.ffmpeg, export.video_path, get_duration=True
)
duration = properties.get("duration", -1)
if duration is None or duration <= 0:
return JSONResponse(
content={
"success": False,
"message": "Could not determine export duration",
},
status_code=400,
)
replay_manager = request.app.replay_manager
source = ExportDebugReplaySource(export=export, duration=float(duration))
try:
job_id = await asyncio.to_thread(
start_debug_replay_job,
source=source,
frigate_config=request.app.frigate_config,
config_publisher=request.app.config_publisher,
replay_manager=replay_manager,

View File

@ -20,6 +20,10 @@ class ChatMessageResponse(BaseModel):
content: Optional[str] = Field(
default=None, description="Message content (None if tool calls present)"
)
reasoning: Optional[str] = Field(
default=None,
description="Separated reasoning/thinking trace if the model emitted one",
)
tool_calls: Optional[list[ToolCallInvocation]] = Field(
default=None, description="Tool calls if LLM wants to call tools"
)

View File

@ -398,7 +398,7 @@ class _StreamingZipBuffer:
def _unique_archive_name(export: Export, used: set[str]) -> str:
base = sanitize_filename(export.name) if export.name else None
if not base:
base = f"{export.camera}_{int(datetime.datetime.timestamp(export.date))}"
base = f"{export.camera}_{int(export.date)}"
candidate = f"{base}.mp4"
counter = 1

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

@ -34,6 +34,8 @@ from frigate.const import (
UPDATE_REVIEW_DESCRIPTION,
UPSERT_REVIEW_SEGMENT,
)
from frigate.models import User
from frigate.output.ws_auth import ws_has_camera_access
logger = logging.getLogger(__name__)
@ -66,6 +68,7 @@ _WS_VIEWER_TOPICS = frozenset(
"audioTranscriptionState",
"birdseyeLayout",
"embeddingsReindexProgress",
"jobState",
}
)
@ -102,6 +105,321 @@ def _check_ws_authorization(
return topic in _WS_VIEWER_TOPICS
# ---- Outbound filtering ---------------------------------------------------
#
# Every WebSocket broadcast is classified into one of a small set of scopes,
# then materialized per recipient. Connections with restricted roles only see
# data for cameras they are authorized to access; admin and full-access roles
# behave as today.
# Topics that are safe to broadcast to every authenticated client.
_WS_GLOBAL_OUTBOUND_TOPICS = frozenset(
{
"model_state",
"embeddings_reindex_progress",
"audio_transcription_state",
"profile/state",
"notifications/state",
"notification_test",
}
)
# Topics that restricted roles must never receive. Birdseye composites span
# all cameras, so the existing JSMPEG policy already restricts birdseye access
# to unrestricted roles; the layout broadcast follows the same rule.
_WS_UNRESTRICTED_ONLY_TOPICS = frozenset(
{
"birdseye_layout",
}
)
# Topics whose payload (parsed as JSON) names a single owning camera at the
# given key path. Used to scope events, reviews, triggers, etc.
_WS_PAYLOAD_CAMERA_TOPICS: dict[str, tuple[str, ...]] = {
"events": ("after", "camera"),
"reviews": ("after", "camera"),
"tracked_object_update": ("camera",),
"triggers": ("camera",),
"camera_monitoring": ("camera",),
}
# Topics whose payload is a dict keyed by camera name; filter keys per
# recipient.
_WS_RESHAPE_BY_CAMERA_KEY_TOPICS = frozenset(
{
"camera_activity",
"audio_detections",
}
)
# Topics whose payload is a dict keyed by job_type, where each entry may
# contain a "camera" or "source_camera" field, or a nested ``results.jobs``
# list of per-camera sub-jobs (export broadcasts).
_WS_RESHAPE_JOB_STATE_TOPICS = frozenset(
{
"job_state",
}
)
# Topics whose payload mixes global aggregates with a ``cameras`` sub-dict
# keyed by camera name. Aggregates and detector data stay; per-camera entries
# are filtered.
_WS_RESHAPE_STATS_TOPICS = frozenset(
{
"stats",
}
)
def _collect_zone_names(config: FrigateConfig) -> set[str]:
"""Return the set of all zone names defined across cameras."""
names: set[str] = set()
for camera in config.cameras.values():
zones = getattr(camera, "zones", None) or {}
names.update(zones.keys())
return names
def _parse_json_payload(payload: Any) -> Any:
"""Return payload parsed as JSON if it is a string, else as-is."""
if isinstance(payload, str):
try:
return json.loads(payload)
except (ValueError, TypeError):
return None
return payload
def _scope_job_entry_to_allowed(entry: Any, allowed: set[str]) -> dict[str, Any] | None:
"""Filter a single job_state entry to the recipient's allowed cameras.
Returns the (possibly reshaped) entry, or None to drop it. Four shapes
are handled:
* Top-level ``camera`` or ``source_camera`` (motion_search, vlm_watch,
export sub-job dicts): drop the entry if not allowed.
* Nested ``results.jobs`` list of per-camera sub-jobs (the aggregated
export broadcast): filter the list; drop the entry if nothing remains.
* Nested ``results.camera`` or ``results.source_camera`` (debug_replay,
which puts replay-specific fields inside ``results``): drop the entry
if not allowed.
* No camera anywhere (e.g. ``media_sync``): treat as global and keep.
"""
if not isinstance(entry, dict):
return None
cam = entry.get("camera") or entry.get("source_camera")
if cam is None:
results = entry.get("results")
if isinstance(results, dict):
sub_jobs = results.get("jobs")
if isinstance(sub_jobs, list):
filtered_jobs = [
j
for j in sub_jobs
if isinstance(j, dict)
and (j.get("camera") or j.get("source_camera")) in allowed
]
if not filtered_jobs:
return None
reshaped = dict(entry)
reshaped["results"] = dict(results)
reshaped["results"]["jobs"] = filtered_jobs
return reshaped
cam = results.get("camera") or results.get("source_camera")
if cam is not None:
return entry if cam in allowed else None
return entry
def _extract_payload_camera(payload: Any, path: tuple[str, ...]) -> str | None:
"""Walk the dotted path through a (possibly JSON-encoded) payload."""
cur = _parse_json_payload(payload)
for key in path:
if not isinstance(cur, dict):
return None
cur = cur.get(key)
return cur if isinstance(cur, str) else None
def _classify_outbound(
topic: str, all_cameras: set[str], all_zones: set[str]
) -> tuple[str, Any]:
"""Classify an outbound topic into (kind, extra).
kind values:
- "global" : send to every authenticated client
- "drop" : send to nobody (fail-closed for unknowns)
- "unrestricted_only" : send only to admin/full-access roles
- "camera" : extra is the owning camera name
- "payload_camera" : extra is the JSON key path to the camera name
- "reshape_by_camera_key"
- "reshape_job_state"
- "reshape_stats"
"""
if topic in _WS_GLOBAL_OUTBOUND_TOPICS:
return ("global", None)
if topic in _WS_UNRESTRICTED_ONLY_TOPICS:
return ("unrestricted_only", None)
if topic in _WS_RESHAPE_BY_CAMERA_KEY_TOPICS:
return ("reshape_by_camera_key", None)
if topic in _WS_RESHAPE_JOB_STATE_TOPICS:
return ("reshape_job_state", None)
if topic in _WS_RESHAPE_STATS_TOPICS:
return ("reshape_stats", None)
if topic in _WS_PAYLOAD_CAMERA_TOPICS:
return ("payload_camera", _WS_PAYLOAD_CAMERA_TOPICS[topic])
# Topic-prefix based: first segment names the owning camera or zone.
first = topic.split("/", 1)[0]
if first in all_cameras:
return ("camera", first)
if first in all_zones:
# Zone aggregates span cameras; restricted users see nothing here.
return ("unrestricted_only", None)
return ("drop", None)
def _ws_role_header(ws: Any) -> str | None:
"""Return the HTTP_REMOTE_ROLE header value, if any."""
environ = getattr(ws, "environ", None)
if not environ:
return None
value = environ.get("HTTP_REMOTE_ROLE")
return value if isinstance(value, str) else None
def _ws_valid_roles(ws: Any, config: FrigateConfig) -> list[str]:
"""Return the list of recognized roles for this connection."""
header = _ws_role_header(ws)
if not header:
return []
roles = [r.strip() for r in header.split(config.proxy.separator) if r.strip()]
return [r for r in roles if r in config.auth.roles]
def _ws_is_unrestricted(ws: Any, config: FrigateConfig) -> bool:
"""True when the connection has unrestricted camera access.
Mirrors the policy in ``frigate.output.ws_auth``: admin or any role with
an empty allow-list grants full access.
"""
roles = _ws_valid_roles(ws, config)
if not roles:
return False
roles_dict = config.auth.roles
return any(r == "admin" or not roles_dict.get(r) for r in roles)
def _ws_allowed_cameras(ws: Any, config: FrigateConfig) -> set[str]:
"""Return the union of cameras this connection may access across its roles."""
roles = _ws_valid_roles(ws, config)
if not roles:
return set()
all_cameras = set(config.cameras.keys())
allowed: set[str] = set()
for role in roles:
if role == "admin" or not config.auth.roles.get(role):
return all_cameras
allowed.update(User.get_allowed_cameras(role, config.auth.roles, all_cameras))
return allowed
def _wrap_envelope(topic: str, inner_payload: Any) -> str:
"""Re-serialize a (topic, payload) message after payload reshaping.
Frigate's wire format keeps payloads as JSON-encoded strings inside the
outer envelope, mirroring what producers send today.
"""
return json.dumps({"topic": topic, "payload": json.dumps(inner_payload)})
def _materialize_for_ws(
ws: Any,
topic: str,
full_message: str,
scope: tuple[str, Any],
parsed_payload: Any,
config: FrigateConfig,
) -> str | None:
"""Return the JSON string to deliver to ``ws``, or None to skip it."""
kind, extra = scope
has_role = _ws_role_header(ws) is not None
if kind == "drop":
return None
if kind == "global":
# Globals still require an authenticated connection. Missing role
# falls back to viewer semantics (matching the inbound rule).
return full_message
# Beyond globals, an authenticated role header is required (fail-closed).
if not has_role:
return None
if kind == "unrestricted_only":
return full_message if _ws_is_unrestricted(ws, config) else None
if kind == "camera":
return full_message if ws_has_camera_access(ws, extra, config) else None
if kind == "payload_camera":
camera = _extract_payload_camera(parsed_payload, extra)
if camera is None:
return None
return full_message if ws_has_camera_access(ws, camera, config) else None
if kind == "reshape_by_camera_key":
if _ws_is_unrestricted(ws, config):
return full_message
if not isinstance(parsed_payload, dict):
return None
allowed = _ws_allowed_cameras(ws, config)
filtered = {cam: data for cam, data in parsed_payload.items() if cam in allowed}
if not filtered:
return None
return _wrap_envelope(topic, filtered)
if kind == "reshape_job_state":
if _ws_is_unrestricted(ws, config):
return full_message
if not isinstance(parsed_payload, dict):
return None
allowed = _ws_allowed_cameras(ws, config)
filtered_jobs: dict[str, Any] = {}
for job_type, job_payload in parsed_payload.items():
scoped = _scope_job_entry_to_allowed(job_payload, allowed)
if scoped is not None:
filtered_jobs[job_type] = scoped
if not filtered_jobs:
return None
return _wrap_envelope(topic, filtered_jobs)
if kind == "reshape_stats":
if _ws_is_unrestricted(ws, config):
return full_message
if not isinstance(parsed_payload, dict):
return None
allowed = _ws_allowed_cameras(ws, config)
cameras_block = parsed_payload.get("cameras")
if isinstance(cameras_block, dict):
filtered_cameras = {
name: data for name, data in cameras_block.items() if name in allowed
}
reshaped = dict(parsed_payload)
reshaped["cameras"] = filtered_cameras
return _wrap_envelope(topic, reshaped)
return full_message
return None
class WebSocket(WebSocket_): # type: ignore[misc]
def unhandled_error(self, error: Any) -> None:
"""
@ -183,6 +501,10 @@ class WebSocketClient(Communicator):
self.websocket_thread.start()
def publish(self, topic: str, payload: Any, _: bool = False) -> None:
if self.websocket_server is None:
logger.debug("Skipping message, websocket not connected yet")
return
try:
ws_message = json.dumps(
{
@ -195,14 +517,42 @@ class WebSocketClient(Communicator):
logger.debug(f"payload for {topic} wasn't text. Skipping...")
return
if self.websocket_server is None:
logger.debug("Skipping message, websocket not connected yet")
all_cameras = set(self.config.cameras.keys())
all_zones = _collect_zone_names(self.config)
scope = _classify_outbound(topic, all_cameras, all_zones)
if scope[0] == "drop":
return
try:
self.websocket_server.manager.broadcast(ws_message)
except ConnectionResetError:
pass
# Pre-parse payload once for topics that need to read its contents.
parsed_payload: Any = None
if scope[0] in (
"payload_camera",
"reshape_by_camera_key",
"reshape_job_state",
"reshape_stats",
):
parsed_payload = _parse_json_payload(payload)
if parsed_payload is None:
# malformed payload — fail closed
return
manager = self.websocket_server.manager
with manager.lock:
websockets = list(manager.websockets.values())
for ws in websockets:
if getattr(ws, "terminated", False):
continue
message = _materialize_for_ws(
ws, topic, ws_message, scope, parsed_payload, self.config
)
if message is None:
continue
try:
ws.send(message)
except (ConnectionResetError, BrokenPipeError, ValueError):
pass
def stop(self) -> None:
if self.websocket_server is not None:

View File

@ -26,7 +26,6 @@ from frigate.plus import PlusApi
from frigate.util.builtin import (
deep_merge,
get_ffmpeg_arg_list,
load_labels,
)
from frigate.util.config import (
CURRENT_CONFIG_VERSION,
@ -81,12 +80,12 @@ logger = logging.getLogger(__name__)
yaml = YAML()
DEFAULT_DETECTORS = {
"ov": {
"type": "openvino",
"device": "CPU",
}
}
# Pydantic field default applied when an existing config omits `detectors:`.
# Kept as cpu tflite for backwards compatibility with 0.17 configs.
DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}}
# Used by the openvino branch below and rendered into the new-config YAML
# template so first-time setups default to openvino on CPU.
DEFAULT_MODEL = {
"width": 300,
"height": 300,
@ -95,6 +94,7 @@ DEFAULT_MODEL = {
"path": "/openvino-model/ssdlite_mobilenet_v2.xml",
"labelmap_path": "/openvino-model/coco_91cl_bkgr.txt",
}
NEW_CONFIG_DETECTORS = {"ov": {"type": "openvino", "device": "CPU"}}
DEFAULT_DETECT_DIMENSIONS = {"width": 1280, "height": 720}
@ -110,7 +110,7 @@ DEFAULT_CONFIG = f"""
mqtt:
enabled: False
{_render_default_yaml({"detectors": DEFAULT_DETECTORS, "model": DEFAULT_MODEL})}
{_render_default_yaml({"detectors": NEW_CONFIG_DETECTORS, "model": DEFAULT_MODEL})}
cameras: {{}} # No cameras defined, UI wizard should be used
version: {CURRENT_CONFIG_VERSION}
"""
@ -629,26 +629,22 @@ class FrigateConfig(FrigateBaseModel):
# set default min_score for object attributes
for attribute in self.model.all_attributes:
if not self.objects.filters.get(attribute):
existing = self.objects.filters.get(attribute)
if existing is None:
self.objects.filters[attribute] = FilterConfig(min_score=0.7)
elif self.objects.filters[attribute].min_score == 0.5:
self.objects.filters[attribute].min_score = 0.7
elif "min_score" not in existing.model_fields_set:
existing.min_score = 0.7
# auto detect hwaccel args
if self.ffmpeg.hwaccel_args == "auto":
self.ffmpeg.hwaccel_args = auto_detect_hwaccel()
# Populate global audio filters for all audio labels
all_audio_labels = {
label
for label in load_labels("/audio-labelmap.txt", prefill=521).values()
if label
}
# Populate global audio filters from listen. Existing user-defined
# entries for labels not in listen are preserved but unused at runtime.
if self.audio.filters is None:
self.audio.filters = {}
for key in sorted(all_audio_labels - self.audio.filters.keys()):
for key in sorted(set(self.audio.listen) - self.audio.filters.keys()):
self.audio.filters[key] = AudioFilterConfig()
self.audio.filters = dict(sorted(self.audio.filters.items()))
@ -840,7 +836,9 @@ class FrigateConfig(FrigateBaseModel):
if camera_config.audio.filters is None:
camera_config.audio.filters = {}
for key in sorted(all_audio_labels - camera_config.audio.filters.keys()):
for key in sorted(
set(camera_config.audio.listen) - camera_config.audio.filters.keys()
):
camera_config.audio.filters[key] = AudioFilterConfig()
camera_config.audio.filters = dict(
@ -862,7 +860,9 @@ class FrigateConfig(FrigateBaseModel):
if mask_config:
coords = mask_config.coordinates
relative_coords = get_relative_coordinates(
coords, camera_config.frame_shape
coords,
camera_config.frame_shape,
camera_name=camera_config.name,
)
# Create a new ObjectMaskConfig with raw_coordinates set
processed_global_masks[mask_id] = ObjectMaskConfig(

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

@ -9,6 +9,7 @@ import logging
import os
import shutil
import threading
import time
from ruamel.yaml import YAML
@ -25,7 +26,15 @@ from frigate.const import (
REPLAY_DIR,
THUMB_DIR,
)
from frigate.jobs.debug_replay import cancel_debug_replay_job, wait_for_runner
from frigate.jobs.debug_replay import (
JOB_TYPE as DEBUG_REPLAY_JOB_TYPE,
)
from frigate.jobs.debug_replay import (
cancel_debug_replay_job,
wait_for_runner,
)
from frigate.jobs.export import JobStatePublisher
from frigate.types import JobStatusTypesEnum
from frigate.util.camera_cleanup import cleanup_camera_db, cleanup_camera_files
from frigate.util.config import find_config_file
@ -49,6 +58,7 @@ class DebugReplayManager:
self.clip_path: str | None = None
self.start_ts: float | None = None
self.end_ts: float | None = None
self._job_state_publisher = JobStatePublisher()
@property
def active(self) -> bool:
@ -150,6 +160,7 @@ class DebugReplayManager:
return
replay_name = self.replay_camera_name
source_camera = self.source_camera
# Only publish remove if the camera was actually added to the live
# config (i.e. the runner reached the starting_camera phase).
@ -163,6 +174,21 @@ class DebugReplayManager:
self._cleanup_db(replay_name)
self._cleanup_files(replay_name)
self._job_state_publisher.publish(
{
"id": "stopped",
"job_type": DEBUG_REPLAY_JOB_TYPE,
"status": JobStatusTypesEnum.cancelled,
"start_time": None,
"end_time": time.time(),
"error_message": None,
"results": {
"source_camera": source_camera,
"replay_camera_name": replay_name,
},
}
)
self._clear_locked()
logger.info("Debug replay stopped and cleaned up: %s", replay_name)

View File

@ -79,7 +79,11 @@ def is_openvino_gpu_npu_available() -> bool:
available_devices = get_openvino_available_devices()
# Check for GPU, NPU, or other acceleration devices (excluding CPU)
acceleration_devices = ["GPU", "MYRIAD", "NPU", "GNA", "HDDL"]
return any(device in available_devices for device in acceleration_devices)
return any(
avail_dev == accel_dev or avail_dev.startswith(accel_dev + ".")
for avail_dev in available_devices
for accel_dev in acceleration_devices
)
class BaseModelRunner(ABC):
@ -278,6 +282,13 @@ class OpenVINOModelRunner(BaseModelRunner):
EnrichmentModelTypeEnum.arcface.value,
]
@staticmethod
def is_detection_model(model_type: str) -> bool:
# Import here to avoid circular imports
from frigate.detectors.detector_config import ModelTypeEnum
return model_type in [m.value for m in ModelTypeEnum]
def __init__(self, model_path: str, device: str, model_type: str, **kwargs):
self.model_path = model_path
self.device = device
@ -306,9 +317,15 @@ class OpenVINOModelRunner(BaseModelRunner):
# Apply performance optimization
self.ov_core.set_property(device, {"PERF_COUNT": "NO"})
if device in ["GPU", "AUTO"]:
if device in ["GPU", "AUTO", "NPU"]:
self.ov_core.set_property(device, {"PERFORMANCE_HINT": "LATENCY"})
if device == "NPU" and OpenVINOModelRunner.is_detection_model(model_type):
try:
self.ov_core.set_property(device, {"NPU_TURBO": "YES"})
except Exception as e:
logger.debug(f"NPU_TURBO not supported by driver: {e}")
# Compile model
self.compiled_model = self.ov_core.compile_model(
model=model_path, device_name=device

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
@ -228,7 +232,7 @@ class EmbeddingMaintainer(threading.Thread):
)
)
if self.config.audio_transcription.enabled and any(
if any(
c.enabled_in_config and c.audio_transcription.enabled
for c in self.config.cameras.values()
):
@ -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,16 +92,18 @@ 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"
if self.config.audio_transcription.enabled:
if any(
c.enabled_in_config and c.audio_transcription.enabled
for c in self.config.cameras.values()
):
self.transcription_model_runner: AudioTranscriptionModelRunner | None = (
AudioTranscriptionModelRunner(
self.config.audio_transcription.device or "AUTO",
@ -112,32 +113,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")
@ -184,7 +209,7 @@ class AudioEventMaintainer(threading.Thread):
self.detection_publisher = DetectionPublisher(DetectionTypeEnum.audio.value)
if (
self.config.audio_transcription.enabled
self.camera_config.audio_transcription.enabled
and self.audio_transcription_model_runner is not None
):
# init the transcription processor for this camera

View File

@ -1,6 +1,5 @@
"""Generative AI module for Frigate."""
import datetime
import importlib
import json
import logging
@ -9,13 +8,18 @@ import re
from typing import Any, Callable, Optional
import numpy as np
from playhouse.shortcuts import model_to_dict
from pydantic import ValidationError
from frigate.config import CameraConfig, GenAIConfig, GenAIProviderEnum
from frigate.const import CLIPS_DIR
from frigate.data_processing.post.types import ReviewMetadata
from frigate.genai.manager import GenAIClientManager
from frigate.genai.prompts import (
build_object_description_prompt,
build_review_description_prompt,
build_review_description_response_format,
build_review_summary_prompt,
)
from frigate.models import Event
logger = logging.getLogger(__name__)
@ -61,75 +65,14 @@ class GenAIClient:
activity_context_prompt: str,
) -> ReviewMetadata | None:
"""Generate a description for the review item activity."""
context_prompt = build_review_description_prompt(
review_data,
thumbnails,
concerns,
preferred_language,
activity_context_prompt,
)
def get_concern_prompt() -> str:
if concerns:
concern_list = "\n - ".join(concerns)
return f"""- `other_concerns` (list of strings): Include a list of any of the following concerns that are occurring:
- {concern_list}"""
else:
return ""
def get_language_prompt() -> str:
if preferred_language:
return f"Provide your answer in {preferred_language}"
else:
return ""
def get_objects_list() -> str:
if review_data["unified_objects"]:
return "\n- " + "\n- ".join(review_data["unified_objects"])
else:
return "\n- (No objects detected)"
context_prompt = f"""
Your task is to analyze a sequence of images taken in chronological order from a security camera.
## Normal Activity Patterns for This Property
{activity_context_prompt}
## Task Instructions
Describe the scene based on observable actions and movements, evaluate the activity against the Activity Indicators above, and assign a potential_threat_level (0, 1, or 2) by applying the threat level indicators consistently.
## Analysis Guidelines
When forming your description:
- **CRITICAL: Only describe objects explicitly listed in "Objects in Scene" below.** Do not infer or mention additional people, vehicles, or objects not present in this list, even if visual patterns suggest them. If only a car is listed, do not describe a person interacting with it unless "person" is also in the objects list.
- **Only describe actions actually visible in the frames.** Do not assume or infer actions that you don't observe happening. If someone walks toward furniture but you never see them sit, do not say they sat. Stick to what you can see across the sequence.
- Describe what you observe: actions, movements, interactions with objects and the environment. Include any observable environmental changes (e.g., lighting changes triggered by activity).
- Note visible details such as clothing, items being carried or placed, tools or equipment present, and how they interact with the property or objects.
- Consider the full sequence chronologically: what happens from start to finish, how duration and actions relate to the location and objects involved.
- **Use the actual timestamp provided in "Activity started at"** below for time of day contextdo not infer time from image brightness or darkness. Unusual hours (late night/early morning) should increase suspicion when the observable behavior itself appears questionable. However, recognize that some legitimate activities can occur at any hour.
- **Consider duration as a primary factor**: Apply the duration thresholds defined in the activity patterns above. Brief sequences during normal hours with apparent purpose typically indicate normal activity unless explicit suspicious actions are visible.
- **Weigh all evidence holistically**: Match the activity against the normal and suspicious patterns defined above, then evaluate based on the complete context (zone, objects, time, actions, duration). Apply the threat level indicators consistently. Use your judgment for edge cases.
## Response Field Guidelines
Respond with a JSON object matching the provided schema. Field-specific guidance:
- `observations`: Include the very start of the activity for example, a vehicle entering the frame or pulling into the driveway even if it lasts only a few frames and the rest of the clip is dominated by a longer activity. Include each arrival, departure, object handled, and notable change in position or state. Each item is a single concrete fact written as a complete sentence.
- `scene`: Describe how the sequence begins, then the progression of events all significant movements and actions in order. For example, if a vehicle arrives and then a person exits, describe both sequentially. For named subjects (those with a `` separator in "Objects in Scene"), always use their name do not replace them with generic terms. For unnamed objects (e.g., "person", "car"), refer to them naturally with articles (e.g., "a person", "the car"). Your description should align with and support the threat level you assign.
- `title`: Name the primary activity across the observations, together with the location. An activity is what is being done with objects, tools, or surfaces; locomotion through the scene qualifies as the activity only when no other interaction is observed. For named subjects, always use their name. For unnamed objects, refer to them naturally with articles.
- `shortSummary`: Briefly summarize the primary activity across the observations.
- `potential_threat_level`: Must be consistent with your scene description and the activity patterns above.
## Sequence Details
- Camera: {review_data["camera"]}
- Total frames: {len(thumbnails)} (Frame 1 = earliest, Frame {len(thumbnails)} = latest)
- Activity started at {review_data["start"]} and lasted {review_data["duration"]} seconds
- Zones involved: {", ".join(review_data["zones"]) if review_data["zones"] else "None"}
## Objects in Scene
Each line represents a detection state, not necessarily unique individuals. The `` symbol separates a recognized subject's name from their object type — use only the name (before the `←`) in your response, not the type after it. The same subject may appear across multiple lines if detected multiple times.
**Note: Unidentified objects (without names) are NOT indicators of suspicious activitythey simply mean the system hasn't identified that object.**
{get_objects_list()}
{get_language_prompt()}
"""
logger.debug(
f"Sending {len(thumbnails)} images to create review description on {review_data['camera']}"
)
@ -143,25 +86,7 @@ Each line represents a detection state, not necessarily unique individuals. The
) as f:
f.write(context_prompt)
# Build JSON schema for structured output from ReviewMetadata model
schema = ReviewMetadata.model_json_schema()
schema.get("properties", {}).pop("time", None)
if "time" in schema.get("required", []):
schema["required"].remove("time")
if not concerns:
schema.get("properties", {}).pop("other_concerns", None)
if "other_concerns" in schema.get("required", []):
schema["required"].remove("other_concerns")
response_format = {
"type": "json_schema",
"json_schema": {
"name": "review_metadata",
"strict": True,
"schema": schema,
},
}
response_format = build_review_description_response_format(concerns)
response = self._send(context_prompt, thumbnails, response_format)
@ -240,61 +165,9 @@ Each line represents a detection state, not necessarily unique individuals. The
debug_save: bool,
) -> str | None:
"""Generate a summary of review item descriptions over a period of time."""
time_range = f"{datetime.datetime.fromtimestamp(start_ts).strftime('%B %d, %Y at %I:%M %p')} to {datetime.datetime.fromtimestamp(end_ts).strftime('%B %d, %Y at %I:%M %p')}"
timeline_summary_prompt = f"""
You are a security officer writing a concise security report.
Time range: {time_range}
Input format: Each event is a JSON object with:
- "title", "scene", "confidence", "potential_threat_level" (0-2), "other_concerns", "camera", "time", "start_time", "end_time"
- "context": array of related events from other cameras that occurred during overlapping time periods
**Note: Use the "scene" field for event descriptions in the report. Ignore any "shortSummary" field if present.**
Report Structure - Use this EXACT format:
# Security Summary - {time_range}
## Overview
[Write 1-2 sentences summarizing the overall activity pattern during this period.]
---
## Timeline
[Group events by time periods (e.g., "Morning (6:00 AM - 12:00 PM)", "Afternoon (12:00 PM - 5:00 PM)", "Evening (5:00 PM - 9:00 PM)", "Night (9:00 PM - 6:00 AM)"). Use appropriate time blocks based on when events occurred.]
### [Time Block Name]
**HH:MM AM/PM** | [Camera Name] | [Threat Level Indicator]
- [Event title]: [Clear description incorporating contextual information from the "context" array]
- Context: [If context array has items, mention them here, e.g., "Delivery truck present on Front Driveway Cam (HH:MM AM/PM)"]
- Assessment: [Brief assessment incorporating context - if context explains the event, note it here]
[Repeat for each event in chronological order within the time block]
---
## Summary
[One sentence summarizing the period. If all events are normal/explained: "Routine activity observed." If review needed: "Some activity requires review but no security concerns." If security concerns: "Security concerns requiring immediate attention."]
Guidelines:
- List ALL events in chronological order, grouped by time blocks
- Threat level indicators: Normal, Needs review, 🔴 Security concern
- Integrate contextual information naturally - use the "context" array to enrich each event's description
- If context explains the event (e.g., delivery truck explains person at door), describe it accordingly (e.g., "delivery person" not "unidentified person")
- Be concise but informative - focus on what happened and what it means
- If contextual information makes an event clearly normal, reflect that in your assessment
- Only create time blocks that have events - don't create empty sections
"""
timeline_summary_prompt += "\n\nEvents:\n"
for event in events:
timeline_summary_prompt += f"\n{event}\n"
if preferred_language:
timeline_summary_prompt += f"\nProvide your answer in {preferred_language}"
timeline_summary_prompt = build_review_summary_prompt(
start_ts, end_ts, events, preferred_language
)
if debug_save:
with open(
@ -326,10 +199,7 @@ Guidelines:
) -> Optional[str]:
"""Generate a description for the frame."""
try:
prompt = camera_config.objects.genai.object_prompts.get(
str(event.label),
camera_config.objects.genai.prompt,
).format(**model_to_dict(event))
prompt = build_object_description_prompt(camera_config, event)
except KeyError as e:
logger.error(f"Invalid key in GenAI prompt: {e}")
return None
@ -430,6 +300,10 @@ Guidelines:
Returns:
Dictionary with:
- 'content': Optional[str] - The text response from the LLM, None if tool calls
- 'reasoning': Optional[str] - The separated reasoning/thinking trace
if the model emitted one (e.g. via OpenAI-compatible
`reasoning_content`). None when the model does not surface a
trace or the provider does not parse it.
- 'tool_calls': Optional[List[Dict]] - List of tool calls if LLM wants to call tools.
Each tool call dict has:
- 'id': str - Unique identifier for this tool call
@ -441,6 +315,14 @@ Guidelines:
- 'length': Hit token limit
- 'error': An error occurred
Streaming counterpart `chat_with_tools_stream` yields
``(kind, value)`` tuples where ``kind`` is one of:
- 'content_delta': value is a string fragment of the answer
- 'reasoning_delta': value is a string fragment of the reasoning
trace (emitted before content for thinking models)
- 'stats': value is a usage stats dict
- 'message': value is the final dict shape described above
Raises:
NotImplementedError: If the provider doesn't implement this method.
"""
@ -451,14 +333,15 @@ Guidelines:
)
return {
"content": None,
"reasoning": None,
"tool_calls": None,
"finish_reason": "error",
}
def load_providers() -> None:
package_dir = os.path.dirname(__file__)
for filename in os.listdir(package_dir):
plugins_dir = os.path.join(os.path.dirname(__file__), "plugins")
for filename in os.listdir(plugins_dir):
if filename.endswith(".py") and filename != "__init__.py":
module_name = f"frigate.genai.{filename[:-3]}"
module_name = f"frigate.genai.plugins.{filename[:-3]}"
importlib.import_module(module_name)

View File

@ -1,305 +0,0 @@
"""Azure OpenAI Provider for Frigate AI."""
import base64
import json
import logging
from typing import Any, AsyncGenerator, Optional
from urllib.parse import parse_qs, urlparse
from openai import AzureOpenAI
from frigate.config import GenAIProviderEnum
from frigate.genai import GenAIClient, register_genai_provider
logger = logging.getLogger(__name__)
@register_genai_provider(GenAIProviderEnum.azure_openai)
class OpenAIClient(GenAIClient):
"""Generative AI client for Frigate using Azure OpenAI."""
provider: AzureOpenAI
def _init_provider(self) -> AzureOpenAI | None:
"""Initialize the client."""
try:
parsed_url = urlparse(self.genai_config.base_url or "")
query_params = parse_qs(parsed_url.query)
api_version = query_params.get("api-version", [None])[0]
azure_endpoint = f"{parsed_url.scheme}://{parsed_url.netloc}/"
if not api_version:
logger.warning("Azure OpenAI url is missing API version.")
return None
except Exception as e:
logger.warning("Error parsing Azure OpenAI url: %s", str(e))
return None
return AzureOpenAI(
api_key=self.genai_config.api_key,
api_version=api_version,
azure_endpoint=azure_endpoint,
)
def _send(
self,
prompt: str,
images: list[bytes],
response_format: Optional[dict] = None,
) -> Optional[str]:
"""Submit a request to Azure OpenAI."""
encoded_images = [base64.b64encode(image).decode("utf-8") for image in images]
try:
request_params = {
"model": self.genai_config.model,
"messages": [
{
"role": "user",
"content": [{"type": "text", "text": prompt}]
+ [
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image}",
"detail": "low",
},
}
for image in encoded_images
],
},
],
"timeout": self.timeout,
**self.genai_config.runtime_options,
}
if response_format:
request_params["response_format"] = response_format
result = self.provider.chat.completions.create(**request_params)
except Exception as e:
logger.warning("Azure OpenAI returned an error: %s", str(e))
return None
if len(result.choices) > 0:
return str(result.choices[0].message.content.strip())
return None
def list_models(self) -> list[str]:
"""Return available model IDs from Azure OpenAI."""
try:
return sorted(m.id for m in self.provider.models.list().data)
except Exception as e:
logger.warning("Failed to list Azure OpenAI models: %s", e)
return []
def get_context_size(self) -> int:
"""Get the context window size for Azure OpenAI."""
return 128000
def chat_with_tools(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
) -> dict[str, Any]:
try:
openai_tool_choice = None
if tool_choice:
if tool_choice == "none":
openai_tool_choice = "none"
elif tool_choice == "auto":
openai_tool_choice = "auto"
elif tool_choice == "required":
openai_tool_choice = "required"
request_params = {
"model": self.genai_config.model,
"messages": messages,
"timeout": self.timeout,
}
if tools:
request_params["tools"] = tools
if openai_tool_choice is not None:
request_params["tool_choice"] = openai_tool_choice
result = self.provider.chat.completions.create(**request_params) # type: ignore[call-overload]
if (
result is None
or not hasattr(result, "choices")
or len(result.choices) == 0
):
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
choice = result.choices[0]
message = choice.message
content = message.content.strip() if message.content else None
tool_calls = None
if message.tool_calls:
tool_calls = []
for tool_call in message.tool_calls:
try:
arguments = json.loads(tool_call.function.arguments)
except (json.JSONDecodeError, AttributeError) as e:
logger.warning(
f"Failed to parse tool call arguments: {e}, "
f"tool: {tool_call.function.name if hasattr(tool_call.function, 'name') else 'unknown'}"
)
arguments = {}
tool_calls.append(
{
"id": tool_call.id if hasattr(tool_call, "id") else "",
"name": tool_call.function.name
if hasattr(tool_call.function, "name")
else "",
"arguments": arguments,
}
)
finish_reason = "error"
if hasattr(choice, "finish_reason") and choice.finish_reason:
finish_reason = choice.finish_reason
elif tool_calls:
finish_reason = "tool_calls"
elif content:
finish_reason = "stop"
return {
"content": content,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
except Exception as e:
logger.warning("Azure OpenAI returned an error: %s", str(e))
return {
"content": None,
"tool_calls": None,
"finish_reason": "error",
}
async def chat_with_tools_stream(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
) -> AsyncGenerator[tuple[str, Any], None]:
"""
Stream chat with tools; yields content deltas then final message.
Implements streaming function calling/tool usage for Azure OpenAI models.
"""
try:
openai_tool_choice = None
if tool_choice:
if tool_choice == "none":
openai_tool_choice = "none"
elif tool_choice == "auto":
openai_tool_choice = "auto"
elif tool_choice == "required":
openai_tool_choice = "required"
request_params = {
"model": self.genai_config.model,
"messages": messages,
"timeout": self.timeout,
"stream": True,
}
if tools:
request_params["tools"] = tools
if openai_tool_choice is not None:
request_params["tool_choice"] = openai_tool_choice
# Use streaming API
content_parts: list[str] = []
tool_calls_by_index: dict[int, dict[str, Any]] = {}
finish_reason = "stop"
stream = self.provider.chat.completions.create(**request_params) # type: ignore[call-overload]
for chunk in stream:
if not chunk or not chunk.choices:
continue
choice = chunk.choices[0]
delta = choice.delta
# Check for finish reason
if choice.finish_reason:
finish_reason = choice.finish_reason
# Extract content deltas
if delta.content:
content_parts.append(delta.content)
yield ("content_delta", delta.content)
# Extract tool calls
if delta.tool_calls:
for tc in delta.tool_calls:
idx = tc.index
fn = tc.function
if idx not in tool_calls_by_index:
tool_calls_by_index[idx] = {
"id": tc.id or "",
"name": fn.name if fn and fn.name else "",
"arguments": "",
}
t = tool_calls_by_index[idx]
if tc.id:
t["id"] = tc.id
if fn and fn.name:
t["name"] = fn.name
if fn and fn.arguments:
t["arguments"] += fn.arguments
# Build final message
full_content = "".join(content_parts).strip() or None
# Convert tool calls to list format
tool_calls_list = None
if tool_calls_by_index:
tool_calls_list = []
for tc in tool_calls_by_index.values():
try:
# Parse accumulated arguments as JSON
parsed_args = json.loads(tc["arguments"])
except (json.JSONDecodeError, Exception):
parsed_args = tc["arguments"]
tool_calls_list.append(
{
"id": tc["id"],
"name": tc["name"],
"arguments": parsed_args,
}
)
finish_reason = "tool_calls"
yield (
"message",
{
"content": full_content,
"tool_calls": tool_calls_list,
"finish_reason": finish_reason,
},
)
except Exception as e:
logger.warning("Azure OpenAI streaming returned an error: %s", str(e))
yield (
"message",
{
"content": None,
"tool_calls": None,
"finish_reason": "error",
},
)

View File

@ -0,0 +1 @@
"""GenAI provider plugins."""

View File

@ -0,0 +1,53 @@
"""Azure OpenAI Provider for Frigate AI.
Azure OpenAI exposes the same chat completions API as OpenAI once the
client is constructed, so this provider inherits all transport, streaming,
reasoning, and tool-calling logic from :class:`OpenAIClient` and only
overrides what is genuinely Azure-specific:
- Client construction: parses ``api-version`` out of the configured
``base_url`` query string and instantiates :class:`openai.AzureOpenAI`
with ``azure_endpoint`` instead of ``base_url``. Raises if the URL is
malformed; :class:`GenAIClientManager` catches the exception and
disables the provider.
- Context size: Azure does not expose a per-model ``max_model_len`` field
reliably, so we keep the historical 128K default rather than the
model-name heuristic used by OpenAI.
"""
import logging
from urllib.parse import parse_qs, urlparse
from openai import AzureOpenAI
from frigate.config import GenAIProviderEnum
from frigate.genai import register_genai_provider
from frigate.genai.plugins.openai import OpenAIClient
logger = logging.getLogger(__name__)
@register_genai_provider(GenAIProviderEnum.azure_openai)
class AzureOpenAIClient(OpenAIClient):
"""Generative AI client for Frigate using Azure OpenAI."""
def _init_provider(self) -> AzureOpenAI:
"""Initialize the AzureOpenAI client from the configured base_url."""
parsed_url = urlparse(self.genai_config.base_url or "")
query_params = parse_qs(parsed_url.query)
api_version = query_params.get("api-version", [None])[0]
if not api_version:
raise ValueError("Azure OpenAI base_url is missing api-version.")
azure_endpoint = f"{parsed_url.scheme}://{parsed_url.netloc}/"
return AzureOpenAI(
api_key=self.genai_config.api_key,
api_version=api_version,
azure_endpoint=azure_endpoint,
)
def get_context_size(self) -> int:
"""Azure does not reliably surface per-model context size; use 128K."""
return 128000

View File

@ -14,6 +14,20 @@ from frigate.genai import GenAIClient, register_genai_provider
logger = logging.getLogger(__name__)
def _stats_from_gemini_usage(usage: Any) -> Optional[dict[str, Any]]:
"""Build a stats dict from a Gemini usage_metadata object."""
prompt_tokens = getattr(usage, "prompt_token_count", None)
completion_tokens = getattr(usage, "candidates_token_count", None)
if prompt_tokens is None and completion_tokens is None:
return None
stats: dict[str, Any] = {}
if isinstance(prompt_tokens, int):
stats["prompt_tokens"] = prompt_tokens
if isinstance(completion_tokens, int):
stats["completion_tokens"] = completion_tokens
return stats or None
@register_genai_provider(GenAIProviderEnum.gemini)
class GeminiClient(GenAIClient):
"""Generative AI client for Frigate using Gemini."""
@ -234,6 +248,13 @@ class GeminiClient(GenAIClient):
if tool_config:
config_params["tool_config"] = tool_config
# Ask thinking-capable models (Gemini 2.5+) to include their
# reasoning trace as separate `thought` parts so we can surface
# it on the reasoning channel. Older models ignore this field.
config_params["thinking_config"] = types.ThinkingConfig(
include_thoughts=True
)
# Merge runtime_options
if isinstance(self.genai_config.runtime_options, dict):
config_params.update(self.genai_config.runtime_options)
@ -248,19 +269,24 @@ class GeminiClient(GenAIClient):
if not response or not response.candidates:
return {
"content": None,
"reasoning": None,
"tool_calls": None,
"finish_reason": "error",
}
candidate = response.candidates[0]
content = None
reasoning_parts: list[str] = []
tool_calls = None
# Extract content and tool calls from response
# Extract content, reasoning, and tool calls from response
if candidate.content and candidate.content.parts:
for part in candidate.content.parts:
if part.text:
content = part.text.strip()
if getattr(part, "thought", False):
reasoning_parts.append(part.text)
else:
content = part.text.strip()
elif part.function_call:
# Handle function call
if tool_calls is None:
@ -283,6 +309,8 @@ class GeminiClient(GenAIClient):
}
)
reasoning = "".join(reasoning_parts).strip() or None
# Determine finish reason
finish_reason = "error"
if hasattr(candidate, "finish_reason") and candidate.finish_reason:
@ -308,6 +336,7 @@ class GeminiClient(GenAIClient):
return {
"content": content,
"reasoning": reasoning,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
@ -316,6 +345,7 @@ class GeminiClient(GenAIClient):
logger.warning("Gemini API error during chat_with_tools: %s", str(e))
return {
"content": None,
"reasoning": None,
"tool_calls": None,
"finish_reason": "error",
}
@ -325,6 +355,7 @@ class GeminiClient(GenAIClient):
)
return {
"content": None,
"reasoning": None,
"tool_calls": None,
"finish_reason": "error",
}
@ -463,14 +494,22 @@ class GeminiClient(GenAIClient):
if tool_config:
config_params["tool_config"] = tool_config
# Ask thinking-capable models to include their reasoning trace
# as separate `thought` parts (Gemini 2.5+; ignored elsewhere).
config_params["thinking_config"] = types.ThinkingConfig(
include_thoughts=True
)
# Merge runtime_options
if isinstance(self.genai_config.runtime_options, dict):
config_params.update(self.genai_config.runtime_options)
# Use streaming API
content_parts: list[str] = []
reasoning_parts: list[str] = []
tool_calls_by_index: dict[int, dict[str, Any]] = {}
finish_reason = "stop"
usage_stats: Optional[dict[str, Any]] = None
stream = await self.provider.aio.models.generate_content_stream(
model=self.genai_config.model,
@ -479,6 +518,12 @@ class GeminiClient(GenAIClient):
)
async for chunk in stream:
chunk_usage = getattr(chunk, "usage_metadata", None)
if chunk_usage is not None:
maybe_stats = _stats_from_gemini_usage(chunk_usage)
if maybe_stats is not None:
usage_stats = maybe_stats
if not chunk or not chunk.candidates:
continue
@ -498,12 +543,16 @@ class GeminiClient(GenAIClient):
]:
finish_reason = "error"
# Extract content and tool calls from chunk
# Extract content, reasoning, and tool calls from chunk
if candidate.content and candidate.content.parts:
for part in candidate.content.parts:
if part.text:
content_parts.append(part.text)
yield ("content_delta", part.text)
if getattr(part, "thought", False):
reasoning_parts.append(part.text)
yield ("reasoning_delta", part.text)
else:
content_parts.append(part.text)
yield ("content_delta", part.text)
elif part.function_call:
# Handle function call
try:
@ -544,6 +593,7 @@ class GeminiClient(GenAIClient):
# Build final message
full_content = "".join(content_parts).strip() or None
full_reasoning = "".join(reasoning_parts).strip() or None
# Convert tool calls to list format
tool_calls_list = None
@ -565,10 +615,14 @@ class GeminiClient(GenAIClient):
)
finish_reason = "tool_calls"
if usage_stats is not None:
yield ("stats", usage_stats)
yield (
"message",
{
"content": full_content,
"reasoning": full_reasoning,
"tool_calls": tool_calls_list,
"finish_reason": finish_reason,
},
@ -580,6 +634,7 @@ class GeminiClient(GenAIClient):
"message",
{
"content": None,
"reasoning": None,
"tool_calls": None,
"finish_reason": "error",
},
@ -592,6 +647,7 @@ class GeminiClient(GenAIClient):
"message",
{
"content": None,
"reasoning": None,
"tool_calls": None,
"finish_reason": "error",
},

View File

@ -4,7 +4,7 @@ import base64
import io
import json
import logging
from typing import Any, AsyncGenerator, Optional
from typing import Any, AsyncGenerator, Optional, cast
import httpx
import numpy as np
@ -18,6 +18,86 @@ from frigate.genai.utils import parse_tool_calls_from_message
logger = logging.getLogger(__name__)
def _stats_from_llama_cpp_chunk(data: dict[str, Any]) -> Optional[dict[str, Any]]:
"""Build a stats dict from a llama.cpp streaming chunk.
Final-chunk `usage` carries authoritative token counts. Per-chunk
`timings` (enabled via timings_per_token) carries the running token
counts (prompt_n, predicted_n) and generation rate, so live updates
work mid-stream.
"""
usage = data.get("usage") or {}
timings = data.get("timings") or {}
prompt_tokens = usage.get("prompt_tokens")
completion_tokens = usage.get("completion_tokens")
predicted_ms = timings.get("predicted_ms")
tps = timings.get("predicted_per_second")
stats: dict[str, Any] = {}
if not isinstance(prompt_tokens, int):
prompt_n = timings.get("prompt_n")
if isinstance(prompt_n, int):
prompt_tokens = prompt_n
if not isinstance(completion_tokens, int):
predicted_n = timings.get("predicted_n")
if isinstance(predicted_n, int):
completion_tokens = predicted_n
if not isinstance(prompt_tokens, int) and not isinstance(completion_tokens, int):
return None
if isinstance(prompt_tokens, int):
stats["prompt_tokens"] = prompt_tokens
if isinstance(completion_tokens, int):
stats["completion_tokens"] = completion_tokens
if isinstance(predicted_ms, (int, float)) and predicted_ms > 0:
stats["completion_duration_ms"] = float(predicted_ms)
if isinstance(tps, (int, float)) and tps > 0:
stats["tokens_per_second"] = float(tps)
return stats or None
def _parse_launch_arg(args: list[str], flag: str) -> str | None:
"""Return the value following `flag` in a positional argv list, or None."""
try:
idx = args.index(flag)
except ValueError:
return None
if idx + 1 >= len(args):
return None
return args[idx + 1]
def _fetch_llama_props(base_url: str, model: str) -> dict[str, Any]:
"""Fetch /props from a llama.cpp server, with llama-swap fallback.
Raises the underlying RequestException if both endpoints fail; callers
decide how to surface the failure.
"""
try:
response = requests.get(
f"{base_url}/props",
params={"model": model},
timeout=10,
)
response.raise_for_status()
return cast(dict[str, Any], response.json())
except Exception:
response = requests.get(
f"{base_url}/upstream/{model}/props",
timeout=10,
)
response.raise_for_status()
return cast(dict[str, Any], response.json())
def _to_jpeg(img_bytes: bytes) -> bytes | None:
"""Convert image bytes to JPEG. llama.cpp/STB does not support WebP."""
try:
@ -71,26 +151,69 @@ class LlamaCppClient(GenAIClient):
base_url = base_url.replace("/v1", "") # Strip /v1 if included in base_url
configured_model = self.genai_config.model
info = self._get_model_info(base_url, configured_model)
# Query /v1/models to validate the configured model exists
if info is None:
return None
self._context_size = info["context_size"]
self._supports_vision = info["supports_vision"]
self._supports_audio = info["supports_audio"]
self._supports_tools = info["supports_tools"]
self._media_marker = info["media_marker"]
logger.info(
"llama.cpp model '%s' initialized — context: %s, vision: %s, audio: %s, tools: %s",
configured_model,
self._context_size or "unknown",
self._supports_vision,
self._supports_audio,
self._supports_tools,
)
return base_url
def _get_model_info(
self, base_url: str, configured_model: str
) -> dict[str, Any] | None:
"""Resolve model metadata from /v1/models with /props fallback.
Returns a dict of capability fields, or None if the server's model
registry was reachable and reported the configured model as missing.
A reachable-but-unparseable /v1/models is treated as soft-pass and
falls through to /props, matching prior behavior.
After ggml-org/llama.cpp#22952, /v1/models exposes per-model
`architecture.input_modalities` (text/image/audio) the primary
source. When proxied through llama-swap, the same entry carries
`status.args` (server launch argv) and, for the loaded model,
`meta.n_ctx`. /props remains the only source for `media_marker`,
which the server randomizes per startup unless LLAMA_MEDIA_MARKER
is set.
"""
info: dict[str, Any] = {
"context_size": None,
"supports_vision": False,
"supports_audio": False,
"supports_tools": False,
"media_marker": "<__media__>",
}
model_entry: dict[str, Any] | None = None
try:
response = requests.get(
f"{base_url}/v1/models",
timeout=10,
)
response = requests.get(f"{base_url}/v1/models", timeout=10)
response.raise_for_status()
models_data = response.json()
model_found = False
for model in models_data.get("data", []):
model_ids = {model.get("id")}
for alias in model.get("aliases", []):
model_ids.add(alias)
if configured_model in model_ids:
model_found = True
model_entry = model
break
if not model_found:
if model_entry is None:
available = []
for m in models_data.get("data", []):
available.append(m.get("id", "unknown"))
@ -109,65 +232,64 @@ class LlamaCppClient(GenAIClient):
e,
)
# Query /props for context size, modalities, and tool support.
# The standard /props?model=<name> endpoint works with llama-server.
# If it fails, try the llama-swap per-model passthrough endpoint which
# returns props for a specific model without requiring it to be loaded.
try:
try:
response = requests.get(
f"{base_url}/props",
params={"model": configured_model},
timeout=10,
)
response.raise_for_status()
props = response.json()
except Exception:
response = requests.get(
f"{base_url}/upstream/{configured_model}/props",
timeout=10,
)
response.raise_for_status()
props = response.json()
if model_entry is not None:
architecture = model_entry.get("architecture") or {}
input_modalities = architecture.get("input_modalities") or []
if isinstance(input_modalities, list):
info["supports_vision"] = "image" in input_modalities
info["supports_audio"] = "audio" in input_modalities
status = model_entry.get("status") or {}
launch_args = status.get("args") if isinstance(status, dict) else None
if not isinstance(launch_args, list):
launch_args = []
meta = model_entry.get("meta") if isinstance(model_entry, dict) else None
n_ctx = meta.get("n_ctx") if isinstance(meta, dict) else None
if not n_ctx:
n_ctx = _parse_launch_arg(launch_args, "--ctx-size")
# Context size from server runtime config
default_settings = props.get("default_generation_settings", {})
n_ctx = default_settings.get("n_ctx")
if n_ctx:
self._context_size = int(n_ctx)
try:
info["context_size"] = int(n_ctx)
except (TypeError, ValueError):
pass
# Modalities (vision, audio)
modalities = props.get("modalities", {})
self._supports_vision = modalities.get("vision", False)
self._supports_audio = modalities.get("audio", False)
# Tool calling on llama-server requires --jinja.
if "--jinja" in launch_args:
info["supports_tools"] = True
# Tool support from chat template capabilities
chat_caps = props.get("chat_template_caps", {})
self._supports_tools = chat_caps.get("supports_tools", False)
try:
props = _fetch_llama_props(base_url, configured_model)
if info["context_size"] is None:
default_settings = props.get("default_generation_settings", {})
n_ctx = default_settings.get("n_ctx")
if n_ctx:
info["context_size"] = int(n_ctx)
if not (info["supports_vision"] or info["supports_audio"]):
modalities = props.get("modalities", {})
info["supports_vision"] = bool(modalities.get("vision", False))
info["supports_audio"] = bool(modalities.get("audio", False))
if not info["supports_tools"]:
chat_caps = props.get("chat_template_caps", {})
info["supports_tools"] = bool(chat_caps.get("supports_tools", False))
# Media marker for multimodal embeddings; the server randomizes this
# per startup unless LLAMA_MEDIA_MARKER is set, so we must read it
# from /props rather than hardcoding "<__media__>".
media_marker = props.get("media_marker")
if isinstance(media_marker, str) and media_marker:
self._media_marker = media_marker
logger.info(
"llama.cpp model '%s' initialized — context: %s, vision: %s, audio: %s, tools: %s",
configured_model,
self._context_size or "unknown",
self._supports_vision,
self._supports_audio,
self._supports_tools,
)
info["media_marker"] = media_marker
except Exception as e:
logger.warning(
"Failed to query llama.cpp /props endpoint: %s. "
"Using defaults for context size and capabilities.",
"Image embeddings may fail if the server randomized its media marker.",
e,
)
return base_url
return info
def _send(
self,
@ -395,6 +517,8 @@ class LlamaCppClient(GenAIClient):
}
if stream:
payload["stream"] = True
payload["stream_options"] = {"include_usage": True}
payload["timings_per_token"] = True
if tools:
payload["tools"] = tools
if openai_tool_choice is not None:
@ -403,19 +527,28 @@ class LlamaCppClient(GenAIClient):
k: v for k, v in self.provider_options.items() if k != "context_size"
}
payload.update(provider_opts)
payload.update(self.genai_config.runtime_options)
return payload
def _message_from_choice(self, choice: dict[str, Any]) -> dict[str, Any]:
"""Parse OpenAI-style choice into {content, tool_calls, finish_reason}."""
"""Parse OpenAI-style choice into {content, reasoning, tool_calls, finish_reason}.
llama.cpp's `--reasoning-format` puts the trace in
`message.reasoning_content` (preferred) or `message.thinking`; both
keys are accepted so different builds work without configuration.
"""
message = choice.get("message", {})
content = message.get("content")
content = content.strip() if content else None
reasoning = message.get("reasoning_content") or message.get("thinking")
reasoning = reasoning.strip() if reasoning else None
tool_calls = parse_tool_calls_from_message(message)
finish_reason = choice.get("finish_reason") or (
"tool_calls" if tool_calls else "stop" if content else "error"
)
return {
"content": content,
"reasoning": reasoning,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
@ -444,6 +577,31 @@ class LlamaCppClient(GenAIClient):
)
return result if result else None
def _refresh_media_marker(self) -> bool:
"""Re-fetch /props and update the cached media marker if it changed.
The server randomizes the marker per startup (unless LLAMA_MEDIA_MARKER
is set), so a stale marker indicates a restart. Returns True iff the
marker was updated to a new value used to gate a one-shot retry of
a failed embeddings request.
"""
if self.provider is None:
return False
try:
props = _fetch_llama_props(self.provider, self.genai_config.model)
except Exception as e:
logger.warning("Failed to refresh llama.cpp media marker: %s", e)
return False
marker = props.get("media_marker")
if not isinstance(marker, str) or not marker or marker == self._media_marker:
return False
logger.info("llama.cpp media marker changed (server restart); refreshed")
self._media_marker = marker
return True
def embed(
self,
texts: list[str] | None = None,
@ -468,30 +626,46 @@ class LlamaCppClient(GenAIClient):
EMBEDDING_DIM = 768
content = []
for text in texts:
content.append({"prompt_string": text})
encoded_images: list[str] = []
for img in images:
# llama.cpp uses STB which does not support WebP; convert to JPEG
jpeg_bytes = _to_jpeg(img)
to_encode = jpeg_bytes if jpeg_bytes is not None else img
encoded = base64.b64encode(to_encode).decode("utf-8")
# prompt_string must contain the server's media marker placeholder.
# The marker is randomized per server startup (read from /props).
content.append(
{
"prompt_string": f"{self._media_marker}\n",
"multimodal_data": [encoded], # type: ignore[dict-item]
}
encoded_images.append(base64.b64encode(to_encode).decode("utf-8"))
def build_content() -> list[dict[str, Any]]:
# prompt_string must contain the server's media marker placeholder
# for each image. The marker is randomized per server startup.
content: list[dict[str, Any]] = []
for text in texts:
content.append({"prompt_string": text})
for encoded in encoded_images:
content.append(
{
"prompt_string": f"{self._media_marker}\n",
"multimodal_data": [encoded],
}
)
return content
def post_embeddings() -> requests.Response:
return requests.post(
f"{self.provider}/embeddings",
json={"model": self.genai_config.model, "content": build_content()},
timeout=self.timeout,
)
try:
response = requests.post(
f"{self.provider}/embeddings",
json={"model": self.genai_config.model, "content": content},
timeout=self.timeout,
)
response.raise_for_status()
try:
response = post_embeddings()
response.raise_for_status()
except requests.exceptions.RequestException:
# The server may have restarted with a new media marker.
# Refresh from /props; only retry if the marker actually changed.
if not encoded_images or not self._refresh_media_marker():
raise
response = post_embeddings()
response.raise_for_status()
result = response.json()
items = result.get("data", result) if isinstance(result, dict) else result
@ -637,6 +811,7 @@ class LlamaCppClient(GenAIClient):
try:
payload = self._build_payload(messages, tools, tool_choice, stream=True)
content_parts: list[str] = []
reasoning_parts: list[str] = []
tool_calls_by_index: dict[int, dict[str, Any]] = {}
finish_reason = "stop"
@ -657,12 +832,24 @@ class LlamaCppClient(GenAIClient):
data = json.loads(data_str)
except json.JSONDecodeError:
continue
maybe_stats = _stats_from_llama_cpp_chunk(data)
if maybe_stats is not None:
yield ("stats", maybe_stats)
choices = data.get("choices") or []
if not choices:
continue
delta = choices[0].get("delta", {})
if choices[0].get("finish_reason"):
finish_reason = choices[0]["finish_reason"]
# llama.cpp emits separated thinking under
# reasoning_content (preferred) or thinking before any
# content tokens arrive
reasoning_delta = delta.get("reasoning_content") or delta.get(
"thinking"
)
if reasoning_delta:
reasoning_parts.append(reasoning_delta)
yield ("reasoning_delta", reasoning_delta)
if delta.get("content"):
content_parts.append(delta["content"])
yield ("content_delta", delta["content"])
@ -688,6 +875,7 @@ class LlamaCppClient(GenAIClient):
)
full_content = "".join(content_parts).strip() or None
full_reasoning = "".join(reasoning_parts).strip() or None
tool_calls_list = self._streamed_tool_calls_to_list(tool_calls_by_index)
if tool_calls_list:
finish_reason = "tool_calls"
@ -695,6 +883,7 @@ class LlamaCppClient(GenAIClient):
"message",
{
"content": full_content,
"reasoning": full_reasoning,
"tool_calls": tool_calls_list,
"finish_reason": finish_reason,
},

View File

@ -18,6 +18,37 @@ from frigate.genai.utils import parse_tool_calls_from_message
logger = logging.getLogger(__name__)
def _extract_ollama_stats(response: Any) -> Optional[dict[str, Any]]:
"""Build a stats dict from Ollama's response metadata.
Ollama reports eval_count/eval_duration (generation) and
prompt_eval_count (context size). Durations are nanoseconds.
"""
if not response:
return None
if hasattr(response, "get"):
getter = response.get
else:
getter = lambda key: getattr(response, key, None) # noqa: E731
eval_count = getter("eval_count")
eval_duration_ns = getter("eval_duration")
prompt_eval_count = getter("prompt_eval_count")
if eval_count is None and prompt_eval_count is None:
return None
stats: dict[str, Any] = {}
if isinstance(prompt_eval_count, int):
stats["prompt_tokens"] = prompt_eval_count
if isinstance(eval_count, int):
stats["completion_tokens"] = eval_count
if isinstance(eval_duration_ns, int) and eval_duration_ns > 0:
stats["completion_duration_ms"] = eval_duration_ns / 1_000_000
if isinstance(eval_count, int) and eval_count > 0:
stats["tokens_per_second"] = eval_count / (eval_duration_ns / 1_000_000_000)
return stats or None
def _normalize_multimodal_content(
content: Any,
) -> tuple[Optional[str], Optional[list[bytes]]]:
@ -278,6 +309,7 @@ class OllamaClient(GenAIClient):
"model": self.genai_config.model,
"messages": request_messages,
**self.provider_options,
**self.genai_config.runtime_options,
}
if stream:
request_params["stream"] = True
@ -305,6 +337,9 @@ class OllamaClient(GenAIClient):
response.get("done"),
)
content = message.get("content", "").strip() if message.get("content") else None
reasoning = (
message.get("thinking", "").strip() if message.get("thinking") else None
)
tool_calls = parse_tool_calls_from_message(message)
finish_reason = "error"
if response.get("done"):
@ -317,6 +352,7 @@ class OllamaClient(GenAIClient):
finish_reason = "stop"
return {
"content": content,
"reasoning": reasoning,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
@ -400,9 +436,15 @@ class OllamaClient(GenAIClient):
)
response = await async_client.chat(**request_params)
result = self._message_from_response(response)
reasoning = result.get("reasoning")
if reasoning:
yield ("reasoning_delta", reasoning)
content = result.get("content")
if content:
yield ("content_delta", content)
stats = _extract_ollama_stats(response)
if stats is not None:
yield ("stats", stats)
yield ("message", result)
return
@ -415,25 +457,38 @@ class OllamaClient(GenAIClient):
headers=self._auth_headers(),
)
content_parts: list[str] = []
reasoning_parts: list[str] = []
final_message: dict[str, Any] | None = None
final_chunk: Any = None
stream = await async_client.chat(**request_params)
async for chunk in stream:
if not chunk or "message" not in chunk:
continue
msg = chunk.get("message", {})
reasoning_delta = msg.get("thinking") or ""
if reasoning_delta:
reasoning_parts.append(reasoning_delta)
yield ("reasoning_delta", reasoning_delta)
delta = msg.get("content") or ""
if delta:
content_parts.append(delta)
yield ("content_delta", delta)
if chunk.get("done"):
final_chunk = chunk
full_content = "".join(content_parts).strip() or None
full_reasoning = "".join(reasoning_parts).strip() or None
final_message = {
"content": full_content,
"reasoning": full_reasoning,
"tool_calls": None,
"finish_reason": "stop",
}
break
stats = _extract_ollama_stats(final_chunk)
if stats is not None:
yield ("stats", stats)
if final_message is not None:
yield ("message", final_message)
else:
@ -441,6 +496,7 @@ class OllamaClient(GenAIClient):
"message",
{
"content": "".join(content_parts).strip() or None,
"reasoning": "".join(reasoning_parts).strip() or None,
"tool_calls": None,
"finish_reason": "stop",
},

View File

@ -14,6 +14,22 @@ from frigate.genai import GenAIClient, register_genai_provider
logger = logging.getLogger(__name__)
def _stats_from_openai_usage(usage: Any) -> Optional[dict[str, Any]]:
"""Build a stats dict from an OpenAI-compatible usage object."""
if usage is None:
return None
prompt_tokens = getattr(usage, "prompt_tokens", None)
completion_tokens = getattr(usage, "completion_tokens", None)
if prompt_tokens is None and completion_tokens is None:
return None
stats: dict[str, Any] = {}
if isinstance(prompt_tokens, int):
stats["prompt_tokens"] = prompt_tokens
if isinstance(completion_tokens, int):
stats["completion_tokens"] = completion_tokens
return stats or None
@register_genai_provider(GenAIProviderEnum.openai)
class OpenAIClient(GenAIClient):
"""Generative AI client for Frigate using OpenAI."""
@ -22,7 +38,11 @@ class OpenAIClient(GenAIClient):
context_size: Optional[int] = None
def _init_provider(self) -> OpenAI:
"""Initialize the client."""
"""Initialize the client.
Subclasses (e.g. Azure) should raise on configuration errors; the
manager catches construction failures and disables the provider.
"""
# Extract context_size from provider_options as it's not a valid OpenAI client parameter
# It will be used in get_context_size() instead
provider_opts = {
@ -187,6 +207,7 @@ class OpenAIClient(GenAIClient):
"model": self.genai_config.model,
"messages": messages,
"timeout": self.timeout,
**self.genai_config.runtime_options,
}
if tools:
@ -203,7 +224,7 @@ class OpenAIClient(GenAIClient):
}
request_params.update(provider_opts)
result = self.provider.chat.completions.create(**request_params) # type: ignore[call-overload]
result = self.provider.chat.completions.create(**request_params)
if (
result is None
@ -219,6 +240,10 @@ class OpenAIClient(GenAIClient):
choice = result.choices[0]
message = choice.message
content = message.content.strip() if message.content else None
raw_reasoning = getattr(message, "reasoning_content", None) or getattr(
message, "reasoning", None
)
reasoning = raw_reasoning.strip() if raw_reasoning else None
tool_calls = None
if message.tool_calls:
@ -253,6 +278,7 @@ class OpenAIClient(GenAIClient):
return {
"content": content,
"reasoning": reasoning,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
@ -261,6 +287,7 @@ class OpenAIClient(GenAIClient):
logger.warning("OpenAI request timed out: %s", str(e))
return {
"content": None,
"reasoning": None,
"tool_calls": None,
"finish_reason": "error",
}
@ -268,6 +295,7 @@ class OpenAIClient(GenAIClient):
logger.warning("OpenAI returned an error: %s", str(e))
return {
"content": None,
"reasoning": None,
"tool_calls": None,
"finish_reason": "error",
}
@ -298,6 +326,8 @@ class OpenAIClient(GenAIClient):
"messages": messages,
"timeout": self.timeout,
"stream": True,
"stream_options": {"include_usage": True},
**self.genai_config.runtime_options,
}
if tools:
@ -316,12 +346,18 @@ class OpenAIClient(GenAIClient):
# Use streaming API
content_parts: list[str] = []
reasoning_parts: list[str] = []
tool_calls_by_index: dict[int, dict[str, Any]] = {}
finish_reason = "stop"
usage_stats: Optional[dict[str, Any]] = None
stream = self.provider.chat.completions.create(**request_params) # type: ignore[call-overload]
stream = self.provider.chat.completions.create(**request_params)
for chunk in stream:
chunk_usage = getattr(chunk, "usage", None)
if chunk_usage is not None:
usage_stats = _stats_from_openai_usage(chunk_usage)
if not chunk or not chunk.choices:
continue
@ -332,6 +368,15 @@ class OpenAIClient(GenAIClient):
if choice.finish_reason:
finish_reason = choice.finish_reason
# Extract reasoning deltas (reasoning_content or reasoning,
# depending on the server)
reasoning_delta = getattr(delta, "reasoning_content", None) or getattr(
delta, "reasoning", None
)
if reasoning_delta:
reasoning_parts.append(reasoning_delta)
yield ("reasoning_delta", reasoning_delta)
# Extract content deltas
if delta.content:
content_parts.append(delta.content)
@ -360,6 +405,7 @@ class OpenAIClient(GenAIClient):
# Build final message
full_content = "".join(content_parts).strip() or None
full_reasoning = "".join(reasoning_parts).strip() or None
# Convert tool calls to list format
tool_calls_list = None
@ -381,10 +427,14 @@ class OpenAIClient(GenAIClient):
)
finish_reason = "tool_calls"
if usage_stats is not None:
yield ("stats", usage_stats)
yield (
"message",
{
"content": full_content,
"reasoning": full_reasoning,
"tool_calls": tool_calls_list,
"finish_reason": finish_reason,
},
@ -396,6 +446,7 @@ class OpenAIClient(GenAIClient):
"message",
{
"content": None,
"reasoning": None,
"tool_calls": None,
"finish_reason": "error",
},
@ -406,6 +457,7 @@ class OpenAIClient(GenAIClient):
"message",
{
"content": None,
"reasoning": None,
"tool_calls": None,
"finish_reason": "error",
},

739
frigate/genai/prompts.py Normal file
View File

@ -0,0 +1,739 @@
"""Prompt and response-format builders for GenAI features.
Centralizes the per-feature prompt framing and structured-output schema
shaping so provider clients in :mod:`frigate.genai.plugins` only handle
transport.
"""
import datetime
from typing import Any, Dict, List, Optional
from playhouse.shortcuts import model_to_dict
from frigate.config import CameraConfig, FrigateConfig
from frigate.config.classification import ObjectClassificationType
from frigate.config.ui import UnitSystemEnum
from frigate.data_processing.post.types import ReviewMetadata
from frigate.models import Event
def build_review_description_prompt(
review_data: dict[str, Any],
thumbnails: list[bytes],
concerns: list[str],
preferred_language: str | None,
activity_context_prompt: str,
) -> str:
"""Build the prompt for review activity description generation."""
def get_concern_prompt() -> str:
if concerns:
concern_list = "\n - ".join(concerns)
return (
"\n- `other_concerns` (list of strings): Include a list of any of "
"the following concerns that are occurring:\n"
f" - {concern_list}"
)
else:
return ""
def get_language_prompt() -> str:
if preferred_language:
return f"Provide your answer in {preferred_language}"
else:
return ""
def get_objects_list() -> str:
if review_data["unified_objects"]:
return "\n- " + "\n- ".join(review_data["unified_objects"])
else:
return "\n- (No objects detected)"
return f"""
Your task is to analyze a sequence of images taken in chronological order from a security camera.
## Normal Activity Patterns for This Property
{activity_context_prompt}
## Task Instructions
Describe the scene based on observable actions and movements, evaluate the activity against the Activity Indicators above, and assign a potential_threat_level (0, 1, or 2) by applying the threat level indicators consistently.
## Analysis Guidelines
When forming your description:
- **CRITICAL: Only describe objects explicitly listed in "Objects in Scene" below.** Do not infer or mention additional people, vehicles, or objects not present in this list, even if visual patterns suggest them. If only a car is listed, do not describe a person interacting with it unless "person" is also in the objects list.
- **Only describe actions actually visible in the frames.** Do not assume or infer actions that you don't observe happening. If someone walks toward furniture but you never see them sit, do not say they sat. Stick to what you can see across the sequence.
- Describe what you observe: actions, movements, interactions with objects and the environment. Include any observable environmental changes (e.g., lighting changes triggered by activity).
- Note visible details such as clothing, items being carried or placed, tools or equipment present, and how they interact with the property or objects.
- Consider the full sequence chronologically: what happens from start to finish, how duration and actions relate to the location and objects involved.
- **Use the actual timestamp provided in "Activity started at"** below for time of day contextdo not infer time from image brightness or darkness. Unusual hours (late night/early morning) should increase suspicion when the observable behavior itself appears questionable. However, recognize that some legitimate activities can occur at any hour.
- **Consider duration as a primary factor**: Apply the duration thresholds defined in the activity patterns above. Brief sequences during normal hours with apparent purpose typically indicate normal activity unless explicit suspicious actions are visible.
- **Weigh all evidence holistically**: Match the activity against the normal and suspicious patterns defined above, then evaluate based on the complete context (zone, objects, time, actions, duration). Apply the threat level indicators consistently. Use your judgment for edge cases.
## Response Field Guidelines
Respond with a JSON object matching the provided schema. Field-specific guidance:
- `observations`: Include the very start of the activity for example, a vehicle entering the frame or pulling into the driveway even if it lasts only a few frames and the rest of the clip is dominated by a longer activity. Include each arrival, departure, object handled, and notable change in position or state. Each item is a single concrete fact written as a complete sentence.
- `scene`: Describe how the sequence begins, then the progression of events all significant movements and actions in order. For example, if a vehicle arrives and then a person exits, describe both sequentially. For named subjects (those with a `` separator in "Objects in Scene"), always use their name do not replace them with generic terms. For unnamed objects (e.g., "person", "car"), refer to them naturally with articles (e.g., "a person", "the car"). Your description should align with and support the threat level you assign.
- `title`: Name the primary activity across the observations, together with the location. An activity is what is being done with objects, tools, or surfaces; locomotion through the scene qualifies as the activity only when no other interaction is observed. For named subjects, always use their name. For unnamed objects, refer to them naturally with articles.
- `shortSummary`: Briefly summarize the primary activity across the observations.
- `potential_threat_level`: Must be consistent with your scene description and the activity patterns above.
{get_concern_prompt()}
## Sequence Details
- Camera: {review_data["camera"]}
- Total frames: {len(thumbnails)} (Frame 1 = earliest, Frame {len(thumbnails)} = latest)
- Activity started at {review_data["start"]} and lasted {review_data["duration"]} seconds
- Zones involved: {", ".join(review_data["zones"]) if review_data["zones"] else "None"}
## Objects in Scene
Each line represents a detection state, not necessarily unique individuals. The `` symbol separates a recognized subject's name from their object type — use only the name (before the `←`) in your response, not the type after it. The same subject may appear across multiple lines if detected multiple times.
**Note: Unidentified objects (without names) are NOT indicators of suspicious activitythey simply mean the system hasn't identified that object.**
{get_objects_list()}
{get_language_prompt()}
"""
def build_review_description_response_format(concerns: list[str]) -> dict[str, Any]:
"""Build the structured-output JSON schema for review descriptions.
Strips the `time` field (populated server-side) and drops
`other_concerns` when no concerns are configured.
"""
schema = ReviewMetadata.model_json_schema()
schema.get("properties", {}).pop("time", None)
if "time" in schema.get("required", []):
schema["required"].remove("time")
if not concerns:
schema.get("properties", {}).pop("other_concerns", None)
if "other_concerns" in schema.get("required", []):
schema["required"].remove("other_concerns")
return {
"type": "json_schema",
"json_schema": {
"name": "review_metadata",
"strict": True,
"schema": schema,
},
}
def build_review_summary_prompt(
start_ts: float,
end_ts: float,
events: list[dict[str, Any]],
preferred_language: str | None,
) -> str:
"""Build the prompt for a multi-event review summary."""
time_range = (
f"{datetime.datetime.fromtimestamp(start_ts).strftime('%B %d, %Y at %I:%M %p')}"
f" to "
f"{datetime.datetime.fromtimestamp(end_ts).strftime('%B %d, %Y at %I:%M %p')}"
)
prompt = f"""
You are a security officer writing a concise security report.
Time range: {time_range}
Input format: Each event is a JSON object with:
- "title", "scene", "confidence", "potential_threat_level" (0-2), "other_concerns", "camera", "time", "start_time", "end_time"
- "context": array of related events from other cameras that occurred during overlapping time periods
**Note: Use the "scene" field for event descriptions in the report. Ignore any "shortSummary" field if present.**
Report Structure - Use this EXACT format:
# Security Summary - {time_range}
## Overview
[Write 1-2 sentences summarizing the overall activity pattern during this period.]
---
## Timeline
[Group events by time periods (e.g., "Morning (6:00 AM - 12:00 PM)", "Afternoon (12:00 PM - 5:00 PM)", "Evening (5:00 PM - 9:00 PM)", "Night (9:00 PM - 6:00 AM)"). Use appropriate time blocks based on when events occurred.]
### [Time Block Name]
**HH:MM AM/PM** | [Camera Name] | [Threat Level Indicator]
- [Event title]: [Clear description incorporating contextual information from the "context" array]
- Context: [If context array has items, mention them here, e.g., "Delivery truck present on Front Driveway Cam (HH:MM AM/PM)"]
- Assessment: [Brief assessment incorporating context - if context explains the event, note it here]
[Repeat for each event in chronological order within the time block]
---
## Summary
[One sentence summarizing the period. If all events are normal/explained: "Routine activity observed." If review needed: "Some activity requires review but no security concerns." If security concerns: "Security concerns requiring immediate attention."]
Guidelines:
- List ALL events in chronological order, grouped by time blocks
- Threat level indicators: Normal, Needs review, 🔴 Security concern
- Integrate contextual information naturally - use the "context" array to enrich each event's description
- If context explains the event (e.g., delivery truck explains person at door), describe it accordingly (e.g., "delivery person" not "unidentified person")
- Be concise but informative - focus on what happened and what it means
- If contextual information makes an event clearly normal, reflect that in your assessment
- Only create time blocks that have events - don't create empty sections
"""
prompt += "\n\nEvents:\n"
for event in events:
prompt += f"\n{event}\n"
if preferred_language:
prompt += f"\nProvide your answer in {preferred_language}"
return prompt
def build_object_description_prompt(
camera_config: CameraConfig,
event: Event,
) -> str:
"""Build the prompt for a per-object description.
Pulls the per-label override from `objects.genai.object_prompts`, falling
back to the camera default, and interpolates event fields.
Raises:
KeyError: if the user-defined prompt template references an unknown
event field.
"""
template = camera_config.objects.genai.object_prompts.get(
str(event.label),
camera_config.objects.genai.prompt,
)
return template.format(**model_to_dict(event))
def get_attribute_classifications(config: FrigateConfig) -> List[Dict[str, Any]]:
"""Return enabled custom classification models of `attribute` type.
Each entry: {"name": <model name>, "objects": [<object label>, ...]}.
These models attach attribute metadata to events on the listed object
types, which can later be filtered via the search_objects `attribute`
field.
"""
result: List[Dict[str, Any]] = []
for model_key, model_config in config.classification.custom.items():
if not model_config.enabled or model_config.object_config is None:
continue
if (
model_config.object_config.classification_type
!= ObjectClassificationType.attribute
):
continue
result.append(
{
"name": model_config.name or model_key,
"objects": list(model_config.object_config.objects or []),
}
)
return result
def get_tool_definitions(
semantic_search_enabled: bool = False,
attribute_classifications: Optional[List[Dict[str, Any]]] = None,
) -> List[Dict[str, Any]]:
"""
Get OpenAI-compatible tool definitions for Frigate.
Returns a list of tool definitions that can be used with OpenAI-compatible
function calling APIs. When semantic search is enabled, the search_objects
tool exposes an additional `semantic_query` parameter for descriptive
queries (e.g. "person riding a lawn mower") and find_similar_objects is
included. When attribute classification models are configured, an
`attribute` parameter is exposed for filtering by their labels.
"""
search_objects_properties: Dict[str, Any] = {
"camera": {
"type": "string",
"description": "Camera name to filter by (optional).",
},
"label": {
"type": "string",
"description": (
"Generic object class to filter by — one of the tracked detector "
"labels such as 'person', 'package', 'car', 'dog', 'bird'. Use "
"this for broad queries like 'show me all cars today'. Combine "
"with semantic_query when the user also describes appearance or "
"behavior (e.g. label='person', semantic_query='riding a lawn "
"mower')."
),
},
"sub_label": {
"type": "string",
"description": (
"Filter by a DISCRETE NAMED entity recognized in the detection. "
"Use this for: a known person's name ('John'), a delivery "
"company ('Amazon', 'UPS'), a recognized animal species or "
"breed ('blue jay', 'cardinal', 'golden retriever'), or a "
"license plate string. When filtering by a specific name, set "
"only sub_label and leave label unset. Do NOT use sub_label "
"for descriptions of appearance, clothing, or actions — those "
"belong in semantic_query."
),
},
"after": {
"type": "string",
"description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').",
},
"before": {
"type": "string",
"description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').",
},
"zones": {
"type": "array",
"items": {"type": "string"},
"description": "List of zone names to filter by.",
},
"limit": {
"type": "integer",
"description": "Maximum number of objects to return (default: 25).",
"default": 25,
},
}
if attribute_classifications:
model_outline = "; ".join(
f"{m['name']} (applies to {', '.join(m['objects']) or 'any object'})"
for m in attribute_classifications
)
search_objects_properties["attribute"] = {
"type": "string",
"description": (
"Filter by a classification attribute label produced by a "
"configured attribute classification model. Use this INSTEAD "
"of semantic_query when the user's request matches one of "
"these classifications. Configured models: "
f"{model_outline}. "
"Set the value to the attribute label that matches the user's "
"phrasing (case-sensitive)."
),
}
if semantic_search_enabled:
search_objects_properties["semantic_query"] = {
"type": "string",
"description": (
"Optional natural-language description of a PHYSICAL "
"CHARACTERISTIC, APPEARANCE, or ACTIVITY the user mentioned, "
"used to semantically narrow results. Only set this when the "
"user describes something beyond what label and sub_label can "
"express on their own.\n"
"USE for descriptive phrases like: 'riding a lawn mower', "
"'wearing a red jacket', 'carrying a package', 'walking a "
"dog', 'on a bicycle', 'holding an umbrella'.\n"
"DO NOT USE for:\n"
"- specific named people, pets, or delivery companies → use sub_label\n"
"- animal species or breed names like 'blue jay', 'cardinal', "
"'golden retriever' → use sub_label\n"
"- license plate strings → use sub_label\n"
"- generic object queries like 'all cars today' or 'every "
"person' → use label alone with no semantic_query\n"
"When set, combine with label/time/camera/zone filters as "
"usual (e.g. label='person', semantic_query='riding a lawn "
"mower', after='2024-05-01T00:00:00Z')."
),
}
search_objects_description = (
"Search the historical record of detected objects in Frigate. "
"Use this ONLY for questions about the PAST — e.g. 'did anyone come by today?', "
"'when was the last car?', 'show me detections from yesterday'. "
"Do NOT use this for monitoring or alerting requests about future events — "
"use start_camera_watch instead for those. "
"An 'object' in Frigate represents a tracked detection (e.g., a person, package, car).\n\n"
"Choose filters based on what the user is asking for:\n"
"- Generic class query ('show me all cars today'): set `label` only.\n"
"- Specific NAMED entity (known person, delivery company, animal "
"species/breed like 'blue jay' or 'golden retriever', license "
"plate): set `sub_label` only and leave `label` unset.\n"
)
if semantic_search_enabled:
search_objects_description += (
"- Physical CHARACTERISTIC, APPEARANCE, or ACTIVITY that is not a "
"discrete name ('person riding a lawn mower', 'someone in a red "
"jacket', 'person carrying a package'): set `semantic_query` with "
"the descriptive phrase, optionally alongside `label` for the "
"object class. Do NOT put descriptive phrases in sub_label."
)
return [
{
"type": "function",
"function": {
"name": "search_objects",
"description": search_objects_description,
"parameters": {
"type": "object",
"properties": search_objects_properties,
},
"required": [],
},
},
{
"type": "function",
"function": {
"name": "find_similar_objects",
"description": (
"Find tracked objects that are visually and semantically similar "
"to a specific past event. Use this when the user references a "
"particular object they have seen and wants to find other "
"sightings of the same or similar one ('that green car', 'the "
"person in the red jacket', 'the package that was delivered'). "
"Prefer this over search_objects whenever the user's intent is "
"'find more like this specific one.' Use search_objects first "
"only if you need to locate the anchor event. Requires semantic "
"search to be enabled."
),
"parameters": {
"type": "object",
"properties": {
"event_id": {
"type": "string",
"description": "The id of the anchor event to find similar objects to.",
},
"after": {
"type": "string",
"description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').",
},
"before": {
"type": "string",
"description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').",
},
"cameras": {
"type": "array",
"items": {"type": "string"},
"description": "Optional list of cameras to restrict to. Defaults to all.",
},
"labels": {
"type": "array",
"items": {"type": "string"},
"description": "Optional list of labels to restrict to. Defaults to the anchor event's label.",
},
"sub_labels": {
"type": "array",
"items": {"type": "string"},
"description": "Optional list of sub_labels (names) to restrict to.",
},
"zones": {
"type": "array",
"items": {"type": "string"},
"description": "Optional list of zones. An event matches if any of its zones overlap.",
},
"similarity_mode": {
"type": "string",
"enum": ["visual", "semantic", "fused"],
"description": "Which similarity signal(s) to use. 'fused' (default) combines visual and semantic.",
"default": "fused",
},
"min_score": {
"type": "number",
"description": "Drop matches with a similarity score below this threshold (0.0-1.0).",
},
"limit": {
"type": "integer",
"description": "Maximum number of matches to return (default: 10).",
"default": 10,
},
},
"required": ["event_id"],
},
},
},
{
"type": "function",
"function": {
"name": "set_camera_state",
"description": (
"Change a camera's feature state (e.g., turn detection on/off, enable/disable recordings). "
"Use camera='*' to apply to all cameras at once. "
"Only call this tool when the user explicitly asks to change a camera setting. "
"Requires admin privileges."
),
"parameters": {
"type": "object",
"properties": {
"camera": {
"type": "string",
"description": "Camera name to target, or '*' to target all cameras.",
},
"feature": {
"type": "string",
"enum": [
"detect",
"record",
"snapshots",
"audio",
"motion",
"enabled",
"birdseye",
"birdseye_mode",
"improve_contrast",
"ptz_autotracker",
"motion_contour_area",
"motion_threshold",
"notifications",
"audio_transcription",
"review_alerts",
"review_detections",
"object_descriptions",
"review_descriptions",
"profile",
],
"description": (
"The feature to change. Most features accept ON or OFF. "
"birdseye_mode accepts CONTINUOUS, MOTION, or OBJECTS. "
"motion_contour_area and motion_threshold accept a number. "
"profile accepts a profile name or 'none' to deactivate (requires camera='*')."
),
},
"value": {
"type": "string",
"description": "The value to set. ON or OFF for toggles, a number for thresholds, a profile name or 'none' for profile.",
},
},
"required": ["camera", "feature", "value"],
},
},
},
{
"type": "function",
"function": {
"name": "get_live_context",
"description": (
"Get the current live image and detection information for a camera: objects being tracked, "
"zones, timestamps. Use this to understand what is visible in the live view. "
"Call this when answering questions about what is happening right now on a specific camera."
),
"parameters": {
"type": "object",
"properties": {
"camera": {
"type": "string",
"description": "Camera name to get live context for.",
},
},
"required": ["camera"],
},
},
},
{
"type": "function",
"function": {
"name": "start_camera_watch",
"description": (
"Start a continuous VLM watch job that monitors a camera and sends a notification "
"when a specified condition is met. Use this when the user wants to be alerted about "
"a future event, e.g. 'tell me when guests arrive' or 'notify me when the package is picked up'. "
"Only one watch job can run at a time. Returns a job ID."
),
"parameters": {
"type": "object",
"properties": {
"camera": {
"type": "string",
"description": "Camera ID to monitor.",
},
"condition": {
"type": "string",
"description": (
"Natural-language description of the condition to watch for, "
"e.g. 'a person arrives at the front door'."
),
},
"max_duration_minutes": {
"type": "integer",
"description": "Maximum time to watch before giving up (minutes, default 60).",
"default": 60,
},
"labels": {
"type": "array",
"items": {"type": "string"},
"description": "Object labels that should trigger a VLM check (e.g. ['person', 'car']). If omitted, any detection on the camera triggers a check.",
},
"zones": {
"type": "array",
"items": {"type": "string"},
"description": "Zone names to filter by. If specified, only detections in these zones trigger a VLM check.",
},
},
"required": ["camera", "condition"],
},
},
},
{
"type": "function",
"function": {
"name": "stop_camera_watch",
"description": (
"Cancel the currently running VLM watch job. Use this when the user wants to "
"stop a previously started watch, e.g. 'stop watching the front door'."
),
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "get_profile_status",
"description": (
"Get the current profile status including the active profile and "
"timestamps of when each profile was last activated. Use this to "
"determine time periods for recap requests — e.g. when the user asks "
"'what happened while I was away?', call this first to find the relevant "
"time window based on profile activation history."
),
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "get_recap",
"description": (
"Get a recap of all activity (alerts and detections) for a given time period. "
"Use this after calling get_profile_status to retrieve what happened during "
"a specific window — e.g. 'what happened while I was away?'. Returns a "
"chronological list of activity with camera, objects, zones, and GenAI-generated "
"descriptions when available. Summarize the results for the user."
),
"parameters": {
"type": "object",
"properties": {
"after": {
"type": "string",
"description": "Start of the time period in ISO 8601 format (e.g. '2025-03-15T08:00:00').",
},
"before": {
"type": "string",
"description": "End of the time period in ISO 8601 format (e.g. '2025-03-15T17:00:00').",
},
"cameras": {
"type": "string",
"description": "Comma-separated camera IDs to include, or 'all' for all cameras. Default is 'all'.",
},
"severity": {
"type": "string",
"enum": ["alert", "detection"],
"description": "Filter by severity level. Omit to include both alerts and detections.",
},
},
"required": ["after", "before"],
},
},
},
]
def build_chat_system_prompt(
config: FrigateConfig,
allowed_cameras: List[str],
semantic_search_enabled: bool,
attribute_classifications: List[Dict[str, Any]],
) -> str:
"""Build the system prompt for the chat completion endpoint.
Composes the static framing with conditional sections describing the
available cameras, speed units, semantic-search routing guidance, and
configured attribute classifications.
"""
current_datetime = datetime.datetime.now()
current_date_str = current_datetime.strftime("%Y-%m-%d")
current_time_str = current_datetime.strftime("%I:%M:%S %p")
cameras_info: List[str] = []
has_speed_zone = False
for camera_id in allowed_cameras:
if camera_id not in config.cameras:
continue
camera_config = config.cameras[camera_id]
friendly_name = (
camera_config.friendly_name
if camera_config.friendly_name
else camera_id.replace("_", " ").title()
)
zone_names = list(camera_config.zones.keys())
if not has_speed_zone:
has_speed_zone = any(
zone.distances for zone in camera_config.zones.values()
)
if zone_names:
cameras_info.append(
f" - {friendly_name} (ID: {camera_id}, zones: {', '.join(zone_names)})"
)
else:
cameras_info.append(f" - {friendly_name} (ID: {camera_id})")
cameras_section = ""
if cameras_info:
cameras_section = (
"\n\nAvailable cameras:\n"
+ "\n".join(cameras_info)
+ "\n\nWhen users refer to cameras by their friendly name (e.g., 'Back Deck Camera'), use the corresponding camera ID (e.g., 'back_deck_cam') in tool calls."
)
speed_units_section = ""
if has_speed_zone:
speed_unit = (
"mph" if config.ui.unit_system == UnitSystemEnum.imperial else "km/h"
)
speed_units_section = f"\n\nReport object speeds to the user in {speed_unit}."
semantic_search_section = ""
if semantic_search_enabled:
semantic_search_section = (
"\n\nWhen routing a search_objects call, pick filters by the shape of the user's request:\n"
"- Generic class ('show me all cars today'): set `label` only.\n"
"- Specific named entity — a known person ('John'), delivery company ('Amazon'), animal species/breed ('blue jay', 'cardinal', 'golden retriever'), or license plate: set `sub_label` only and leave `label` unset.\n"
"- Physical characteristic, appearance, or activity that is NOT a discrete name ('find me people riding a lawn mower', 'someone in a red jacket', 'a person carrying a package'): set `semantic_query` with the descriptive phrase, optionally combined with `label` for the object class. Never put descriptive phrases in `sub_label`."
)
attribute_classification_section = ""
if attribute_classifications:
model_lines = "\n".join(
f"- {m['name']}: applies to {', '.join(m['objects']) or 'any object'}"
for m in attribute_classifications
)
attribute_classification_section = (
"\n\nAttribute classification models are configured for the following object types:\n"
f"{model_lines}\n"
"When the user's request matches one of these classifications, set the search_objects `attribute` field to the matching label rather than using `semantic_query`. Reserve `semantic_query` for descriptive phrases that fall outside the configured attribute labels."
)
return f"""You are a helpful assistant for Frigate, a security camera NVR system. You help users answer questions about their cameras, detected objects, and events.
Current server local date and time: {current_date_str} at {current_time_str}
Do not start your response with phrases like "I will check...", "Let me see...", or "Let me look...". Answer directly.
Always present times to the user in the server's local timezone. When tool results include start_time_local and end_time_local, use those exact strings when listing or describing detection times—do not convert or invent timestamps. Do not use UTC or ISO format with Z for the user-facing answer unless the tool result only provides Unix timestamps without local time fields.
When users ask about "today", "yesterday", "this week", etc., use the current date above as reference.
When searching for objects or events, use ISO 8601 format for dates (e.g., {current_date_str}T00:00:00Z for the start of today).
Always be accurate with time calculations based on the current date provided.
When a user refers to a specific object they have seen or describe with identifying details ("that green car", "the person in the red jacket", "a package left today"), prefer the find_similar_objects tool over search_objects. Use search_objects first only to locate the anchor event, then pass its id to find_similar_objects. For generic queries like "show me all cars today", keep using search_objects. If a user message begins with [attached_event:<id>], treat that event id as the anchor for any similarity or "tell me more" request in the same message and call find_similar_objects with that id.{semantic_search_section}{attribute_classification_section}{cameras_section}{speed_units_section}"""

View File

@ -12,6 +12,7 @@ import os
import subprocess as sp
import threading
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Optional, cast
@ -23,7 +24,7 @@ from frigate.const import REPLAY_CAMERA_PREFIX, REPLAY_DIR
from frigate.jobs.export import JobStatePublisher
from frigate.jobs.job import Job
from frigate.jobs.manager import job_is_running, set_current_job
from frigate.models import Recordings
from frigate.models import Export, Recordings
from frigate.types import JobStatusTypesEnum
from frigate.util.ffmpeg import run_ffmpeg_with_progress
@ -114,6 +115,125 @@ def query_recordings(source_camera: str, start_ts: float, end_ts: float) -> Mode
return cast(ModelSelect, query)
class DebugReplaySource(ABC):
"""Abstract source for a debug replay session.
Provides the camera identity and time range the replay represents,
validates that usable content exists, and supplies the ffmpeg input
args used to build the replay clip.
"""
@property
@abstractmethod
def source_camera(self) -> str:
"""Camera name the replay is derived from."""
@property
@abstractmethod
def start_ts(self) -> float:
"""Unix timestamp marking the start of the replay range."""
@property
@abstractmethod
def end_ts(self) -> float:
"""Unix timestamp marking the end of the replay range."""
@abstractmethod
def validate(self) -> None:
"""Raise ValueError if the source has no usable content."""
@abstractmethod
def ffmpeg_input_args(self, working_dir: str) -> list[str]:
"""Return ffmpeg input args (including -i). May write temp files in working_dir."""
def cleanup(self, working_dir: str) -> None:
"""Remove any temp files the source created in working_dir. Default no-op."""
class RecordingDebugReplaySource(DebugReplaySource):
"""Replay source backed by the Recordings table.
Builds a concat playlist of recording files covering the time range
and feeds it to ffmpeg's concat demuxer.
"""
def __init__(self, source_camera: str, start_ts: float, end_ts: float) -> None:
self._camera = source_camera
self._start_ts = start_ts
self._end_ts = end_ts
self._concat_file: Optional[str] = None
@property
def source_camera(self) -> str:
return self._camera
@property
def start_ts(self) -> float:
return self._start_ts
@property
def end_ts(self) -> float:
return self._end_ts
def validate(self) -> None:
if self._end_ts <= self._start_ts:
raise ValueError("End time must be after start time")
if not query_recordings(self._camera, self._start_ts, self._end_ts).count():
raise ValueError(
f"No recordings found for camera '{self._camera}' in the specified time range"
)
def ffmpeg_input_args(self, working_dir: str) -> list[str]:
replay_name = f"{REPLAY_CAMERA_PREFIX}{self._camera}"
concat_file = os.path.join(working_dir, f"{replay_name}_concat.txt")
recordings = query_recordings(self._camera, self._start_ts, self._end_ts)
with open(concat_file, "w") as f:
for recording in recordings:
f.write(f"file '{recording.path}'\n")
self._concat_file = concat_file
return ["-f", "concat", "-safe", "0", "-i", concat_file]
def cleanup(self, working_dir: str) -> None:
if self._concat_file:
_remove_silent(self._concat_file)
class ExportDebugReplaySource(DebugReplaySource):
"""Replay source backed by an existing Export.
Uses the export's video file directly as the ffmpeg input — does not
require recordings to still exist for the time range.
"""
def __init__(self, export: Export, duration: float) -> None:
self._camera = cast(str, export.camera)
# Export.date is declared DateTimeField but Frigate writes raw unix
# timestamps to the column.
self._start_ts = float(cast(Any, export.date))
self._video_path = cast(str, export.video_path)
self._duration = duration
@property
def source_camera(self) -> str:
return self._camera
@property
def start_ts(self) -> float:
return self._start_ts
@property
def end_ts(self) -> float:
return self._start_ts + self._duration
def validate(self) -> None:
if not os.path.exists(self._video_path):
raise ValueError(f"Export video file not found: {self._video_path}")
def ffmpeg_input_args(self, working_dir: str) -> list[str]:
return ["-i", self._video_path]
class DebugReplayJobRunner(threading.Thread):
"""Worker thread that drives the startup job to completion.
@ -126,6 +246,7 @@ class DebugReplayJobRunner(threading.Thread):
def __init__(
self,
job: DebugReplayJob,
source: DebugReplaySource,
frigate_config: FrigateConfig,
config_publisher: CameraConfigUpdatePublisher,
replay_manager: "DebugReplayManager",
@ -133,6 +254,7 @@ class DebugReplayJobRunner(threading.Thread):
) -> None:
super().__init__(daemon=True, name=f"debug_replay_{job.id}")
self.job = job
self.source = source
self.frigate_config = frigate_config
self.config_publisher = config_publisher
self.replay_manager = replay_manager
@ -183,7 +305,6 @@ class DebugReplayJobRunner(threading.Thread):
def run(self) -> None:
replay_name = self.job.replay_camera_name
os.makedirs(REPLAY_DIR, exist_ok=True)
concat_file = os.path.join(REPLAY_DIR, f"{replay_name}_concat.txt")
clip_path = os.path.join(REPLAY_DIR, f"{replay_name}.mp4")
self.job.status = JobStatusTypesEnum.running
@ -192,23 +313,13 @@ class DebugReplayJobRunner(threading.Thread):
self._broadcast(force=True)
try:
recordings = query_recordings(
self.job.source_camera, self.job.start_ts, self.job.end_ts
)
with open(concat_file, "w") as f:
for recording in recordings:
f.write(f"file '{recording.path}'\n")
input_args = self.source.ffmpeg_input_args(REPLAY_DIR)
ffmpeg_cmd = [
self.frigate_config.ffmpeg.ffmpeg_path,
"-hide_banner",
"-y",
"-f",
"concat",
"-safe",
"0",
"-i",
concat_file,
*input_args,
"-c",
"copy",
"-movflags",
@ -285,7 +396,7 @@ class DebugReplayJobRunner(threading.Thread):
self.replay_manager.clear_session()
_remove_silent(clip_path)
finally:
_remove_silent(concat_file)
self.source.cleanup(REPLAY_DIR)
_set_active_runner(None)
def _finalize_cancelled(self, clip_path: str) -> None:
@ -309,52 +420,43 @@ def _remove_silent(path: str) -> None:
def start_debug_replay_job(
*,
source_camera: str,
start_ts: float,
end_ts: float,
source: DebugReplaySource,
frigate_config: FrigateConfig,
config_publisher: CameraConfigUpdatePublisher,
replay_manager: "DebugReplayManager",
) -> str:
"""Validate, create job, start runner. Returns the job id.
Raises ValueError for bad params (camera missing, time range
invalid, no recordings) and RuntimeError if a session is already
active.
Raises ValueError for an invalid source (camera missing, source has
no usable content) and RuntimeError if a session is already active.
"""
if job_is_running(JOB_TYPE) or replay_manager.active:
raise RuntimeError("A replay session is already active")
if source_camera not in frigate_config.cameras:
raise ValueError(f"Camera '{source_camera}' not found")
if source.source_camera not in frigate_config.cameras:
raise ValueError(f"Camera '{source.source_camera}' not found")
if end_ts <= start_ts:
raise ValueError("End time must be after start time")
source.validate()
recordings = query_recordings(source_camera, start_ts, end_ts)
if not recordings.count():
raise ValueError(
f"No recordings found for camera '{source_camera}' in the specified time range"
)
replay_name = f"{REPLAY_CAMERA_PREFIX}{source_camera}"
replay_name = f"{REPLAY_CAMERA_PREFIX}{source.source_camera}"
replay_manager.mark_starting(
source_camera=source_camera,
source_camera=source.source_camera,
replay_camera_name=replay_name,
start_ts=start_ts,
end_ts=end_ts,
start_ts=source.start_ts,
end_ts=source.end_ts,
)
job = DebugReplayJob(
source_camera=source_camera,
source_camera=source.source_camera,
replay_camera_name=replay_name,
start_ts=start_ts,
end_ts=end_ts,
start_ts=source.start_ts,
end_ts=source.end_ts,
)
set_current_job(job)
runner = DebugReplayJobRunner(
job=job,
source=source,
frigate_config=frigate_config,
config_publisher=config_publisher,
replay_manager=replay_manager,

View File

@ -45,6 +45,7 @@ class VLMWatchJob(Job):
last_reasoning: str = ""
notification_message: str = ""
iteration_count: int = 0
username: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@ -374,6 +375,7 @@ def start_vlm_watch_job(
dispatcher: Any,
labels: list[str] | None = None,
zones: list[str] | None = None,
username: str = "",
) -> str:
"""Start a new VLM watch job. Returns the job ID.
@ -397,6 +399,7 @@ def start_vlm_watch_job(
max_duration_minutes=max_duration_minutes,
labels=labels or [],
zones=zones or [],
username=username,
)
cancel_ev = threading.Event()
_current_job = job

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,109 @@
"""Resolve human-readable names for Intel GPUs via OpenVINO."""
import logging
import re
from typing import Optional
logger = logging.getLogger(__name__)
class IntelGpuNameResolver:
"""Build a pdev -> normalized device name map by enumerating OpenVINO GPUs.
The lookup is performed once on first access and cached for the process
lifetime. OpenVINO exposes DEVICE_PCI_INFO (domain/bus/device/function) and
FULL_DEVICE_NAME for each GPU it can see, which is enough to associate the
name with the pdev string used by DRM fdinfo.
"""
_names: Optional[dict[str, str]] = None
def get_names(self) -> dict[str, str]:
if self._names is not None:
return self._names
names: dict[str, str] = {}
try:
from openvino import Core
except ImportError:
logger.debug("OpenVINO unavailable; cannot resolve Intel GPU names")
self._names = names
return names
try:
core = Core()
devices = core.available_devices
except Exception as exc:
logger.debug(f"OpenVINO Core initialization failed: {exc}")
self._names = names
return names
cpu_name: Optional[str] = None
if "CPU" in devices:
try:
cpu_name = self._strip_trademarks(
core.get_property("CPU", "FULL_DEVICE_NAME")
)
except Exception as exc:
logger.debug(f"Failed to read CPU FULL_DEVICE_NAME: {exc}")
for device in devices:
if not device.startswith("GPU"):
continue
try:
pci = core.get_property(device, "DEVICE_PCI_INFO")
raw_name = core.get_property(device, "FULL_DEVICE_NAME")
device_type = core.get_property(device, "DEVICE_TYPE")
except Exception as exc:
logger.debug(f"Failed to read properties for {device}: {exc}")
continue
pdev = self._format_pdev(pci)
if not pdev:
continue
names[pdev] = self._resolve_name(raw_name, device_type, cpu_name)
self._names = names
return names
@staticmethod
def _format_pdev(pci) -> Optional[str]:
try:
return f"{pci.domain:04x}:{pci.bus:02x}:{pci.device:02x}.{pci.function:x}"
except AttributeError:
return None
@classmethod
def _resolve_name(cls, raw_name: str, device_type, cpu_name: Optional[str]) -> str:
"""Build a display name for a GPU.
Modern integrated Intel GPUs are reported by OpenVINO with a generic
FULL_DEVICE_NAME like "Intel(R) Graphics (iGPU)" that gives no model
information. Since the iGPU is part of the CPU on these platforms, fall
back to the CPU name (which OpenVINO does report specifically) and
suffix it with "iGPU" so it's clear what the entry is.
"""
is_integrated = "INTEGRATED" in str(device_type).upper()
if is_integrated and cpu_name:
short_cpu = re.sub(r"^Intel\s+", "", cpu_name)
return f"{short_cpu} iGPU"
return cls._normalize_name(raw_name)
@classmethod
def _normalize_name(cls, name: str) -> str:
cleaned = cls._strip_trademarks(name)
cleaned = re.sub(r"\s*\((?:i|d)GPU\)\s*$", "", cleaned, flags=re.IGNORECASE)
return " ".join(cleaned.split())
@staticmethod
def _strip_trademarks(name: str) -> str:
cleaned = re.sub(r"\(R\)|\(TM\)", "", name)
return " ".join(cleaned.split())
intel_gpu_name_resolver = IntelGpuNameResolver()

View File

@ -230,6 +230,7 @@ async def set_gpu_stats(
hwaccel_args.append(args)
stats: dict[str, dict] = {}
intel_gpu_collected = False
for args in hwaccel_args:
if args in hwaccel_errors:
@ -242,6 +243,7 @@ async def set_gpu_stats(
if nvidia_usage:
for i in range(len(nvidia_usage)):
stats[nvidia_usage[i]["name"]] = {
"vendor": "nvidia",
"gpu": str(round(float(nvidia_usage[i]["gpu"]), 2)) + "%",
"mem": str(round(float(nvidia_usage[i]["mem"]), 2)) + "%",
"enc": str(round(float(nvidia_usage[i]["enc"]), 2)) + "%",
@ -250,31 +252,34 @@ async def set_gpu_stats(
}
else:
stats["nvidia-gpu"] = {"gpu": "", "mem": ""}
stats["nvidia-gpu"] = {"vendor": "nvidia", "gpu": "", "mem": ""}
hwaccel_errors.append(args)
elif "nvmpi" in args or "jetson" in args:
# nvidia Jetson
jetson_usage = get_jetson_stats()
if jetson_usage:
stats["jetson-gpu"] = jetson_usage
stats["jetson-gpu"] = {"vendor": "nvidia", **jetson_usage}
else:
stats["jetson-gpu"] = {"gpu": "", "mem": ""}
stats["jetson-gpu"] = {"vendor": "nvidia", "gpu": "", "mem": ""}
hwaccel_errors.append(args)
elif "qsv" in args or ("vaapi" in args and not is_vaapi_amd_driver()):
if not config.telemetry.stats.intel_gpu_stats:
continue
if "intel-gpu" not in stats:
if not intel_gpu_collected:
# intel GPU (QSV or VAAPI both use the same physical GPU)
intel_gpu_collected = True
intel_usage = get_intel_gpu_stats(
config.telemetry.stats.intel_gpu_device
)
if intel_usage is not None:
stats["intel-gpu"] = intel_usage or {"gpu": "", "mem": ""}
if intel_usage:
for entry in intel_usage.values():
name = entry.pop("name")
stats[name] = entry
else:
stats["intel-gpu"] = {"gpu": "", "mem": ""}
stats["intel-gpu"] = {"vendor": "intel", "gpu": "", "mem": ""}
hwaccel_errors.append(args)
elif "vaapi" in args:
if not config.telemetry.stats.amd_gpu_stats:
@ -284,18 +289,18 @@ async def set_gpu_stats(
amd_usage = get_amd_gpu_stats()
if amd_usage:
stats["amd-vaapi"] = amd_usage
stats["amd-vaapi"] = {"vendor": "amd", **amd_usage}
else:
stats["amd-vaapi"] = {"gpu": "", "mem": ""}
stats["amd-vaapi"] = {"vendor": "amd", "gpu": "", "mem": ""}
hwaccel_errors.append(args)
elif "preset-rk" in args:
rga_usage = get_rockchip_gpu_stats()
if rga_usage:
stats["rockchip"] = rga_usage
stats["rockchip"] = {"vendor": "rockchip", **rga_usage}
elif "v4l2m2m" in args or "rpi" in args:
# RPi v4l2m2m is currently not able to get usage stats
stats["rpi-v4l2m2m"] = {"gpu": "", "mem": ""}
stats["rpi-v4l2m2m"] = {"vendor": "rpi", "gpu": "", "mem": ""}
if stats:
all_stats["gpu_usages"] = stats

View File

@ -15,11 +15,12 @@ class TestDebugReplayAPI(BaseTestHttp):
# Stub the factory to skip validation/threading and just record the
# name on the manager the way the real factory's mark_starting would.
def fake_start(**kwargs):
source = kwargs["source"]
kwargs["replay_manager"].mark_starting(
source_camera=kwargs["source_camera"],
source_camera=source.source_camera,
replay_camera_name="_replay_front",
start_ts=kwargs["start_ts"],
end_ts=kwargs["end_ts"],
start_ts=source.start_ts,
end_ts=source.end_ts,
)
return "job-1234"

View File

@ -1,3 +1,4 @@
import os
from unittest.mock import patch
from fastapi import HTTPException, Request
@ -357,6 +358,51 @@ class TestGo2rtcStreamAccess(BaseTestHttp):
f"got {resp.status_code}"
)
def test_add_stream_rejects_restricted_source(self):
"""PUT /go2rtc/streams must reject exec:/echo:/expr: sources even for
admins"""
app = self._make_app(_MULTI_CAMERA_CONFIG)
with AuthTestClient(app) as client:
for src in (
"exec:/tmp/rev.sh",
"echo:foo",
"expr:bar",
" exec:/tmp/rev.sh",
):
resp = client.put(f"/go2rtc/streams/revshell?src={src}")
assert resp.status_code == 400, (
f"Expected 400 for restricted src {src!r}; got {resp.status_code}"
)
assert resp.json().get("success") is False
def test_add_stream_allows_non_restricted_source(self):
"""A normal stream URL should pass the restricted-source check and reach
the (unavailable in tests) go2rtc proxy so we expect 500, not 400."""
app = self._make_app(_MULTI_CAMERA_CONFIG)
with AuthTestClient(app) as client:
resp = client.put("/go2rtc/streams/legit?src=rtsp://10.0.0.1:554/video")
assert resp.status_code != 400, (
f"Non-restricted source should not be rejected with 400; got {resp.status_code}"
)
def test_add_stream_allows_restricted_source_when_override_set(self):
"""When GO2RTC_ALLOW_ARBITRARY_EXEC is set, the API must defer to operator
intent and forward the request to go2rtc instead of short-circuiting with 400."""
app = self._make_app(_MULTI_CAMERA_CONFIG)
mock_response = type("R", (), {"ok": True, "status_code": 200, "text": "ok"})()
with patch.dict(os.environ, {"GO2RTC_ALLOW_ARBITRARY_EXEC": "true"}):
with patch(
"frigate.api.camera.requests.put", return_value=mock_response
) as mock_put:
with AuthTestClient(app) as client:
resp = client.put("/go2rtc/streams/legit?src=exec:/tmp/something")
assert resp.status_code == 200, (
f"Restricted src should be forwarded when override set; got {resp.status_code}"
)
mock_put.assert_called_once()
forwarded_src = mock_put.call_args.kwargs["params"]["src"]
assert forwarded_src == "exec:/tmp/something"
def test_stream_alias_blocked_when_owning_camera_disallowed(self):
"""limited_user cannot access a stream alias that belongs to a camera they
are not allowed to see."""

View File

@ -10,7 +10,7 @@ from ruamel.yaml.constructor import DuplicateKeyError
from frigate.config import BirdseyeModeEnum, FrigateConfig
from frigate.const import MODEL_CACHE_DIR
from frigate.detectors import DetectorTypeEnum
from frigate.util.builtin import deep_merge, load_labels
from frigate.util.builtin import deep_merge
class TestConfig(unittest.TestCase):
@ -64,9 +64,9 @@ class TestConfig(unittest.TestCase):
def test_config_class(self):
frigate_config = FrigateConfig(**self.minimal)
assert "ov" in frigate_config.detectors.keys()
assert frigate_config.detectors["ov"].type == DetectorTypeEnum.openvino
assert frigate_config.detectors["ov"].model.width == 300
assert "cpu" in frigate_config.detectors.keys()
assert frigate_config.detectors["cpu"].type == DetectorTypeEnum.cpu
assert frigate_config.detectors["cpu"].model.width == 320
@patch("frigate.detectors.detector_config.load_labels")
def test_detector_custom_model_path(self, mock_labels):
@ -309,16 +309,11 @@ class TestConfig(unittest.TestCase):
}
frigate_config = FrigateConfig(**config)
all_audio_labels = {
label
for label in load_labels("/audio-labelmap.txt", prefill=521).values()
if label
assert set(frigate_config.cameras["back"].audio.filters.keys()) == {
"speech",
"yell",
}
assert all_audio_labels.issubset(
set(frigate_config.cameras["back"].audio.filters.keys())
)
def test_override_audio_filters(self):
config = {
"mqtt": {"host": "mqtt"},
@ -345,7 +340,8 @@ class TestConfig(unittest.TestCase):
frigate_config = FrigateConfig(**config)
assert "speech" in frigate_config.cameras["back"].audio.filters
assert frigate_config.cameras["back"].audio.filters["speech"].threshold == 0.9
assert "babbling" in frigate_config.cameras["back"].audio.filters
assert "yell" in frigate_config.cameras["back"].audio.filters
assert "babbling" not in frigate_config.cameras["back"].audio.filters
def test_inherit_object_filters(self):
config = {
@ -1677,5 +1673,60 @@ class TestConfig(unittest.TestCase):
self.assertRaises(ValueError, lambda: FrigateConfig(**config))
class TestAttributeFilterDefaults(unittest.TestCase):
"""Verify attribute filter min_score handling at config load."""
def setUp(self):
self.minimal = {
"mqtt": {"host": "mqtt"},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
}
},
}
def _build_config(self, object_filters: dict | None = None) -> FrigateConfig:
config = deep_merge({}, self.minimal)
if object_filters is not None:
config.setdefault("objects", {})["filters"] = object_filters
return FrigateConfig(**config)
def test_attribute_with_no_filter_gets_default_min_score(self):
"""Attribute with no user-provided filter gets created with min_score=0.7."""
config = self._build_config()
face_filter = config.objects.filters.get("face")
self.assertIsNotNone(face_filter)
self.assertEqual(face_filter.min_score, 0.7)
def test_attribute_filter_without_min_score_gets_bumped(self):
"""If user sets some FilterConfig field but not min_score, min_score is bumped to 0.7."""
config = self._build_config({"face": {"min_area": 500}})
face_filter = config.objects.filters["face"]
self.assertEqual(face_filter.min_area, 500)
self.assertEqual(face_filter.min_score, 0.7)
def test_attribute_filter_explicit_min_score_half_is_preserved(self):
"""User-provided min_score=0.5 must NOT be silently rewritten to 0.7."""
config = self._build_config({"face": {"min_score": 0.5}})
face_filter = config.objects.filters["face"]
self.assertEqual(face_filter.min_score, 0.5)
def test_attribute_filter_explicit_min_score_other_value_is_preserved(self):
"""Sanity: explicit non-0.5 values pass through unchanged."""
config = self._build_config({"face": {"min_score": 0.3}})
face_filter = config.objects.filters["face"]
self.assertEqual(face_filter.min_score, 0.3)
if __name__ == "__main__":
unittest.main(verbosity=2)

View File

@ -71,6 +71,14 @@ class TestDebugReplayManagerSession(unittest.TestCase):
class TestDebugReplayManagerStop(unittest.TestCase):
def setUp(self) -> None:
# stop() publishes a terminal job_state via a real JobStatePublisher,
# which opens a ZMQ REQ socket and blocks on REP. No dispatcher runs
# in unit tests, so substitute a no-op publisher.
patcher = patch("frigate.debug_replay.JobStatePublisher")
patcher.start()
self.addCleanup(patcher.stop)
def test_stop_when_inactive_is_a_noop(self) -> None:
from frigate.debug_replay import DebugReplayManager

View File

@ -9,6 +9,7 @@ from unittest.mock import MagicMock, patch
from frigate.debug_replay import DebugReplayManager
from frigate.jobs.debug_replay import (
DebugReplayJob,
RecordingDebugReplaySource,
cancel_debug_replay_job,
get_active_runner,
start_debug_replay_job,
@ -99,9 +100,9 @@ class TestStartDebugReplayJob(unittest.TestCase):
def test_rejects_unknown_camera(self) -> None:
with self.assertRaises(ValueError):
start_debug_replay_job(
source_camera="missing",
start_ts=100.0,
end_ts=200.0,
source=RecordingDebugReplaySource(
source_camera="missing", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
replay_manager=self.manager,
@ -110,9 +111,9 @@ class TestStartDebugReplayJob(unittest.TestCase):
def test_rejects_invalid_time_range(self) -> None:
with self.assertRaises(ValueError):
start_debug_replay_job(
source_camera="front",
start_ts=200.0,
end_ts=100.0,
source=RecordingDebugReplaySource(
source_camera="front", start_ts=200.0, end_ts=100.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
replay_manager=self.manager,
@ -124,9 +125,9 @@ class TestStartDebugReplayJob(unittest.TestCase):
with patch("frigate.jobs.debug_replay.query_recordings", return_value=empty_qs):
with self.assertRaises(ValueError):
start_debug_replay_job(
source_camera="front",
start_ts=100.0,
end_ts=200.0,
source=RecordingDebugReplaySource(
source_camera="front", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
replay_manager=self.manager,
@ -154,9 +155,9 @@ class TestStartDebugReplayJob(unittest.TestCase):
patch("builtins.open", unittest.mock.mock_open()),
):
job_id = start_debug_replay_job(
source_camera="front",
start_ts=100.0,
end_ts=200.0,
source=RecordingDebugReplaySource(
source_camera="front", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
replay_manager=self.manager,
@ -191,9 +192,9 @@ class TestStartDebugReplayJob(unittest.TestCase):
patch("builtins.open", unittest.mock.mock_open()),
):
start_debug_replay_job(
source_camera="front",
start_ts=100.0,
end_ts=200.0,
source=RecordingDebugReplaySource(
source_camera="front", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
replay_manager=self.manager,
@ -201,9 +202,9 @@ class TestStartDebugReplayJob(unittest.TestCase):
with self.assertRaises(RuntimeError):
start_debug_replay_job(
source_camera="front",
start_ts=100.0,
end_ts=200.0,
source=RecordingDebugReplaySource(
source_camera="front", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
replay_manager=self.manager,
@ -269,9 +270,9 @@ class TestRunnerHappyPath(unittest.TestCase):
patch("builtins.open", unittest.mock.mock_open()),
):
start_debug_replay_job(
source_camera="front",
start_ts=100.0,
end_ts=200.0,
source=RecordingDebugReplaySource(
source_camera="front", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
replay_manager=self.manager,
@ -340,9 +341,9 @@ class TestRunnerFailurePath(unittest.TestCase):
patch("builtins.open", unittest.mock.mock_open()),
):
start_debug_replay_job(
source_camera="front",
start_ts=100.0,
end_ts=200.0,
source=RecordingDebugReplaySource(
source_camera="front", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
replay_manager=self.manager,
@ -418,9 +419,9 @@ class TestRunnerCancellation(unittest.TestCase):
patch("builtins.open", unittest.mock.mock_open()),
):
start_debug_replay_job(
source_camera="front",
start_ts=100.0,
end_ts=200.0,
source=RecordingDebugReplaySource(
source_camera="front", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
replay_manager=self.manager,

View File

@ -17,12 +17,14 @@ class TestGpuStats(unittest.TestCase):
amd_stats = get_amd_gpu_stats()
assert amd_stats == {"gpu": "4.17%", "mem": "60.37%"}
@patch("frigate.stats.intel_gpu_info.intel_gpu_name_resolver.get_names")
@patch("frigate.util.services.time.sleep")
@patch("frigate.util.services.time.monotonic")
@patch("frigate.util.services._read_intel_drm_fdinfo")
def test_intel_gpu_stats_fdinfo(self, read_fdinfo, monotonic, sleep):
def test_intel_gpu_stats_fdinfo(self, read_fdinfo, monotonic, sleep, get_names):
# 1 second of wall clock between snapshots
monotonic.side_effect = [0.0, 1.0]
get_names.return_value = {"0000:00:02.0": "Intel Graphics"}
# Two i915 clients on the same iGPU. Engine values are cumulative ns.
# Deltas over the 1s window:
@ -79,11 +81,15 @@ class TestGpuStats(unittest.TestCase):
sleep.assert_called_once()
assert intel_stats == {
"gpu": "90.0%",
"mem": "-%",
"compute": "30.0%",
"dec": "60.0%",
"clients": {"100": "80.0%", "200": "10.0%"},
"0000:00:02.0": {
"name": "Intel Graphics",
"vendor": "intel",
"gpu": "90.0%",
"mem": "-%",
"compute": "30.0%",
"dec": "60.0%",
"clients": {"100": "80.0%", "200": "10.0%"},
},
}
@patch("frigate.util.services._read_intel_drm_fdinfo")

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=int(datetime.datetime.now().timestamp()),
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

@ -0,0 +1,806 @@
"""Tests for outbound WebSocket broadcast filtering."""
import json
import threading
import unittest
from types import SimpleNamespace
from typing import Any
from frigate.comms.ws import (
WebSocketClient,
_classify_outbound,
_collect_zone_names,
_extract_payload_camera,
_materialize_for_ws,
_ws_allowed_cameras,
_ws_is_unrestricted,
)
from frigate.config import FrigateConfig
def _build_config(
*,
extra_roles: dict[str, list[str]] | None = None,
extra_cameras: dict[str, dict[str, Any]] | None = None,
extra_zones: dict[str, dict[str, dict[str, Any]]] | None = None,
) -> FrigateConfig:
"""Construct a FrigateConfig used by the outbound filter tests.
The default fixture has three cameras: front_door, back_door, garage.
Restricted role "house_only" sees front_door + back_door but not garage.
"""
cameras: dict[str, dict[str, Any]] = {
"front_door": {
"ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.1:554/v", "roles": ["detect"]}],
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
},
"back_door": {
"ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.2:554/v", "roles": ["detect"]}],
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
},
"garage": {
"ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.3:554/v", "roles": ["detect"]}],
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
},
}
if extra_cameras:
cameras.update(extra_cameras)
if extra_zones:
for cam_name, zones in extra_zones.items():
cameras[cam_name]["zones"] = zones
roles = {"house_only": ["front_door", "back_door"]}
if extra_roles:
roles.update(extra_roles)
return FrigateConfig(
mqtt={"host": "mqtt"},
auth={"roles": roles},
cameras=cameras,
)
def _ws(role: str | None) -> Any:
"""Build a fake ws4py-style websocket exposing ``environ``."""
environ = {} if role is None else {"HTTP_REMOTE_ROLE": role}
return SimpleNamespace(environ=environ, terminated=False, sent=[])
class TestClassifyOutbound(unittest.TestCase):
"""The pure classifier — bucket every topic into a scope."""
def setUp(self):
self.config = _build_config(
extra_zones={"front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}}}
)
self.all_cameras = set(self.config.cameras.keys())
self.all_zones = _collect_zone_names(self.config)
def _classify(self, topic: str) -> tuple[str, Any]:
return _classify_outbound(topic, self.all_cameras, self.all_zones)
# --- Global allowlist ---
def test_model_state_is_global(self):
self.assertEqual(self._classify("model_state"), ("global", None))
def test_profile_state_is_global(self):
self.assertEqual(self._classify("profile/state"), ("global", None))
def test_bare_notifications_state_is_global(self):
"""The 2-segment ``notifications/state`` is global; the 3-segment
``<camera>/notifications/state`` is camera-scoped (see below)."""
self.assertEqual(self._classify("notifications/state"), ("global", None))
def test_notification_test_is_global(self):
self.assertEqual(self._classify("notification_test"), ("global", None))
# --- Unrestricted-only ---
def test_birdseye_layout_is_unrestricted_only(self):
self.assertEqual(self._classify("birdseye_layout"), ("unrestricted_only", None))
# --- Camera-prefixed ---
def test_camera_state_topic_resolves_to_camera(self):
self.assertEqual(
self._classify("front_door/detect/state"), ("camera", "front_door")
)
def test_camera_motion_topic_resolves_to_camera(self):
self.assertEqual(self._classify("back_door/motion"), ("camera", "back_door"))
def test_camera_per_notification_topic_resolves_to_camera(self):
self.assertEqual(
self._classify("front_door/notifications/state"),
("camera", "front_door"),
)
def test_camera_label_counter_resolves_to_camera(self):
self.assertEqual(self._classify("front_door/person"), ("camera", "front_door"))
def test_camera_object_mask_state_resolves_to_camera(self):
self.assertEqual(
self._classify("front_door/object_mask/zone_1/state"),
("camera", "front_door"),
)
# --- Zone-prefixed ---
def test_zone_aggregate_topic_is_unrestricted_only(self):
self.assertEqual(self._classify("driveway/person"), ("unrestricted_only", None))
def test_zone_all_topic_is_unrestricted_only(self):
self.assertEqual(self._classify("driveway/all"), ("unrestricted_only", None))
# --- Payload-camera ---
def test_events_topic_marks_payload_camera_path(self):
self.assertEqual(
self._classify("events"), ("payload_camera", ("after", "camera"))
)
def test_reviews_topic_marks_payload_camera_path(self):
self.assertEqual(
self._classify("reviews"), ("payload_camera", ("after", "camera"))
)
def test_triggers_topic_marks_payload_camera_path(self):
self.assertEqual(self._classify("triggers"), ("payload_camera", ("camera",)))
def test_tracked_object_update_marks_payload_camera_path(self):
self.assertEqual(
self._classify("tracked_object_update"), ("payload_camera", ("camera",))
)
# --- Reshape ---
def test_camera_activity_is_reshape_by_camera_key(self):
self.assertEqual(
self._classify("camera_activity"), ("reshape_by_camera_key", None)
)
def test_audio_detections_is_reshape_by_camera_key(self):
self.assertEqual(
self._classify("audio_detections"), ("reshape_by_camera_key", None)
)
def test_job_state_is_reshape_job_state(self):
self.assertEqual(self._classify("job_state"), ("reshape_job_state", None))
def test_stats_is_reshape_stats(self):
self.assertEqual(self._classify("stats"), ("reshape_stats", None))
# --- Fail-closed ---
def test_unknown_topic_is_dropped(self):
self.assertEqual(self._classify("some_random_topic"), ("drop", None))
def test_unknown_camera_prefix_is_dropped(self):
self.assertEqual(self._classify("ghost_camera/detect/state"), ("drop", None))
class TestCollectZoneNames(unittest.TestCase):
def test_zones_from_all_cameras(self):
config = _build_config(
extra_zones={
"front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}},
"back_door": {"yard": {"coordinates": "0,0,1,0,1,1,0,1"}},
}
)
self.assertEqual(_collect_zone_names(config), {"driveway", "yard"})
def test_no_zones_returns_empty(self):
self.assertEqual(_collect_zone_names(_build_config()), set())
class TestExtractPayloadCamera(unittest.TestCase):
def test_extract_from_dict_path(self):
payload = {"after": {"camera": "front_door"}}
self.assertEqual(
_extract_payload_camera(payload, ("after", "camera")), "front_door"
)
def test_extract_from_json_string(self):
payload = json.dumps({"after": {"camera": "front_door"}})
self.assertEqual(
_extract_payload_camera(payload, ("after", "camera")), "front_door"
)
def test_extract_single_segment_path(self):
self.assertEqual(
_extract_payload_camera({"camera": "garage"}, ("camera",)), "garage"
)
def test_missing_key_returns_none(self):
self.assertIsNone(_extract_payload_camera({}, ("after", "camera")))
def test_malformed_json_returns_none(self):
self.assertIsNone(_extract_payload_camera("not-json", ("camera",)))
def test_non_string_camera_returns_none(self):
self.assertIsNone(_extract_payload_camera({"camera": 42}, ("camera",)))
class TestWsRoleHelpers(unittest.TestCase):
def setUp(self):
self.config = _build_config()
def test_admin_is_unrestricted(self):
self.assertTrue(_ws_is_unrestricted(_ws("admin"), self.config))
def test_viewer_is_unrestricted(self):
self.assertTrue(_ws_is_unrestricted(_ws("viewer"), self.config))
def test_restricted_role_is_not_unrestricted(self):
self.assertFalse(_ws_is_unrestricted(_ws("house_only"), self.config))
def test_missing_role_is_not_unrestricted(self):
self.assertFalse(_ws_is_unrestricted(_ws(None), self.config))
def test_unknown_role_is_not_unrestricted(self):
self.assertFalse(_ws_is_unrestricted(_ws("ghost"), self.config))
def test_admin_allowed_cameras_is_all(self):
self.assertEqual(
_ws_allowed_cameras(_ws("admin"), self.config),
{"front_door", "back_door", "garage"},
)
def test_restricted_role_allowed_cameras_is_subset(self):
self.assertEqual(
_ws_allowed_cameras(_ws("house_only"), self.config),
{"front_door", "back_door"},
)
def test_missing_role_allowed_cameras_is_empty(self):
self.assertEqual(_ws_allowed_cameras(_ws(None), self.config), set())
def test_multi_role_union_grants_widest(self):
self.assertEqual(
_ws_allowed_cameras(_ws("house_only,admin"), self.config),
{"front_door", "back_door", "garage"},
)
class TestMaterializeForWs(unittest.TestCase):
def setUp(self):
self.config = _build_config(
extra_zones={"front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}}}
)
self.all_cameras = set(self.config.cameras.keys())
self.all_zones = _collect_zone_names(self.config)
def _materialize(self, ws: Any, topic: str, payload: Any) -> str | None:
scope = _classify_outbound(topic, self.all_cameras, self.all_zones)
from frigate.comms.ws import _parse_json_payload
parsed = (
_parse_json_payload(payload)
if scope[0]
in (
"payload_camera",
"reshape_by_camera_key",
"reshape_job_state",
"reshape_stats",
)
else None
)
full = json.dumps({"topic": topic, "payload": payload})
return _materialize_for_ws(ws, topic, full, scope, parsed, self.config)
# --- Globals: every authenticated client sees them ---
def test_globals_reach_admin(self):
self.assertIsNotNone(self._materialize(_ws("admin"), "model_state", "{}"))
def test_globals_reach_restricted(self):
self.assertIsNotNone(self._materialize(_ws("house_only"), "model_state", "{}"))
def test_globals_reach_no_role(self):
"""A missing role header still gets globals (matches viewer-default
for inbound)."""
self.assertIsNotNone(self._materialize(_ws(None), "model_state", "{}"))
# --- Unknown topic dropped for everyone ---
def test_unknown_topic_dropped_for_admin(self):
self.assertIsNone(self._materialize(_ws("admin"), "rogue_topic", "{}"))
# --- Non-global topics require a role (fail-closed) ---
def test_no_role_blocked_from_camera_topic(self):
self.assertIsNone(self._materialize(_ws(None), "front_door/detect/state", "ON"))
def test_no_role_blocked_from_events(self):
payload = json.dumps({"after": {"camera": "front_door"}})
self.assertIsNone(self._materialize(_ws(None), "events", payload))
# --- Camera-prefixed ---
def test_restricted_role_sees_allowed_camera(self):
self.assertIsNotNone(
self._materialize(_ws("house_only"), "front_door/detect/state", "ON")
)
def test_restricted_role_blocked_from_unallowed_camera(self):
self.assertIsNone(
self._materialize(_ws("house_only"), "garage/detect/state", "ON")
)
def test_admin_sees_all_camera_topics(self):
self.assertIsNotNone(
self._materialize(_ws("admin"), "garage/detect/state", "ON")
)
# --- Unrestricted-only (zones, birdseye_layout) ---
def test_zone_aggregate_blocked_for_restricted(self):
self.assertIsNone(self._materialize(_ws("house_only"), "driveway/person", 3))
def test_zone_aggregate_visible_to_admin(self):
self.assertIsNotNone(self._materialize(_ws("admin"), "driveway/person", 3))
def test_birdseye_layout_blocked_for_restricted(self):
payload = json.dumps(
{"front_door": {"x": 0, "y": 0, "width": 100, "height": 100}}
)
self.assertIsNone(
self._materialize(_ws("house_only"), "birdseye_layout", payload)
)
def test_birdseye_layout_visible_to_admin(self):
payload = json.dumps(
{"front_door": {"x": 0, "y": 0, "width": 100, "height": 100}}
)
self.assertIsNotNone(
self._materialize(_ws("admin"), "birdseye_layout", payload)
)
# --- Payload-camera ---
def test_events_filtered_by_payload_camera(self):
payload = json.dumps({"after": {"camera": "garage"}})
self.assertIsNone(self._materialize(_ws("house_only"), "events", payload))
payload = json.dumps({"after": {"camera": "front_door"}})
self.assertIsNotNone(self._materialize(_ws("house_only"), "events", payload))
def test_events_with_missing_camera_dropped(self):
payload = json.dumps({"after": {}})
self.assertIsNone(self._materialize(_ws("house_only"), "events", payload))
def test_triggers_filtered_by_payload_camera(self):
payload = json.dumps({"name": "t1", "camera": "garage"})
self.assertIsNone(self._materialize(_ws("house_only"), "triggers", payload))
# --- Reshape: dict keyed by camera ---
def test_camera_activity_filtered_to_allowed_keys(self):
payload = json.dumps(
{
"front_door": {"objects": 1},
"back_door": {"objects": 0},
"garage": {"objects": 2},
}
)
message = self._materialize(_ws("house_only"), "camera_activity", payload)
self.assertIsNotNone(message)
envelope = json.loads(message) # type: ignore[arg-type]
inner = json.loads(envelope["payload"])
self.assertEqual(set(inner.keys()), {"front_door", "back_door"})
self.assertNotIn("garage", inner)
def test_camera_activity_unchanged_for_admin(self):
payload = json.dumps({"front_door": {}, "back_door": {}, "garage": {}})
message = self._materialize(_ws("admin"), "camera_activity", payload)
envelope = json.loads(message) # type: ignore[arg-type]
self.assertEqual(envelope["payload"], payload)
def test_camera_activity_with_no_allowed_returns_none(self):
payload = json.dumps({"garage": {"objects": 2}})
self.assertIsNone(
self._materialize(_ws("house_only"), "camera_activity", payload)
)
def test_audio_detections_filtered_to_allowed_keys(self):
payload = json.dumps({"front_door": {"bark": {}}, "garage": {"speech": {}}})
message = self._materialize(_ws("house_only"), "audio_detections", payload)
envelope = json.loads(message) # type: ignore[arg-type]
inner = json.loads(envelope["payload"])
self.assertEqual(set(inner.keys()), {"front_door"})
# --- Reshape: job_state ---
def test_job_state_admin_sees_full_payload(self):
payload = json.dumps(
{
"motion_search": {"job_type": "motion_search", "camera": "garage"},
"media_sync": {"job_type": "media_sync"},
}
)
message = self._materialize(_ws("admin"), "job_state", payload)
envelope = json.loads(message) # type: ignore[arg-type]
self.assertEqual(envelope["payload"], payload)
def test_job_state_restricted_keeps_allowed_camera_jobs(self):
"""Top-level camera field on a job entry: drop if not allowed."""
payload = json.dumps(
{
"motion_search": {"job_type": "motion_search", "camera": "front_door"},
"vlm_watch": {"job_type": "vlm_watch", "camera": "garage"},
}
)
message = self._materialize(_ws("house_only"), "job_state", payload)
envelope = json.loads(message) # type: ignore[arg-type]
inner = json.loads(envelope["payload"])
self.assertIn("motion_search", inner)
self.assertNotIn("vlm_watch", inner)
def test_job_state_export_results_jobs_filtered_per_recipient(self):
"""The aggregated export broadcast nests per-camera sub-jobs under
``results.jobs``. Restricted users must only see allowed entries."""
payload = json.dumps(
{
"export": {
"job_type": "export",
"status": "running",
"results": {
"jobs": [
{"job_type": "export", "camera": "front_door", "id": "a"},
{"job_type": "export", "camera": "garage", "id": "b"},
{"job_type": "export", "camera": "back_door", "id": "c"},
]
},
}
}
)
message = self._materialize(_ws("house_only"), "job_state", payload)
envelope = json.loads(message) # type: ignore[arg-type]
inner = json.loads(envelope["payload"])
self.assertIn("export", inner)
kept_cameras = [j["camera"] for j in inner["export"]["results"]["jobs"]]
self.assertEqual(kept_cameras, ["front_door", "back_door"])
# Sibling fields like ``status`` must survive reshaping.
self.assertEqual(inner["export"]["status"], "running")
def test_job_state_export_entry_dropped_when_no_jobs_allowed(self):
payload = json.dumps(
{
"export": {
"job_type": "export",
"status": "running",
"results": {
"jobs": [
{"job_type": "export", "camera": "garage", "id": "b"},
]
},
}
}
)
self.assertIsNone(self._materialize(_ws("house_only"), "job_state", payload))
# --- Reshape: stats ---
def _stats_payload(self) -> str:
return json.dumps(
{
"cameras": {
"front_door": {"camera_fps": 5.0, "pid": 1234},
"back_door": {"camera_fps": 5.0, "pid": 1235},
"garage": {"camera_fps": 5.0, "pid": 1236},
},
"detectors": {"cpu": {"detection_start": 0.0, "inference_speed": 10}},
"service": {"uptime": 12345, "version": "0.16.0"},
"camera_fps": 15.0,
"detection_fps": 6.0,
}
)
def test_stats_admin_sees_full_payload(self):
message = self._materialize(_ws("admin"), "stats", self._stats_payload())
envelope = json.loads(message) # type: ignore[arg-type]
self.assertEqual(envelope["payload"], self._stats_payload())
def test_stats_restricted_filters_camera_keys_but_keeps_aggregates(self):
message = self._materialize(_ws("house_only"), "stats", self._stats_payload())
envelope = json.loads(message) # type: ignore[arg-type]
inner = json.loads(envelope["payload"])
self.assertEqual(set(inner["cameras"].keys()), {"front_door", "back_door"})
self.assertNotIn("garage", inner["cameras"])
# Aggregates, detectors, and service block must survive.
self.assertEqual(inner["camera_fps"], 15.0)
self.assertEqual(inner["detection_fps"], 6.0)
self.assertIn("detectors", inner)
self.assertIn("service", inner)
def test_stats_restricted_with_no_allowed_cameras_still_sends_aggregates(self):
"""A restricted role whose allow-list contains only nonexistent cameras
still gets the global aggregates and service block."""
config = _build_config(extra_roles={"empty_role": ["nonexistent"]})
from frigate.comms.ws import _parse_json_payload
payload = self._stats_payload()
all_cameras = set(config.cameras.keys())
scope = _classify_outbound("stats", all_cameras, _collect_zone_names(config))
full = json.dumps({"topic": "stats", "payload": payload})
message = _materialize_for_ws(
_ws("empty_role"),
"stats",
full,
scope,
_parse_json_payload(payload),
config,
)
envelope = json.loads(message) # type: ignore[arg-type]
inner = json.loads(envelope["payload"])
self.assertEqual(inner["cameras"], {})
self.assertEqual(inner["camera_fps"], 15.0)
self.assertIn("service", inner)
def test_stats_without_cameras_key_passes_through(self):
"""A malformed stats payload missing the cameras sub-dict shouldn't
break delivery for restricted users fall back to the full message."""
payload = json.dumps({"detectors": {}, "service": {}, "detection_fps": 0.0})
message = self._materialize(_ws("house_only"), "stats", payload)
envelope = json.loads(message) # type: ignore[arg-type]
self.assertEqual(envelope["payload"], payload)
def test_job_state_export_entry_unchanged_for_admin(self):
payload = json.dumps(
{
"export": {
"job_type": "export",
"status": "running",
"results": {
"jobs": [
{"job_type": "export", "camera": "garage", "id": "b"},
]
},
}
}
)
message = self._materialize(_ws("admin"), "job_state", payload)
envelope = json.loads(message) # type: ignore[arg-type]
self.assertEqual(envelope["payload"], payload)
def test_job_state_restricted_keeps_global_jobs(self):
"""media_sync has no camera field; restricted users still see it."""
payload = json.dumps(
{"media_sync": {"job_type": "media_sync", "status": "running"}}
)
message = self._materialize(_ws("house_only"), "job_state", payload)
envelope = json.loads(message) # type: ignore[arg-type]
inner = json.loads(envelope["payload"])
self.assertIn("media_sync", inner)
def test_job_state_debug_replay_nested_source_camera_filtered(self):
"""debug_replay puts ``source_camera`` inside ``results`` (see
jobs/debug_replay.py:to_dict). Restricted users must not receive
entries whose nested source camera is unauthorized."""
payload = json.dumps(
{
"debug_replay": {
"id": "bd6dc99d-a7d",
"job_type": "debug_replay",
"status": "running",
"start_time": 1.0,
"end_time": None,
"error_message": None,
"results": {
"current_step": "preparing_clip",
"progress_percent": 0.0,
"source_camera": "garage",
"replay_camera_name": "_replay_garage",
"start_ts": 0.0,
"end_ts": 1.0,
},
}
}
)
self.assertIsNone(self._materialize(_ws("house_only"), "job_state", payload))
def test_job_state_debug_replay_nested_source_camera_allowed(self):
payload = json.dumps(
{
"debug_replay": {
"id": "bd6dc99d-a7d",
"job_type": "debug_replay",
"status": "running",
"results": {
"source_camera": "front_door",
"replay_camera_name": "_replay_front_door",
},
}
}
)
message = self._materialize(_ws("house_only"), "job_state", payload)
envelope = json.loads(message) # type: ignore[arg-type]
inner = json.loads(envelope["payload"])
self.assertIn("debug_replay", inner)
self.assertEqual(
inner["debug_replay"]["results"]["source_camera"], "front_door"
)
class _FakeManager:
"""Minimal ws4py manager: holds clients and exposes a lock."""
def __init__(self, clients: list[Any]) -> None:
self.lock = threading.Lock()
self.websockets = {id(c): c for c in clients}
class _FakeServer:
def __init__(self, manager: _FakeManager) -> None:
self.manager = manager
class _CapturingWs(SimpleNamespace):
"""Fake ws4py client that records what was sent."""
def __init__(self, role: str | None) -> None:
environ = {} if role is None else {"HTTP_REMOTE_ROLE": role}
super().__init__(environ=environ, terminated=False)
self.sent: list[str] = []
def send(self, message: str) -> None: # noqa: D401 - matches ws4py API
self.sent.append(message)
class TestPublishEndToEnd(unittest.TestCase):
"""Drive WebSocketClient.publish() against fake clients with different roles."""
def setUp(self):
self.config = _build_config(
extra_zones={"front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}}}
)
self.admin = _CapturingWs("admin")
self.restricted = _CapturingWs("house_only")
self.anon = _CapturingWs(None)
self.client = WebSocketClient(self.config)
self.client.websocket_server = _FakeServer(
_FakeManager([self.admin, self.restricted, self.anon])
)
def _payloads(self, ws: _CapturingWs) -> list[Any]:
return [json.loads(m)["payload"] for m in ws.sent]
def test_global_topic_reaches_everyone(self):
self.client.publish("model_state", "{}")
self.assertEqual(len(self.admin.sent), 1)
self.assertEqual(len(self.restricted.sent), 1)
self.assertEqual(len(self.anon.sent), 1)
def test_camera_topic_filters_restricted_recipient(self):
self.client.publish("garage/detect/state", "ON")
self.assertEqual(len(self.admin.sent), 1)
self.assertEqual(len(self.restricted.sent), 0)
self.assertEqual(len(self.anon.sent), 0)
def test_camera_topic_allows_restricted_recipient_for_allowed_camera(self):
self.client.publish("front_door/detect/state", "ON")
self.assertEqual(len(self.admin.sent), 1)
self.assertEqual(len(self.restricted.sent), 1)
self.assertEqual(len(self.anon.sent), 0)
def test_events_payload_filtered(self):
self.client.publish("events", json.dumps({"after": {"camera": "garage"}}))
self.assertEqual(len(self.admin.sent), 1)
self.assertEqual(len(self.restricted.sent), 0)
def test_camera_activity_reshaped_per_recipient(self):
self.client.publish(
"camera_activity",
json.dumps(
{
"front_door": {"objects": 1},
"back_door": {"objects": 0},
"garage": {"objects": 2},
}
),
)
self.assertEqual(len(self.admin.sent), 1)
admin_inner = json.loads(self._payloads(self.admin)[0])
self.assertEqual(set(admin_inner.keys()), {"front_door", "back_door", "garage"})
self.assertEqual(len(self.restricted.sent), 1)
restricted_inner = json.loads(self._payloads(self.restricted)[0])
self.assertEqual(set(restricted_inner.keys()), {"front_door", "back_door"})
self.assertEqual(len(self.anon.sent), 0)
def test_birdseye_layout_blocked_for_restricted_and_anon(self):
self.client.publish(
"birdseye_layout",
json.dumps({"front_door": {"x": 0, "y": 0, "width": 1, "height": 1}}),
)
self.assertEqual(len(self.admin.sent), 1)
self.assertEqual(len(self.restricted.sent), 0)
self.assertEqual(len(self.anon.sent), 0)
def test_zone_aggregate_blocked_for_restricted(self):
self.client.publish("driveway/person", 2)
self.assertEqual(len(self.admin.sent), 1)
self.assertEqual(len(self.restricted.sent), 0)
def test_stats_reshaped_per_recipient(self):
self.client.publish(
"stats",
json.dumps(
{
"cameras": {
"front_door": {"camera_fps": 5.0},
"garage": {"camera_fps": 5.0},
},
"service": {"uptime": 1},
"camera_fps": 10.0,
}
),
)
self.assertEqual(len(self.admin.sent), 1)
admin_inner = json.loads(self._payloads(self.admin)[0])
self.assertEqual(set(admin_inner["cameras"].keys()), {"front_door", "garage"})
self.assertEqual(len(self.restricted.sent), 1)
restricted_inner = json.loads(self._payloads(self.restricted)[0])
self.assertEqual(set(restricted_inner["cameras"].keys()), {"front_door"})
self.assertEqual(restricted_inner["camera_fps"], 10.0)
self.assertIn("service", restricted_inner)
# Stats requires a role; anonymous gets nothing.
self.assertEqual(len(self.anon.sent), 0)
def test_export_job_state_filters_results_jobs_per_recipient(self):
self.client.publish(
"job_state",
json.dumps(
{
"export": {
"job_type": "export",
"status": "running",
"results": {
"jobs": [
{"camera": "front_door", "id": "a"},
{"camera": "garage", "id": "b"},
]
},
}
}
),
)
self.assertEqual(len(self.admin.sent), 1)
admin_inner = json.loads(self._payloads(self.admin)[0])
self.assertEqual(
[j["camera"] for j in admin_inner["export"]["results"]["jobs"]],
["front_door", "garage"],
)
self.assertEqual(len(self.restricted.sent), 1)
restricted_inner = json.loads(self._payloads(self.restricted)[0])
self.assertEqual(
[j["camera"] for j in restricted_inner["export"]["results"]["jobs"]],
["front_door"],
)
def test_unknown_topic_dropped_for_everyone(self):
self.client.publish("some_rogue_topic", "data")
self.assertEqual(self.admin.sent, [])
self.assertEqual(self.restricted.sent, [])
self.assertEqual(self.anon.sent, [])
def test_terminated_client_is_skipped(self):
self.restricted.terminated = True
self.client.publish("front_door/detect/state", "ON")
self.assertEqual(len(self.admin.sent), 1)
self.assertEqual(len(self.restricted.sent), 0)
if __name__ == "__main__":
unittest.main()

View File

@ -357,6 +357,9 @@ class TrackedObjectProcessor(threading.Thread):
def get_current_frame_time(self, camera: str) -> float:
"""Returns the latest frame time for a given camera."""
if camera not in self.camera_states:
return 0.0
return self.camera_states[camera].current_frame_time
def set_sub_label(

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
@ -608,11 +608,14 @@ def migrate_018_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]
def get_relative_coordinates(
mask: Optional[Union[str, list]], frame_shape: tuple[int, int]
mask: Optional[Union[str, list]],
frame_shape: tuple[int, int],
camera_name: str = "",
) -> Union[str, list]:
# masks and zones are saved as relative coordinates
# we know if any points are > 1 then it is using the
# old native resolution coordinates
where = f" for camera {camera_name}" if camera_name else ""
if mask:
if isinstance(mask, list) and any(x > "1.0" for x in mask[0].split(",")):
relative_masks = []
@ -627,7 +630,7 @@ def get_relative_coordinates(
if x > frame_shape[1] or y > frame_shape[0]:
logger.error(
f"Not applying mask due to invalid coordinates. {x},{y} is outside of the detection resolution {frame_shape[1]}x{frame_shape[0]}. Use the editor in the UI to correct the mask."
f"Not applying mask due to invalid coordinates{where}. {x},{y} is outside of the detection resolution {frame_shape[1]}x{frame_shape[0]}. Use the editor in the UI to correct the mask."
)
continue
@ -650,7 +653,7 @@ def get_relative_coordinates(
if x > frame_shape[1] or y > frame_shape[0]:
logger.error(
f"Not applying mask due to invalid coordinates. {x},{y} is outside of the detection resolution {frame_shape[1]}x{frame_shape[0]}. Use the editor in the UI to correct the mask."
f"Not applying mask due to invalid coordinates{where}. {x},{y} is outside of the detection resolution {frame_shape[1]}x{frame_shape[0]}. Use the editor in the UI to correct the mask."
)
return []

View File

@ -393,8 +393,10 @@ def _read_intel_drm_fdinfo(target_pdev: Optional[str]) -> dict:
return snapshot
def get_intel_gpu_stats(intel_gpu_device: Optional[str]) -> Optional[dict[str, Any]]:
"""Get stats by reading DRM fdinfo files.
def get_intel_gpu_stats(
intel_gpu_device: Optional[str],
) -> Optional[dict[str, dict[str, Any]]]:
"""Get stats by reading DRM fdinfo files, bucketed per-pdev.
Each DRM client FD exposes monotonic per-engine busy counters via
/proc/<pid>/fdinfo/<fd> (i915 since kernel 5.19, Xe since first release).
@ -402,7 +404,14 @@ def get_intel_gpu_stats(intel_gpu_device: Optional[str]) -> Optional[dict[str, A
utilization. Render/3D and Compute are pooled into "compute"; Video and
VideoEnhance into "dec". Overall "gpu" is the sum of those pools (clamped
to 100%).
The return value is keyed by the GPU's drm-pdev string so multiple Intel
GPUs in the same system are reported separately. Each entry carries a
"name" populated from OpenVINO (falling back to the pdev) so callers can
surface a real device name in the UI.
"""
from frigate.stats.intel_gpu_info import intel_gpu_name_resolver
target_pdev = _resolve_intel_gpu_pdev(intel_gpu_device)
snapshot_a = _read_intel_drm_fdinfo(target_pdev)
@ -417,19 +426,21 @@ def get_intel_gpu_stats(intel_gpu_device: Optional[str]) -> Optional[dict[str, A
if not snapshot_b or elapsed_ns <= 0:
return None
engine_pct: dict[str, float] = {
"render": 0.0,
"video": 0.0,
"video-enhance": 0.0,
"compute": 0.0,
}
pid_pct: dict[str, float] = {}
def _new_engine_pct() -> dict[str, float]:
return {"render": 0.0, "video": 0.0, "video-enhance": 0.0, "compute": 0.0}
per_pdev_engine_pct: dict[str, dict[str, float]] = {}
per_pdev_pid_pct: dict[str, dict[str, float]] = {}
for key, data_b in snapshot_b.items():
data_a = snapshot_a.get(key)
if not data_a or data_a["driver"] != data_b["driver"]:
continue
pdev = key[0]
engine_pct = per_pdev_engine_pct.setdefault(pdev, _new_engine_pct())
pid_pct = per_pdev_pid_pct.setdefault(pdev, {})
client_total = 0.0
for engine, (busy_b, total_b) in data_b["engines"].items():
if engine not in engine_pct:
@ -452,25 +463,37 @@ def get_intel_gpu_stats(intel_gpu_device: Optional[str]) -> Optional[dict[str, A
pid_pct[data_b["pid"]] = pid_pct.get(data_b["pid"], 0.0) + client_total
for engine in engine_pct:
engine_pct[engine] = min(100.0, engine_pct[engine])
if not per_pdev_engine_pct:
return None
compute_pct = min(100.0, engine_pct["render"] + engine_pct["compute"])
dec_pct = min(100.0, engine_pct["video"] + engine_pct["video-enhance"])
overall_pct = min(100.0, compute_pct + dec_pct)
names = intel_gpu_name_resolver.get_names()
results: dict[str, dict[str, Any]] = {}
results: dict[str, Any] = {
"gpu": f"{round(overall_pct, 2)}%",
"mem": "-%",
"compute": f"{round(compute_pct, 2)}%",
"dec": f"{round(dec_pct, 2)}%",
}
for pdev, engine_pct in per_pdev_engine_pct.items():
for engine in engine_pct:
engine_pct[engine] = min(100.0, engine_pct[engine])
if pid_pct:
results["clients"] = {
pid: f"{round(min(100.0, pct), 2)}%" for pid, pct in pid_pct.items()
compute_pct = min(100.0, engine_pct["render"] + engine_pct["compute"])
dec_pct = min(100.0, engine_pct["video"] + engine_pct["video-enhance"])
overall_pct = min(100.0, compute_pct + dec_pct)
entry: dict[str, Any] = {
"name": names.get(pdev) or f"Intel GPU {pdev}",
"vendor": "intel",
"gpu": f"{round(overall_pct, 2)}%",
"mem": "-%",
"compute": f"{round(compute_pct, 2)}%",
"dec": f"{round(dec_pct, 2)}%",
}
pid_pct = per_pdev_pid_pct.get(pdev)
if pid_pct:
entry["clients"] = {
pid: f"{round(min(100.0, pct), 2)}%" for pid, pct in pid_pct.items()
}
results[pdev] = entry
return results
@ -755,6 +778,41 @@ def get_hailo_temps() -> dict[str, float]:
return temps
def _go2rtc_arbitrary_exec_allowed() -> bool:
"""Read the GO2RTC_ALLOW_ARBITRARY_EXEC override from env, docker
secrets, or the Home Assistant add-on options file."""
raw: Optional[str] = None
if "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.environ:
raw = os.environ.get("GO2RTC_ALLOW_ARBITRARY_EXEC")
elif (
os.path.isdir("/run/secrets")
and os.access("/run/secrets", os.R_OK)
and "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.listdir("/run/secrets")
):
try:
with open("/run/secrets/GO2RTC_ALLOW_ARBITRARY_EXEC") as f:
raw = f.read().strip()
except OSError:
raw = None
elif os.path.isfile("/data/options.json"):
try:
with open("/data/options.json") as f:
options = json.loads(f.read())
raw = options.get("go2rtc_allow_arbitrary_exec")
except (OSError, json.JSONDecodeError):
raw = None
return raw is not None and str(raw).lower() in ("true", "1", "yes")
def is_restricted_go2rtc_source(stream_source: str) -> bool:
"""Check if a stream source is a restricted type (echo, expr, or exec)
and the GO2RTC_ALLOW_ARBITRARY_EXEC override is not set."""
if not stream_source.strip().startswith(("echo:", "expr:", "exec:")):
return False
return not _go2rtc_arbitrary_exec_allowed()
def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedProcess:
"""Run ffprobe on stream."""
clean_path = escape_special_characters(path)

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
@ -342,6 +364,64 @@ def main():
continue
section_data.pop(key, None)
if field_name == "objects":
# Produce a parallel `filters_attribute` block alongside `filters`,
# with object-wording rewritten for attribute filters (face,
# license_plate, courier logos). The frontend's
# buildTranslationPath routes `filters.<attr>.<field>` lookups to
# `filters_attribute.<field>` when `<attr>` is in
# `model.all_attributes`. Keep this rewrite list explicit rather
# than running a blanket s/object/attribute/ so unrelated
# descriptions (e.g. "JSON object") never accidentally flip.
filters_block = section_data.get("filters")
if isinstance(filters_block, dict):
attribute_rewrites = [
("Object filters", "Attribute filters"),
("detected objects", "detected attributes"),
("object area", "attribute area"),
("object type", "attribute"),
("the object", "the attribute"),
]
# Per-field overrides for cases where the generic rewrite
# doesn't capture the attribute-specific semantics. Keys
# match the FilterConfig field name; values are partial
# overrides applied AFTER the generic rewrites.
attribute_field_overrides: Dict[str, Dict[str, str]] = {
"min_score": {
"description": (
"Minimum single-frame detection confidence required "
"to associate this attribute with its parent object."
),
},
}
def rewrite(text: str) -> str:
for source, replacement in attribute_rewrites:
text = text.replace(source, replacement)
return text
attribute_variant: Dict[str, Any] = {}
for key, value in filters_block.items():
if key in ("label", "description"):
if isinstance(value, str):
attribute_variant[key] = rewrite(value)
continue
if not isinstance(value, dict):
continue
field_trans: Dict[str, str] = {}
if isinstance(value.get("label"), str):
field_trans["label"] = rewrite(value["label"])
if isinstance(value.get("description"), str):
field_trans["description"] = rewrite(value["description"])
overrides = attribute_field_overrides.get(key)
if overrides:
field_trans.update(overrides)
if field_trans:
attribute_variant[key] = field_trans
if attribute_variant:
section_data["filters_attribute"] = attribute_variant
if not section_data:
logger.warning(f"No translations found for section: {field_name}")
continue

View File

@ -0,0 +1,55 @@
/**
* Detectors and model settings page tests -- HIGH tier.
*
* Tests rendering of the merged page and navigation from the Frigate+ page.
*/
import { test, expect } from "../../fixtures/frigate-test";
test.describe("Detectors and model Settings @high", () => {
test("page renders with detector and model cards", async ({ frigateApp }) => {
await frigateApp.goto("/settings?page=systemDetectorsAndModel");
await frigateApp.page.waitForTimeout(2000);
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
const text = await frigateApp.page.textContent("#pageRoot");
expect(text).toContain("Detectors and model");
expect(text?.toLowerCase()).toContain("detector hardware");
expect(text?.toLowerCase()).toContain("detection model");
});
test("Frigate+ page links to the merged page", async ({ frigateApp }) => {
await frigateApp.goto("/settings?page=frigateplus");
await frigateApp.page.waitForTimeout(2000);
const button = frigateApp.page.getByRole("button", {
name: /Change in Detectors and model/,
});
// Button only appears when Frigate+ is enabled in the test config; skip
// the click assertion if it's not present.
if ((await button.count()) > 0) {
await button.first().click();
await frigateApp.page.waitForURL(/page=systemDetectorsAndModel/);
await expect(frigateApp.page.locator("#pageRoot")).toContainText(
"Detectors and model",
);
} else {
test.skip(
true,
"Frigate+ not enabled in this test config; skipping link assertion",
);
}
});
test("old systemDetectionModel deep-link no longer routes here", async ({
frigateApp,
}) => {
await frigateApp.goto("/settings?page=systemDetectionModel");
await frigateApp.page.waitForTimeout(2000);
// The old page key is no longer in allSettingsViews; the router
// falls back to its default settings page (uiSettings).
const text = await frigateApp.page.textContent("#pageRoot");
expect(text).not.toContain("Detection model");
});
});

View File

@ -0,0 +1,235 @@
/**
* go2rtc streams settings page tests -- MEDIUM tier.
*
* Regression coverage for the compat-mode (ffmpeg:) URL editor: unknown
* fragments like #timeout=10 must remain visible and editable when the
* stream is using compatibility mode.
*/
import { test, expect } from "../../fixtures/frigate-test";
import type { Page } from "@playwright/test";
const STREAM_NAME = "dome_sub";
const FFMPEG_URL_WITH_TIMEOUT =
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#timeout=10";
async function installRawPathsRoute(page: Page, streamUrl: string) {
let lastSavedConfig: unknown = null;
await page.route("**/api/config/raw_paths", (route) =>
route.fulfill({
json: {
cameras: {},
go2rtc: { streams: { [STREAM_NAME]: [streamUrl] } },
},
}),
);
await page.route("**/api/config/set", async (route) => {
lastSavedConfig = route.request().postDataJSON();
await route.fulfill({ json: { success: true, require_restart: false } });
});
return {
capturedConfig: () => lastSavedConfig,
};
}
async function expandStream(page: Page, streamName: string) {
// Each StreamCard renders the stream name as an h4 next to a rename
// button, with the chevron toggle as the last button in the header row.
// Scope to the header row (h4's grandparent) and click that last button.
const headerRow = page
.locator(`h4:text-is("${streamName}")`)
.locator("xpath=../..");
await headerRow.getByRole("button").last().click();
}
test.describe("go2rtc streams settings — ffmpeg compat mode @medium", () => {
test("preserves unknown fragments like #timeout= in the URL input", async ({
frigateApp,
}) => {
await installRawPathsRoute(frigateApp.page, FFMPEG_URL_WITH_TIMEOUT);
await frigateApp.goto("/settings?page=systemGo2rtcStreams");
await expect(
frigateApp.page.getByRole("heading", { name: STREAM_NAME }),
).toBeVisible();
await expandStream(frigateApp.page, STREAM_NAME);
const urlInput = frigateApp.page.getByPlaceholder(
"e.g., rtsp://user:pass@192.168.1.100/stream",
);
await expect(urlInput).toBeVisible();
// Focus the input so credential masking is bypassed and the raw value
// is rendered — this matches how a user would inspect the URL before
// editing it.
await urlInput.focus();
await expect(urlInput).toHaveValue(
"rtsp://user:pass@192.168.0.20:554/Stream1#timeout=10",
);
});
test("lets the user add an extra fragment in compat mode", async ({
frigateApp,
}) => {
const capture = await installRawPathsRoute(
frigateApp.page,
FFMPEG_URL_WITH_TIMEOUT,
);
await frigateApp.goto("/settings?page=systemGo2rtcStreams");
await expandStream(frigateApp.page, STREAM_NAME);
const urlInput = frigateApp.page.getByPlaceholder(
"e.g., rtsp://user:pass@192.168.1.100/stream",
);
await urlInput.focus();
await urlInput.fill(
"rtsp://user:pass@192.168.0.20:554/Stream1#timeout=10#backchannel=0",
);
await urlInput.blur();
// Reopen and re-focus to assert the new value round-tripped through
// parseFfmpegBaseAndExtras + buildFfmpegUrl back into the displayed text.
await urlInput.focus();
await expect(urlInput).toHaveValue(
"rtsp://user:pass@192.168.0.20:554/Stream1#timeout=10#backchannel=0",
);
// Save and verify the persisted URL includes both extras after the
// recognized video/audio directives.
await frigateApp.page.getByRole("button", { name: "Save" }).click();
await expect
.poll(() => capture.capturedConfig(), { timeout: 5_000 })
.toMatchObject({
config_data: {
go2rtc: {
streams: {
[STREAM_NAME]: [
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#timeout=10#backchannel=0",
],
},
},
},
});
});
test("preserves repeatable #audio= fallback chain and lets the user add another codec", async ({
frigateApp,
}) => {
const capture = await installRawPathsRoute(
frigateApp.page,
// Idiomatic go2rtc fallback: copy if source has the codec, else transcode
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#audio=opus",
);
await frigateApp.goto("/settings?page=systemGo2rtcStreams");
await expandStream(frigateApp.page, STREAM_NAME);
// Two pre-populated audio rows — one per #audio= fragment.
const audioLabel = frigateApp.page.locator(`label:text-is("Audio")`);
const audioRowsContainer = audioLabel.locator("xpath=../..");
await expect(audioRowsContainer.getByRole("combobox")).toHaveCount(2);
await expect(audioRowsContainer.getByRole("combobox").first()).toHaveText(
"Copy",
);
await expect(audioRowsContainer.getByRole("combobox").nth(1)).toHaveText(
"Transcode to Opus",
);
// Add a third audio codec via the LuPlus next to the "Audio" label.
await audioRowsContainer
.getByRole("button", { name: "Add audio codec" })
.click();
await expect(audioRowsContainer.getByRole("combobox")).toHaveCount(3);
// Change the newly-added entry to AAC.
await audioRowsContainer.getByRole("combobox").nth(2).click();
await frigateApp.page
.getByRole("option", { name: "Transcode to AAC" })
.click();
await frigateApp.page.getByRole("button", { name: "Save" }).click();
await expect
.poll(() => capture.capturedConfig(), { timeout: 5_000 })
.toMatchObject({
config_data: {
go2rtc: {
streams: {
[STREAM_NAME]: [
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#audio=opus#audio=aac",
],
},
},
},
});
});
test("LuX is only shown on fallback rows and removes only that codec", async ({
frigateApp,
}) => {
const capture = await installRawPathsRoute(
frigateApp.page,
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#audio=opus",
);
await frigateApp.goto("/settings?page=systemGo2rtcStreams");
await expandStream(frigateApp.page, STREAM_NAME);
const audioLabel = frigateApp.page.locator(`label:text-is("Audio")`);
const audioRowsContainer = audioLabel.locator("xpath=../..");
const removeButtons = audioRowsContainer.getByRole("button", {
name: "Remove codec",
});
// Primary (audio=copy) row is permanent and has no X; only the audio=opus
// fallback exposes a remove button.
await expect(removeButtons).toHaveCount(1);
await removeButtons.first().click();
await expect(audioRowsContainer.getByRole("combobox")).toHaveCount(1);
await expect(audioRowsContainer.getByRole("combobox")).toHaveText("Copy");
await frigateApp.page.getByRole("button", { name: "Save" }).click();
await expect
.poll(() => capture.capturedConfig(), { timeout: 5_000 })
.toMatchObject({
config_data: {
go2rtc: {
streams: {
[STREAM_NAME]: [
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy",
],
},
},
},
});
});
test("picking Exclude on the primary row drops the #video= fragment entirely", async ({
frigateApp,
}) => {
const capture = await installRawPathsRoute(
frigateApp.page,
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy",
);
await frigateApp.goto("/settings?page=systemGo2rtcStreams");
await expandStream(frigateApp.page, STREAM_NAME);
const videoLabel = frigateApp.page.locator(`label:text-is("Video")`);
const videoRowsContainer = videoLabel.locator("xpath=../..");
await videoRowsContainer.getByRole("combobox").first().click();
await frigateApp.page.getByRole("option", { name: "Exclude" }).click();
await frigateApp.page.getByRole("button", { name: "Save" }).click();
await expect
.poll(() => capture.capturedConfig(), { timeout: 5_000 })
.toMatchObject({
config_data: {
go2rtc: {
streams: {
[STREAM_NAME]: [
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#audio=copy",
],
},
},
},
});
});
});

33
web/package-lock.json generated
View File

@ -81,7 +81,7 @@
"strftime": "^0.10.3",
"swr": "^2.4.1",
"tailwind-merge": "^2.4.0",
"tailwind-scrollbar": "^3.1.0",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss-animate": "^1.0.7",
"use-long-press": "^3.2.0",
"vaul": "^1.1.2",
@ -5762,6 +5762,12 @@
"undici-types": "~5.26.4"
}
},
"node_modules/@types/prismjs": {
"version": "1.26.6",
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz",
"integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
@ -11896,6 +11902,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/prism-react-renderer": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz",
"integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==",
"license": "MIT",
"dependencies": {
"@types/prismjs": "^1.26.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.0.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@ -13328,14 +13347,18 @@
}
},
"node_modules/tailwind-scrollbar": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.1.0.tgz",
"integrity": "sha512-pmrtDIZeHyu2idTejfV59SbaJyvp1VRjYxAjZBH0jnyrPRo6HL1kD5Glz8VPagasqr6oAx6M05+Tuw429Z8jxg==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-4.0.2.tgz",
"integrity": "sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==",
"license": "MIT",
"dependencies": {
"prism-react-renderer": "^2.4.1"
},
"engines": {
"node": ">=12.13.0"
},
"peerDependencies": {
"tailwindcss": "3.x"
"tailwindcss": "4.x"
}
},
"node_modules/tailwindcss": {

View File

@ -95,7 +95,7 @@
"strftime": "^0.10.3",
"swr": "^2.4.1",
"tailwind-merge": "^2.4.0",
"tailwind-scrollbar": "^3.1.0",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss-animate": "^1.0.7",
"use-long-press": "^3.2.0",
"vaul": "^1.1.2",

View File

@ -6,7 +6,8 @@
"title": "يتم إعادة تشغيل فرايجيت",
"content": "العد التنازلي",
"button": "فرض إعادة التحميل الآن"
}
},
"description": "هذا سيؤدي لإيقاف Frigate مؤقتا أثناء إعادة تشغيلها"
},
"explore": {
"plus": {

View File

@ -1,3 +1,6 @@
{
"label": "اعدادات الكاميرا"
"label": "اعدادات الكاميرا",
"name": {
"label": "إسم الكاميرا"
}
}

View File

@ -1 +1,6 @@
{}
{
"version": {
"label": "إصدار الإعدادات الحالية",
"description": "نسحة عددية أو نصية من الإعدادات الحالية الفعالة للمساعدة على اكتشاف الانتقال أو التغير في الصِّيَغ"
}
}

View File

@ -1,7 +1,8 @@
{
"audio": {
"global": {
"detection": "التحري العام"
"detection": "التحري العام",
"sensitivity": "الحساسية العامة"
}
}
}

View File

@ -1 +1,4 @@
{}
{
"minimum": "يجب أن تكون {{limit}} على الأقل",
"maximum": "يجب أن تكون {{limit}} كحد أقصى"
}

View File

@ -0,0 +1,3 @@
{
"documentTitle": "المحادثات - Frigate"
}

View File

@ -0,0 +1,3 @@
{
"documentTitle": "البحث عن الحركة - Frigate"
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,503 @@
{
"speech": "Govor",
"babbling": "Babavljanje",
"bicycle": "Kolo",
"yell": "Vik",
"bellow": "Bubanj",
"whoop": "Vrisak",
"whispering": "Šaputanje",
"laughter": "Smijeh",
"snicker": "Prijem",
"crying": "Plač",
"sigh": "Usklik",
"singing": "Pjevanje",
"choir": "Hors",
"yodeling": "Jodelanje",
"chant": "Pjevanje",
"mantra": "Mantra",
"child_singing": "Dječje pjevanje",
"synthetic_singing": "Sintetičko pjevanje",
"rapping": "Rap",
"humming": "Hum",
"groan": "Grokot",
"grunt": "Groktanje",
"whistling": "Pucanje",
"breathing": "Disanje",
"wheeze": "Pijuckanje",
"snoring": "Kicanje",
"gasp": "Udah",
"pant": "Pantanje",
"snort": "Snortanje",
"cough": "Kašljanje",
"throat_clearing": "Očišćavanje grla",
"sneeze": "Prašanje",
"sniff": "Njuhanje",
"run": "Trčanje",
"shuffle": "Prelazak",
"footsteps": "Koraci",
"chewing": "Zubljanje",
"biting": "Gubitak",
"gargling": "Peranje grla",
"stomach_rumble": "Grušenje",
"burping": "Puknutje",
"hiccup": "Kikot",
"fart": "Pucanje",
"hands": "Ruke",
"finger_snapping": "Prašanje prstiju",
"clapping": "Ključanje",
"heartbeat": "Taktilno",
"heart_murmur": "Šum srca",
"cheering": "Pozdrav",
"applause": "Pozdravljati",
"chatter": "Šaputanje",
"crowd": "Gomila",
"children_playing": "Dječja igra",
"animal": "Životinja",
"pets": "Hrana",
"dog": "Pas",
"bark": "Glavu",
"yip": "Jauk",
"howl": "Vijuk",
"bow_wow": "Vau vau",
"growling": "Gručenje",
"whimper_dog": "Pijuckanje psa",
"cat": "Mačka",
"purr": "Mrmor",
"meow": "Mjau",
"hiss": "Zujanje",
"caterwaul": "Krik",
"livestock": "Stoke",
"horse": "Konj",
"clip_clop": "Klik klok",
"neigh": "Kijanje",
"cattle": "Stoke",
"moo": "Muu",
"cowbell": "Kovčeg",
"pig": "Svinja",
"oink": "Roktanje",
"goat": "Koza",
"bleat": "Blejkanje",
"sheep": "Ovca",
"fowl": "Ptica",
"chicken": "Pilica",
"cluck": "Kukanje",
"cock_a_doodle_doo": "Kukavica",
"turkey": "Gusa",
"gobble": "Gubljanje",
"duck": "Kuja",
"quack": "Kvaka",
"goose": "Guska",
"honk": "Trubljenje",
"wild_animals": "Divlja životinja",
"roaring_cats": "Vrišćeći mački",
"roar": "Vrištanje",
"bird": "Ptica",
"chirp": "Pijuckanje",
"squawk": "Krik",
"pigeon": "Papiga",
"coo": "Kukanje",
"crow": "Vran",
"caw": "Vranje",
"owl": "Kukavica",
"hoot": "Kukavica",
"flapping_wings": "Mahanje krilima",
"dogs": "Psi",
"rats": "Štakori",
"mouse": "Miš",
"patter": "Topotanje",
"insect": "Insekt",
"cricket": "Cvrčak",
"mosquito": "Komarac",
"fly": "Muha",
"buzz": "Zujanje",
"frog": "Žaba",
"croak": "Kreketanje",
"snake": "Zmija",
"rattle": "Zveckanje",
"whale_vocalization": "Glasanje kita",
"music": "Muzika",
"musical_instrument": "Muzički instrument",
"plucked_string_instrument": "Plucked String Instrument",
"guitar": "Gitara",
"electric_guitar": "Električna gitara",
"bass_guitar": "Bas gitara",
"acoustic_guitar": "Akustična gitara",
"steel_guitar": "Steel gitara",
"tapping": "Tapping",
"strum": "Strum",
"banjo": "Bendžo",
"sitar": "Sitar",
"mandolin": "Mandolina",
"zither": "Citra",
"ukulele": "Ukulele",
"keyboard": "Klaviatura",
"piano": "Klavir",
"electric_piano": "Električni piano",
"organ": "Orgulje",
"electronic_organ": "Elektronski organ",
"hammond_organ": "Hammond organ",
"synthesizer": "Sintetizator",
"sampler": "Sampler",
"harpsichord": "Harfura",
"percussion": "Percuzija",
"drum_kit": "Set bubnjeva",
"drum_machine": "Mašina za bubnjeve",
"drum": "Bubanj",
"snare_drum": "Bubanj sa zavojima",
"rimshot": "Rimshot",
"drum_roll": "Bubanj za roliranje",
"bass_drum": "Bubanj za bas",
"timpani": "Timpani",
"tabla": "Tabla",
"cymbal": "Cimbale",
"hi_hat": "Hi-Hat",
"wood_block": "Drveni blok",
"tambourine": "Tamburina",
"maraca": "Maraka",
"gong": "Gong",
"tubular_bells": "Cijevasti zvoni",
"mallet_percussion": "Percusija s mljevima",
"marimba": "Marimba",
"glockenspiel": "Glockenspiel",
"vibraphone": "Vibrafon",
"steelpan": "Stelpan",
"orchestra": "Orkestar",
"brass_instrument": "Bronski instrument",
"french_horn": "Francuski rog",
"trumpet": "Truba",
"trombone": "Trombon",
"bowed_string_instrument": "Užadno strunski instrument",
"string_section": "Strunski sekcija",
"violin": "Violina",
"pizzicato": "Pizzicato",
"cello": "Celula",
"double_bass": "Dvostruki bas",
"wind_instrument": "Vjetreni instrument",
"flute": "Flauta",
"saxophone": "Saksafon",
"clarinet": "Klarinet",
"harp": "Harfa",
"bell": "Zvono",
"church_bell": "Crkveno zvono",
"jingle_bell": "Zvono za igračke",
"bicycle_bell": "Zvono za bicikl",
"tuning_fork": "Zvučnik",
"chime": "Zvono",
"wind_chime": "Vjetrenjac",
"harmonica": "Harmonika",
"accordion": "Akkordon",
"bagpipes": "Bogovina",
"didgeridoo": "Didgeridoo",
"theremin": "Teremin",
"singing_bowl": "Pjevni čaša",
"scratching": "Skrečing",
"pop_music": "Pop muzika",
"hip_hop_music": "Hip-Hop muzika",
"beatboxing": "Bitboksing",
"rock_music": "Rock muzika",
"heavy_metal": "Heavy metal",
"punk_rock": "Punk rock",
"grunge": "Grandž",
"progressive_rock": "Progressivni rock",
"rock_and_roll": "Rock and roll",
"psychedelic_rock": "Psihederički rock",
"rhythm_and_blues": "Ritam i blues",
"soul_music": "Soul glazba",
"reggae": "Rege",
"country": "Kantri",
"swing_music": "Swing glazba",
"bluegrass": "Bluegrass",
"funk": "Fank",
"folk_music": "Folklorno glazba",
"middle_eastern_music": "Glazba Bliskog istoka",
"jazz": "Džez",
"disco": "Disko",
"classical_music": "Klasična glazba",
"opera": "Opera",
"electronic_music": "Elektronska glazba",
"house_music": "House glazba",
"techno": "Tehno",
"dubstep": "Dubstep",
"drum_and_bass": "Drum i bass",
"electronica": "Elektronika",
"electronic_dance_music": "Elektronska plesna glazba",
"ambient_music": "Ambient glazba",
"trance_music": "Trance glazba",
"music_of_latin_america": "Glazba Latinske Amerike",
"salsa_music": "Salsa glazba",
"flamenco": "Flamenko",
"blues": "Bluz",
"music_for_children": "Muzika za djecu",
"new-age_music": "Muzika novog doba",
"vocal_music": "Vokalna muzika",
"a_capella": "A Capella",
"music_of_africa": "Afrička muzika",
"afrobeat": "Afrobeat",
"christian_music": "Kršćanska muzika",
"gospel_music": "Gospel muzika",
"music_of_asia": "Azijatska muzika",
"carnatic_music": "Karnatička muzika",
"music_of_bollywood": "Bollywood muzika",
"ska": "Ska",
"traditional_music": "Tradicionalna muzika",
"independent_music": "Nezavisna muzika",
"song": "Pjesma",
"background_music": "Pozadinska muzika",
"theme_music": "Tema muzika",
"jingle": "Jingle",
"soundtrack_music": "Soundtrack muzika",
"lullaby": "Pjesma za uspavanje",
"video_game_music": "Muzika za video igre",
"christmas_music": "Božićna muzika",
"dance_music": "Dance muzika",
"wedding_music": "Venčanska glazba",
"happy_music": "Sretna glazba",
"sad_music": "Tužna glazba",
"tender_music": "Tenderna glazba",
"exciting_music": "Uzbudljiva glazba",
"angry_music": "Zlobna glazba",
"scary_music": "Strašna glazba",
"wind": "Vjetar",
"rustling_leaves": "Šum listova",
"wind_noise": "Šum vjetra",
"thunderstorm": "Grmljavina",
"thunder": "Grmljavac",
"water": "Voda",
"rain": "Kisa",
"raindrop": "Kap kise",
"rain_on_surface": "Kisa na površini",
"stream": "Tok",
"waterfall": "Padina",
"ocean": "Okean",
"waves": "Valovi",
"steam": "Par",
"gurgling": "Gurkanje",
"fire": "Vatra",
"crackle": "Krik",
"vehicle": "Vozilo",
"boat": "Brod",
"sailboat": "Jedrilica",
"rowboat": "Čamac",
"motorboat": "Motorni čamac",
"ship": "Brod",
"motor_vehicle": "Motorno vozilo",
"car": "Automobil",
"toot": "Zvuk klaksona",
"car_alarm": "Automobilski alarm",
"power_windows": "Električna prozora",
"skidding": "Klizanje",
"tire_squeal": "Krik kotača",
"car_passing_by": "Automobil prolazi",
"race_car": "Racing automobil",
"truck": "Kamion",
"air_brake": "Vazdušni kočnici",
"air_horn": "Vazdušni signal",
"reversing_beeps": "Zvukovi za odlazak unazad",
"ice_cream_truck": "Kamion za sladoled",
"bus": "Autobus",
"emergency_vehicle": "Hitni vozilo",
"police_car": "Policijski automobil",
"ambulance": "Ambulansa",
"fire_engine": "Pogonski automobil",
"motorcycle": "Motocikl",
"traffic_noise": "Prometni šum",
"rail_transport": "Željeznički transport",
"train": "Vlak",
"train_whistle": "Vlakovni svirac",
"train_horn": "Vlakovni rohorn",
"railroad_car": "Željeznički vagon",
"train_wheels_squealing": "Vlakove točkove koje zavijaju",
"subway": "Metropolitena",
"aircraft": "Avion",
"aircraft_engine": "Avionski motor",
"jet_engine": "Reaktivni motor",
"propeller": "Vijak",
"helicopter": "Heličopter",
"fixed-wing_aircraft": "Avion s krilima",
"skateboard": "Skejtbord",
"engine": "Motor",
"light_engine": "Lagani motor",
"dental_drill's_drill": "Stomatološki bušilica",
"lawn_mower": "Kosilica",
"chainsaw": "Pilica",
"medium_engine": "Srednji motor",
"heavy_engine": "Teški motor",
"engine_knocking": "Kloping motora",
"engine_starting": "Pokretanje motora",
"idling": "Miris",
"accelerating": "Ubrzavanje",
"door": "Vrata",
"doorbell": "Zvonce",
"ding-dong": "Ding-dong",
"sliding_door": "Klizna vrata",
"slam": "Zatvaranje",
"knock": "Kucanje",
"tap": "Kucanje",
"squeak": "Krik",
"cupboard_open_or_close": "Otvorenje ili zatvaranje police",
"drawer_open_or_close": "Otvorenje ili zatvaranje vunca",
"dishes": "Posuđe",
"cutlery": "Posuđe za jelo",
"chopping": "Rezanje",
"frying": "Praženje",
"microwave_oven": "Mikrotalasna pećnica",
"blender": "Miksere",
"water_tap": "Kran",
"sink": "Lavabo",
"bathtub": "Kupatilo",
"hair_dryer": "Sušilac za kosu",
"toilet_flush": "Očišćavanje toaleta",
"toothbrush": "Šetka za zube",
"electric_toothbrush": "Električna šetka za zube",
"vacuum_cleaner": "Praškoljac",
"zipper": "Zatvarac",
"keys_jangling": "Ključevi koji se škripi",
"coin": "Novčanik",
"scissors": "Škare",
"electric_shaver": "Električni šavac",
"shuffling_cards": "Premještanje karata",
"typing": "Kucanje",
"typewriter": "Tipkovnica",
"computer_keyboard": "Računalna tipkovnica",
"writing": "Pisanje",
"alarm": "Alarm",
"telephone": "Telefon",
"telephone_bell_ringing": "Zvono telefona",
"ringtone": "Ton za poziv",
"telephone_dialing": "Pozivanje telefona",
"dial_tone": "Ton za poziv",
"busy_signal": "Signal zauzetosti",
"alarm_clock": "Budilica",
"siren": "Sirena",
"civil_defense_siren": "Sirena za civilnu zaštitu",
"buzzer": "Buzer",
"smoke_detector": "Detektor dima",
"fire_alarm": "Pozar alarm",
"foghorn": "Mlazni svirac",
"whistle": "Štiklja",
"steam_whistle": "Parni zvono",
"mechanisms": "Mehanizmi",
"ratchet": "Ratchet",
"clock": "Sat",
"tick": "Tik",
"tick-tock": "Tik-tak",
"gears": "Zupčanici",
"pulleys": "Koturači",
"sewing_machine": "Šitna mašina",
"mechanical_fan": "Mehanički ventilator",
"air_conditioning": "Klima uređaj",
"cash_register": "Gotovinska kasica",
"printer": "Štampač",
"camera": "Kamera",
"single-lens_reflex_camera": "Kamera s jednim objektivom",
"tools": "Alati",
"hammer": "Klubica",
"jackhammer": "Betonomijak",
"sawing": "Sečenje",
"filing": "Flešanje",
"sanding": "Šljokanje",
"power_tool": "Električni alat",
"drill": "Bušilica",
"explosion": "Eksplozija",
"gunshot": "Pucanj",
"machine_gun": "Automatska puška",
"fusillade": "Fusiladža",
"artillery_fire": "Pucanj topovima",
"cap_gun": "Pistolj za pucanje",
"fireworks": "Pucanje svjetiljki",
"firecracker": "Svjetiljka",
"burst": "Izbič",
"eruption": "Eruptija",
"boom": "Tutnjava",
"wood": "Drvo",
"chop": "Rezanje",
"splinter": "Razlomak",
"crack": "Klackanje",
"glass": "Staklo",
"chink": "Prozor",
"shatter": "Razbijanje",
"silence": "Tišina",
"sound_effect": "Zvučni efekt",
"environmental_noise": "Okolišni šum",
"static": "Statički šum",
"white_noise": "Bijeli šum",
"pink_noise": "Rumeni šum",
"television": "Televizija",
"radio": "Radio",
"field_recording": "Snimka na terenu",
"scream": "Vrisak",
"sodeling": "Sodeling",
"chird": "Chird",
"change_ringing": "Promjena zvona",
"shofar": "Šofar",
"liquid": "Tekućina",
"splash": "Pljuskanje",
"slosh": "Sloš",
"squish": "Škripanje",
"drip": "Kapanje",
"pour": "Prelivanje",
"trickle": "Tijek",
"gush": "Gusenje",
"fill": "Popunjavanje",
"spray": "Sprajanje",
"pump": "Pumpa",
"stir": "Miješanje",
"boiling": "Vrećenje",
"sonar": "Sonar",
"arrow": "Strela",
"whoosh": "Šum",
"thump": "Tupanje",
"thunk": "Tunk",
"electronic_tuner": "Elektronski tuner",
"effects_unit": "Jedinica efekata",
"chorus_effect": "Efekt korusa",
"basketball_bounce": "Košarkaški skok",
"bang": "Bum",
"slap": "Pljeska",
"whack": "Perc",
"smash": "Sprem",
"breaking": "Raskidanje",
"bouncing": "Skakanje",
"whip": "Škripanje",
"flap": "Klizanje",
"scratch": "Oštećenje",
"scrape": "Prašenje",
"rub": "Trenje",
"roll": "Kotrljanje",
"crushing": "Stiskanje",
"crumpling": "Sklapanje",
"tearing": "Raskidanje",
"beep": "Bip",
"ping": "Poziv",
"ding": "Ding",
"clang": "Zveket",
"squeal": "Cika",
"creak": "Škripa",
"rustle": "Šuškanje",
"whir": "Brujanje",
"clatter": "Tropot",
"sizzle": "Šištanje",
"clicking": "Klikanje",
"clickety_clack": "Klik-tak",
"rumble": "Rumbljanje",
"plop": "Pljus",
"hum": "Pjevušenje",
"zing": "Zing",
"boing": "Boing",
"crunch": "Crunch",
"sine_wave": "Sinusna valna",
"harmonic": "Harmonični",
"chirp_tone": "Tanjirasti ton",
"pulse": "Impuls",
"inside": "Unutra",
"outside": "Van",
"reverberation": "Reverberacija",
"echo": "Odjek",
"noise": "Šum",
"mains_hum": "Glavni šum",
"distortion": "Distorzija",
"sidetone": "Sidetone",
"cacophony": "Kacofonija",
"throbbing": "Tremor",
"vibration": "Vibracija"
}

View File

@ -0,0 +1,326 @@
{
"time": {
"untilForTime": "Do {{time}}",
"untilForRestart": "Do ponovnog pokretanja Frigate.",
"untilRestart": "Do ponovnog pokretanja",
"never": "Nikad",
"ago": "{{timeAgo}} prije",
"justNow": "Sada",
"today": "Danas",
"yesterday": "Jučer",
"last7": "Prošlih 7 dana",
"last14": "Prošlih 14 dana",
"last30": "Prošlih 30 dana",
"thisWeek": "Ova sedmica",
"lastWeek": "Prošla sedmica",
"thisMonth": "Ovaj mjesec",
"lastMonth": "Prošli mjesec",
"5minutes": "5 minuta",
"10minutes": "10 minuta",
"30minutes": "30 minuta",
"1hour": "1 sat",
"12hours": "12 sati",
"24hours": "24 sata",
"pm": "posle podne",
"am": "pre podne",
"yr": "{{time}} god",
"year_one": "{{time}} godina",
"year_few": "{{time}} godine",
"year_other": "{{time}} godina",
"mo": "{{time}} mjes",
"month_one": "{{time}} mjesec",
"month_few": "{{time}} mjeseca",
"month_other": "{{time}} mjeseci",
"d": "{{time}}d",
"day_one": "{{time}} dan",
"day_few": "{{time}} dana",
"day_other": "{{time}} dana",
"h": "{{time}}h",
"hour_one": "{{time}} sat",
"hour_few": "{{time}} sata",
"hour_other": "{{time}} sati",
"m": "{{time}}m",
"minute_one": "{{time}} minuta",
"minute_few": "{{time}} minute",
"minute_other": "{{time}} minuta",
"s": "{{time}}s",
"second_one": "{{time}} sekunda",
"second_few": "{{time}} sekunde",
"second_other": "{{time}} sekundi",
"formattedTimestamp": {
"12hour": "MMM d, h:mm:ss aaa",
"24hour": "MMM d, HH:mm:ss"
},
"formattedTimestamp2": {
"12hour": "MM/dd h:mm:ssa",
"24hour": "d MMM HH:mm:ss"
},
"formattedTimestampHourMinute": {
"12hour": "h:mm aaa",
"24hour": "HH:mm"
},
"formattedTimestampHourMinuteSecond": {
"12hour": "h:mm:ss aaa",
"24hour": "HH:mm:ss"
},
"formattedTimestampMonthDayHourMinute": {
"12hour": "MMM d, h:mm aaa",
"24hour": "MMM d, HH:mm"
},
"formattedTimestampMonthDayYear": {
"12hour": "MMM d, yyyy",
"24hour": "MMM d, yyyy"
},
"formattedTimestampMonthDayYearHourMinute": {
"12hour": "MMM d yyyy, h:mm aaa",
"24hour": "MMM d yyyy, HH:mm"
},
"formattedTimestampMonthDay": "MMM d",
"formattedTimestampFilename": {
"12hour": "MM-dd-yy-h-mm-ss-a",
"24hour": "MM-dd-yy-HH-mm-ss"
},
"inProgress": "U toku",
"invalidStartTime": "Neispravno početno vrijeme",
"invalidEndTime": "Neispravno krajnje vrijeme"
},
"unit": {
"speed": {
"mph": "mph",
"kph": "kph"
},
"length": {
"feet": "fut",
"meters": "metar"
},
"data": {
"kbps": "kB/s",
"mbps": "MB/s",
"gbps": "GB/s",
"kbph": "kB/hour",
"mbph": "MB/hour",
"gbph": "GB/hour"
}
},
"label": {
"back": "Povratak",
"hide": "Sakrij {{item}}",
"show": "Prikaži {{item}}",
"ID": "ID",
"none": "Nijedan",
"all": "Sve",
"other": "Ostalo"
},
"list": {
"two": "{{0}} i {{1}}",
"many": "{{items}}, i {{last}}",
"separatorWithSpace": ", "
},
"field": {
"optional": "Opcionalno",
"internalID": "Unutarnji ID koji Frigate koristi u konfiguraciji i bazi podataka"
},
"button": {
"add": "Dodaj",
"apply": "Primijeni",
"applying": "Primjenjuje se…",
"reset": "Resetuj",
"undo": "Poništi",
"done": "Gotovo",
"enabled": "Omogućeno",
"enable": "Omogući",
"disabled": "Onemogućeno",
"disable": "Onemogući",
"save": "Sačuvaj",
"saving": "Sačuvanje…",
"cancel": "Otkaži",
"close": "Zatvori",
"copy": "Kopiraj",
"copiedToClipboard": "Kopirano u međuspremnik",
"back": "Nazad",
"history": "Historija",
"fullscreen": "Pun ekran",
"exitFullscreen": "Napusti pun ekran",
"pictureInPicture": "Slika u slici",
"twoWayTalk": "Dvostrani razgovor",
"cameraAudio": "Zvuk kamere",
"on": "Uključeno",
"off": "Isključeno",
"edit": "Uredi",
"copyCoordinates": "Kopiraj koordinate",
"delete": "Obriši",
"yes": "Da",
"no": "Ne",
"download": "Preuzmi",
"info": "Informacija",
"suspended": "Otkazano",
"unsuspended": "Ponovi",
"play": "Reproduciraj",
"unselect": "Odznači",
"export": "Izvoz",
"deleteNow": "Obriši sada",
"next": "Sljedeće",
"continue": "Nastavi",
"modified": "Izmijenjeno",
"overridden": "Preklopljeno",
"resetToGlobal": "Vrati na globalno",
"resetToDefault": "Vrati na podrazumijevano",
"saveAll": "Sačuvaj sve",
"savingAll": "Sačuvanje svih…",
"undoAll": "Poništi sve",
"retry": "Pokušaj ponovno"
},
"menu": {
"system": "Sistem",
"systemMetrics": "Sistem metrike",
"configuration": "Konfiguracija",
"systemLogs": "Sistemski zapisi",
"profiles": "Profili",
"settings": "Postavke",
"configurationEditor": "Uređivač konfiguracije",
"languages": "Jezici",
"language": {
"en": "Engleski (English)",
"es": "Španjolski (Spanish)",
"zhCN": "Jednostavni kineski (Simplified Chinese)",
"hi": "Hindi (Hindi)",
"fr": "Francuski (French)",
"ar": "Arapski (Arabic)",
"pt": "Portugalski (Portuguese)",
"ptBR": "Portugalski brazilski (Brazilian Portuguese)",
"ru": "Ruski (Russian)",
"de": "Nemački (German)",
"ja": "Japanski (Japanese)",
"tr": "Turski (Turkish)",
"it": "Talijanski (Italian)",
"nl": "Nizozemski (Dutch)",
"sv": "Švedski (Swedish)",
"cs": "Češki (Czech)",
"nb": "Norveški bokmål (Norwegian Bokmål)",
"ko": "Koreanski (Korean)",
"vi": "Vietnamski (Vietnamese)",
"fa": "Perzijski (Persian)",
"pl": "Polski (Poljski)",
"uk": "Українська (Ukrajinski)",
"he": "עברית (Hebrejski)",
"el": "Ελληνικά (Grčki)",
"ro": "Română (Romunski)",
"hu": "Magyar (Mađarski)",
"fi": "Suomi (Finski)",
"da": "Dansk (Danski)",
"sk": "Slovenčina (Slovački)",
"yue": "粵語 (Kantonski)",
"th": "ไทย (Tajski)",
"ca": "Català (Katalonski)",
"hr": "Hrvatski (Hrvatski)",
"sr": "Српски (Srpski)",
"sl": "Slovenščina (Slovenski)",
"lt": "Lietuvių (Lietuvių)",
"bg": "Български (Bugarinski)",
"gl": "Galego (Galicijski)",
"id": "Bahasa Indonesia (Indoneziski)",
"ur": "اردو (Urdu)",
"withSystem": {
"label": "Koristite postavke sistema za jezik"
}
},
"appearance": "Izgled",
"darkMode": {
"label": "Tamni režim",
"light": "Svijetla",
"dark": "Tamna",
"withSystem": {
"label": "Koristite postavke sistema za svjetlosni ili tamni režim"
}
},
"withSystem": "Sistem",
"theme": {
"label": "Tema",
"blue": "Plava",
"green": "Zelena",
"nord": "Nord",
"red": "Crvena",
"highcontrast": "Visok kontrast",
"default": "Zadano"
},
"help": "Pomoć",
"documentation": {
"title": "Dokumentacija",
"label": "Dokumentacija za Frigate"
},
"restart": "Ponovno pokreni Frigate",
"live": {
"title": "Uživo",
"allCameras": "Sve Kamere",
"cameras": {
"title": "Kamere",
"count_one": "{{count}} Kamera",
"count_few": "{{count}} Kamere",
"count_other": "{{count}} Kamere"
}
},
"review": "Pregled",
"explore": "Istraži",
"export": "Izvoz",
"actions": "Akcije",
"uiPlayground": "UI Playground",
"features": "Funkcije",
"faceLibrary": "Biblioteka lica",
"classification": "Klasifikacija",
"chat": "Razgovor",
"user": {
"title": "Korisnik",
"account": "Račun",
"current": "Trenutni korisnik: {{user}}",
"anonymous": "anons",
"logout": "Odjava",
"setPassword": "Postavi lozinku"
}
},
"toast": {
"copyUrlToClipboard": "URL kopiran u međuspremnik.",
"save": {
"title": "Sačuvaj",
"error": {
"title": "Nije uspješno sačuvana promjena konfiguracije: {{errorMessage}}",
"noMessage": "Nije uspješno sačuvana promjena konfiguracije"
},
"success": "Uspješno sačuvana promjena konfiguracije."
}
},
"role": {
"title": "Uloga",
"admin": "Administrator",
"viewer": "Pregledač",
"desc": "Admini imaju pun pristup svim funkcijama u korisničkom sučelju Frigate. Pregledači su ograničeni na pregled kamere, pregled stavki i povijesne snimke u korisničkom sučelju."
},
"pagination": {
"label": "paginacija",
"previous": {
"title": "Prethodno",
"label": "Idi na prethodnu stranicu"
},
"next": {
"title": "Sljedeće",
"label": "Idi na sljedeću stranicu"
},
"more": "Više stranica"
},
"accessDenied": {
"documentTitle": "Pristup odbijen - Frigate",
"title": "Pristup odbijen",
"desc": "Nemate dozvolu za pregled ove stranice."
},
"notFound": {
"documentTitle": "Nije pronađeno - Frigate",
"title": "404",
"desc": "Stranica nije pronađena"
},
"selectItem": "Odaberite {{item}}",
"readTheDocumentation": "Pročitajte dokumentaciju",
"information": {
"pixels": "{{area}}px"
},
"no_items": "Nema stavki",
"validation_errors": "Greške validacije"
}

View File

@ -0,0 +1,16 @@
{
"form": {
"user": "Korisničko ime",
"password": "Lozinka",
"login": "Prijava",
"firstTimeLogin": "Pokušavate se prijaviti prvi put? Vjerodajnice su ispisane u logovima Frigate.",
"errors": {
"usernameRequired": "Korisničko ime je obavezno",
"passwordRequired": "Lozinka je obavezna",
"rateLimit": "Premašen je limit brzine. Pokušajte kasnije.",
"loginFailed": "Prijava nije uspješna",
"unknownError": "Nepoznata greška. Provjerite zapise.",
"webUnknownError": "Nepoznata greška. Provjerite konzolne zapise."
}
}
}

View File

@ -0,0 +1,87 @@
{
"group": {
"label": "Grupe kamere",
"add": "Dodaj grupu kamere",
"edit": "Uredi grupu kamera",
"delete": {
"label": "Obriši grupu kamere",
"confirm": {
"title": "Potvrdi brisanje",
"desc": "Sigurno li želite da obrišete grupu kamere <em>{{name}}</em>?"
}
},
"name": {
"label": "Ime",
"placeholder": "Unesite ime…",
"errorMessage": {
"mustLeastCharacters": "Ime grupe kamere mora imati najmanje 2 karaktera.",
"exists": "Ime grupe kamere već postoji.",
"nameMustNotPeriod": "Ime grupe kamere ne smije sadržavati tačku.",
"invalid": "Neispravno ime grupe kamere."
}
},
"cameras": {
"label": "Kamere",
"desc": "Odaberite kamere za ovu grupu."
},
"icon": "Ikona",
"success": "Grupa kamere ({{name}}) je sačuvana.",
"camera": {
"birdseye": "Birdseye",
"setting": {
"label": "Postavke prenošenja kamere",
"title": "Postavke prenošenja {{cameraName}}",
"desc": "Promijenite opcije uživo prenošenja za tablicu upravljanja ove grupe kamere. <em>Ove postavke su specifične za uređaj/pretvarač.</em>",
"audioIsAvailable": "Audio je dostupan za ovaj stream",
"audioIsUnavailable": "Zvuk nije dostupan za ovaj tok",
"audio": {
"tips": {
"title": "Audio mora biti izlaz iz vaše kamere i konfiguriran u go2rtc za ovaj stream."
}
},
"stream": "Tok",
"placeholder": "Odaberite tok",
"streamMethod": {
"label": "Način prenošenja",
"placeholder": "Odaberite način prenošenja",
"method": {
"noStreaming": {
"label": "Bez prenošenja",
"desc": "Slike kamere će se ažurirati samo jednom na minut i neće se dogoditi uživo prenošenje."
},
"smartStreaming": {
"label": "Pametno prenošenje (preporučeno)",
"desc": "Pametno prenošenje će ažurirati sliku kamere jednom na minut kada se ne događa detektovana aktivnost kako bi se uštedjelo na širovini i resursima. Kada se detektuje aktivnost, slika se glatko prebacuje u uživo prenošenje."
},
"continuousStreaming": {
"label": "Neprekidno prenošenje",
"desc": {
"title": "Slika kamere uvijek će biti živo prenošenje kada je vidljiva na ploči, čak i ako se ne detektira aktivnost.",
"warning": "Neprekidno prenošenje može uzrokovati visoku upotrebu širine pojasa i probleme s performansama. Koristite s oprezom."
}
}
}
},
"compatibilityMode": {
"label": "Režim kompatibilnosti",
"desc": "Omogućite ovu opciju samo ako se živo prenošenje vaše kamere prikazuje s bojnim artefaktima i dijagonalnom linijom na desnoj strani slike."
}
}
}
},
"debug": {
"options": {
"label": "Postavke",
"title": "Opcije",
"showOptions": "Prikaži opcije",
"hideOptions": "Sakrij opcije"
},
"boundingBox": "Okvir",
"timestamp": "Vremenski pečat",
"zones": "Zone",
"mask": "Maska",
"motion": "Kretanje",
"regions": "Regije",
"paths": "Putanje"
}
}

View File

@ -0,0 +1,197 @@
{
"restart": {
"title": "Sigurni li ste da želite ponovno pokrenuti Frigate?",
"description": "Ovo privremeno zaustavi Frigate dok se ponovno pokreće.",
"button": "Ponovno pokretanje",
"restarting": {
"title": "Frigate se ponovo pokreće",
"content": "Ova stranica će se ponovno učitati za {{countdown}} sekundi.",
"button": "Silovito ponovno učitavanje sada"
}
},
"explore": {
"plus": {
"submitToPlus": {
"label": "Pošalji na Frigate+",
"desc": "Predmeti u lokacijama koje želite izbjeći nisu lažni pozitivi. Pošiljanje ih kao lažne pozitive zbunjuje model."
},
"review": {
"question": {
"label": "Potvrdite ovu oznaku za Frigate Plus",
"ask_a": "Je li ovaj objekt <code>{{label}}</code>?",
"ask_an": "Je li ovaj objekt <code>{{label}}</code>?",
"ask_full": "Je li ovaj objekt <code>{{untranslatedLabel}}</code> ({{translatedLabel}})?"
},
"state": {
"submitted": "Pošlato"
}
}
},
"video": {
"viewInHistory": "Pregledajte u povijesti"
}
},
"export": {
"time": {
"fromTimeline": "Odaberite iz vremenske linije",
"lastHour_one": "Prošli sat",
"lastHour_few": "Prošla {{count}} sata",
"lastHour_other": "Prošlih {{count}} sati",
"custom": "Prilagođeno",
"start": {
"title": "Vrijeme početka",
"label": "Odaberite vrijeme početka"
},
"end": {
"title": "Vrijeme kraja",
"label": "Odaberite vrijeme kraja"
}
},
"name": {
"placeholder": "Nazovite izvoz"
},
"case": {
"newCaseOption": "Napravite novi slučaj",
"newCaseNamePlaceholder": "Novo ime slučaja",
"newCaseDescriptionPlaceholder": "Opis slučaja",
"label": "Slučaj",
"nonAdminHelp": "Za ove izvoze će se stvoriti novi slučaj.",
"placeholder": "Odaberite slučaj"
},
"select": "Odaberite",
"export": "Izvoz",
"queueing": "Stavljanje izvoza u red...",
"selectOrExport": "Odaberite ili izvozite",
"tabs": {
"export": "Jedna kamera",
"multiCamera": "Više kamera"
},
"multiCamera": {
"timeRange": "Vremenski opseg",
"selectFromTimeline": "Odaberite iz vremenske linije",
"cameraSelection": "Kamere",
"cameraSelectionHelp": "Kamere s praćenim objektima u ovom vremenskom opsegu su preselektirane",
"checkingActivity": "Provjeravamo aktivnost kamere...",
"noCameras": "Nema dostupnih kamera",
"detectionCount_one": "1 praćen objekt",
"detectionCount_few": "{{count}} praćena objekta",
"detectionCount_other": "{{count}} praćenih objekata",
"nameLabel": "Ime izvoza",
"namePlaceholder": "Nepovlačenje baznog imena za ove izvoze",
"queueingButton": "Stavljanje izvoza u red...",
"exportButton_one": "Izvoz 1 kamere",
"exportButton_few": "Izvoz {{count}} kamere",
"exportButton_other": "Izvoz {{count}} kamera"
},
"multi": {
"title_one": "Izvoz 1 pregleda",
"title_few": "Izvoz {{count}} pregleda",
"title_other": "Izvoz {{count}} pregleda",
"description": "Izvoz svakog odabranih pregleda. Svi izvozi bit će grupirani pod jedan slučaj.",
"descriptionNoCase": "Izvoz svakog odabranih pregleda.",
"caseNamePlaceholder": "Pregled izvoza - {{date}}",
"exportButton_one": "Izvoz 1 pregleda",
"exportButton_few": "Izvoz {{count}} pregleda",
"exportButton_other": "Izvoz {{count}} pregleda",
"exportingButton": "Izvoz...",
"toast": {
"started_one": "Pokrenut 1 izvoz. Otvaranje slučaja sada.",
"started_few": "Pokrenuta {{count}} izvoza. Otvaranje slučaja sada.",
"started_other": "Pokrenuto {{count}} izvoza. Otvaranje slučaja sada.",
"startedNoCase_one": "Pokrenut 1 izvoz.",
"startedNoCase_few": "Pokrenuta {{count}} izvoza.",
"startedNoCase_other": "Pokrenuto {{count}} izvoza.",
"partial": "Pokrenuto {{successful}} od {{total}} izvoza. Neuspješno: {{failedItems}}",
"failed": "Neuspješno pokretanje {{total}} izvoza. Neuspješno: {{failedItems}}"
}
},
"toast": {
"success": "Uspješno pokrenut izvoz. Pregledajte datoteku na stranici izvoza.",
"queued": "Izvoz u redu. Pregledajte napredak na stranici izvoza.",
"view": "Pregled",
"batchSuccess_one": "Pokrenut 1 izvoz. Otvaranje slučaja sada.",
"batchSuccess_few": "Pokrenuta {{count}} izvoza. Otvaranje slučaja sada.",
"batchSuccess_other": "Pokrenuto {{count}} izvoza. Otvaranje slučaja sada.",
"batchPartial": "Pokrenuto {{successful}} od {{total}} izvoza. Neuspješne kamere: {{failedCameras}}",
"batchFailed": "Neuspješno pokretanje {{total}} izvoza. Neuspješne kamere: {{failedCameras}}",
"batchQueuedSuccess_one": "U red stavljen 1 izvoz. Otvaranje slučaja sada.",
"batchQueuedSuccess_few": "U red stavljena {{count}} izvoza. Otvaranje slučaja sada.",
"batchQueuedSuccess_other": "U red stavljeno {{count}} izvoza. Otvaranje slučaja sada.",
"batchQueuedPartial": "U redu {{successful}} od {{total}} izvoza. Neuspješne kamere: {{failedCameras}}",
"batchQueueFailed": "Neuspješno dodavanje {{total}} izvoza. Neuspješne kamere: {{failedCameras}}",
"error": {
"failed": "Neuspješno dodavanje izvoza: {{error}}",
"endTimeMustAfterStartTime": "Krajnje vrijeme mora biti nakon početnog vremena",
"noVaildTimeSelected": "Nije odabran valjan vremenski opseg"
}
},
"fromTimeline": {
"saveExport": "Sačuvaj izvoz",
"queueingExport": "Kopiranje izvoza...",
"previewExport": "Pregled izvoza",
"useThisRange": "Koristi ovaj opseg"
}
},
"streaming": {
"label": "Tok",
"restreaming": {
"disabled": "Restreaming nije omogućeno za ovu kameru.",
"desc": {
"title": "Postavite go2rtc za dodatne opcije uživog pregleda i zvuk za ovu kameru."
}
},
"showStats": {
"label": "Prikaži statistiku strima",
"desc": "Omogući ovu opciju da prikaže statistiku prijenosa kao preklapanje na toku kamere."
},
"debugView": "Pregled za otklanjanje grešaka"
},
"search": {
"saveSearch": {
"label": "Sačuvaj pretragu",
"desc": "Navedite ime za ovu sačuvanu pretragu.",
"placeholder": "Unesite ime za svoju pretragu",
"overwrite": "{{searchName}} već postoji. Sačuvavanje će prebrisati postojet će vrijednost.",
"success": "Pretraga ({{searchName}}) je sačuvana.",
"button": {
"save": {
"label": "Sačuvaj ovu pretragu"
}
}
}
},
"recording": {
"shareTimestamp": {
"label": "Dijeli vremensku oznaku",
"title": "Dijeli vremensku oznaku",
"description": "Dijelite URL označen vremenom trenutne pozicije igrača ili odaberite prilagođenu vremensku oznaku. Napomena: ovo nije javni URL za dijeljenje i dostupan je samo korisnicima koji imaju pristup Frigate i ovoj kameri.",
"custom": "Prilagođena vremenska oznaka",
"button": "URL za dijeljenje vremenske oznake",
"shareTitle": "Vremenska oznaka pregleda Frigate: {{camera}}"
},
"confirmDelete": {
"title": "Potvrdi brisanje",
"desc": {
"selected": "Sigurni li ste da želite izbrisati sve snimljeno video povezano s ovim preglednim stavkom?<br /><br />Zadržite tipku <em>Shift</em> da biste preskočili ovaj dijalog u budućnosti."
},
"toast": {
"success": "Video snimke povezane s odabranim preglednim stavcima uspješno su izbrisane.",
"error": "Neuspješno brisanje: {{error}}"
}
},
"button": {
"export": "Izvoz",
"markAsReviewed": "Označi kao pregledano",
"markAsUnreviewed": "Označi kao nepregledano",
"deleteNow": "Obriši sada"
}
},
"imagePicker": {
"selectImage": "Odaberite minijaturu praćenog objekta",
"unknownLabel": "Sačuvana slika izazivača",
"search": {
"placeholder": "Pretraga po oznaci ili podoznaci..."
},
"noImages": "Nema mini prikaza za ovu kameru"
}
}

View File

@ -0,0 +1,140 @@
{
"filter": "Filtar",
"classes": {
"label": "Klase",
"all": {
"title": "Sve klase"
},
"count_one": "{{count}} Klasa",
"count_other": "{{count}} Klase"
},
"labels": {
"label": "Oznake",
"all": {
"title": "Sve oznake",
"short": "Oznake"
},
"count_one": "{{count}} Oznaka",
"count_other": "{{count}} Oznake"
},
"zones": {
"label": "Zone",
"all": {
"title": "Sve zone",
"short": "Zone"
}
},
"dates": {
"selectPreset": "Odaberite predpostavku…",
"all": {
"title": "Svi datumi",
"short": "Datumi"
}
},
"more": "Više filtera",
"reset": {
"label": "Poništi filtere na zadane vrijednosti"
},
"timeRange": "Vremenski opseg",
"subLabels": {
"label": "Podoznake",
"all": "Sve podoznake"
},
"attributes": {
"label": "Atributi klasifikacije",
"all": "Svi atributi"
},
"score": "Rezultat",
"estimatedSpeed": "Procijenjena brzina ({{unit}})",
"features": {
"label": "Funkcije",
"hasSnapshot": "Ima snimak",
"hasVideoClip": "Ima video zapis",
"submittedToFrigatePlus": {
"label": "Predano Frigate+",
"tips": "Prvo morate filtrirati prateće objekte koji imaju snimak.<br /><br />Prateći objekti bez snimka ne mogu se poslati na Frigate+."
}
},
"sort": {
"label": "Sortiraj",
"dateAsc": "Datum (Uzlazno)",
"dateDesc": "Datum (Silazno)",
"scoreAsc": "Ocjena objekta (Uzlazno)",
"scoreDesc": "Ocjena objekta (Silazno)",
"speedAsc": "Procijenjena brzina (Uzlazno)",
"speedDesc": "Procijenjena brzina (Silazno)",
"relevance": "Relevantnost"
},
"cameras": {
"label": "Filter kamere",
"all": {
"title": "Sve Kamere",
"short": "Kamere"
}
},
"review": {
"showReviewed": "Prikaži pregledane"
},
"motion": {
"showMotionOnly": "Prikaži samo pokret"
},
"explore": {
"settings": {
"title": "Postavke",
"defaultView": {
"title": "Zadani prikaz",
"desc": "Kada nisu odabrani filteri, prikazuje se sažetak najnovijih pratećih objekata po oznaci, ili prikazuje se mreža bez filtriranja.",
"summary": "Sažetak",
"unfilteredGrid": "Mreža bez filtriranja"
},
"gridColumns": {
"title": "Kolone mreže",
"desc": "Odaberite broj kolona u prikazu mreže."
},
"searchSource": {
"label": "Izvor pretrage",
"desc": "Odaberite da li ćete pretraživati miniaturne slike ili opise vaših praćenih objekata.",
"options": {
"thumbnailImage": "Miniaturna slika",
"description": "Opis"
}
}
},
"date": {
"selectDateBy": {
"label": "Odaberite datum za filtriranje"
}
}
},
"logSettings": {
"label": "Filtrirajte nivo zapisa",
"filterBySeverity": "Filtrirajte zapise prema ozbiljnosti",
"loading": {
"title": "Učitavanje",
"desc": "Kada se panel zapisa pomakne do dna, novi zapisi automatski se prikazuju kada se dodaju."
},
"disableLogStreaming": "Onemogući praćenje zapisa",
"allLogs": "Svi zapisi"
},
"trackedObjectDelete": {
"title": "Potvrdi brisanje",
"desc": "Brisanje ovih {{objectLength}} praćenih objekata uklanja snimku, bilo koje sačuvane ugradnje, i sve povezane uloge objekata. Snimljeni materijal ovih praćenih objekata u pogledu Historija <em>NEĆE</em> biti obrisan.<br /><br />Sigurni ste da želite nastaviti?<br /><br />Zadržite tipku <em>Shift</em> da biste preskočili ovaj dijalog u budućnosti.",
"toast": {
"success": "Praćeni objekti uspješno obrisani.",
"error": "Neuspješno brisanje praćenih objekata: {{errorMessage}}"
}
},
"zoneMask": {
"filterBy": "Filtriraj po maski zone"
},
"recognizedLicensePlates": {
"title": "Prepoznate tablice",
"loadFailed": "Neuspješno učitavanje prepoznatih tablica.",
"loading": "Učitavanje prepoznatih tablica…",
"placeholder": "Unesite za pretragu tablica…",
"noLicensePlatesFound": "Nema pronađenih tablica.",
"selectPlatesFromList": "Odaberite jednu ili više tablica iz liste.",
"selectAll": "Odaberite sve",
"clearAll": "Očistite sve"
}
}

View File

@ -0,0 +1,8 @@
{
"iconPicker": {
"selectIcon": "Odaberite ikonu",
"search": {
"placeholder": "Pretražite ikonu…"
}
}
}

View File

@ -0,0 +1,10 @@
{
"button": {
"downloadVideo": {
"label": "Preuzimanje videa",
"toast": {
"success": "Vaš video stavke pregleda je započelo preuzimanje."
}
}
}
}

View File

@ -0,0 +1,52 @@
{
"noRecordingsFoundForThisTime": "Nisu pronađeni snimci za ovo vrijeme",
"noPreviewFound": "Nije pronađen pregled",
"noPreviewFoundFor": "Nije pronađen pregled za {{cameraName}}",
"submitFrigatePlus": {
"title": "Pošalji ovaj okvir Frigate+?",
"submit": "Pošalji",
"previewError": "Nije moguće učitati prikaz snimke. Snimka možda trenutno nije dostupna."
},
"livePlayerRequiredIOSVersion": "Za ovaj tip uživo prijenosa potreban je iOS 17.1 ili noviji.",
"streamOffline": {
"title": "Prijenos je offline",
"desc": "Nisu primljeni okviri na {{cameraName}} <code>detect</code> prijenos, provjerite zapise o greškama"
},
"cameraDisabled": "Kamera je onemogućena",
"stats": {
"streamType": {
"title": "Tip prijenosa:",
"short": "Tip"
},
"bandwidth": {
"title": "Širina pojasa:",
"short": "Širina pojasa"
},
"latency": {
"title": "Kasnjenje:",
"value": "{{seconds}} sekundi",
"short": {
"title": "Kasnjenje",
"value": "{{seconds}} sek"
}
},
"totalFrames": "Ukupno okvira:",
"droppedFrames": {
"title": "Izgubljeni okviri:",
"short": {
"title": "Izgubljeni",
"value": "{{droppedFrames}} okvira"
}
},
"decodedFrames": "Dekodirani okviri:",
"droppedFrameRate": "Stopa izgubljenih okvira:"
},
"toast": {
"success": {
"submittedFrigatePlus": "Uspješno je poslano okvir Frigate+"
},
"error": {
"submitFrigatePlusFailed": "Neuspješno slanje okvira Frigate+"
}
}
}

View File

@ -0,0 +1,949 @@
{
"label": "KameraKonfig",
"zones": {
"label": "Zone",
"description": "Zona omogućava da definirate specifičnu područje okvira da biste odredili je li objekt unutar određenog područja.",
"friendly_name": {
"label": "Ime zone",
"description": "Korisničko ime za zonu, prikazano u UI Frigate. Ako nije postavljeno, koristi se oblikovana verzija imena zone."
},
"enabled": {
"label": "Omogućeno",
"description": "Omogući ili onemogući ovu zonu. Onemogućene zone zanemaruju se tijekom izvršavanja."
},
"enabled_in_config": {
"label": "Zapamti originalno stanje zone."
},
"filters": {
"label": "Filtri zone",
"description": "Filtri za primjenu na objekte unutar ove zone. Koriste se za smanjenje lažnih pozitiva ili ograničavanje kojih objekata se smatraju prisutnim u zoni.",
"min_area": {
"label": "Minimalna površina objekta",
"description": "Minimalna površina okvira (pikseli ili postotak) potrebna za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)."
},
"max_area": {
"label": "Maksimalna površina objekta",
"description": "Maksimalna površina okvira (pikseli ili postotak) dozvoljena za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)."
},
"min_ratio": {
"label": "Minimalni omjer visine/širine",
"description": "Minimalni omjer širine/visine potreban da bi okvir bio prihvaćen."
},
"max_ratio": {
"label": "Maksimalni omjer visine/širine",
"description": "Maksimalni omjer širine/visine dozvoljen da bi okvir bio prihvaćen."
},
"threshold": {
"label": "Prag pouzdanosti",
"description": "Prosjek pragova pouzdanosti detekcije potreban da bi objekt bio smatravan pravim pozitivom."
},
"min_score": {
"label": "Minimalna pouzdanost",
"description": "Minimalna pouzdanost detekcije po okviru potrebna da bi objekt bio brojan."
},
"mask": {
"label": "Maska filtriranja",
"description": "Koordinate poligona koje definiraju područje na kojem se ovaj filter primjenjuje unutar okvira."
},
"raw_mask": {
"label": "Ručna maska"
}
},
"coordinates": {
"label": "Koordinate",
"description": "Koordinate poligona koje definiraju područje zone. Može biti niz razdvojen zarezom ili lista nizova koordinata. Koordinate trebaju biti relativne (0-1) ili apsolutne (stariji format)."
},
"distances": {
"label": "Stvarne udaljenosti",
"description": "Nepovlačni stvarne udaljenosti za svaku stranu kvadrilateralne zone, koristi se za izračun brzine ili udaljenosti. Moraju imati tačno 4 vrijednosti ako su postavljene."
},
"inertia": {
"label": "Okviri inertnosti",
"description": "Broj uzastopnih okvira u kojima mora biti detektovan objekt u zoni da bi bio smatravan prisutnim. Pomaže u filtriranju privremenih detekcija."
},
"loitering_time": {
"label": "Sekunde loiteranja",
"description": "Broj sekundi koje objekt mora ostati u zoni da bi bio smatravan loiteranjem. Postaviti na 0 za onemogućavanje detekcije loiteranja."
},
"speed_threshold": {
"label": "Minimalna brzina",
"description": "Minimalna brzina (u stvarnim jedinicama ako su udaljenosti postavljene) potrebna da bi objekt bio smatravan prisutnim u zoni. Koristi se za zone koje se aktiviraju na osnovu brzine."
},
"objects": {
"label": "Objekti koji izazivaju",
"description": "Lista tipova objekata (iz labelmapa) koji mogu izazvati ovu zonu. Može biti niz ili lista nizova. Ako je prazna, svi objekti se uzimaju u obzir."
}
},
"name": {
"label": "Ime kamere",
"description": "Ime kamere je obavezno"
},
"friendly_name": {
"label": "Prijateljsko ime",
"description": "Prijateljsko ime kamere korišteno u korisničkom sučelju Frigate"
},
"enabled": {
"label": "Omogućeno",
"description": "Omogućeno"
},
"audio": {
"label": "Audio događaji",
"description": "Postavke za detekciju događaja temeljene na audio.",
"enabled": {
"label": "Omogući detekciju zvuka",
"description": "Omogući ili onemogući detekciju događaja temeljenu na audio za ovu kameru."
},
"max_not_heard": {
"label": "Vrijeme trajanja do kraja",
"description": "Količina sekundi bez konfiguriranog tipa zvuka prije nego što se audio događaj završi."
},
"min_volume": {
"label": "Minimalna zapremina",
"description": "Minimalni prag RMS zapremine potreban za pokretanje detekcije zvuka; niže vrijednosti povećavaju osjetljivost (npr. 200 visoko, 500 srednje, 1000 nisko)."
},
"listen": {
"label": "Tipovi slušanja",
"description": "Popis tipova audio događaja za detekciju (npr. zavijanje, požarne zvona, vrisak, govorenje, vikanje)."
},
"filters": {
"label": "Audio filteri",
"description": "Postavke filtera po tipu zvuka kao što su pragovi pouzdanosti za smanjenje lažnih pozitiva."
},
"enabled_in_config": {
"label": "Originalno stanje zvuka",
"description": "Indikuje je li detekcija zvuka izvorno omogućena u statičkoj konfiguracijskoj datoteci."
},
"num_threads": {
"label": "Dretve detekcije",
"description": "Broj dretvi za korištenje za obradu detekcije zvuka."
}
},
"audio_transcription": {
"label": "Transkripcija zvuka",
"description": "Postavke za transkripciju živog i govornog zvuka korištenih za događaje i žive podnaslove.",
"enabled": {
"label": "Omogući transkripciju",
"description": "Omogući ili onemogući transkripciju audio događaja pokrenutu ručno."
},
"enabled_in_config": {
"label": "Originalni stanje transkripcije"
},
"live_enabled": {
"label": "Uživo transkripcija",
"description": "Omogući streaming uživo transkripcije za audio dok se prima."
}
},
"birdseye": {
"label": "Birdseye",
"description": "Postavke za sastavni prikaz Birdseye koji kombinuje više snimke kamere u jedinstveni raspored.",
"enabled": {
"label": "Omogući Birdseye",
"description": "Omogući ili onemogući funkciju prikaza Birdseye."
},
"mode": {
"label": "Način praćenja",
"description": "Način uključivanja kamera u Birdseye: 'objekti', 'kretanje' ili 'kontinuirano'."
},
"order": {
"label": "Pozicija",
"description": "Numerička pozicija koja kontroliše redoslijed kamera u rasporedu Birdseye."
}
},
"detect": {
"label": "Detekcija objekata",
"description": "Postavke za ulogu detekcije/detekcija koja se koristi za pokretanje detekcije objekata i inicijalizaciju praćenja.",
"enabled": {
"label": "Omogući detekciju objekata",
"description": "Omogući ili onemogući detekciju objekata za ovu kameru."
},
"height": {
"label": "Visina detekcije",
"description": "Visina (pikseli) okvira korištenih za detekciju stream-a; ostavite prazno za korištenje originalne rezolucije stream-a."
},
"width": {
"label": "Širina detekcije",
"description": "Širina (pikseli) okvira korištenih za detekciju stream-a; ostavite prazno za korištenje originalne rezolucije stream-a."
},
"fps": {
"label": "Detekcija FPS",
"description": "Željeni broj okvira po sekundi za pokretanje detekcije; niže vrijednosti smanjuju upotrebu CPU-a (preporučena vrijednost je 5, postavite više - najviše 10 - samo ako praćite vrlo brze objekte)."
},
"min_initialized": {
"label": "Minimalni broj okvira inicijalizacije",
"description": "Broj uzastopnih detekcija potreban prije stvaranja praćenog objekta. Povećajte da biste smanjili lažne inicijalizacije. Zadana vrijednost je fps podijeljeno sa 2."
},
"max_disappeared": {
"label": "Maksimalni broj okvira koji su nestali",
"description": "Broj okvira bez detekcije prije nego što se praćeni objekt smatra izgubljenim."
},
"stationary": {
"label": "Konfiguracija stacionarnih objekata",
"description": "Postavke za detekciju i upravljanje objektima koji ostaju stacionarni tokom određenog vremena.",
"interval": {
"label": "Stacionarni interval",
"description": "Kako često (u snimcima) pokretati provjeru detekcije da biste potvrdili stacionarni objekt."
},
"threshold": {
"label": "Stacionarni prag",
"description": "Broj snimaka bez promjene pozicije potreban da bi objekt bio označen kao stacionarni."
},
"max_frames": {
"label": "Maksimalni snimci",
"description": "Ograničava koliko dugo se stacionarni objekti praćaju prije nego što se odbacuju.",
"default": {
"label": "Zadani maksimalni snimci",
"description": "Zadani maksimalni broj snimaka za praćenje stacionarnog objekta prije prestanka."
},
"objects": {
"label": "Maksimalni snimci po objektu",
"description": "Podešavanja po objektu za maksimalni broj snimaka za praćenje stacionarnih objekata."
}
},
"classifier": {
"label": "Omogući vizualni klasifikator",
"description": "Koristi vizualni klasifikator za detekciju pravozadanih stacionarnih objekata čak i kada se okviri tresu."
}
},
"annotation_offset": {
"label": "Pomak oznake",
"description": "Milisekunde za pomak detektiranih oznaka kako bi se bolje poravnali vremenski okviri s snimcima; može biti pozitivan ili negativan."
}
},
"face_recognition": {
"label": "Prepoznavanje lica",
"description": "Postavke za detekciju i prepoznavanje lica za ovu kameru.",
"enabled": {
"label": "Omogući prepoznavanje lica",
"description": "Omogući ili onemogući prepoznavanje lica."
},
"min_area": {
"label": "Minimalna površina lica",
"description": "Minimalna površina (pikseli) detektiranog okvira lica potrebna za pokušaj prepoznavanja."
}
},
"ffmpeg": {
"label": "FFmpeg",
"description": "Postavke FFmpeg uključuju putanju binarne datoteke, argumente, opcije hwaccel i izlazne argumente po ulozi.",
"path": {
"label": "Putanja do FFmpeg binarne datoteke",
"description": "Putanja do FFmpeg binarne datoteke ili verzija alias (\"5.0\" ili \"7.0\")."
},
"global_args": {
"label": "Globalni argumenti FFmpeg-a",
"description": "Globalni argumenti prebačeni na procese FFmpeg."
},
"hwaccel_args": {
"label": "Argumenti za ubrzanje hardvera",
"description": "Argumenti za ubrzanje hardvera za FFmpeg. Preporučuju se predložci specifični za dobavljača."
},
"input_args": {
"label": "Unos argumenata",
"description": "Ulazni argumenti primjenjeni na ulazne snimke FFmpeg."
},
"output_args": {
"label": "Izlazni argumenti",
"description": "Zadani izlazni argumenti korišteni za različite uloge FFmpeg-a poput detekcije i snimanja.",
"detect": {
"label": "Izlazni argumenti za detekciju",
"description": "Zadani izlazni argumenti za snimke uloga detekcije."
},
"record": {
"label": "Izlazni argumenti za snimanje",
"description": "Zadani izlazni argumenti za snimke uloga snimanja."
}
},
"retry_interval": {
"label": "Vrijeme ponovnog pokušaja FFmpeg-a",
"description": "Sekunde koje treba čekati prije nego što se pokuša ponovno uspostaviti veza s tokom kamere nakon neuspjeha. Zadano je 10."
},
"apple_compatibility": {
"label": "Kompatibilnost s Apple-om",
"description": "Omogući označavanje HEVC za bolju kompatibilnost s igračima Apple-a prilikom snimanja H.265."
},
"gpu": {
"label": "Indeks GPU-a",
"description": "Zadani indeks GPU-a korišten za ubrzanje hardvera ako je dostupan."
},
"inputs": {
"label": "Ulazni podaci kamere",
"description": "Popis definicija ulaznih tokova (putanje i uloge) za ovu kameru.",
"path": {
"label": "Putanja ulaza",
"description": "URL ili putanja ulaznog toka kamere."
},
"roles": {
"label": "Uloge ulaza",
"description": "Uloge za ovaj ulazni tok."
},
"global_args": {
"label": "Globalni argumenti FFmpeg-a",
"description": "Globalni argumenti FFmpeg-a za ovaj ulazni tok."
},
"hwaccel_args": {
"label": "Argumenti za ubrzanje hardvera",
"description": "Argumenti za ubrzanje hardvera za ovaj ulazni stream."
},
"input_args": {
"label": "Unos argumenata",
"description": "Argumeti unosa specifični za ovaj stream."
}
}
},
"live": {
"label": "Uživo prikaz",
"description": "Postavke korištenje Web UI za kontrolu izbora živog streama, rezolucije i kvalitete.",
"streams": {
"label": "Imena živih streamova",
"description": "Mapiranje konfiguriranih imena streamova na imena restream/go2rtc korишtena za uživo prikaz."
},
"height": {
"label": "Visina uživo",
"description": "Visina (piksela) za prikaz jsmpeg živog streama u Web UI; mora biti <= visina detektiranog streama."
},
"quality": {
"label": "Kvalitet uživo",
"description": "Kvalitet kodiranja za jsmpeg stream (1 najviši, 31 najniži)."
}
},
"lpr": {
"label": "Prepoznavanje tablice vozila",
"description": "Postavke prepoznavanja tablice vozila uključujući pragovi detekcije, formatiranje i poznate tablice.",
"enabled": {
"label": "Omogući LPR",
"description": "Omogući ili onemogući LPR na ovoj kameri."
},
"expire_time": {
"label": "Sekunde isteka",
"description": "Vrijeme u sekundama nakon kojeg nevidljiva tablica istječe iz praćenja (samo za dedikovane LPR kamere)."
},
"min_area": {
"label": "Minimalna površina tablice",
"description": "Minimalna površina tablice (piksela) potrebna za pokušaj prepoznavanja."
},
"enhancement": {
"label": "Nivo poboljšanja",
"description": "Nivo poboljšanja (0-10) za primjenu na isječke tablice prije OCR-a; veće vrijednosti ne moraju uvijek poboljšati rezultate, nivoi iznad 5 mogu raditi samo s tablicama u noćnom vremenu i trebaju se koristiti s oprezom."
}
},
"motion": {
"label": "Detekcija pokreta",
"description": "Zadane postavke detekcije pokreta za ovu kameru.",
"enabled": {
"label": "Omogući detekciju pokreta",
"description": "Omogući ili onemogući detekciju pokreta za ovu kameru."
},
"threshold": {
"label": "Prag pokreta",
"description": "Prag razlike piksela korišten za detektor pokreta; veće vrijednosti smanjuju osjetljivost (opseg 1-255)."
},
"lightning_threshold": {
"label": "Prag munje",
"description": "Prag za detekciju i zanemarivanje kratkih iskri svjetlosti (niže vrijednosti povećavaju osjetljivost, vrijednosti između 0.3 i 1.0). Ovo ne spriječava detekciju pokreta u potpunosti; jednostavno zaustavlja detektor da analizira dodatne okvire nakon što se prag premaši. Snimci temeljeni na pokretima i dalje se stvaraju tijekom ovih događaja."
},
"skip_motion_threshold": {
"label": "Preskoči prag pokreta",
"description": "Ako se postavi na vrijednost između 0.0 i 1.0, i ako se više od ovog udjela slike promijeni u jednom okviru, detektor neće vratiti kutije pokreta i odmah će se ponovno kalibrirati. Ovo može uštedjeti CPU i smanjiti lažne pozitive tijekom munje, oluje itd., ali može propustiti stvarne događaje kao što je automatsko praćenje objekta PTZ kamerom. Tržište je između izgube nekoliko megabajta snimaka i pregleda nekoliko kratkih zapisnika. Ostavite nepostavljeno (Nijedno) za onemogućavanje ove funkcije."
},
"improve_contrast": {
"label": "Poboljšaj kontrast",
"description": "Primijeni poboljšanje kontrasta na okvire prije analize pokreta kako bi pomoću detekcije."
},
"contour_area": {
"label": "Površina kontura",
"description": "Minimalna površina kontura u pikselima potrebna za brojanje kontura pokreta."
},
"delta_alpha": {
"label": "Delta alfa",
"description": "Faktor alfa spajanja korišten za razliku okvira za izračun pokreta."
},
"frame_alpha": {
"label": "Alfa okvira",
"description": "Vrijednost alfa korištena prilikom spajanja okvira za predobradbu pokreta."
},
"frame_height": {
"label": "Visina okvira",
"description": "Visina u pikselima na koju se skaliraju okviri prilikom izračuna pokreta."
},
"mask": {
"label": "Koordinate maska",
"description": "Uredno x,y koordinate koje definiraju poligon maska pokreta za uključivanje/isključivanje područja."
},
"mqtt_off_delay": {
"label": "MQTT zakasnjenje isključivanja",
"description": "Sekunde koje se čekaju nakon posljednjeg pokreta prije objave MQTT 'isključeno' stanje."
},
"enabled_in_config": {
"label": "Originalno stanje pokreta",
"description": "Indikira je li detekcija pokreta bila omogućena u originalnoj statičkoj konfiguraciji."
},
"raw_mask": {
"label": "Ručna maska"
}
},
"objects": {
"label": "Objekti",
"description": "Zadani parametri praćenja objekata uključujući koje oznake praćenja i filtre po objektu.",
"track": {
"label": "Objekti za praćenje",
"description": "Popis oznaka objekata za praćenje za ovu kameru."
},
"filters": {
"label": "Filtar objekata",
"description": "Filtar primijenjen na detektirane objekte kako bi se smanjila broj lažnih pozitiva (površina, omjer, pouzdanost).",
"min_area": {
"label": "Minimalna površina objekta",
"description": "Minimalna površina okvira (pikseli ili postotak) potrebna za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)."
},
"max_area": {
"label": "Maksimalna površina objekta",
"description": "Maksimalna površina okvira (pikseli ili postotak) dozvoljena za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)."
},
"min_ratio": {
"label": "Minimalni omjer visine/širine",
"description": "Minimalni omjer širine/visine potreban da bi okvir bio prihvaćen."
},
"max_ratio": {
"label": "Maksimalni omjer visine/širine",
"description": "Maksimalni omjer širine/visine dozvoljen da bi okvir bio prihvaćen."
},
"threshold": {
"label": "Prag pouzdanosti",
"description": "Prosjek pragova pouzdanosti detekcije potreban da bi objekt bio smatravan pravim pozitivom."
},
"min_score": {
"label": "Minimalna pouzdanost",
"description": "Minimalna pouzdanost detekcije po okviru potrebna da bi objekt bio brojan."
},
"mask": {
"label": "Maska filtriranja",
"description": "Koordinate poligona koje definiraju područje na kojem se ovaj filter primjenjuje unutar okvira."
},
"raw_mask": {
"label": "Ručna maska"
}
},
"mask": {
"label": "Maska objekta",
"description": "Poligonalna maska korištena za spriječavanje detekcije objekta u određenim područjima."
},
"raw_mask": {
"label": "Ručna maska"
},
"genai": {
"label": "Konfiguracija GenAI objekta",
"description": "Opcije GenAI za opisivanje praćenih objekata i slanje okvira za generisanje.",
"enabled": {
"label": "Omogući GenAI",
"description": "Omogući generisanje opisa za praćene objekte po zadanim postavkama."
},
"use_snapshot": {
"label": "Koristi snimke",
"description": "Koristi snimke objekata umjesto miniaturnih slika za generisanje opisa GenAI."
},
"prompt": {
"label": "Naslovni prompt",
"description": "Zadani šablon upita korišten za generisanje opisa pomoću GenAI."
},
"object_prompts": {
"label": "Prompti za objekte",
"description": "Prompti po objektu za prilagođavanje izlaza GenAI za specifične oznake."
},
"objects": {
"label": "GenAI objekti",
"description": "Popis oznaka objekata koje se po defaultu šalju GenAI."
},
"required_zones": {
"label": "Potrebne zone",
"description": "Zone koje moraju biti unesene za objekte da bi se kvalifikovali za generisanje opisa GenAI."
},
"debug_save_thumbnails": {
"label": "Sačuvajte miniaturne slike",
"description": "Sačuvaj miniaturne slike koje se šalju GenAI za ispravljanje i pregled."
},
"send_triggers": {
"label": "GenAI izazivači",
"description": "Definiše kada bi se trebale slati okvir za GenAI (na kraju, nakon ažuriranja, itd.).",
"tracked_object_end": {
"label": "Pošalji na kraju",
"description": "Pošalji zahtjev GenAI kada praćeni objekt završi."
},
"after_significant_updates": {
"label": "Raniji GenAI izazivač",
"description": "Pošalji zahtjev GenAI nakon određenog broja značajnih ažuriranja za praćeni objekt."
}
},
"enabled_in_config": {
"label": "Originalno stanje GenAI",
"description": "Pokazuje je li GenAI bio omogućen u originalnoj statičkoj konfiguraciji."
}
}
},
"record": {
"label": "Snimanje",
"description": "Postavke snimanja i zadržavanja za ovu kameru.",
"enabled": {
"label": "Omogući snimanje",
"description": "Omogući ili onemogući snimanje za ovu kameru."
},
"expire_interval": {
"label": "Interval čišćenja snimanja",
"description": "Minute između čišćenja koja uklanjaju istekle segmente snimaka."
},
"continuous": {
"label": "Neprekidna retencija",
"description": "Broj dana za čuvanje snimaka bez obzira na praćene objekte ili pokret. Postavite na 0 ako želite da čuvate samo snimke upozorenja i detekcije.",
"days": {
"label": "Dane zadržavanja",
"description": "Dana za čuvanje snimaka."
}
},
"motion": {
"label": "Retencija pokreta",
"description": "Broj dana za čuvanje snimaka izazvanih pokretom bez obzira na praćene objekte. Postavite na 0 ako želite da čuvate samo snimke upozorenja i detekcije.",
"days": {
"label": "Dane zadržavanja",
"description": "Dana za čuvanje snimaka."
}
},
"detections": {
"label": "Retencija detekcije",
"description": "Postavke retencije snimaka za događaje detekcije uključujući trajanje pre/post snimanja.",
"pre_capture": {
"label": "Sekundi pre snimanja",
"description": "Broj sekundi prije događaja detekcije koje treba uključiti u snimak."
},
"post_capture": {
"label": "Sekunde nakon snimanja",
"description": "Broj sekundi nakon događaja detekcije koje se uključuju u snimanje."
},
"retain": {
"label": "Zadržavanje događaja",
"description": "Postavke zadržavanja za snimke događaja detekcije.",
"days": {
"label": "Dane zadržavanja",
"description": "Broj dana za koje se zadržavaju snimke događaja detekcije."
},
"mode": {
"label": "Način zadržavanja",
"description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)."
}
}
},
"alerts": {
"label": "Retencija upozorenja",
"description": "Postavke retencije snimaka za događaje upozorenja uključujući trajanje pre/post snimanja.",
"pre_capture": {
"label": "Sekundi pre snimanja",
"description": "Broj sekundi prije događaja detekcije koje treba uključiti u snimak."
},
"post_capture": {
"label": "Sekunde nakon snimanja",
"description": "Broj sekundi nakon događaja detekcije koje se uključuju u snimanje."
},
"retain": {
"label": "Zadržavanje događaja",
"description": "Postavke zadržavanja za snimke događaja detekcije.",
"days": {
"label": "Dane zadržavanja",
"description": "Broj dana za koje se zadržavaju snimke događaja detekcije."
},
"mode": {
"label": "Način zadržavanja",
"description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)."
}
}
},
"export": {
"label": "Konfiguracija izvoza",
"description": "Postavke koje se koriste prilikom izvoza snimaka kao što su timelapse i ubrzavanje dretve.",
"hwaccel_args": {
"label": "Argumeti ubrzavanja dretve za izvoz",
"description": "Argumeti ubrzavanja dretve za operacije izvoza/prenosa."
},
"max_concurrent": {
"label": "Maksimalan broj istovremenih izvoza",
"description": "Maksimalan broj poslova izvoza koji se obrađuju istovremeno."
}
},
"preview": {
"label": "Konfiguracija pregleda",
"description": "Postavke koje kontrolišu kvalitet pregleda snimanja prikazanih u UI.",
"quality": {
"label": "Kvaliteta pregleda",
"description": "Nivo kvalitete pregleda (vrlo_nizak, nizak, srednji, visok, vrlo_visok)."
}
},
"enabled_in_config": {
"label": "Originalno stanje snimanja",
"description": "Pokazuje je li snimanje bilo omogućeno u originalnoj statičkoj konfiguraciji."
}
},
"review": {
"label": "Pregled",
"description": "Postavke koje kontrolišu upozorenja, detekcije i sažetke pregleda GenAI korišteni od strane UI i skladišta za ovu kameru.",
"alerts": {
"label": "Konfiguracija upozorenja",
"description": "Postavke za koje objekti praćeni generišu upozorenja i kako se upozorenja zadržavaju.",
"enabled": {
"label": "Omogući upozorenja",
"description": "Omogući ili onemogući generisanje upozorenja za ovu kameru."
},
"labels": {
"label": "Oznake upozorenja",
"description": "Lista oznaka objekata koje se smatraju upozorenjima (npr. automobil, osoba)."
},
"required_zones": {
"label": "Potrebne zone",
"description": "Zone koje objekt mora ući da bi se smatrao upozorenjem; ostavite prazno da omogućite bilo koju zonu."
},
"enabled_in_config": {
"label": "Originalno stanje upozorenja",
"description": "Pratiti je li upozorenja izvorno omogućena u statičkoj konfiguraciji."
},
"cutoff_time": {
"label": "Vrijeme prekida upozorenja",
"description": "Sekunde koje treba čekati nakon što nema aktivnosti koja uzrokuje upozorenje prije nego se prekine upozorenje."
}
},
"detections": {
"label": "Konfiguracija detekcija",
"description": "Postavke koje objekti koje se praćenje generišu detekcije (nepozornja) i kako se detekcije čuvaju.",
"enabled": {
"label": "Omogući detekcije",
"description": "Omogući ili onemogući događaje detekcije za ovu kameru."
},
"labels": {
"label": "Oznake detekcije",
"description": "Popis oznaka objekata koje kvalifikuju kao događaji detekcije."
},
"required_zones": {
"label": "Potrebne zone",
"description": "Zone koje objekt mora ući da bi se smatrao detekcijom; ostavite prazno da omogućite bilo koju zonu."
},
"cutoff_time": {
"label": "Vrijeme prekida detekcija",
"description": "Sekunde koje treba čekati nakon što nema aktivnosti koja uzrokuje detekciju prije nego se prekine detekcija."
},
"enabled_in_config": {
"label": "Originalno stanje detekcija",
"description": "Pratiti je li detekcije izvorno omogućene u statičkoj konfiguraciji."
}
},
"genai": {
"label": "Konfiguracija GenAI",
"description": "Kontrolira korištenje generativne AI za proizvodnju opisa i sažetaka stavki za pregled.",
"enabled": {
"label": "Omogući opise GenAI",
"description": "Omogući ili onemogući opise i sažetke generirane GenAI za stavke za pregled."
},
"alerts": {
"label": "Omogući GenAI za upozorenja",
"description": "Koristi GenAI za generiranje opisa stavki upozorenja."
},
"detections": {
"label": "Omogući GenAI za detekcije",
"description": "Koristite GenAI za generiranje opisa predmeta detekcije."
},
"image_source": {
"label": "Pregledajte izvor slike",
"description": "Izvor slika poslatih GenAIJ-u ('preview' ili 'recordings'); 'recordings' koristi kvalitetnije okvire, ali više tokena."
},
"additional_concerns": {
"label": "Dodatne brige",
"description": "Popis dodatnih briga ili napomena koje GenAI treba uzeti u obzir prilikom procjene aktivnosti na ovoj kameri."
},
"debug_save_thumbnails": {
"label": "Sačuvajte miniaturne slike",
"description": "Sačuvajte miniaturne slike koje se šalju GenAI provajderu za ispravljanje grešaka i pregled."
},
"enabled_in_config": {
"label": "Originalno stanje GenAI",
"description": "Pratiti je li pregled GenAI izvorno omogućen u statičkoj konfiguraciji."
},
"preferred_language": {
"label": "Preferirani jezik",
"description": "Preferirani jezik za zahtijevanje od GenAI provajdera za generirane odgovore."
},
"activity_context_prompt": {
"label": "Prompt konteksta aktivnosti",
"description": "Prilagođeni prompt koji opisuje što je i što nije sumnjivo ponašanje kako bi pružio kontekst za sažetke GenAI."
}
}
},
"semantic_search": {
"label": "Semantička pretraga",
"description": "Postavke za semantičku pretragu koja konstruira i upita uključivanje objekata kako bi pronašla slične stavke.",
"triggers": {
"label": "Pokretači",
"description": "Akcije i kriteriji za usklađivanje za pokretače semantičke pretrage specifične za kameru.",
"friendly_name": {
"label": "Prijateljsko ime",
"description": "Nepovlačno prijateljsko ime prikazano u korisničkom sučelju za ovaj pokretač."
},
"enabled": {
"label": "Omogući ovaj pokretač",
"description": "Omogući ili onemogući ovaj pokretač semantičke pretrage."
},
"type": {
"label": "Tip pokretača",
"description": "Tip pokretača: 'thumbnail' (uspoređivanje slikom) ili 'description' (uspoređivanje teksta)."
},
"data": {
"label": "Sadržaj pokretača",
"description": "Tekstualni izraz ili ID miniaturne slike za uspoređivanje s praćenim objektima."
},
"threshold": {
"label": "Prag aktivacije",
"description": "Minimalna ocjena sličnosti (0-1) potrebna za aktivaciju ovog izazivača."
},
"actions": {
"label": "Akcije izazivača",
"description": "Popis akcija koje se izvršavaju kada izazivač odgovara (obavijest, pod_naziv, atribute)."
}
}
},
"snapshots": {
"label": "Snimci",
"description": "Postavke za snimke generirane preko API-ja za praćene objekte za ovu kameru.",
"enabled": {
"label": "Omogući snimke",
"description": "Omogući ili onemogući snimanje snimaka za ovu kameru."
},
"timestamp": {
"label": "Preklapanje vremenske oznake",
"description": "Preklopiti vremensku oznaku na snimke iz API-ja."
},
"bounding_box": {
"label": "Preklapanje okvira",
"description": "Crtanje okvira za praćene objekte na snimke iz API-ja."
},
"crop": {
"label": "Izrezivanje snimke",
"description": "Izrezivanje snimki iz API-ja do okvira detektiranog objekta."
},
"required_zones": {
"label": "Potrebne zone",
"description": "Zone koje objekt mora ući da bi snimka bila sačuvana."
},
"height": {
"label": "Visina snimke",
"description": "Visina (pikseli) za promjenu veličine snimki iz API-ja; ostavite prazno da biste sačuvali originalnu veličinu."
},
"retain": {
"label": "Zadržavanje snimki",
"description": "Postavke zadržavanja snimki uključujući zadane dane i prekriženja po objektu.",
"default": {
"label": "Zadano zadržavanje",
"description": "Zadani broj dana za zadržavanje snimki."
},
"mode": {
"label": "Način zadržavanja",
"description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)."
},
"objects": {
"label": "Zadržavanje objekata",
"description": "Prekriženja po objektu za dane zadržavanja snimki."
}
},
"quality": {
"label": "Kvaliteta snimka",
"description": "Kvaliteta kodiranja za sačuvane snimke (0-100)."
}
},
"timestamp_style": {
"label": "Stil vremenske oznake",
"description": "Opcije stilizacije za vremenske oznake u snimcima i snimcima.",
"position": {
"label": "Pozicija vremenske oznake",
"description": "Pozicija vremenske oznake na slici (tl/tr/bl/br)."
},
"format": {
"label": "Format vremenske oznake",
"description": "String formata datuma i vremena korišten za vremenske oznake (Python format koda za datum i vrijeme)."
},
"color": {
"label": "Boja vremenske oznake",
"description": "RGB vrijednosti boja za tekst vremenske oznake (sve vrijednosti 0-255).",
"red": {
"label": "Crvena",
"description": "Crveni komponent (0-255) za boju vremenske oznake."
},
"green": {
"label": "Zelena",
"description": "Zeleni komponent (0-255) za boju vremenske oznake."
},
"blue": {
"label": "Plava",
"description": "Plavi komponent (0-255) za boju vremenske oznake."
}
},
"thickness": {
"label": "Debljina vremenske oznake",
"description": "Debljina linije teksta vremenske oznake."
},
"effect": {
"label": "Efekt vremenske oznake",
"description": "Vizualni efekt za tekst vremenske oznake (none, solid, shadow)."
}
},
"best_image_timeout": {
"label": "Vrijeme čekanja za najbolju sliku",
"description": "Koliko dugo čekati na sliku s najvišim stupnjem pouzdanosti."
},
"mqtt": {
"label": "MQTT",
"description": "Postavke objave slika preko MQTT.",
"enabled": {
"label": "Pošalji sliku",
"description": "Omogući objavljivanje snimaka slika za objekte na MQTT teme za ovu kameru."
},
"timestamp": {
"label": "Dodaj vremensku oznaku",
"description": "Preklopiti vremensku oznaku na slike objavljene preko MQTT."
},
"bounding_box": {
"label": "Dodaj okvir",
"description": "Crtaj okvire na slikama objavljenim preko MQTT."
},
"crop": {
"label": "Iscijepi sliku",
"description": "Iscijepi slike objavljene preko MQTT na okvir detektiranog objekta."
},
"height": {
"label": "Visina slike",
"description": "Visina (piksela) za promjenu veličine slika objavljenih preko MQTT."
},
"required_zones": {
"label": "Potrebne zone",
"description": "Zone koje objekt mora ući da bi se slika preko MQTT objavila."
},
"quality": {
"label": "Kvaliteta JPEG",
"description": "Kvaliteta JPEG za slike objavljene preko MQTT (0-100)."
}
},
"notifications": {
"label": "Obavještenja",
"description": "Postavke za omogućavanje i kontrolu obavijesti za ovu kameru.",
"enabled": {
"label": "Omogući obavijesti",
"description": "Omogući ili onemogući obavijesti za ovu kameru."
},
"email": {
"label": "E-mail za obavijesti",
"description": "Adresa e-maila koja se koristi za obavijesti putem push-a ili je potrebna određenim dobavljačima obavijesti."
},
"cooldown": {
"label": "Period hlađenja",
"description": "Period hlađenja (sekunde) između obavijesti kako bi se izbjeglo spaming primateljima."
},
"enabled_in_config": {
"label": "Originalno stanje obavijesti",
"description": "Pokazuje je li obavijesti bile omogućene u originalnoj statičkoj konfiguraciji."
}
},
"onvif": {
"label": "ONVIF",
"description": "Postavke povezivanja preko ONVIF i automatskog praćenja PTZ za ovu kameru.",
"host": {
"label": "Gost ONVIF",
"description": "Gost (i opcionalni shema) za uslugu ONVIF za ovu kameru."
},
"port": {
"label": "Port ONVIF",
"description": "Broj porta za uslugu ONVIF."
},
"user": {
"label": "Korisničko ime za ONVIF",
"description": "Korisničko ime za autentifikaciju ONVIF; neki uređaji zahtijevaju korisnika admin za ONVIF."
},
"password": {
"label": "Lozinka za ONVIF",
"description": "Lozinka za autentifikaciju ONVIF."
},
"tls_insecure": {
"label": "Onemogući provjeru TLS",
"description": "Preskoči provjeru TLS i onemogući digest autentifikaciju za ONVIF (nebezbedno; koristiti samo u sigurnim mrežama)."
},
"profile": {
"label": "ONVIF profil",
"description": "Specifičan ONVIF medij profil za korištenje za kontrolu PTZ, prilagođen tokenom ili imenom. Ako nije postavljen, prvi profil s važećom konfiguracijom PTZ automatski se odabire."
},
"autotracking": {
"label": "Autotračenje",
"description": "Automatski praćenje pokretanja objekata i držanje ih u sredini okvira korištenjem pokreta kamere PTZ.",
"enabled": {
"label": "Omogući automatsko praćenje",
"description": "Omogući ili onemogući automatsko praćenje kamere PTZ detektiranih objekata."
},
"calibrate_on_startup": {
"label": "Kalibriraj na početku",
"description": "Mjeri brzine motora PTZ pri pokretanju kako bi poboljšao preciznost praćenja. Frigate će ažurirati konfiguraciju s težinama pokreta nakon kalibracije."
},
"zooming": {
"label": "Režim zumiranja",
"description": "Kontrola ponašanja zumiranja: onemogućeno (samo pan/tilt), apsolutno (najkompatibilnije) ili relativno (konkurentno pan/tilt/zum)."
},
"zoom_factor": {
"label": "Faktor zumiranja",
"description": "Kontrola razine zumiranja na praćenim objektima. Niže vrijednosti drže više scene u pogledu; više vrijednosti zumiraju bliže, ali mogu izgubiti praćenje. Vrijednosti između 0.1 i 0.75."
},
"track": {
"label": "Praćeni objekti",
"description": "Popis vrsta objekata koji trebaju pokrenuti automatsko praćenje."
},
"required_zones": {
"label": "Potrebne zone",
"description": "Objekti moraju ući u jednu od ovih zona prije nego što započne automatsko praćenje."
},
"return_preset": {
"label": "Povratak na predpostavku",
"description": "Ime predpostavke konfigurirano u firmware kamere za povratak nakon završetka praćenja."
},
"timeout": {
"label": "Vrijeme čekanja povratka",
"description": "Čekajte ovaj broj sekundi nakon gubitka praćenja prije povratka kamere na predpostavljeno mjesto."
},
"movement_weights": {
"label": "Težine pokreta",
"description": "Vrijednosti kalibracije automatski generirane kroz kalibraciju kamere. Ne mijenjajte ručno."
},
"enabled_in_config": {
"label": "Originalni stanje autotračenja",
"description": "Unutarnje polje za praćenje je li autotračenje bilo omogućeno u konfiguraciji."
}
},
"ignore_time_mismatch": {
"label": "Zanemari razliku u vremenu",
"description": "Zanemari razlike u sinhronizaciji vremena između kamere i Frigate servera za komunikaciju ONVIF."
}
},
"type": {
"label": "Tip kamere",
"description": "Tip kamere"
},
"ui": {
"label": "Korisnički interfejs kamere",
"description": "Prikaz redoslijeda i vidljivosti za ovu kameru u UI. Redoslijed utječe na zadani nadzorno pločo. Za detaljniju kontrolu koristite grupe kamere.",
"order": {
"label": "Redoslijed UI",
"description": "Numerički redoslijed koristi se za sortiranje kamere u UI (zadani nadzorno pločo i popisi); veći brojevi pojavljuju se kasnije."
},
"dashboard": {
"label": "Prikaži u UI",
"description": "Prekidač je li ova kamera vidljiva svuda u UI Frigate. Onemogućavanje ovoga zahtijeva ručno uređivanje konfiguracije za ponovno prikazivanje ove kamere u UI."
}
},
"webui_url": {
"label": "URL kamere",
"description": "URL za pristup kamere izravno iz stranice sustava"
},
"profiles": {
"label": "Profili",
"description": "Imenovane konfiguracijske profile s parcijalnim preklopima koji se mogu aktivirati tijekom izvršavanja."
},
"enabled_in_config": {
"label": "Originalno stanje kamere",
"description": "Pratite originalno stanje kamere."
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,73 @@
{
"audio": {
"global": {
"detection": "Globalna detekcija",
"sensitivity": "Globalna osjetljivost"
},
"cameras": {
"detection": "Detekcija",
"sensitivity": "Osjetljivost"
}
},
"timestamp_style": {
"global": {
"appearance": "Globalno izgled"
},
"cameras": {
"appearance": "Izgled"
}
},
"motion": {
"global": {
"sensitivity": "Globalna osjetljivost",
"algorithm": "Globalni algoritam"
},
"cameras": {
"sensitivity": "Osjetljivost",
"algorithm": "Algoritam"
}
},
"snapshots": {
"global": {
"display": "Globalno prikazivanje"
},
"cameras": {
"display": "Prikazivanje"
}
},
"detect": {
"global": {
"resolution": "Globalna rezolucija",
"tracking": "Globalno praćenje"
},
"cameras": {
"resolution": "Rezolucija",
"tracking": "Praćenje"
}
},
"objects": {
"global": {
"tracking": "Globalno praćenje",
"filtering": "Globalno filtriranje"
},
"cameras": {
"tracking": "Praćenje",
"filtering": "Filtriranje"
}
},
"record": {
"global": {
"retention": "Globalno zadržavanje",
"events": "Globalni događaji"
},
"cameras": {
"retention": "Zadržavanje",
"events": "Događaji"
}
},
"ffmpeg": {
"cameras": {
"cameraFfmpeg": "Argumeni FFmpeg specifični za kameru"
}
}
}

View File

@ -0,0 +1,32 @@
{
"minimum": "Bar {{limit}}",
"maximum": "Mora biti najviše {{limit}}",
"exclusiveMinimum": "Mora biti veći od {{limit}}",
"exclusiveMaximum": "Mora biti manje od {{limit}}",
"minLength": "Bar {{limit}} znak(ovi)",
"maxLength": "Mora biti najviše {{limit}} znak(ovi)",
"minItems": "Mora imati bar {{limit}} stavke",
"maxItems": "Mora imati najviše {{limit}} stavke",
"pattern": "Neispravan format",
"required": "Ovo polje je obavezno",
"type": "Neispravan tip vrijednosti",
"enum": "Mora biti jedan od dopuštenih vrijednosti",
"const": "Vrijednost se ne podudara s očekivanom konstantom",
"uniqueItems": "Sve stavke moraju biti jedinstvene",
"format": "Neispravan format",
"additionalProperties": "Nepoznato svojstvo nije dozvoljeno",
"oneOf": "Mora se podudarati s točno jednim od dopuštenih shema",
"anyOf": "Mora se podudarati s bar jednim od dopuštenih shema",
"proxy": {
"header_map": {
"roleHeaderRequired": "Zaglavlje uloge je obavezno kada su konfigurirane mapiranja uloga."
}
},
"ffmpeg": {
"inputs": {
"rolesUnique": "Svaka uloga može biti dodijeljena samo jednom ulaznom toku.",
"detectRequired": "Bar jedan ulazni tok mora biti dodijeljen ulozi 'detektirati'.",
"hwaccelDetectOnly": "Samo ulazni tok s ulogom detektiranja može definirati argumente ubrzavanja hardvera."
}
}
}

View File

@ -0,0 +1,125 @@
{
"person": "Ljudsko bit će",
"bicycle": "Kolo",
"animal": "Životinja",
"dog": "Pas",
"bark": "Glavu",
"cat": "Mačka",
"horse": "Konj",
"goat": "Koza",
"sheep": "Ovca",
"bird": "Ptica",
"mouse": "Miš",
"keyboard": "Klaviatura",
"vehicle": "Vozilo",
"boat": "Brod",
"car": "Automobil",
"bus": "Autobus",
"motorcycle": "Motocikl",
"train": "Vlak",
"skateboard": "Skejtbord",
"door": "Vrata",
"blender": "Miksere",
"sink": "Lavabo",
"hair_dryer": "Sušilac za kosu",
"toothbrush": "Šetka za zube",
"scissors": "Škare",
"clock": "Sat",
"airplane": "Avion",
"traffic_light": "Svetofor",
"fire_hydrant": "Vatrostaničar",
"street_sign": "Ulični znak",
"stop_sign": "Znak zaustavljanja",
"parking_meter": "Parkirni metar",
"bench": "Banko",
"cow": "Korova",
"elephant": "Slon",
"bear": "Medvjed",
"zebra": "Zebra",
"giraffe": "Žirafa",
"hat": "Kaputa",
"backpack": "Torba",
"umbrella": "Kreveta",
"shoe": "Cizma",
"eye_glasses": "Očna stakla",
"handbag": "Ručna torba",
"tie": "Kremplj",
"suitcase": "Kufer",
"frisbee": "Frizbi",
"skis": "Ski",
"snowboard": "Snjegobord",
"sports_ball": "Sportska lopta",
"kite": "Let",
"baseball_bat": "Batsa za baseball",
"baseball_glove": "Rukavica za baseball",
"surfboard": "Surfbord",
"tennis_racket": "Teniski raketa",
"bottle": "Bocica",
"plate": "Ploča",
"wine_glass": "Vinsko čaša",
"cup": "Kupa",
"fork": "Škarpe",
"knife": "Nož",
"spoon": "Lajna",
"bowl": "Tanjir",
"banana": "Banana",
"apple": "Jabuka",
"sandwich": "Sandučić",
"orange": "Portakal",
"broccoli": "Brobkoli",
"carrot": "Mahunika",
"hot_dog": "Hot dog",
"pizza": "Pica",
"donut": "Krofna",
"cake": "Torta",
"chair": "Stolica",
"couch": "Divan",
"potted_plant": "Ukrasna biljka",
"bed": "Krevet",
"mirror": "Zrcalo",
"dining_table": "Stol za ručak",
"window": "Prozor",
"desk": "Radni stol",
"toilet": "Toalet",
"tv": "TV",
"laptop": "Laptop",
"remote": "Udaljeno upravljanje",
"cell_phone": "Mobilni telefon",
"microwave": "Mikrotalasna pećnica",
"oven": "Pećnica",
"toaster": "Tostera",
"refrigerator": "Hladnjak",
"book": "Knjiga",
"vase": "Vaza",
"teddy_bear": "Biberon",
"hair_brush": "Kosmetička četka",
"squirrel": "Šumski pas",
"deer": "Jelen",
"fox": "Lisica",
"rabbit": "Zajac",
"raccoon": "Rakun",
"robot_lawnmower": "Robotska kosilica",
"waste_bin": "Kanta za otpad",
"on_demand": "Na zahtjev",
"face": "Lice",
"license_plate": "Tablica",
"package": "Paket",
"bbq_grill": "Grill za BBQ",
"amazon": "Amazon",
"usps": "USPS",
"ups": "UPS",
"fedex": "FedEx",
"dhl": "DHL",
"an_post": "An Post",
"purolator": "Purolator",
"postnl": "PostNL",
"nzpost": "NZPost",
"postnord": "PostNord",
"gls": "GLS",
"dpd": "DPD",
"canada_post": "Canada Post",
"royal_mail": "Royal Mail",
"school_bus": "Školski autobus",
"skunk": "Mrdka",
"kangaroo": "Kanguru"
}

View File

@ -0,0 +1,46 @@
{
"documentTitle": "Razgovor - Frigate",
"title": "Frigate razgovor",
"subtitle": "Vaš AI asistent za upravljanje kamerama i insighti",
"placeholder": "Pitajte bilo što...",
"error": "Nešto je pošlo po zlu. Molimo pokušajte ponovo.",
"processing": "Obrađivanje...",
"toolsUsed": "Korišteno: {{tools}}",
"showTools": "Prikaži alate ({{count}})",
"hideTools": "Sakrij alate",
"call": "Poziv",
"result": "Rezultat",
"arguments": "Argumenti:",
"response": "Odgovor:",
"attachment_chip_label": "{{label}} na {{camera}}",
"attachment_chip_remove": "Ukloni privitak",
"open_in_explore": "Otvori u Explore",
"attach_event_aria": "Prikači događaj {{eventId}}",
"attachment_picker_paste_label": "Ili zalijepite ID događaja",
"attachment_picker_attach": "Prikači",
"attachment_picker_placeholder": "Prikači događaj",
"quick_reply_find_similar": "Pronađi slične susreti",
"quick_reply_tell_me_more": "Recite mi više o ovome",
"quick_reply_when_else": "Kada je još puta vidjeno?",
"quick_reply_find_similar_text": "Pronađi slične susreti za ovaj.",
"quick_reply_tell_me_more_text": "Recite mi više o ovom.",
"quick_reply_when_else_text": "Kada je to još puta vidjeno?",
"anchor": "Referenca",
"similarity_score": "Sličnost",
"no_similar_objects_found": "Nisu pronađeni slični objekti.",
"semantic_search_required": "Semantička pretraga mora biti omogućena da bi se pronašli slični objekti.",
"send": "Pošalji",
"suggested_requests": "Pokušaj pitati:",
"starting_requests": {
"show_recent_events": "Prikaži nedavne događaje",
"show_camera_status": "Prikaži status kamere",
"recap": "Što se desilo dok sam bio odsutan?",
"watch_camera": "Pratite kameru za aktivnost"
},
"starting_requests_prompts": {
"show_recent_events": "Prikaži mi nedavne događaje iz posljednjeg sata",
"show_camera_status": "Koji je trenutni status mojih kamera?",
"recap": "Što se desilo dok sam bio odsutan?",
"watch_camera": "Pratite ulazna vrata i obavijestite me ako netko dođe"
}
}

View File

@ -0,0 +1,205 @@
{
"documentTitle": "Modeli klasifikacije - Frigate",
"details": {
"scoreInfo": "Ocjena predstavlja prosjek pouzdanosti klasifikacije kroz sve detekcije ovog objekta.",
"none": "Nijedan",
"unknown": "Nepoznato"
},
"button": {
"deleteClassificationAttempts": "Obriši slike klasifikacije",
"renameCategory": "Preimenuj klasu",
"deleteCategory": "Obriši klasu",
"deleteImages": "Obriši slike",
"trainModel": "Obuci model",
"addClassification": "Dodaj klasifikaciju",
"deleteModels": "Obriši modele",
"editModel": "Uredi model"
},
"tooltip": {
"trainingInProgress": "Model trenutno obučava",
"noNewImages": "Nema novih slika za obuku. Prvo klasificirajte više slika u skupu podataka.",
"noChanges": "Nema promjena u skupu podataka od posljednje obuke.",
"modelNotReady": "Model nije spreman za obuku"
},
"toast": {
"success": {
"deletedModel_one": "Uspješno obrisan {{count}} model",
"deletedModel_few": "Uspješno obrisani {{count}} modeli",
"deletedModel_other": "Uspješno obrisani {{count}} modeli",
"categorizedImage": "Uspješno klasificirana slika",
"reclassifiedImage": "Uspješno ponovno klasificirana slika",
"trainedModel": "Uspješno obučen model.",
"trainingModel": "Uspješno pokrenuta obuka modela.",
"updatedModel": "Uspješno ažurirana konfiguracija modela",
"renamedCategory": "Uspješno preimenovana klasa u {{name}}",
"deletedCategory_one": "Obrisana {{count}} klasa",
"deletedCategory_few": "Obrisane {{count}} klase",
"deletedCategory_other": "Obrisane {{count}} klase",
"deletedImage_one": "Izbrisana {{count}} slika",
"deletedImage_few": "Izbrisane {{count}} slike",
"deletedImage_other": "Izbrisane {{count}} slike"
},
"error": {
"deleteImageFailed": "Neuspješno brisanje: {{errorMessage}}",
"deleteCategoryFailed": "Neuspješno brisanje klase: {{errorMessage}}",
"deleteModelFailed": "Neuspješno brisanje modela: {{errorMessage}}",
"categorizeFailed": "Neuspješno kategoriziranje slike: {{errorMessage}}",
"trainingFailed": "Obuka modela nije uspješna. Provjerite zapise Frigate za detalje.",
"trainingFailedToStart": "Neuspješno pokretanje obuke modela: {{errorMessage}}",
"updateModelFailed": "Neuspješno ažuriranje modela: {{errorMessage}}",
"renameCategoryFailed": "Neuspješno preimenovanje klase: {{errorMessage}}",
"reclassifyFailed": "Neuspješno ponovno klasifikovanje slike: {{errorMessage}}"
}
},
"deleteCategory": {
"title": "Izbriši klasu",
"desc": "Sigurni li ste da želite izbrisati klasu {{name}}? Ovo će trajno izbrisati sve povezane slike i zahtijevati ponovnu obuku modela.",
"minClassesTitle": "Nemoguće izbrisati klasu",
"minClassesDesc": "Model klasifikacije mora imati najmanje 2 klase. Dodajte još jednu klasu prije brisanja ove."
},
"deleteModel": {
"title": "Izbriši model klasifikacije",
"single": "Sigurni li ste da želite izbrisati {{name}}? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena.",
"desc_one": "Sigurni li ste da želite izbrisati {{count}} model? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena.",
"desc_few": "Sigurni li ste da želite izbrisati {{count}} modele? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena.",
"desc_other": "Sigurni li ste da želite izbrisati {{count}} modele? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena."
},
"edit": {
"title": "Uredi model klasifikacije",
"descriptionState": "Uredi klase za ovaj model klasifikacije stanja. Promjene će zahtijevati ponovnu obuku modela.",
"descriptionObject": "Uredi vrstu objekta i vrstu klasifikacije za ovaj model klasifikacije objekta.",
"stateClassesInfo": "Napomena: Promjene klasa stanja zahtijevaju ponovnu obuku modela sa ažuriranim klasama."
},
"deleteDatasetImages": {
"title": "Izbriši slike skupa podataka",
"desc_one": "Sigurni li ste da želite izbrisati {{count}} sliku iz {{dataset}}? Ova akcija ne može biti poništena i zahtijevati će ponovnu obuku modela.",
"desc_few": "Sigurni li ste da želite obrisati {{count}} slike iz {{dataset}}? Ova akcija ne može se poništiti i zahtijevat će ponovno treniranje modela.",
"desc_other": "Sigurni li ste da želite obrisati {{count}} slike iz {{dataset}}? Ova akcija ne može se poništiti i zahtijevat će ponovno treniranje modela."
},
"deleteTrainImages": {
"title": "Obriši slike za treniranje",
"desc_one": "Sigurni li ste da želite obrisati {{count}} sliku? Ova akcija ne može se poništiti.",
"desc_few": "Sigurni li ste da želite obrisati {{count}} slike? Ova akcija ne može se poništiti.",
"desc_other": "Sigurni li ste da želite obrisati {{count}} slike? Ova akcija ne može se poništiti."
},
"renameCategory": {
"title": "Preimenuj klasu",
"desc": "Unesite novo ime za {{name}}. Za promjenu imena će vam se zahtijevati ponovno treniranje modela."
},
"description": {
"invalidName": "Neprihvatljivo ime. Imena mogu sadržavati samo slova, brojeve, razmake, aposrofe, donje crte i crte."
},
"train": {
"title": "Nedavne klasifikacije",
"titleShort": "Nedavno",
"aria": "Odaberite nedavne klasifikacije"
},
"categories": "Klase",
"createCategory": {
"new": "Stvori novu klasu"
},
"categorizeImageAs": "Klasificiraj sliku kao:",
"categorizeImage": "Klasificiraj sliku",
"reclassifyImageAs": "Ponovno klasificiraj sliku kao:",
"reclassifyImage": "Ponovno klasificiraj sliku",
"menu": {
"objects": "Objekti",
"states": "Stanja"
},
"noModels": {
"object": {
"title": "Nema modela za klasifikaciju objekata",
"description": "Stvorite prilagođeni model za klasifikaciju detektiranih objekata.",
"buttonText": "Stvori model objekta"
},
"state": {
"title": "Nema modela za klasifikaciju stanja",
"description": "Stvorite prilagođeni model za praćenje i klasifikaciju promjena stanja u određenim područjima kamere.",
"buttonText": "Stvori model stanja"
}
},
"wizard": {
"title": "Stvori novu klasifikaciju",
"steps": {
"nameAndDefine": "Ime i definicija",
"stateArea": "Područje stanja",
"chooseExamples": "Odaberite primjere"
},
"step1": {
"description": "Modeli stanja nadziraju fiksne područja kamere za promjene (npr. vrata otvorena/zatvorena). Modeli objekata dodaju klasifikacije detektiranim objektima (npr. poznati životinje, dostavljači, itd.).",
"name": "Ime",
"namePlaceholder": "Unesite ime modela...",
"type": "Tip",
"typeState": "Stanje",
"typeObject": "Objekt",
"objectLabel": "Oznaka objekta",
"objectLabelPlaceholder": "Odaberite vrstu objekta...",
"classificationType": "Vrsta klasifikacije",
"classificationTypeTip": "Učite više o vrstama klasifikacije",
"classificationTypeDesc": "Podoznake dodaju dodatni tekst oznaci objekta (npr. 'Ljudsko bit će: UPS'). Atributi su pretraživi metapodaci pohranjeni zasebno u metapodacima objekta.",
"classificationSubLabel": "Podoznaka",
"classificationAttribute": "Atribut",
"classes": "Klase",
"states": "Stanja",
"classesTip": "Učite više o klasama",
"classesStateDesc": "Definirajte različita stanja koja može imati područje kamere. Na primjer: 'otvoreno' i 'zatvoreno' za vrata garaže.",
"classesObjectDesc": "Definirajte različite kategorije u koje ćete klasificirati detektirane objekte. Na primjer: 'dostavljac', 'stanovnik', 'stranac' za klasifikaciju ljudi.",
"classPlaceholder": "Unesite ime klase...",
"errors": {
"nameRequired": "Ime modela je obavezno",
"nameLength": "Ime modela mora imati 64 znaka ili manje",
"nameOnlyNumbers": "Ime modela ne može sadržavati samo brojeve",
"classRequired": "Potrebna je bar jedna klasa",
"classesUnique": "Imena klasa moraju biti jedinstvena",
"noneNotAllowed": "Klasa 'none' nije dozvoljena",
"stateRequiresTwoClasses": "Modeli stanja zahtijevaju bar dvije klase",
"objectLabelRequired": "Molimo odaberite oznaku objekta",
"objectTypeRequired": "Molimo odaberite vrstu klasifikacije"
}
},
"step2": {
"description": "Odaberite kamere i definirajte područje koje ćete nadzirati za svaku kameru. Model će klasificirati stanje ovih područja.",
"cameras": "Kamere",
"selectCamera": "Odaberite Kameru",
"noCameras": "Kliknite + za dodavanje kamera",
"selectCameraPrompt": "Odaberite kameru iz popisa da biste definirali njezino područje nadzora"
},
"step3": {
"selectImagesPrompt": "Odaberite sve slike s: {{className}}",
"selectImagesDescription": "Kliknite na slike da biste ih odabrali. Kliknite Nadalje kada završite s ovom klasifikacijom.",
"allImagesRequired_one": "Molimo klasificirajte sve slike. Preostala je {{count}} slika.",
"allImagesRequired_few": "Molimo klasificirajte sve slike. Preostale su {{count}} slike.",
"allImagesRequired_other": "Molimo klasificirajte sve slike. Preostale su {{count}} slike.",
"generating": {
"title": "Generisanje uzoraka slika",
"description": "Frigate učitava reprezentativne slike iz vaših snimaka. Ovo može trajati trenutak..."
},
"training": {
"title": "Obučavanje modela",
"description": "Vaš model se trenutno obučava u pozadini. Zatvorite ovaj dijalog, a vaš model će započeti raditi odmah kada se obuka završi."
},
"retryGenerate": "Ponovno generisanje",
"noImages": "Nema generisanih uzoraka slika",
"classifying": "Klasifikacija i obuka...",
"trainingStarted": "Obuka je uspješno pokrenuta",
"modelCreated": "Model je uspješno stvoren. Koristite pogled Najnovije klasifikacije da dodate slike za nedostajuće stanja, a zatim obučite model.",
"errors": {
"noCameras": "Nema konfigurisanih kamera",
"noObjectLabel": "Nije odabrana oznaka objekta",
"generateFailed": "Neuspješno generisanje primera: {{error}}",
"generationFailed": "Generisanje nije uspješno. Molimo pokušajte ponovo.",
"classifyFailed": "Neuspješna klasifikacija slika: {{error}}"
},
"generateSuccess": "Uspješno generisane uzorak slike",
"refreshExamples": "Generiši nove primjere",
"refreshConfirm": {
"title": "Generiši nove primjere?",
"description": "Ovo će generisati novi skup slika i obrisati sve odabire, uključujući prethodne klase. Trebat će vam ponovno odabrati primjere za sve klase."
},
"missingStatesWarning": {
"title": "Primjeri nedostajućih klasa",
"description": "Nisu sve klase imale primjere. Pokušajte generisanje novih primjera da biste pronašli nedostajuću klasu, ili nastavite i koristite pogled Najnovije klasifikacije da biste kasnije dodali slike."
}
}
}
}

View File

@ -0,0 +1,18 @@
{
"documentTitle": "Uređivač konfiguracije - Frigate",
"configEditor": "Uređivač konfiguracije",
"safeConfigEditor": "Uređivač konfiguracije (Sigurnosni režim)",
"safeModeDescription": "Frigate je u sigurnosnom režimu zbog greške u validaciji konfiguracije.",
"copyConfig": "Kopiraj konfiguraciju",
"saveAndRestart": "Sačuvaj i ponovo pokreni",
"saveOnly": "Sačuvaj samo",
"confirm": "Napusti bez čuvanja?",
"toast": {
"success": {
"copyToClipboard": "Konfiguracija kopirana u međuspremnik."
},
"error": {
"savingError": "Greška prilikom čuvanja konfiguracije"
}
}
}

View File

@ -0,0 +1,92 @@
{
"alerts": "Upozorenja",
"detections": "Detekcije",
"camera": "Kamera",
"motion": {
"label": "Kretanje",
"only": "Samo pokret"
},
"allCameras": "Sve Kamere",
"empty": {
"alert": "Nema upozorenja za pregled",
"detection": "Nema detekcija za pregled",
"motion": "Nema podataka o pokretu",
"recordingsDisabled": {
"title": "Snimci moraju biti omogućeni",
"description": "Pregledni stavci mogu se stvarati samo za kameru kada su snimci omogućeni za tu kameru."
}
},
"timeline": {
"label": "vremenska linija",
"aria": "Odaberite vremensku liniju"
},
"zoomIn": "Uvećajte",
"zoomOut": "Umanjite",
"events": {
"label": "Događaji",
"aria": "Odaberite događaje",
"noFoundForTimePeriod": "Nema događaja za ovaj vremenski period."
},
"detail": {
"label": "Detalj",
"noDataFound": "Nema detaljnih podataka za pregled",
"aria": "Prekidač pregleda detalja",
"trackedObject_one": "{{count}} objekt",
"trackedObject_other": "{{count}} objekta",
"noObjectDetailData": "Nema dostupnih detaljnih podataka o objektu.",
"settings": "Postavke pregleda detalja",
"alwaysExpandActive": {
"title": "Uvijek proširujte aktivno",
"desc": "Uvijek proširite detalje objekta aktivnog stavka pregleda kada su dostupni."
}
},
"objectTrack": {
"trackedPoint": "Praćeni točka",
"clickToSeek": "Kliknite da biste prešli na ovo vrijeme"
},
"documentTitle": "Pregled - Frigate",
"recordings": {
"documentTitle": "Snimci - Frigate",
"invalidSharedLink": "Nemoguće otvoriti vezu za snimak sa vremenskom oznakom zbog greške u parsiranju.",
"invalidSharedCamera": "Nemoguće otvoriti vezu za snimak sa vremenskom oznakom zbog nepoznate ili neovlašćene kamere."
},
"calendarFilter": {
"last24Hours": "Poslednje 24 sata"
},
"markAsReviewed": "Označi kao pregledano",
"markTheseItemsAsReviewed": "Označi ove stavke kao pregledane",
"newReviewItems": {
"label": "Pregledaj nove stavke za pregled",
"button": "Nove stavke za pregled"
},
"selected_one": "{{count}} odabrano",
"selected_other": "{{count}} odabrano",
"select_all": "Sve",
"detected": "detektovano",
"normalActivity": "Normal",
"needsReview": "Treba pregledati",
"securityConcern": "Sigurnosna zabrinutost",
"motionSearch": {
"menuItem": "Pretraga kretanja",
"openMenu": "Opcije kamere"
},
"motionPreviews": {
"menuItem": "Pregledaj preglednike kretanja",
"title": "Preglednici kretanja: {{camera}}",
"mobileSettingsTitle": "Postavke preglednika kretanja",
"mobileSettingsDesc": "Prilagodite brzinu reprodukcije i osvetljavanje, i odaberite datum za pregled snimaka samo sa kretanjem.",
"dim": "Osvetljavanje",
"dimAria": "Prilagodite intenzitet osvetljavanja",
"dimDesc": "Povećajte osvetljavanje da biste povećali vidljivost područja kretanja.",
"speed": "Brzina",
"speedAria": "Odaberite brzinu reprodukcije preglednika",
"speedDesc": "Odaberite koliko brzo će se pregledni snimci reproducirati.",
"back": "Nazad",
"empty": "Nema pregleda dostupnih",
"noPreview": "Pregled nije dostupan",
"seekAria": "Pretражuj {{camera}} igraču do {{time}}",
"filter": "Filtar",
"filterDesc": "Odaberite područja da biste prikazali samo klipove sa kretanjem u tim područjima.",
"filterClear": "Očisti"
}
}

View File

@ -0,0 +1,267 @@
{
"documentTitle": "Istraživanje - Frigate",
"generativeAI": "Generativna AI",
"exploreMore": "Istražite više {{label}} objekata",
"exploreIsUnavailable": {
"title": "Istraživanje nije dostupno",
"embeddingsReindexing": {
"context": "Istraživanje može se koristiti nakon što se reindeksiranje ugrađenih objekata završi.",
"startingUp": "Pokretanje…",
"estimatedTime": "Procijenjeno preostalo vrijeme:",
"finishingShortly": "Završetak uskoro",
"step": {
"thumbnailsEmbedded": "Ugrađene miniaturne slike: ",
"descriptionsEmbedded": "Ugrađene opise: ",
"trackedObjectsProcessed": "Obrađeni praćeni objekti: "
}
},
"downloadingModels": {
"context": "Frigate preuzima potrebne modele ugrađenih objekata kako bi podržao funkciju Semantičke pretrage. Ovo može trajati nekoliko minuta ovisno o brzini vaše mreže.",
"setup": {
"visionModel": "Model vida",
"visionModelFeatureExtractor": "Izvođač značajki modela vida",
"textModel": "Model teksta",
"textTokenizer": "Tokenizator teksta"
},
"tips": {
"context": "Moguće je da želite ponovno indeksirati ugrađene objekte koji se prate nakon što se modele preuzmu."
},
"error": "Dogodila se greška. Provjerite zapise Frigate."
}
},
"trackedObjectDetails": "Detalji praćenih objekata",
"type": {
"details": "Detalji",
"snapshot": "Snimak",
"thumbnail": "miniaturna slika",
"video": "Video",
"tracking_details": "detalji praćenja"
},
"trackingDetails": {
"title": "Detalji praćenja",
"noImageFound": "Nije pronađena slika za ovaj vremenski moment.",
"createObjectMask": "Kreirajte masku objekta",
"adjustAnnotationSettings": "Prilagodite postavke oznaka",
"scrollViewTips": "Kliknite da biste vidjeli važne trenutke životnog ciklusa ovog objekta.",
"autoTrackingTips": "Pozicije okvirnih kutija neće biti tačne za autotracking kamere.",
"count": "{{first}} od {{second}}",
"trackedPoint": "Praćena tačka",
"lifecycleItemDesc": {
"visible": "{{label}} detektovan",
"entered_zone": "{{label}} ušao u {{zones}}",
"active": "{{label}} postao aktivno",
"stationary": "{{label}} postao stacionarno",
"attribute": {
"faceOrLicense_plate": "{{attribute}} detektovan za {{label}}",
"other": "{{label}} prepoznat kao {{attribute}}"
},
"gone": "{{label}} otišao",
"heard": "{{label}} čujeo",
"external": "{{label}} detektovan",
"header": {
"zones": "Zone",
"ratio": "Omjer",
"area": "Površina",
"score": "Rezultat",
"computedScore": "Izračunata ocjena",
"topScore": "Najbolja ocjena",
"toggleAdvancedScores": "Prekidač naprednih ocjena"
}
},
"annotationSettings": {
"title": "Postavke oznaka",
"showAllZones": {
"title": "Prikaži sve zone",
"desc": "Uvijek prikazujte zone na okvirima gdje su objekti ušli u zonu."
},
"offset": {
"label": "Pomak oznaka",
"desc": "Ova podatka dolaze iz vaše kamere detektovane snimke, ali se preklapaju na slikama iz snimke snimke. Vjerojatno nije moguće da su dva toka savršeno sinhronizirana. Kao rezultat, okvirni kutiji i snimke neće se savršeno poklopiti. Možete koristiti ovu postavku da pomaknete oznake unaprijed ili unazad u vremenu da bi ih bolje uskladili s snimljenim snimkom.",
"millisecondsToOffset": "Milisekunde za pomak detektovanih oznaka. <em>Podrazumevano: 0</em>",
"tips": "Smanjite vrijednost ako je reprodukcija videa ispred kutija i tačaka putanje, a povećajte vrijednost ako je reprodukcija videa iza njih. Ova vrijednost može biti negativna.",
"toast": {
"success": "Pomak anotacije za {{camera}} je sačuvan u konfiguracionu datoteku."
}
}
},
"carousel": {
"previous": "Prethodni slajd",
"next": "Sljedeći slajd"
}
},
"details": {
"item": {
"title": "Pregled detalja stavke",
"desc": "Detalji stavke za pregled",
"button": {
"share": "Dijelite ovu stavku za pregled",
"viewInExplore": "Pregledajte u Explore"
},
"tips": {
"mismatch_one": "{{count}} nedostupan objekat je detektovan i uključen u ovu stavku pregleda. Ti objekti se nisu kvalifikovali kao upozorenje ili detekcija, ili su već očišćeni/obrisani.",
"mismatch_few": "{{count}} nedostupnih objekata je detektovano i uključeno u ovu stavku pregleda. Ti objekti se nisu kvalifikovali kao upozorenje ili detekciju, ili su već očišćeni/obrisani.",
"mismatch_other": "{{count}} nedostupnih objekata je detektovano i uključeno u ovu stavku pregleda. Ti objekti se nisu kvalifikovali kao upozorenje ili detekciju, ili su već očišćeni/obrisani.",
"hasMissingObjects": "Prilagodite svoju konfiguraciju ako želite da Frigate sačuva pratiti objekte za sljedeće oznake: <em>{{objects}}</em>"
},
"toast": {
"success": {
"regenerate": "Zahtjev za novi opis je poslat {{provider}}. Ovisno o brzini vašeg provajdera, novi opis može potrajati neko vrijeme da se ponovno generira.",
"updatedSublabel": "Uspješno ažurirana podjezika.",
"updatedLPR": "Uspješno ažurirana tablica.",
"updatedAttributes": "Uspješno ažurirana atribute.",
"audioTranscription": "Uspješno zahtjev za audio transkripciju. Ovisno o brzini vašeg Frigate servera, transkripcija može potrajati neko vrijeme da se završi."
},
"error": {
"regenerate": "Neuspješno poziv {{provider}} za novi opis: {{errorMessage}}",
"updatedSublabelFailed": "Neuspješno ažuriranje podjezika: {{errorMessage}}",
"updatedLPRFailed": "Neuspješno ažuriranje tablice: {{errorMessage}}",
"updatedAttributesFailed": "Neuspješno ažuriranje atribute: {{errorMessage}}",
"audioTranscription": "Neuspješno zahtjev za audio transkripciju: {{errorMessage}}"
}
}
},
"label": "Oznaka",
"editSubLabel": {
"title": "Uredi podjeziku",
"desc": "Unesite novu podjeziku za ovaj {{label}}",
"descNoLabel": "Unesite novu podjeziku za ovaj pratiti objekt"
},
"editLPR": {
"title": "Uredi tablica",
"desc": "Unesite novu vrijednost tablice za ovaj {{label}}",
"descNoLabel": "Unesite novu vrijednost tablice za ovaj praćeni objekt"
},
"editAttributes": {
"title": "Uredi atribute",
"desc": "Odaberite atribute klasifikacije za ovaj {{label}}"
},
"snapshotScore": {
"label": "Snimak Rezultat"
},
"topScore": {
"label": "Najbolji Rezultat",
"info": "Najbolji rezultat je najviši srednji rezultat za praćeni objekt, pa se može razlikovati od rezultata prikazanog na minijaturi rezultata pretrage."
},
"score": {
"label": "Rezultat"
},
"recognizedLicensePlate": "Prepoznata tablica",
"attributes": "Atributi klasifikacije",
"estimatedSpeed": "Procijenjena brzina",
"objects": "Objekti",
"camera": "Kamera",
"zones": "Zone",
"timestamp": "Vremenski pečat",
"button": {
"findSimilar": "Pronađi slične",
"regenerate": {
"title": "Regeneriraj",
"label": "Regeneriraj opis praćenog objekta"
}
},
"description": {
"label": "Opis",
"placeholder": "Opis praćenog objekta",
"aiTips": "Frigate neće tražiti opis od vašeg generativnog AI provajdera dok se životni vijek praćenog objekta ne završi."
},
"expandRegenerationMenu": "Proširi izbornik regeneracije",
"regenerateFromSnapshot": "Regeneriraj iz snimka",
"regenerateFromThumbnails": "Regeneriraj iz minijatura",
"tips": {
"descriptionSaved": "Uspješno sačuvan opis",
"saveDescriptionFailed": "Neuspješno ažuriranje opisa: {{errorMessage}}"
},
"title": {
"label": "Naslov"
},
"scoreInfo": "Informacije o rezultatu"
},
"itemMenu": {
"downloadVideo": {
"label": "Preuzmi video",
"aria": "Preuzmi video"
},
"downloadSnapshot": {
"label": "Preuzmi snimak",
"aria": "Preuzmi snimak"
},
"downloadCleanSnapshot": {
"label": "Preuzmi čist snimak",
"aria": "Preuzmi čist snimak"
},
"viewTrackingDetails": {
"label": "Pregledaj detalje praćenja",
"aria": "Prikaži detalje praćenja"
},
"findSimilar": {
"label": "Pronađi slične",
"aria": "Pronađi slične praćene objekte"
},
"addTrigger": {
"label": "Dodaj izazov",
"aria": "Dodaj izazov za ovaj praćeni objekt"
},
"audioTranscription": {
"label": "Transkriptiraj",
"aria": "Zatraži transkripciju zvuka"
},
"submitToPlus": {
"label": "Pošalji na Frigate+",
"aria": "Pošalji na Frigate Plus"
},
"viewInHistory": {
"label": "Pregledajte u povijesti",
"aria": "Pregledajte u povijesti"
},
"deleteTrackedObject": {
"label": "Obriši ovaj praćeni objekt"
},
"showObjectDetails": {
"label": "Prikaži put objekta"
},
"hideObjectDetails": {
"label": "Sakrij put objekta"
},
"debugReplay": {
"label": "Debug ponovno snimanje",
"aria": "Pregledaj ovaj praćeni objekt u pogledu debug ponovnog snimanja"
},
"more": {
"aria": "Više"
}
},
"dialog": {
"confirmDelete": {
"title": "Potvrdi brisanje",
"desc": "Brisanje ovog praćenog objekta uklanja snimak, bilo kakve sačuvane ugradnje, i sve povezane unose detalja praćenja. Snimljeni materijal ovog praćenog objekta u pogledu povijesti <em>NEĆE</em> biti obrisan.<br /><br />Sigurno li želite nastaviti?"
},
"toast": {
"error": "Greška prilikom brisanja ovog praćenog objekta: {{errorMessage}}"
}
},
"noTrackedObjects": "Nijedan praćeni objekt nije pronađen",
"fetchingTrackedObjectsFailed": "Greška prilikom dohvaćanja praćenih objekata: {{errorMessage}}",
"trackedObjectsCount_one": "{{count}} praćeni objekt ",
"trackedObjectsCount_few": "{{count}} praćena objekta ",
"trackedObjectsCount_other": "{{count}} praćena objekta ",
"searchResult": {
"tooltip": "Pronađeno {{type}} na {{confidence}}%",
"previousTrackedObject": "Prethodni praćeni objekt",
"nextTrackedObject": "Sljedeći praćeni objekt",
"deleteTrackedObject": {
"toast": {
"success": "Praćeni objekt je uspješno obrisan.",
"error": "Neuspješno brisanje praćenog objekta: {{errorMessage}}"
}
}
},
"aiAnalysis": {
"title": "Analiza AI"
},
"concerns": {
"label": "Pitanja"
},
"objectLifecycle": {
"noImageFound": "Nije pronađena slika za ovaj praćeni objekt."
}
}

View File

@ -0,0 +1,128 @@
{
"search": "Pretraga",
"documentTitle": "Izvoz - Frigate",
"selected_one": "{{count}} odabrano",
"selected_other": "{{count}} odabrano",
"noExports": "Nijedan izvoz nije pronađen",
"headings": {
"cases": "Slučajevi",
"uncategorizedExports": "Nekategorizirani izvozi"
},
"deleteExport": {
"label": "Obriši izvoz",
"desc": "Da li ste sigurni da želite da obrišete {{exportName}}?"
},
"editExport": {
"title": "Preimenuj izvoz",
"desc": "Unesite novi naziv za ovaj izvoz.",
"saveExport": "Sačuvaj izvoz"
},
"tooltip": {
"shareExport": "Dijeli izvoz",
"downloadVideo": "Preuzmi video",
"editName": "Uredi naziv",
"deleteExport": "Obriši izvoz",
"assignToCase": "Dodaj u slučaj",
"removeFromCase": "Ukloni iz slučaja"
},
"toolbar": {
"newCase": "Novi slučaj",
"addExport": "Dodaj izvoz",
"editCase": "Uredi slučaj",
"deleteCase": "Obriši slučaj"
},
"toast": {
"error": {
"renameExportFailed": "Neuspješno preimenovanje izvoza: {{errorMessage}}",
"assignCaseFailed": "Neuspješno ažuriranje dodjele slučaja: {{errorMessage}}",
"caseSaveFailed": "Neuspješno čuvanje slučaja: {{errorMessage}}",
"caseDeleteFailed": "Neuspješno brisanje slučaja: {{errorMessage}}"
}
},
"deleteCase": {
"label": "Obriši slučaj",
"desc": "Da li ste sigurni da želite da obrišete {{caseName}}?",
"descKeepExports": "Izvozi će ostati dostupni kao nekategorizirani izvozi.",
"descDeleteExports": "Svi izvozi u ovom slučaju trajno će biti obrisani.",
"deleteExports": "Takođe izbriši izvoze"
},
"caseDialog": {
"title": "Dodaj u slučaj",
"description": "Odaberite postojeći slučaj ili napravite novi.",
"selectLabel": "Slučaj",
"newCaseOption": "Napravite novi slučaj",
"nameLabel": "Ime slučaja",
"descriptionLabel": "Opis"
},
"caseCard": {
"emptyCase": "Nema još izvoza"
},
"jobCard": {
"defaultName": "{{camera}} izvoz",
"queued": "U redu",
"running": "Pokretanje",
"preparing": "Priprema",
"copying": "Kopiranje",
"encoding": "Kodiranje",
"encodingRetry": "Kodiranje (ponovi)",
"finalizing": "Završavanje"
},
"caseView": {
"noDescription": "Nema opisa",
"createdAt": "Kreirano {{value}}",
"exportCount_one": "1 izvoz",
"exportCount_other": "{{count}} izvozi",
"cameraCount_one": "1 kamera",
"cameraCount_other": "{{count}} kamere",
"showMore": "Prikaži više",
"showLess": "Prikaži manje",
"emptyTitle": "Ovaj slučaj je prazan",
"emptyDescription": "Dodaj postojet će nekategorizirane izvoze kako bi slučaj ostao organizovan.",
"emptyDescriptionNoExports": "Nema dostupnih nekategoriziranih izvoza koje je moguće dodati još."
},
"caseEditor": {
"createTitle": "Kreiraj slučaj",
"editTitle": "Uredi slučaj",
"namePlaceholder": "Ime slučaja",
"descriptionPlaceholder": "Dodaj napomene ili kontekst za ovaj slučaj"
},
"addExportDialog": {
"title": "Dodaj izvoz u {{caseName}}",
"searchPlaceholder": "Pretraga nekategoriziranih izvoza",
"empty": "Nema nekategoriziranih izvoza koji odgovaraju ovoj pretrazi.",
"addButton_one": "Dodaj 1 izvoz",
"addButton_other": "Dodaj {{count}} izvoza",
"adding": "Dodavanje..."
},
"bulkActions": {
"addToCase": "Dodaj u slučaj",
"moveToCase": "Premjesti u slučaj",
"removeFromCase": "Ukloni iz slučaja",
"delete": "Obriši",
"deleteNow": "Obriši sada"
},
"bulkDelete": {
"title": "Obriši izvoze",
"desc_one": "Sigurni li ste da želite obrisati {{count}} izvoz?",
"desc_other": "Sigurni li ste da želite obrisati {{count}} izvoze?"
},
"bulkRemoveFromCase": {
"title": "Ukloni iz slučaja",
"desc_one": "Ukloni {{count}} izvoz iz ovog slučaja?",
"desc_other": "Ukloni {{count}} izvoze iz ovog slučaja?",
"descKeepExports": "Izvozi će biti premješteni u nekategorizirane.",
"descDeleteExports": "Izvozi će biti trajno obrisani.",
"deleteExports": "Umjesto toga, obriši izvoze"
},
"bulkToast": {
"success": {
"delete": "Uspješno obrisani izvozi",
"reassign": "Uspješno ažurirana dodjela slučaja",
"remove": "Uspješno uklonjeni izvozi iz slučaja"
},
"error": {
"deleteFailed": "Neuspješno brisanje izvoza: {{errorMessage}}",
"reassignFailed": "Neuspješno ažuriranje dodjele slučaja: {{errorMessage}}"
}
}
}

View File

@ -0,0 +1,98 @@
{
"description": {
"addFace": "Dodajte novu kolekciju u Biblioteku lica prema učitavanju svoje prve slike.",
"placeholder": "Unesite ime za ovu kolekciju",
"invalidName": "Neprihvatljivo ime. Imena mogu sadržavati samo slova, brojeve, razmake, aposrofe, donje crte i crte.",
"nameCannotContainHash": "Ime ne može sadržavati #."
},
"details": {
"unknown": "Nepoznato",
"timestamp": "Vremenski pečat",
"scoreInfo": "Ocjena je težinski prosjek svih ocjena lica, težinski određen prema veličini lica u svakoj slici."
},
"train": {
"titleShort": "Nedavno",
"title": "Najnovije prepoznavanja",
"aria": "Odaberite nedavna prepoznavanja",
"empty": "Nema nedavnih pokušaja prepoznavanja lica"
},
"documentTitle": "Biblioteka lica - Frigate",
"uploadFaceImage": {
"title": "Učitajte sliku lica",
"desc": "Učitajte sliku za skeniranje lica i uključite za {{pageToggle}}"
},
"collections": "Kolekcije",
"createFaceLibrary": {
"new": "Stvori novo lice",
"nextSteps": "Da biste izgradili čvrstu osnovu:<li>Koristite karticu Najnovije prepoznavanja da biste odabrali i trenirali se na slikama za svaku detektiranu osobu.</li><li>Fokusirajte se na slike iz pravog ugla za najbolje rezultate; izbjegavajte slike za treniranje koje prikazuju lica pod uglom.</li></ul>"
},
"steps": {
"faceName": "Unesite ime lica",
"uploadFace": "Učitajte sliku lica",
"nextSteps": "Sljedeći koraci",
"description": {
"uploadFace": "Učitajte sliku od {{name}} koja prikazuje njihovo lice iz pravog ugla. Slika ne mora biti izrezana samo na njihovo lice."
}
},
"deleteFaceLibrary": {
"title": "Izbrišite ime",
"desc": "Da li ste sigurni da želite izbrisati kolekciju {{name}}? Ovo će trajno izbrisati sva povezana lica."
},
"deleteFaceAttempts": {
"title": "Izbrišite lica",
"desc_one": "Da li ste sigurni da želite izbrisati {{count}} lice? Ova akcija ne može se poništiti.",
"desc_few": "Da li ste sigurni da želite izbrisati {{count}} lica? Ova akcija ne može se poništiti.",
"desc_other": "Da li ste sigurni da želite izbrisati {{count}} lica? Ova akcija ne može se poništiti."
},
"renameFace": {
"title": "Preimenujte lice",
"desc": "Unesite novo ime za {{name}}"
},
"button": {
"deleteFaceAttempts": "Izbrišite lica",
"addFace": "Dodaj lice",
"renameFace": "Preimenuj lice",
"deleteFace": "Obriši lice",
"uploadImage": "Prenesi sliku",
"reprocessFace": "Ponovno obradi lice"
},
"imageEntry": {
"validation": {
"selectImage": "Molimo izaberite datoteku slike."
},
"dropActive": "Pustite sliku ovdje…",
"dropInstructions": "Povucite i ispišite, zalijepite sliku ovdje ili kliknite za odabir",
"maxSize": "Maksimalna veličina: {{size}}MB"
},
"nofaces": "Nema dostupnih lica",
"trainFaceAs": "Obuči lice kao:",
"trainFace": "Obuči lice",
"reclassifyFaceAs": "Ponovno klasificiraj lice kao:",
"reclassifyFace": "Ponovno klasificiraj lice",
"toast": {
"success": {
"uploadedImage": "Uspješno prenesena slika.",
"addFaceLibrary": "{{name}} je uspješno dodan u biblioteku lica!",
"deletedFace_one": "Uspješno obrisano {{count}} lice.",
"deletedFace_few": "Uspješno obrisana {{count}} lica.",
"deletedFace_other": "Uspješno obrisana {{count}} lica.",
"deletedName_one": "{{count}} lice je uspješno obrisano.",
"deletedName_few": "{{count}} lica su uspješno obrisana.",
"deletedName_other": "{{count}} lica su uspješno obrisana.",
"renamedFace": "Uspješno preimenovan lice na {{name}}",
"trainedFace": "Uspješno obučeno lice.",
"reclassifiedFace": "Uspješno ponovno klasificirano lice.",
"updatedFaceScore": "Uspješno ažurirana ocjena lica na {{name}} ({{score}})."
},
"error": {
"uploadingImageFailed": "Nije uspješno prenijeti sliku: {{errorMessage}}",
"addFaceLibraryFailed": "Nije uspješno postaviti ime lica: {{errorMessage}}",
"deleteFaceFailed": "Neuspješno brisanje: {{errorMessage}}",
"deleteNameFailed": "Nije uspješno obrisati ime: {{errorMessage}}",
"renameFaceFailed": "Nije uspješno preimenovati lice: {{errorMessage}}",
"trainFailed": "Nije uspješno trenirati: {{errorMessage}}",
"reclassifyFailed": "Nije uspješno ponovno klasifikovati lice: {{errorMessage}}",
"updateFaceScoreFailed": "Nije uspješno ažurirati bodove lica: {{errorMessage}}"
}
}
}

View File

@ -0,0 +1,199 @@
{
"documentTitle": {
"default": "Uživo - Frigate",
"withCamera": "{{camera}} - Uživo - Frigate"
},
"lowBandwidthMode": "Nizopojasni režim",
"twoWayTalk": {
"enable": "Omogući dvostrani razgovor",
"disable": "Onemogući dvostrani razgovor"
},
"cameraAudio": {
"enable": "Omogući zvuk kamere",
"disable": "Onemogući zvuk kamere"
},
"ptz": {
"move": {
"clickMove": {
"label": "Kliknite unutar okvira da biste centrirali kameru",
"enable": "Omogući klik za pomak",
"enableWithZoom": "Omogući klik za pomak / povucite za uvećanje",
"disable": "Onemogući klik za pomak"
},
"left": {
"label": "Pomaknite PTZ kameru ulevo"
},
"up": {
"label": "Pomaknite PTZ kameru gore"
},
"down": {
"label": "Pomaknite PTZ kameru dolje"
},
"right": {
"label": "Pomaknite PTZ kameru udesno"
}
},
"zoom": {
"in": {
"label": "Uvećajte PTZ kameru"
},
"out": {
"label": "Umanjite PTZ kameru"
}
},
"focus": {
"in": {
"label": "Fokusirajte PTZ kameru unapred"
},
"out": {
"label": "Fokusirajte PTZ kameru unazad"
}
},
"frame": {
"center": {
"label": "Kliknite unutar okvira da biste centrirali PTZ kameru"
}
},
"presets": "Preseti PTZ kamere"
},
"camera": {
"enable": "Omogući kameru",
"disable": "Onemogući kameru"
},
"muteCameras": {
"enable": "Utišajte sve kamere",
"disable": "Ponovo uključite zvuk za sve kamere"
},
"detect": {
"enable": "Omogući detekciju",
"disable": "Onemogući detekciju"
},
"recording": {
"enable": "Omogući snimanje",
"disable": "Onemogući snimanje"
},
"snapshots": {
"enable": "Omogući snimke",
"disable": "Onemogući snimke"
},
"snapshot": {
"takeSnapshot": "Preuzmi trenutni snimak",
"noVideoSource": "Nema dostupnog video izvora za snimak.",
"captureFailed": "Neuspješno snimanje trenutnog snimka.",
"downloadStarted": "Preuzimanje trenutnog snimka započeto."
},
"audioDetect": {
"enable": "Omogući detekciju zvuka",
"disable": "Onemogući detekciju zvuka"
},
"transcription": {
"enable": "Omogući prepoznavanje zvuka uživo",
"disable": "Onemogući prepoznavanje zvuka uživo"
},
"autotracking": {
"enable": "Omogući automatsko praćenje",
"disable": "Onemogući automatsko praćenje"
},
"streamStats": {
"enable": "Prikaži statistiku prijenosa",
"disable": "Sakrij statistiku prijenosa"
},
"manualRecording": {
"title": "Na zahtjev",
"tips": "Preuzmi trenutni snimak ili pokreni ručni događaj na temelju postavki trajanja snimanja ove kamere.",
"playInBackground": {
"label": "Ponovno postavi stream",
"desc": "Omogući ovu opciju da nastavi streamanje kada je pokazivač sakriven."
},
"showStats": {
"label": "Prikaži statistiku",
"desc": "Omogući ovu opciju da prikaže statistiku prijenosa kao preklapanje na toku kamere."
},
"debugView": "Pregled za otklanjanje grešaka",
"start": "Počni snimanje na zahtjev",
"started": "Pokrenuto ručno snimanje na zahtjev.",
"failedToStart": "Neuspješno pokretanje ručnog snimanja na zahtjev.",
"recordDisabledTips": "Kako je snimanje onemogućeno ili ograničeno u konfiguraciji za ovu kameru, spremat će se samo snimak.",
"end": "Završi snimanje na zahtjev",
"ended": "Završeno ručno snimanje na zahtjev.",
"failedToEnd": "Neuspješno završavanje ručnog snimanja na zahtjev."
},
"streamingSettings": "Postavke streamanja",
"notifications": "Obavještenja",
"audio": "Audio",
"suspend": {
"forTime": "Pauziraj za: "
},
"stream": {
"title": "Tok",
"audio": {
"tips": {
"title": "Audio mora biti izlaz iz vaše kamere i konfiguriran u go2rtc za ovaj stream."
},
"available": "Audio je dostupan za ovaj stream",
"unavailable": "Audio nije dostupan za ovaj stream"
},
"debug": {
"picker": "Izbor streama nije dostupan u režimu debuga. Pregled debuga uvijek koristi stream dodeljen ulozi detekcije."
},
"twoWayTalk": {
"tips": "Vaš uređaj mora podržavati funkciju, a WebRTC mora biti konfiguriran za dvosmernu komunikaciju.",
"available": "Dvosmerna komunikacija je dostupna za ovaj stream",
"unavailable": "Dvosmerna komunikacija nije dostupna za ovaj stream"
},
"lowBandwidth": {
"tips": "Živo prikazivanje je u režimu niske propusnosti zbog buferiranja ili grešaka u streamu.",
"resetStream": "Ponovno postavi stream"
},
"playInBackground": {
"label": "Ponovno postavi stream",
"tips": "Omogući ovu opciju da nastavi streamanje kada je pokazivač sakriven."
}
},
"cameraSettings": {
"title": "{{camera}} Postavke",
"cameraEnabled": "Kamera omogućena",
"objectDetection": "Detekcija objekata",
"recording": "Snimanje",
"snapshots": "Snimci",
"audioDetection": "Detekcija zvuka",
"transcription": "Transkripcija zvuka",
"autotracking": "Autotračenje"
},
"history": {
"label": "Prikaži povijesne snimke"
},
"effectiveRetainMode": {
"modes": {
"all": "Sve",
"motion": "Kretanje",
"active_objects": "Aktivni objekti"
}
},
"editLayout": {
"label": "Uredi raspored",
"group": {
"label": "Uredi grupu kamera"
},
"exitEdit": "Izađi iz uređivanja"
},
"noCameras": {
"title": "Nema konfiguriranih kamera",
"description": "Počnite tako što ćete povezati kameru s Frigate.",
"buttonText": "Dodaj kameru",
"restricted": {
"title": "Nema dostupnih kamera",
"description": "Nemate dozvolu za pregled bilo koje kamere u ovoj grupi."
},
"default": {
"title": "Nema konfiguriranih kamera",
"description": "Počnite tako što ćete povezati kameru s Frigate.",
"buttonText": "Dodaj kameru"
},
"group": {
"title": "Nema kamera u grupi",
"description": "Ova grupa kamera nema dodeljene ili omogućene kamere.",
"buttonText": "Upravljajte grupama"
}
}
}

View File

@ -0,0 +1,77 @@
{
"documentTitle": "Pretraga pokreta - Frigate",
"title": "Pretraga pokreta",
"description": "Nacrtaj poligon da biste definirali regiju interesa, a zatim navedite vremenski raspon za pretragu promjena pokreta unutar te regije.",
"selectCamera": "Pretraga pokreta učitava se",
"startSearch": "Počni pretragu",
"searchStarted": "Pretraga započeta",
"searchCancelled": "Pretraga otkazana",
"cancelSearch": "Otkaži",
"searching": "Pretraga u toku.",
"searchComplete": "Pretraga završena",
"noResultsYet": "Pokrenite pretragu da biste pronašli promjene pokreta u odabranoj regiji",
"noChangesFound": "Nisu otkrivene promjene piksela u odabranoj regiji",
"changesFound_one": "Pronađeno {{count}} promjena pokreta",
"changesFound_few": "Pronađeno {{count}} nekoliko pokreta",
"changesFound_other": "Pronađeno {{count}} promjene pokreta",
"framesProcessed": "{{count}} okvir procesiran",
"jumpToTime": "Preskoči na ovo vrijeme",
"results": "Rezultati",
"showSegmentHeatmap": "Top mapa",
"newSearch": "Nova pretraga",
"clearResults": "Očisti rezultate",
"clearROI": "Očisti poligon",
"polygonControls": {
"points_one": "{{count}} tačka",
"points_few": "{{count}} tačke",
"points_other": "{{count}} tačke",
"undo": "Poništi posljednju tačku",
"reset": "Ponovi poligon"
},
"motionHeatmapLabel": "Top mapa pokreta",
"dialog": {
"title": "Pretraga pokreta",
"cameraLabel": "Kamera",
"previewAlt": "Pregled kamere za {{camera}}"
},
"timeRange": {
"title": "Opseg pretrage",
"start": "Početno vrijeme",
"end": "Krajnje vrijeme"
},
"settings": {
"title": "Postavke pretrage",
"parallelMode": "Paralelni način",
"parallelModeDesc": "Skeniranje više segmenata snimaka istovremeno (brže, ali značajno intenzivnije za CPU)",
"threshold": "Praga osjetljivosti",
"thresholdDesc": "Niže vrijednosti detektiraju manje promjene (1-255)",
"minArea": "Minimalna površina promjene",
"minAreaDesc": "Minimalni postotak područja interesa koji mora promijeniti da bi se smatrao značajnim",
"frameSkip": "Preskoči okvir",
"frameSkipDesc": "Obrađujte svaki N-ti okvir. Postavite ovo na brzinu okvira vaše kamere da biste obradili jedan okvir po sekundi (npr. 5 za 5 FPS kameru, 30 za 30 FPS kameru). Više vrijednosti će biti brže, ali mogu propustiti kratke događaje pokreta.",
"maxResults": "Maksimalni rezultati",
"maxResultsDesc": "Zaustavi nakon ovog broja odgovarajućih vremenskih oznaka"
},
"errors": {
"noCamera": "Molimo odaberite kameru",
"noROI": "Molimo nacrtajte područje interesa",
"noTimeRange": "Molimo odaberite vremenski opseg",
"invalidTimeRange": "Krajnje vrijeme mora biti nakon početnog vremena",
"searchFailed": "Pretraga neuspješna: {{message}}",
"polygonTooSmall": "Poligon mora imati najmanje 3 točke",
"unknown": "Nepoznata greška"
},
"changePercentage": "{{percentage}}% promijenjeno",
"metrics": {
"title": "Metrike pretrage",
"segmentsScanned": "Skenirani segmenti",
"segmentsProcessed": "Obrađeno",
"segmentsSkippedInactive": "Preskočeno (bez aktivnosti)",
"segmentsSkippedHeatmap": "Preskočeno (bez preklapanja ROI)",
"fallbackFullRange": "Povratni put skeniranje cijelog opsega",
"framesDecoded": "Dekodirani okviri",
"wallTime": "Vrijeme pretrage",
"segmentErrors": "Greške segmenta",
"seconds": "{{seconds}}s"
}
}

View File

@ -0,0 +1,12 @@
{
"filter": "Filtar",
"export": "Izvoz",
"calendar": "Kalendar",
"filters": "Filtari",
"toast": {
"error": {
"noValidTimeSelected": "Nije odabran valjan vremenski opseg",
"endTimeMustAfterStartTime": "Krajnje vrijeme mora biti nakon početnog vremena"
}
}
}

View File

@ -0,0 +1,59 @@
{
"title": "Debug ponavljanje",
"description": "Ponovno prikazivanje snimaka kamere za ispitivanje. Lista objekata prikazuje zakasnjelje sažetak detektiranih objekata, a kartica Zapisi prikazuje tok unutrašnjih poruka Frigate iz snimaka ponavljanja.",
"websocket_messages": "Poruke",
"dialog": {
"title": "Počni debug ponavljanje",
"description": "Kreiraj privremenu kameru za ponavljanje koja ponavlja povijesne snimke za ispitivanje problema detekcije i praćenja objekata. Kamera za ponavljanje će imati istu konfiguraciju detekcije kao i izvorna kamera. Odaberite vremenski raspon za početak.",
"camera": "Izvorna kamera",
"timeRange": "Vremenski opseg",
"preset": {
"1m": "Posljednja 1 minuta",
"5m": "Posljednje 5 minuta",
"timeline": "Iz vremenske linije",
"custom": "Prilagođeno"
},
"startButton": "Počni ponavljanje",
"selectFromTimeline": "Odaberite",
"starting": "Pokretanje ponavljanja...",
"startLabel": "Početak",
"endLabel": "Kraj",
"toast": {
"error": "Neuspješno pokretanje debug ponavljanja: {{error}}",
"alreadyActive": "Već postoji aktivna sesija ponavljanja",
"stopError": "Neuspješno zaustavljanje debug ponavljanja: {{error}}",
"goToReplay": "Idi na ponavljanje"
}
},
"page": {
"noSession": "Nema aktivne sesije ponavljanja",
"noSessionDesc": "Pokrenite debug ponavljanje iz pogleda Povijest klikom na dugme Debug Replay u alatnoj traci.",
"goToRecordings": "Idi na povijest",
"sourceCamera": "Izvorna kamera",
"replayCamera": "Kamera za ponavljanje",
"initializingReplay": "Inicijalizacija ponavljanja...",
"stoppingReplay": "Zaustavljanje ponavljanja...",
"stopReplay": "Zaustavi ponavljanje",
"confirmStop": {
"title": "Zaustavi režim ponavljanja za debagovanje?",
"description": "Ovo će zaustaviti sesiju ponavljanja i očistiti sve privremene podatke. Sigurni li?",
"confirm": "Zaustavi ponavljanje",
"cancel": "Otkaži"
},
"activity": "Aktivnost",
"objects": "Popis objekata",
"audioDetections": "Audio detekcije",
"noActivity": "Nema detektovane aktivnosti",
"activeTracking": "Aktivno praćenje",
"noActiveTracking": "Nema aktivnog praćenja",
"configuration": "Konfiguracija",
"configurationDesc": "Podesiti precizno detekciju pokreta i praćenje objekata za kameru za debagovanje ponavljanja. Promjene se ne čuvaju u datoteci konfiguracije Frigate.",
"preparingClip": "Pripremam klip…",
"preparingClipDesc": "Frigate spaja snimke za odabrani vremenski raspon. Ovo može potrajati minut za duže raspone.",
"startingCamera": "Pokretanje ponovnog pokretanja otklanjanja grešaka…",
"startError": {
"title": "Neuspjelo pokretanje ponovnog prikaza otklanjanja grešaka",
"back": "Povratak na historiju"
}
}
}

View File

@ -0,0 +1,73 @@
{
"search": "Pretraga",
"button": {
"save": "Sačuvaj pretragu",
"clear": "Očisti pretragu",
"delete": "Obriši sačuvanu pretragu",
"filterInformation": "Filtrirajte informacije",
"filterActive": "Filtari aktivni"
},
"savedSearches": "Sačuvane pretrage",
"searchFor": "Pretraga za {{inputValue}}",
"trackedObjectId": "ID praćenog objekta",
"filter": {
"label": {
"cameras": "Kamere",
"labels": "Oznake",
"zones": "Zone",
"sub_labels": "Podoznake",
"attributes": "Atributi",
"search_type": "Tip pretrage",
"time_range": "Vremenski opseg",
"before": "Prije",
"after": "Nakon",
"min_score": "Min. bodovi",
"max_score": "Max. bodovi",
"min_speed": "Min. brzina",
"max_speed": "Max. brzina",
"recognized_license_plate": "Prepoznata tablica",
"has_clip": "Ima klip",
"has_snapshot": "Ima snimak"
},
"searchType": {
"thumbnail": "Minijatura",
"description": "Opis"
},
"toast": {
"error": {
"beforeDateBeLaterAfter": "Datum 'before' mora biti kasniji od datuma 'after'.",
"afterDatebeEarlierBefore": "Datum 'after' mora biti raniji od datuma 'before'.",
"minScoreMustBeLessOrEqualMaxScore": "Vrijednost 'min_score' mora biti manja ili jednaka vrijednosti 'max_score'.",
"maxScoreMustBeGreaterOrEqualMinScore": "Vrijednost 'max_score' mora biti veća ili jednaka vrijednosti 'min_score'.",
"minSpeedMustBeLessOrEqualMaxSpeed": "Vrijednost 'min_speed' mora biti manja ili jednaka vrijednosti 'max_speed'.",
"maxSpeedMustBeGreaterOrEqualMinSpeed": "Vrijednost 'max_speed' mora biti veća ili jednaka vrijednosti 'min_speed'."
}
},
"tips": {
"title": "Kako koristiti tekstualne filtere",
"desc": {
"text": "Filteri vam pomažu da sužite rezultate pretrage. Evo kako ih koristiti u polju za unos:",
"step1": "Unesite ime ključa filtera, zatim dvojtočku (npr. \"kamere:\").",
"step2": "Izaberite vrijednost iz predloga ili unesite vlastitu.",
"step3": "Koristite više filtera dodavanjem jednog za drugim s razmakom između.",
"step4": "Filteri datuma (pre: i nakon:) koriste {{DateFormat}} format.",
"step5": "Filter raspona vremena koristi format {{exampleTime}}.",
"step6": "Uklonite filtre klikom na 'x' pored njih.",
"exampleLabel": "Primjer:"
}
},
"header": {
"currentFilterType": "Vrijednosti filtera",
"noFilters": "Filtari",
"activeFilters": "Aktivni filteri"
}
},
"similaritySearch": {
"title": "Pretraga sličnosti",
"active": "Pretraga sličnosti aktivna",
"clear": "Očisti pretragu sličnosti"
},
"placeholder": {
"search": "Pretraži…"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,256 @@
{
"documentTitle": {
"cameras": "Statistika kamere - Frigate",
"storage": "Statistika skladišta - Frigate",
"general": "Opća statistika - Frigate",
"enrichments": "Statistika bogatstva - Frigate",
"logs": {
"frigate": "Zapisi Frigate - Frigate",
"go2rtc": "Zapisi Go2RTC - Frigate",
"nginx": "Zapisi Nginx - Frigate",
"websocket": "Zapisi poruka - Frigate"
}
},
"title": "Sistem",
"metrics": "Sistem metrike",
"logs": {
"websocket": {
"label": "Zapisi",
"pause": "Pauziraj",
"resume": "Nastavi",
"clear": "Očisti",
"filter": {
"all": "Svi temi",
"topics": "Teme",
"events": "Događaji",
"reviews": "Pregledi",
"classification": "Klasifikacija",
"face_recognition": "Prepoznavanje lica",
"lpr": "LPR",
"camera_activity": "Aktivnost kamere",
"system": "Sistem",
"camera": "Kamera",
"all_cameras": "Sve kamere",
"cameras_count_one": "{{count}} Kamera",
"cameras_count_other": "{{count}} Kamere"
},
"empty": "Nema još prihvaćenih poruka",
"count_one": "{{count}} poruka",
"count_other": "{{count}} poruke",
"expanded": {
"payload": "Opterećenje"
}
},
"download": {
"label": "Preuzimanje zapisa"
},
"copy": {
"label": "Kopiraj u clipboard",
"success": "Zapisi su kopirani u clipboard",
"error": "Nije moguće kopirati zapise u clipboard"
},
"type": {
"label": "Tip",
"timestamp": "Vremenski pečat",
"tag": "Oznaka",
"message": "Poruka"
},
"tips": "Zapisi se prenose sa servera",
"toast": {
"error": {
"fetchingLogsFailed": "Greška prilikom preuzimanja zapisa: {{errorMessage}}",
"whileStreamingLogs": "Greška prilikom prijenosa protokola: {{errorMessage}}"
}
}
},
"general": {
"title": "Općenito",
"detector": {
"title": "Detektori",
"inferenceSpeed": "Brzina zaključivanja detektora",
"temperature": "Temperatura detektora",
"cpuUsage": "Korištenje CPU detektora",
"cpuUsageInformation": "CPU korištena za pripremu ulaznih i izlaznih podataka za/iz modela detekcije. Ova vrijednost ne mjeri korištenje zaključivanja, čak i ako se koristi GPU ili ubrzivač.",
"memoryUsage": "Korištenje memorije detektora"
},
"hardwareInfo": {
"title": "Hardverske informacije",
"gpuUsage": "Korištenje GPU",
"gpuMemory": "Memorija GPU",
"gpuEncoder": "Kodiralo GPU",
"gpuCompute": "GPU Izračunavanje / Kodiranje",
"gpuDecoder": "Dekodiranje GPU",
"gpuTemperature": "Temperatura GPU",
"gpuInfo": {
"vainfoOutput": {
"title": "Vainfo Izlaz",
"returnCode": "Kod povratka: {{code}}",
"processOutput": "Izlaz procesa:",
"processError": "Greška procesa:"
},
"nvidiaSMIOutput": {
"title": "Nvidia SMI Izlaz",
"name": "Ime: {{name}}",
"driver": "Vozač: {{driver}}",
"cudaComputerCapability": "CUDA sposobnost izračunavanja: {{cuda_compute}}",
"vbios": "VBios informacije: {{vbios}}"
},
"closeInfo": {
"label": "Zatvori informacije GPU"
},
"copyInfo": {
"label": "Kopiraj informacije GPU"
},
"toast": {
"success": "Kopirano informacije GPU u međuspremnik"
}
},
"npuUsage": "Korišćenje NPU",
"npuMemory": "Memorija NPU",
"npuTemperature": "Temperatura NPU",
"intelGpuWarning": {
"title": "Upozorenje o statistikama Intel GPU",
"message": "Statistike GPU nedostupne",
"description": "Ovo je poznati bug u alatima za prikaz statistika Intel GPU (intel_gpu_top) gdje će se prekiniti i ponovo vratiti GPU korišćenje od 0% čak i u slučajevima kada se hardverska akceleracija i detekcija objekata ispravno izvršavaju na (i)GPU. Ovo nije bug Frigate. Možete ponovo pokrenuti host kako biste privremeno popravili problem i potvrdili da GPU radi ispravno. Ovo ne utiče na performanse."
}
},
"otherProcesses": {
"title": "Drugi procesi",
"processCpuUsage": "Korišćenje CPU procesa",
"processMemoryUsage": "Korišćenje memorije procesa",
"series": {
"go2rtc": "go2rtc",
"recording": "Snimanje",
"review_segment": "pregled segmenta",
"embeddings": "Ugrađivanja",
"audio_detector": "audio detektor"
}
}
},
"storage": {
"title": "Skladište",
"overview": "Pregled",
"recordings": {
"title": "Snimci",
"tips": "Ova vrijednost predstavlja ukupno skladište koje se koristi za snimke u bazi podataka Frigate. Frigate ne praćenje korišćenje skladišta za sve datoteke na vašem disku.",
"earliestRecording": "Najstariji dostupni snimak:"
},
"shm": {
"title": "Alokacija SHM (deljenja memorije)",
"warning": "Trenutna veličina SHM od {{total}}MB je prevelika. Povećajte je na najmanje {{min_shm}}MB.",
"frameLifetime": {
"title": "Vijek trajanja okvira",
"description": "Svaka kamera ima {{frames}} slotova za okvire u deljenoj memoriji. Na najbržoj brzini okvira kamere, svaki okvir je dostupan za približno {{lifetime}}s prije nego što se prepiše."
}
},
"cameraStorage": {
"title": "Skladište kamere",
"camera": "Kamera",
"unusedStorageInformation": "Informacije o neiskorišćenom skladištu",
"storageUsed": "Skladište",
"percentageOfTotalUsed": "Postotak ukupno",
"bandwidth": "Širina pojasa",
"unused": {
"title": "Neiskorišćeno",
"tips": "Ova vrijednost može nepravilno predstavljati slobodno prostor dostupan Frigate ako imate druge datoteke pohranjene na vašem disku izvan snimaka Frigate. Frigate ne praćenje korišćenje skladišta izvan svojih snimaka."
}
}
},
"cameras": {
"title": "Kamere",
"overview": "Pregled",
"info": {
"aspectRatio": "odnos stranica",
"cameraProbeInfo": "{{camera}} Informacije o ispitivanju kamere",
"streamDataFromFFPROBE": "Podaci o prijenosu se dobijaju pomoću <code>ffprobe</code>.",
"fetching": "Prenošenje podataka o kameri",
"stream": "Prijenos {{idx}}",
"video": "Video:",
"codec": "Kodek:",
"resolution": "Rješenje:",
"fps": "FPS:",
"unknown": "Nepoznato",
"audio": "Zvuk:",
"error": "Greška: {{error}}",
"tips": {
"title": "Informacije o ispitivanju kamere"
}
},
"framesAndDetections": "Okviri / Detekcije",
"label": {
"camera": "Kamera",
"detect": "detektirati",
"skipped": "preskočeno",
"ffmpeg": "FFmpeg",
"capture": "snimiti",
"overallFramesPerSecond": "ukupni okviri po sekundi",
"overallDetectionsPerSecond": "ukupne detekcije po sekundi",
"overallSkippedDetectionsPerSecond": "ukupno preskočene detekcije po sekundi",
"cameraFfmpeg": "{{camName}} FFmpeg",
"cameraCapture": "{{camName}} snimiti",
"cameraDetect": "{{camName}} detektirati",
"cameraGpu": "{{camName}} GPU",
"cameraFramesPerSecond": "{{camName}} okviri po sekundi",
"cameraDetectionsPerSecond": "{{camName}} detekcije po sekundi",
"cameraSkippedDetectionsPerSecond": "{{camName}} preskočenih detekcija u sekundi"
},
"connectionQuality": {
"title": "Kvaliteta veze",
"excellent": "Izuzetno dobra",
"fair": "Uredna",
"poor": "Loša",
"unusable": "Nepogodna",
"fps": "FPS",
"expectedFps": "Očekivani FPS",
"reconnectsLastHour": "Ponovne povezivanja (posljednje satu)",
"stallsLastHour": "Pauze (posljednje satu)"
},
"toast": {
"success": {
"copyToClipboard": "Podaci o testiranju kopirani u clipboard."
},
"error": {
"unableToProbeCamera": "Nemoguće testiranje kamere: {{errorMessage}}"
}
}
},
"lastRefreshed": "Posljednje ažuriranje: ",
"stats": {
"ffmpegHighCpuUsage": "{{camera}} ima visoku upotrebu CPU za FFmpeg ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} ima visoku upotrebu CPU za detekciju ({{detectAvg}}%)",
"healthy": "Sistem je zdrav",
"reindexingEmbeddings": "Ponovno indeksiranje ugrađenih vjerodajnica ({{processed}}% završeno)",
"cameraIsOffline": "{{camera}} je offline",
"detectIsSlow": "{{detect}} je spor ({{speed}} ms)",
"detectIsVerySlow": "{{detect}} je vrlo spor ({{speed}} ms)",
"shmTooLow": "/dev/shm alokacija ({{total}} MB) treba povećati na najmanje {{min}} MB.",
"debugReplayActive": "Debug ponavljanje sesije je aktivno"
},
"enrichments": {
"title": "Obogaćivanja",
"infPerSecond": "Inferencije po sekundi",
"averageInf": "Prosjek vremena inferencije",
"embeddings": {
"image_embedding": "Slika ugrađenih vjerodajnica",
"text_embedding": "Tekst ugrađenih vjerodajnica",
"face_recognition": "Prepoznavanje lica",
"plate_recognition": "Prepoznavanje ploča",
"image_embedding_speed": "Brzina ugradnje slika",
"face_embedding_speed": "Brzina ugradnje lica",
"face_recognition_speed": "Brzina prepoznavanja lica",
"plate_recognition_speed": "Brzina prepoznavanja ploča",
"text_embedding_speed": "Brzina ugradnje teksta",
"yolov9_plate_detection_speed": "Brzina detekcije ploča YOLOv9",
"yolov9_plate_detection": "Detekcija ploča YOLOv9",
"review_description": "Pregled opisa",
"review_description_speed": "Brzina pregleda opisa",
"review_description_events_per_second": "Pregled opisa",
"object_description": "Opis objekta",
"object_description_speed": "Brzina opisa objekta",
"object_description_events_per_second": "Opis objekta",
"classification": "{{name}} Klasifikacija",
"classification_speed": "{{name}} Brzina klasifikacije",
"classification_events_per_second": "{{name}} Događaji klasifikacije po sekundi"
}
}
}

View File

@ -138,7 +138,7 @@
"plucked_string_instrument": "Instrument de corda pinçada",
"guitar": "Guitarra",
"electric_guitar": "Guitarra elèctrica",
"bass_guitar": "Baix",
"bass_guitar": "Guitarra baixa",
"acoustic_guitar": "Guitarra acústica",
"steel_guitar": "Guitarra steel",
"tapping": "Tapping",

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",
@ -242,7 +243,7 @@
"done": "Fet",
"disabled": "Deshabilitat",
"disable": "Deshabilitar",
"save": "Guardar",
"save": "Desa",
"copy": "Copiar",
"back": "Enrere",
"pictureInPicture": "Imatge en Imatge",

View File

@ -13,7 +13,7 @@
"description": "Habilitat"
},
"audio": {
"label": "Esdeveniments d'àudio",
"label": "Detecció d'àudio",
"description": "Configuració per a la detecció d'esdeveniments basats en àudio per a aquesta càmera.",
"enabled": {
"label": "Habilita la detecció d'àudio",
@ -33,7 +33,11 @@
},
"filters": {
"label": "Filtres d'àudio",
"description": "Paràmetres de filtre per-àudio-tipus, com ara llindars de confiança utilitzats per reduir falsos positius."
"description": "Paràmetres de filtre per-àudio-tipus, com ara llindars de confiança utilitzats per reduir falsos positius.",
"threshold": {
"label": "Confiança mínima de l'àudio",
"description": "Llindar mínim de confiança per a l'esdeveniment d'àudio a comptar."
}
},
"enabled_in_config": {
"label": "Estat d'àudio original",
@ -485,6 +489,10 @@
"hwaccel_args": {
"label": "Exporta els arguments de l'hwaccel",
"description": "Args d'acceleració de maquinari a utilitzar per a operacions d'exportació/transcodificació."
},
"max_concurrent": {
"label": "Màxim d'exportacions concurrents",
"description": "Nombre màxim de treballs d'exportació a processar al mateix temps."
}
},
"preview": {

Some files were not shown because too many files have changed in this diff Show More