Compare commits

...

26 Commits

Author SHA1 Message Date
Josh Hawkins
ce29c370b1
Merge b5a360be39 into 03f4f76b72 2026-05-19 20:03:09 -04: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
b5a360be39 add test 2026-04-17 17:18:11 -05:00
Josh Hawkins
54a7c5015e fix birdseye layout calculation
replace the two pass layout with a single pass pixel space algorithm
2026-04-17 17:18:04 -05:00
218 changed files with 18652 additions and 2048 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

@ -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

@ -774,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/"):

View File

@ -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,390 +72,6 @@ class VLMMonitorRequest(BaseModel):
zones: List[str] = []
def get_tool_definitions(
semantic_search_enabled: bool = False,
) -> 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.
"""
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 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"],
},
},
},
]
@router.get(
"/chat/tools",
dependencies=[Depends(allow_any_authenticated())],
@ -460,10 +80,13 @@ def get_tool_definitions(
)
def get_tools(request: Request) -> JSONResponse:
"""Get list of available tools for LLM function calling."""
semantic_search_enabled = bool(
getattr(request.app.frigate_config.semantic_search, "enabled", False)
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,
)
tools = get_tool_definitions(semantic_search_enabled=semantic_search_enabled)
return JSONResponse(content={"tools": tools})
@ -554,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,
@ -626,6 +252,7 @@ async def _execute_search_objects_semantic(
label = arguments.get("label")
sub_label = arguments.get("sub_label")
attribute = arguments.get("attribute")
zones = arguments.get("zones")
if isinstance(zones, list) and zones:
@ -668,6 +295,10 @@ async def _execute_search_objects_semantic(
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))
@ -1481,72 +1112,19 @@ async def chat_completion(
config = request.app.frigate_config
semantic_search_enabled = bool(getattr(config.semantic_search, "enabled", False))
tools = get_tool_definitions(semantic_search_enabled=semantic_search_enabled)
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 = []
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`."
)
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.{semantic_search_section}{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(
{
@ -1607,6 +1185,13 @@ 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")
@ -1707,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:
@ -1721,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 (
@ -1741,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"),

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

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

@ -629,10 +629,11 @@ 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":

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

@ -282,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
@ -310,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

@ -232,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()
):

View File

@ -100,7 +100,10 @@ class AudioProcessor(FrigateProcess):
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",
@ -206,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,315 +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
from frigate.genai.openai import _stats_from_openai_usage
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,
"stream_options": {"include_usage": 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"
usage_stats: Optional[dict[str, Any]] = None
stream = self.provider.chat.completions.create(**request_params) # type: ignore[call-overload]
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
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"
if usage_stats is not None:
yield ("stats", usage_stats)
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

@ -248,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)
@ -262,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:
@ -297,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:
@ -322,6 +336,7 @@ class GeminiClient(GenAIClient):
return {
"content": content,
"reasoning": reasoning,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
@ -330,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",
}
@ -339,6 +355,7 @@ class GeminiClient(GenAIClient):
)
return {
"content": None,
"reasoning": None,
"tool_calls": None,
"finish_reason": "error",
}
@ -477,12 +494,19 @@ 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
@ -519,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:
@ -565,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
@ -593,6 +622,7 @@ class GeminiClient(GenAIClient):
"message",
{
"content": full_content,
"reasoning": full_reasoning,
"tool_calls": tool_calls_list,
"finish_reason": finish_reason,
},
@ -604,6 +634,7 @@ class GeminiClient(GenAIClient):
"message",
{
"content": None,
"reasoning": None,
"tool_calls": None,
"finish_reason": "error",
},
@ -616,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
@ -75,6 +75,29 @@ def _parse_launch_arg(args: list[str], flag: str) -> str | 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:
@ -239,21 +262,7 @@ class LlamaCppClient(GenAIClient):
info["supports_tools"] = True
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()
props = _fetch_llama_props(base_url, configured_model)
if info["context_size"] is None:
default_settings = props.get("default_generation_settings", {})
@ -518,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,
}
@ -559,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,
@ -583,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
@ -752,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"
@ -781,6 +841,15 @@ class LlamaCppClient(GenAIClient):
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"])
@ -806,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"
@ -813,6 +883,7 @@ class LlamaCppClient(GenAIClient):
"message",
{
"content": full_content,
"reasoning": full_reasoning,
"tool_calls": tool_calls_list,
"finish_reason": finish_reason,
},

View File

@ -309,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
@ -336,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"):
@ -348,6 +352,7 @@ class OllamaClient(GenAIClient):
finish_reason = "stop"
return {
"content": content,
"reasoning": reasoning,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
@ -431,6 +436,9 @@ 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)
@ -449,6 +457,7 @@ 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)
@ -456,6 +465,10 @@ class OllamaClient(GenAIClient):
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)
@ -463,8 +476,10 @@ class OllamaClient(GenAIClient):
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",
}
@ -481,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

@ -38,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 = {
@ -203,6 +207,7 @@ class OpenAIClient(GenAIClient):
"model": self.genai_config.model,
"messages": messages,
"timeout": self.timeout,
**self.genai_config.runtime_options,
}
if tools:
@ -219,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
@ -235,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:
@ -269,6 +278,7 @@ class OpenAIClient(GenAIClient):
return {
"content": content,
"reasoning": reasoning,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
@ -277,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",
}
@ -284,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",
}
@ -315,6 +327,7 @@ class OpenAIClient(GenAIClient):
"timeout": self.timeout,
"stream": True,
"stream_options": {"include_usage": True},
**self.genai_config.runtime_options,
}
if tools:
@ -333,11 +346,12 @@ 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)
@ -354,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)
@ -382,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
@ -410,6 +434,7 @@ class OpenAIClient(GenAIClient):
"message",
{
"content": full_content,
"reasoning": full_reasoning,
"tool_calls": tool_calls_list,
"finish_reason": finish_reason,
},
@ -421,6 +446,7 @@ class OpenAIClient(GenAIClient):
"message",
{
"content": None,
"reasoning": None,
"tool_calls": None,
"finish_reason": "error",
},
@ -431,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

@ -595,112 +595,92 @@ class BirdsEyeFrameManager:
) -> Optional[list[list[Any]]]:
"""Calculate the optimal layout for 2+ cameras."""
def map_layout(
camera_layout: list[list[Any]], row_height: int
) -> tuple[int, int, Optional[list[list[Any]]]]:
"""Map the calculated layout."""
candidate_layout = []
starting_x = 0
x = 0
max_width = 0
y = 0
def find_available_x(
current_x: int,
width: int,
reserved_ranges: list[tuple[int, int]],
max_width: int,
) -> Optional[int]:
"""Find the first horizontal slot that does not collide with reservations."""
x = current_x
for row in camera_layout:
final_row = []
max_width = max(max_width, x)
x = starting_x
for cameras in row:
camera_dims = self.cameras[cameras[0]]["dimensions"].copy()
camera_aspect = cameras[1]
for reserved_start, reserved_end in sorted(reserved_ranges):
if x >= reserved_end:
continue
if camera_dims[1] > camera_dims[0]:
scaled_height = int(row_height * 2)
scaled_width = int(scaled_height * camera_aspect)
starting_x = scaled_width
else:
scaled_height = row_height
scaled_width = int(scaled_height * camera_aspect)
if x + width <= reserved_start:
return x
# layout is too large
if (
x + scaled_width > self.canvas.width
or y + scaled_height > self.canvas.height
):
return x + scaled_width, y + scaled_height, None
x = max(x, reserved_end)
final_row.append((cameras[0], (x, y, scaled_width, scaled_height)))
x += scaled_width
if x + width <= max_width:
return x
y += row_height
candidate_layout.append(final_row)
if max_width == 0:
max_width = x
return max_width, y, candidate_layout
canvas_aspect_x, canvas_aspect_y = self.canvas.get_aspect(coefficient)
camera_layout: list[list[Any]] = []
camera_layout.append([])
starting_x = 0
x = starting_x
y = 0
y_i = 0
max_y = 0
for camera in cameras_to_add:
camera_dims = self.cameras[camera]["dimensions"].copy()
camera_aspect_x, camera_aspect_y = self.canvas.get_camera_aspect(
camera, camera_dims[0], camera_dims[1]
)
if camera_dims[1] > camera_dims[0]:
portrait = True
else:
portrait = False
if (x + camera_aspect_x) <= canvas_aspect_x:
# insert if camera can fit on current row
camera_layout[y_i].append(
(
camera,
camera_aspect_x / camera_aspect_y,
)
)
if portrait:
starting_x = camera_aspect_x
else:
max_y = max(
max_y,
camera_aspect_y,
)
x += camera_aspect_x
else:
# move on to the next row and insert
y += max_y
y_i += 1
camera_layout.append([])
x = starting_x
if x + camera_aspect_x > canvas_aspect_x:
return None
camera_layout[y_i].append(
(
camera,
camera_aspect_x / camera_aspect_y,
)
)
x += camera_aspect_x
if y + max_y > canvas_aspect_y:
return None
row_height = int(self.canvas.height / coefficient)
total_width, total_height, standard_candidate_layout = map_layout(
camera_layout, row_height
)
def map_layout(row_height: int) -> tuple[int, int, Optional[list[list[Any]]]]:
"""Lay out cameras row by row while reserving portrait spans for the next row."""
candidate_layout: list[list[Any]] = []
reserved_ranges: dict[int, list[tuple[int, int]]] = {}
current_row: list[Any] = []
row_index = 0
row_y = 0
row_x = 0
max_width = 0
max_height = 0
for camera in cameras_to_add:
camera_dims = self.cameras[camera]["dimensions"].copy()
camera_aspect_x, camera_aspect_y = self.canvas.get_camera_aspect(
camera, camera_dims[0], camera_dims[1]
)
portrait = camera_dims[1] > camera_dims[0]
scaled_height = row_height * 2 if portrait else row_height
scaled_width = int(scaled_height * (camera_aspect_x / camera_aspect_y))
while True:
x = find_available_x(
row_x,
scaled_width,
reserved_ranges.get(row_index, []),
self.canvas.width,
)
if x is not None and row_y + scaled_height <= self.canvas.height:
current_row.append(
(camera, (x, row_y, scaled_width, scaled_height))
)
row_x = x + scaled_width
max_width = max(max_width, row_x)
max_height = max(max_height, row_y + scaled_height)
if portrait:
reserved_ranges.setdefault(row_index + 1, []).append(
(x, row_x)
)
break
if current_row:
candidate_layout.append(current_row)
current_row = []
row_index += 1
row_y = row_index * row_height
row_x = 0
if row_y + scaled_height > self.canvas.height:
overflow_width = max(max_width, scaled_width)
overflow_height = row_y + scaled_height
return overflow_width, overflow_height, None
if current_row:
candidate_layout.append(current_row)
return max_width, max_height, candidate_layout
row_height = max(1, int(self.canvas.height / coefficient))
total_width, total_height, standard_candidate_layout = map_layout(row_height)
if not standard_candidate_layout:
# if standard layout didn't work
@ -709,9 +689,9 @@ class BirdsEyeFrameManager:
total_width / self.canvas.width,
total_height / self.canvas.height,
)
row_height = int(row_height / scale_down_percent)
row_height = max(1, int(row_height / scale_down_percent))
total_width, total_height, standard_candidate_layout = map_layout(
camera_layout, row_height
row_height
)
if not standard_candidate_layout:
@ -725,8 +705,8 @@ class BirdsEyeFrameManager:
1 / (total_width / self.canvas.width),
1 / (total_height / self.canvas.height),
)
row_height = int(row_height * scale_up_percent)
_, _, scaled_layout = map_layout(camera_layout, row_height)
row_height = max(1, int(row_height * scale_up_percent))
_, _, scaled_layout = map_layout(row_height)
if scaled_layout:
return scaled_layout

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,11 +1,64 @@
"""Test camera user and password cleanup."""
"""Tests for Birdseye canvas sizing and layout behavior."""
import unittest
from multiprocessing import Event
from frigate.output.birdseye import get_canvas_shape
from frigate.config import FrigateConfig
from frigate.output.birdseye import BirdsEyeFrameManager, get_canvas_shape
class TestBirdseye(unittest.TestCase):
def _build_manager(
self, camera_dimensions: dict[str, tuple[int, int]]
) -> BirdsEyeFrameManager:
config = {
"mqtt": {"host": "mqtt"},
"birdseye": {"width": 1280, "height": 720},
"cameras": {},
}
for order, (camera, dimensions) in enumerate(
camera_dimensions.items(), start=1
):
config["cameras"][camera] = {
"ffmpeg": {
"inputs": [
{
"path": f"rtsp://10.0.0.1:554/{camera}",
"roles": ["detect"],
}
]
},
"detect": {
"width": dimensions[0],
"height": dimensions[1],
"fps": 5,
},
"birdseye": {"order": order},
}
return BirdsEyeFrameManager(FrigateConfig(**config), Event())
def _assert_no_overlaps(
self, layout: list[list[tuple[str, tuple[int, int, int, int]]]]
):
rectangles = [position for row in layout for _, position in row]
for index, rect in enumerate(rectangles):
x1, y1, width1, height1 = rect
for other in rectangles[index + 1 :]:
x2, y2, width2, height2 = other
overlap = (
x1 < x2 + width2
and x2 < x1 + width1
and y1 < y2 + height2
and y2 < y1 + height1
)
self.assertFalse(
overlap,
msg=f"Overlapping rectangles found: {rect} and {other}",
)
def test_16x9(self):
"""Test 16x9 aspect ratio works as expected for birdseye."""
width = 1280
@ -45,3 +98,104 @@ class TestBirdseye(unittest.TestCase):
canvas_width, canvas_height = get_canvas_shape(width, height)
assert canvas_width == width # width will be the same
assert canvas_height != height
def test_portrait_camera_does_not_overlap_next_row(self):
"""Portrait cameras should reserve their real horizontal position on the next row."""
manager = self._build_manager(
{
"cam_a": (1280, 720),
"cam_p": (360, 640),
"cam_b": (1280, 720),
"cam_c": (640, 480),
}
)
layout = manager.calculate_layout(["cam_a", "cam_p", "cam_b", "cam_c"], 3)
self.assertIsNotNone(layout)
assert layout is not None
self._assert_no_overlaps(layout)
cam_c = [
position for row in layout for camera, position in row if camera == "cam_c"
][0]
self.assertEqual(cam_c[0], 0)
def test_portrait_reservation_only_applies_to_next_row(self):
"""Portrait reservations should not push later rows after the span ends."""
manager = self._build_manager(
{
"cam_a": (1280, 720),
"cam_p": (360, 640),
"cam_b": (1280, 720),
"cam_c": (1280, 720),
"cam_d": (1280, 720),
"cam_e": (1280, 720),
}
)
layout = manager.calculate_layout(
["cam_a", "cam_p", "cam_b", "cam_c", "cam_d", "cam_e"],
3,
)
self.assertIsNotNone(layout)
assert layout is not None
self._assert_no_overlaps(layout)
cam_e = [
position for row in layout for camera, position in row if camera == "cam_e"
][0]
self.assertEqual(cam_e[0], 0)
def test_multiple_portraits_reserve_distinct_ranges(self):
"""Multiple portrait cameras in one row should reserve separate spans below them."""
manager = self._build_manager(
{
"cam_a": (640, 480),
"cam_p1": (360, 640),
"cam_p2": (360, 640),
"cam_b": (640, 480),
"cam_c": (1280, 720),
"cam_d": (640, 480),
}
)
layout = manager.calculate_layout(
["cam_a", "cam_p1", "cam_p2", "cam_b", "cam_c", "cam_d"],
4,
)
self.assertIsNotNone(layout)
assert layout is not None
self._assert_no_overlaps(layout)
def test_two_landscapes_then_portrait_then_two_landscapes(self):
"""A portrait after two landscapes should reserve only its own tail span."""
manager = self._build_manager(
{
"cam_a": (1280, 720),
"cam_b": (1280, 720),
"cam_p": (360, 640),
"cam_c": (1280, 720),
"cam_d": (1280, 720),
}
)
layout = manager.calculate_layout(
["cam_a", "cam_b", "cam_p", "cam_c", "cam_d"],
3,
)
self.assertIsNotNone(layout)
assert layout is not None
self._assert_no_overlaps(layout)
cam_c = [
position for row in layout for camera, position in row if camera == "cam_c"
][0]
cam_d = [
position for row in layout for camera, position in row if camera == "cam_d"
][0]
self.assertEqual(cam_c[0], 0)
self.assertEqual(cam_d[0], cam_c[0] + cam_c[2])

View File

@ -1673,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

@ -230,7 +230,7 @@ class TestExportResolution(unittest.TestCase):
id=export_id,
camera=camera,
name=f"export-{export_id}",
date=datetime.datetime.now(),
date=int(datetime.datetime.now().timestamp()),
video_path=f"/media/frigate/exports/{filename}",
thumb_path=f"/media/frigate/exports/{filename}.jpg",
in_progress=False,

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

@ -364,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,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",
],
},
},
},
});
});
});

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",

View File

@ -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",

View File

@ -258,6 +258,41 @@
},
"raw_mask": {
"label": "Màscara en brut"
},
"filters_attribute": {
"label": "Filtres d'atribut",
"description": "Filtres aplicats als atributs detectats per reduir falsos positius (àrea, relació, confiança).",
"min_area": {
"label": "Àrea mínima de l'atribut",
"description": "Es requereix una àrea de caixa contenidora mínima (píxels o percentatge) per a aquest atribut. Pot ser píxels (int) o percentatge (float entre 0,000001 i 0.99)."
},
"max_area": {
"label": "Àrea màxima de l'atribut",
"description": "Es permet l'àrea màxima del contenidor (píxels o percentatge) per a aquest atribut. Pot ser píxels (int) o percentatge (float entre 0,000001 i 0.99)."
},
"min_ratio": {
"label": "Relació mínima d'aspecte",
"description": "Relació mínima d'amplada/alçada requerida per a la casella contenidora a qualificar."
},
"max_ratio": {
"label": "Relació màxima d'aspecte",
"description": "Es permet la relació màxima d'amplada/alçada per a la casella contenidora a qualificar."
},
"threshold": {
"label": "Llindar de confiança",
"description": "Es requereix un llindar de confiança mitjà per a la detecció perquè l'atribut es consideri un veritable positiu."
},
"min_score": {
"label": "Confiança mínima",
"description": "Es requereix una confiança mínima de detecció d'un sol fotograma per a associar aquest atribut amb el seu objecte pare."
},
"mask": {
"label": "Màscara de filtre",
"description": "Coordenades de polígon que defineixen on s'aplica aquest filtre dins del marc."
},
"raw_mask": {
"label": "Màscara en brut"
}
}
},
"record": {
@ -1987,7 +2022,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",
@ -2207,7 +2246,7 @@
},
"match_distance": {
"label": "Distància de la coincidència",
"description": "Nombre de desajustos de caràcters permesos quan es comparen les plaques detectades amb les plaques conegudes."
"description": "Nombre de discrepàncies de caràcters permesos en comparar les plaques detectades amb les plaques conegudes."
},
"known_plates": {
"label": "Matricules conegudes",

View File

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

View File

@ -42,5 +42,28 @@
"show_camera_status": "Quin és l'estat actual de les meves càmeres?",
"recap": "Què va passar mentre jo era fora?",
"watch_camera": "Vigila la porta d'entrada i fes-me saber si algú apareix"
},
"new_chat": "Xat nou",
"settings": {
"title": "Configuració del xat",
"show_stats": {
"title": "Mostra les estadístiques",
"desc": "Mostra la velocitat de generació i la mida del context per a les respostes del xat.",
"while_generating": "En generar",
"always": "Sempre"
},
"auto_scroll": {
"title": "Desplaçament automàtic",
"desc": "Segueix els missatges nous a mesura que arriben."
}
},
"stats": {
"context": "{{tokens}} tokens",
"tokens_per_second": "{{rate}} t/s"
},
"reasoning": {
"active": "Raonant…",
"show": "Mostra el raonament",
"hide": "Amaga el raonament"
}
}

View File

@ -14,7 +14,11 @@
"empty": "No hi ha intents recents de reconeixement de rostres",
"title": "Reconeixements recents",
"aria": "Selecciona els reconeixements recents",
"titleShort": "Recent"
"titleShort": "Recent",
"emptyNoLibrary": {
"title": "Puja una cara",
"description": "Heu d'afegir com a mínim una cara a la biblioteca perquè el reconeixement de la cara funcioni."
}
},
"description": {
"addFace": "Afegiu una col·lecció nova a la biblioteca de cares pujant la vostra primera imatge.",

View File

@ -15,7 +15,8 @@
"globalConfig": "Configuració global - Frigate",
"cameraConfig": "Configuració de la càmera - Frigate",
"maintenance": "Manteniment - Frigate",
"profiles": "Perfils - Frigate"
"profiles": "Perfils - Frigate",
"detectorsAndModel": "Detectors i model - Frigate"
},
"menu": {
"ui": "Interfície d'usuari",
@ -90,7 +91,8 @@
"regionGrid": "Quadrícula de la regió",
"uiSettings": "Paràmetres de la IU",
"profiles": "Perfils",
"systemGo2rtcStreams": "go2rtc streams"
"systemGo2rtcStreams": "go2rtc streams",
"systemDetectorsAndModel": "Detectors i model"
},
"dialog": {
"unsavedChanges": {
@ -526,7 +528,7 @@
},
"title": "Afinador de detecció de moviment",
"toast": {
"success": "Els ajustos de la detecció de moviment s'han desat."
"success": "S'han desat els paràmetres del moviment."
},
"unsavedChanges": "Canvis no desats en l'ajust de moviment {{camera}}"
},
@ -724,7 +726,7 @@
"trainDate": "Data d'entrenament",
"title": "Informació del model",
"supportedDetectors": "Detectors compatibles",
"availableModels": "Models disponibles",
"availableModels": "Models Frigate+ disponibles",
"cameras": "Càmeres",
"plusModelType": {
"userModel": "Afinat",
@ -733,7 +735,15 @@
"loadingAvailableModels": "Carregant models disponibles…",
"loading": "Carregant informació del model…",
"error": "No s'ha pogut carregar la informació del model",
"modelSelect": "Els models disponibles a Frigate+ es poden seleccionar aquí. Tingues en compte que només es poden triar els models compatibles amb la configuració actual del detector."
"modelSelect": "Els models disponibles a Frigate+ es poden seleccionar aquí. Tingues en compte que només es poden triar els models compatibles amb la configuració actual del detector.",
"noModelLoaded": "Actualment no s'ha carregat cap model Frigate+.",
"selectModel": "Selecciona un model",
"noModelsAvailable": "No hi ha models disponibles",
"filter": {
"ariaLabel": "Filtra els models per tipus",
"baseModels": "Models de base",
"fineTunedModels": "Models ajustats"
}
},
"apiKey": {
"plusLink": "Llegeix més sobre Frigate+",
@ -755,7 +765,8 @@
"currentModel": "Model actual",
"otherModels": "Altres models",
"configuration": "Configuració"
}
},
"changeInDetectorsAndModel": "Canviar model"
},
"enrichments": {
"semanticSearch": {
@ -1295,7 +1306,7 @@
"title": "Habilita / Inhabilita les càmeres",
"desc": "Inhabilita temporalment una càmera fins que es reiniciï la fragata. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles.<br /> <em>Nota: això no desactiva les retransmissions de go2rtc.</em>",
"enableLabel": "Càmeres habilitades",
"enableDesc": "Inhabilita temporalment una càmera habilitada fins que es reiniciï Frigate. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles.<br /> <em>Nota: això no desactiva les retransmissions de go2rtc.</em>",
"enableDesc": "Inhabilita temporalment una càmera habilitada fins que es reiniciï Frigate. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles.<br /> <em>Nota: això no inhabilita els restreams go2rtc.</em><br /><br />Drag el handle per reordenar les càmeres tal com apareixen a la interfície d'usuari. L'ordre de les càmeres habilitades es reflectirà en tota la interfície d'usuari, incloent el tauler en viu i els desplegables de selecció de càmeres.",
"disableLabel": "Càmeres inhabilitades",
"disableDesc": "Habilita una càmera que actualment no és visible a la interfície d'usuari i està desactivada a la configuració. Es requereix un reinici de Frigate després d'activar-la.",
"enableSuccess": "{{cameraName}} activat a la configuració. Reinicia Frigate per aplicar els canvis.",
@ -1304,7 +1315,10 @@
"title": "Edita el nom de la pantalla",
"description": "Estableix el nom amigable que es mostra per a aquesta càmera a tota la interfície d'usuari de la Fragata. Deixeu-ho en blanc per utilitzar l'ID de la càmera.",
"rename": "Canvia el nom"
}
},
"reorderHandle": "Arrossega per reordenar",
"saving": "S'està desant…",
"saved": "Desat"
},
"cameraConfig": {
"add": "Afegeix una càmera",
@ -1362,7 +1376,8 @@
"dedicatedLpr": "LPR dedicat",
"saveSuccess": "Tipus de càmera actualitzat per {{cameraName}}. Reinicia la fragata per aplicar els canvis.",
"normal": "Normal"
}
},
"description": "Afegiu, editeu i suprimiu les càmeres, controleu quines càmeres estan habilitades, i configureu les superposicions per perfil i tipus de càmera. Per a configurar fluxos, detecció, moviment i altres paràmetres específics de la càmera, trieu la secció específica a Configuració de la càmera."
},
"cameraReview": {
"object_descriptions": {
@ -1661,7 +1676,9 @@
"options": {
"embeddings": "Incrustació",
"vision": "Visió",
"tools": "Eines"
"tools": "Eines",
"descriptions": "Descripcions",
"chat": "Xat"
}
},
"semanticSearchModel": {
@ -1718,7 +1735,10 @@
"saveAllPartial_many": "{{successCount}} de {{totalCount}} seccions desades. {{failCount}} ha fallat.",
"saveAllPartial_other": "{{successCount}} de {{totalCount}} seccions desades. {{failCount}} ha fallat.",
"saveAllFailure": "Ha fallat en desar totes les seccions.",
"applied": "La configuració s'ha aplicat correctament"
"applied": "La configuració s'ha aplicat correctament",
"saveAllSuccessRestartRequired_one": "S'ha desat la secció {{count}} correctament. Reinicia la fragata per aplicar els canvis.",
"saveAllSuccessRestartRequired_many": "Totes les {{count}} seccions s'han desat correctament. Reinicia la fragata per aplicar els canvis.",
"saveAllSuccessRestartRequired_other": "Totes les {{count}} seccions s'han desat correctament. Reinicia la fragata per aplicar els canvis."
},
"unsavedChanges": "Teniu canvis sense desar",
"confirmReset": "Confirma el restabliment",
@ -1743,7 +1763,15 @@
"othersField_many": "{{count}} altres",
"othersField_other": "{{count}} altres",
"profilePrefix": "Perfil {{profile}}: {{fields}}"
}
},
"overriddenGlobalHeading_one": "Aquesta càmera substitueix el camp {{count}} de la configuració global:",
"overriddenGlobalHeading_many": "Aquesta càmera anul·la {{count}} camps de la configuració global:",
"overriddenGlobalHeading_other": "Aquesta càmera anul·la {{count}} camps de la configuració global:",
"overriddenGlobalNoDeltas": "Aquesta càmera anul·la la configuració global, però no hi ha valors de camp diferents.",
"overriddenBaseConfigHeading_one": "El perfil {{profile}} substitueix el camp {{count}} de la configuració base:",
"overriddenBaseConfigHeading_many": "El perfil {{profile}} substitueix {{count}} camps de la configuració base:",
"overriddenBaseConfigHeading_other": "El perfil {{profile}} substitueix {{count}} camps de la configuració base:",
"overriddenBaseConfigNoDeltas": "El perfil {{profile}} substitueix aquesta secció, però no hi ha valors de camp diferents de la configuració base."
},
"profiles": {
"title": "Perfils",
@ -1827,8 +1855,17 @@
"audioMp3": "Transcodifica a MP3",
"audioExclude": "Exclou",
"hardwareNone": "Sense acceleració de hardware",
"hardwareAuto": "Acceleració de hardware automàtica"
}
"hardwareAuto": "Automàtic (recomanat)",
"addVideoCodec": "Afegeix un còdec de vídeo",
"addAudioCodec": "Afegeix un còdec d'àudio",
"removeCodec": "Elimina el còdec",
"hardwareVaapi": "VAAPI",
"hardwareCuda": "CUDA",
"hardwareV4l2m2m": "V4L2 M2M",
"hardwareDxva2": "DXVA2",
"hardwareVideotoolbox": "VideoToolbox"
},
"streamNumber": "Flux {{index}}"
},
"timestampPosition": {
"tl": "A dalt a l'esquerra",
@ -1838,7 +1875,14 @@
},
"onvif": {
"profileAuto": "Automàtic",
"profileLoading": "S'estan carregant perfils..."
"profileLoading": "S'estan carregant perfils...",
"autotracking": {
"zooming": {
"disabled": "Desactivat",
"absolute": "Absolut",
"relative": "Relatiu"
}
}
},
"configMessages": {
"review": {
@ -1886,5 +1930,104 @@
"semanticSearch": {
"jinav2SmallModelSize": "La mida 'petita' amb el model Jina V2 té un alt cost de RAM i d'inferència. Es recomana el model 'gran' amb una GPU discreta."
}
},
"modelSize": {
"large": "Gran",
"small": "Petit"
},
"birdseye": {
"trackingMode": {
"objects": "Objectes",
"motion": "Moviment",
"continuous": "Continu"
},
"cameraOrder": {
"label": "Ordre de la càmera",
"description": "Arrossega les càmeres per establir el seu ordre en la disposició Birdseye.",
"reorderHandle": "Arrossega per reordenar",
"saving": "S'està desant…",
"saved": "Desat"
}
},
"snapshot": {
"retainMode": {
"all": "Tots",
"motion": "Moviment",
"active_objects": "Objectes Actius"
}
},
"ui": {
"timeFormat": {
"browser": "Visor",
"12hour": "12 hores",
"24hour": "24 hores"
},
"TimeOrDateStyle": {
"full": "Complet",
"long": "Llarg",
"medium": "Mitjà",
"short": "Curt"
},
"unitSystem": {
"metric": "Métric",
"imperial": "Imperial"
}
},
"review": {
"imageSource": {
"recordings": "Gravacions",
"previews": "Previsualitzacions"
}
},
"logger": {
"logLevel": {
"debug": "Depurar",
"info": "Informació",
"warning": "Avís",
"error": "Error",
"critical": "Crític"
}
},
"retainMode": {
"all": "Tots",
"motion": "Moviment",
"active_objects": "Objectes actius"
},
"previewQuality": {
"very_high": "Molt alta",
"high": "Alta",
"medium": "Mitja",
"low": "Baix",
"very_low": "Molt baix"
},
"detectorsAndModel": {
"restartRequired": "Reinici requerit (canvi en detector o model)",
"title": "Detectors i model",
"description": "Configuri el detector final que corre la detecció d'objectes i el model que usa. Els canvis es gravaràn junts i així el detector i el model estan sincronitzats.",
"cardTitles": {
"detector": "Detector Hardware",
"model": "Model de detecció"
},
"tabs": {
"plus": "Frigate+",
"custom": "Model personalitzat"
},
"mismatch": {
"warning": "El model actual de Frigate+ \"{{model}}\" requereix el detector {{required}}. Selecciona un model compatible a baix o canvía e model personalitzat abans de gravar."
},
"plusModel": {
"requiresDetector": "Requereix: {{detector}}",
"noModelSelected": "Selecciona un model Frigate+"
},
"toast": {
"saveSuccess": "Configuració de detectors i model guardats. Reinicia Frigate per aplicar els canvis.",
"saveError": "Fallo en gravar la configuració de detector i model"
},
"unsavedChanges": "Canvis de detector i model no gravats"
},
"menuDot": {
"overrideGlobal": "Aquesta secció substitueix la configuració global",
"overrideProfile": "Aquesta secció està substituïda pel perfil {{profile}}",
"unsaved": "Aquesta secció té canvis sense desar"
}
}

View File

@ -192,7 +192,8 @@
"bg": "Български (bulgarisch)",
"gl": "Galego (Galicisch)",
"id": "Bahasa Indonesia (Indonesisch)",
"hr": "Hrvatski (Kroatisch)"
"hr": "Hrvatski (Kroatisch)",
"bs": "Bosnisch"
},
"appearance": "Erscheinung",
"theme": {

View File

@ -25,7 +25,11 @@
},
"filters": {
"label": "Audiofilter",
"description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden."
"description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden.",
"threshold": {
"label": "Mindestvertrauensgrad für Audio",
"description": "Mindestschwellenwert für die Zuverlässigkeit, damit das Audioereignis gezählt wird."
}
},
"max_not_heard": {
"label": "Ende Timeout",

View File

@ -23,7 +23,11 @@
},
"filters": {
"label": "Audiofilter",
"description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden."
"description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden.",
"threshold": {
"label": "Mindestvertrauensgrad für Audio",
"description": "Mindestschwellenwert für die Zuverlässigkeit, damit das Audioereignis gezählt wird."
}
},
"max_not_heard": {
"label": "Ende Timeout",

View File

@ -121,5 +121,9 @@
"royal_mail": "Royal-Mail",
"school_bus": "Schulbus",
"skunk": "Stinktier",
"kangaroo": "Känguruh"
"kangaroo": "Känguruh",
"baby": "Baby",
"baby_stroller": "Kinderwagen",
"rickshaw": "Rikscha",
"rodent": "Nagetier"
}

View File

@ -42,5 +42,23 @@
"show_camera_status": "Wie ist der aktuelle Status meiner Kameras?",
"recap": "Was ist passiert, während ich weg war?",
"watch_camera": "Pass auf die Haustür auf und sag mir Bescheid, wenn jemand kommt"
},
"new_chat": "Neuer Chat",
"settings": {
"title": "Chat Einstellung",
"show_stats": {
"title": "Statistiken anzeigen",
"desc": "Generierungsrate und Kontextgröße für Chat-Antworten anzeigen.",
"while_generating": "Während der Erstellung",
"always": "Immer"
},
"auto_scroll": {
"title": "Auto scrollen",
"desc": "Verfolgen Sie neue Nachrichten, sobald sie eintreffen."
}
},
"stats": {
"context": "{{tokens}} tokens",
"tokens_per_second": "{{rate}} t/s"
}
}

View File

@ -48,7 +48,11 @@
"title": "Neueste Erkennungen",
"aria": "Wähle aktuelle Erkennungen",
"empty": "Es gibt keine aktuellen Versuche zur Gesichtserkennung",
"titleShort": "frisch"
"titleShort": "frisch",
"emptyNoLibrary": {
"title": "Gesicht hinzufügen",
"description": "Sie müssen mindestens ein Gesicht zur Bibliothek hinzufügen, damit die Gesichtserkennung funktioniert."
}
},
"deleteFaceLibrary": {
"title": "Lösche Name",

View File

@ -803,7 +803,15 @@
"availableModels": "Verfügbare Modelle",
"loadingAvailableModels": "Lade verfügbare Modelle…",
"baseModel": "Basis Model",
"title": "Model Informationen"
"title": "Model Informationen",
"noModelLoaded": "Derzeit ist kein „Frigate+“-Modell geladen.",
"selectModel": "Wählen Sie ein Modell aus",
"noModelsAvailable": "Keine Modelle verfügbar",
"filter": {
"ariaLabel": "Modelle nach Typ filtern",
"baseModels": "Basismodelle",
"fineTunedModels": "Optimierte Modelle"
}
},
"toast": {
"error": "Speichern der Konfigurationsänderungen fehlgeschlagen: {{errorMessage}}",
@ -1415,7 +1423,8 @@
"normal": "Normal",
"dedicatedLpr": "Spezielles LPR-System",
"saveSuccess": "Der Kameratyp für {{cameraName}} wurde aktualisiert. Starte Frigate neu, um die Änderungen zu übernehmen."
}
},
"description": "Fügen Sie Kameras hinzu, bearbeiten und löschen Sie sie, legen Sie fest, welche Kameras aktiviert sind, und konfigurieren Sie profil- und kameratypabhängige Übersteuerungen. Um Streams, Erkennung, Bewegung und andere kameraspezifische Einstellungen zu konfigurieren, wählen Sie den entsprechenden Abschnitt unter „Kamerakonfiguration“ aus."
},
"cameraReview": {
"title": "Kamera-Einstellungen überprüfen",
@ -1489,7 +1498,13 @@
"othersField_one": "{{count}} andere",
"othersField_other": "{{count}} weitere",
"profilePrefix": "{{profile}} Profile: {{fields}}"
}
},
"overriddenGlobalHeading_one": "Diese Kamera überschreibt das Feld {{count}} aus der globalen Konfiguration:",
"overriddenGlobalHeading_other": "Diese Kamera überschreibt alle Felder {{count}} aus der globalen Konfiguration:",
"overriddenGlobalNoDeltas": "Diese Kamera überschreibt die globale Konfiguration, es gibt jedoch keine Abweichungen bei den Feldwerten.",
"overriddenBaseConfigHeading_one": "Das Profil {{profile}} überschreibt das Feld {{count}} aus der Basiskonfiguration:",
"overriddenBaseConfigHeading_other": "Das Profil {{profile}} überschreibt di Felder {{count}} aus der Basiskonfiguration:",
"overriddenBaseConfigNoDeltas": "Das Profil {{profile}} überschreibt diesen Abschnitt, jedoch weichen keine Feldwerte von der Basiskonfiguration ab."
},
"timestampPosition": {
"tl": "Oben links",
@ -1726,7 +1741,9 @@
"options": {
"embeddings": "Einbetten",
"vision": "Vision",
"tools": "Werkzeuge"
"tools": "Werkzeuge",
"descriptions": "Beschreibung",
"chat": "Chat"
}
},
"semanticSearchModel": {
@ -1884,7 +1901,14 @@
},
"onvif": {
"profileAuto": "Auto",
"profileLoading": "Profile werden geladen..."
"profileLoading": "Profile werden geladen...",
"autotracking": {
"zooming": {
"disabled": "deaktiviert",
"absolute": "Absolut",
"relative": "Verwandter"
}
}
},
"configMessages": {
"review": {
@ -1932,5 +1956,60 @@
"semanticSearch": {
"jinav2SmallModelSize": "Die „kleine“ Variante des Jina V2-Modells verursacht hohe RAM- und Inferenzkosten. Es wird das „große“ Modell mit einer dedizierten GPU empfohlen."
}
},
"birdseye": {
"trackingMode": {
"objects": "Objekte",
"motion": "Bewegung",
"continuous": "Fortlaufend"
}
},
"retainMode": {
"all": "Alle",
"motion": "Bewegung",
"active_objects": "Aktive Objekte"
},
"previewQuality": {
"very_high": "sehr hoch",
"high": "hoch",
"medium": "Mittel",
"low": "niedrig",
"very_low": "sehr niedrig"
},
"ui": {
"timeFormat": {
"browser": "Browser",
"12hour": "12 Stunden",
"24hour": "24 Stunden"
},
"TimeOrDateStyle": {
"full": "vollständig",
"long": "lang",
"medium": "mittel",
"short": "kurz"
},
"unitSystem": {
"metric": "Metrik",
"imperial": "Imperial"
}
},
"review": {
"imageSource": {
"recordings": "Aufnahmen",
"previews": "Vorschau"
}
},
"logger": {
"logLevel": {
"debug": "Debug",
"info": "Info",
"warning": "Warnung",
"error": "Fehler",
"critical": "Kritisch"
}
},
"modelSize": {
"small": "klein",
"large": "groß"
}
}

View File

@ -950,4 +950,4 @@
"label": "Original camera state",
"description": "Keep track of original state of camera."
}
}
}

View File

@ -921,6 +921,41 @@
"label": "Original GenAI state",
"description": "Indicates whether GenAI was enabled in the original static config."
}
},
"filters_attribute": {
"label": "Attribute filters",
"description": "Filters applied to detected attributes to reduce false positives (area, ratio, confidence).",
"min_area": {
"label": "Minimum attribute area",
"description": "Minimum bounding box area (pixels or percentage) required for this attribute. Can be pixels (int) or percentage (float between 0.000001 and 0.99)."
},
"max_area": {
"label": "Maximum attribute area",
"description": "Maximum bounding box area (pixels or percentage) allowed for this attribute. Can be pixels (int) or percentage (float between 0.000001 and 0.99)."
},
"min_ratio": {
"label": "Minimum aspect ratio",
"description": "Minimum width/height ratio required for the bounding box to qualify."
},
"max_ratio": {
"label": "Maximum aspect ratio",
"description": "Maximum width/height ratio allowed for the bounding box to qualify."
},
"threshold": {
"label": "Confidence threshold",
"description": "Average detection confidence threshold required for the attribute to be considered a true positive."
},
"min_score": {
"label": "Minimum confidence",
"description": "Minimum single-frame detection confidence required to associate this attribute with its parent object."
},
"mask": {
"label": "Filter mask",
"description": "Polygon coordinates defining where this filter applies within the frame."
},
"raw_mask": {
"label": "Raw Mask"
}
}
},
"record": {
@ -1597,4 +1632,4 @@
"description": "Ignore time synchronization differences between camera and Frigate server for ONVIF communication."
}
}
}
}

View File

@ -60,5 +60,10 @@
"stats": {
"context": "{{tokens}} tokens",
"tokens_per_second": "{{rate}} t/s"
},
"reasoning": {
"active": "Reasoning…",
"show": "Show reasoning",
"hide": "Hide reasoning"
}
}

View File

@ -222,7 +222,7 @@
"label": "Hide object path"
},
"debugReplay": {
"label": "Debug replay",
"label": "Debug Replay",
"aria": "View this tracked object in the debug replay view"
},
"more": {

View File

@ -40,6 +40,11 @@
"profilePrefix": "{{profile}} profile: {{fields}}"
}
},
"menuDot": {
"overrideGlobal": "This section overrides the global configuration",
"overrideProfile": "This section is overridden by the {{profile}} profile",
"unsaved": "This section has unsaved changes"
},
"menu": {
"general": "General",
"globalConfig": "Global configuration",
@ -472,10 +477,13 @@
"streams": {
"title": "Enable / Disable Cameras",
"enableLabel": "Enabled cameras",
"enableDesc": "Temporarily disable an enabled camera until Frigate restarts. Disabling a camera completely stops Frigate's processing of this camera's streams. Detection, recording, and debugging will be unavailable.<br /> <em>Note: This does not disable go2rtc restreams.</em>",
"enableDesc": "Temporarily disable an enabled camera until Frigate restarts. Disabling a camera completely stops Frigate's processing of this camera's streams. Detection, recording, and debugging will be unavailable.<br /> <em>Note: This does not disable go2rtc restreams.</em><br /><br />Drag the handle to reorder the cameras as they appear in the UI. The order of enabled cameras will be reflected throughout the UI including the Live dashboard and camera selection dropdowns.",
"disableLabel": "Disabled cameras",
"disableDesc": "Enable a camera that is currently not visible in the UI and disabled in the configuration. A restart of Frigate is required after enabling.",
"enableSuccess": "Enabled {{cameraName}} in configuration. Restart Frigate to apply the changes.",
"reorderHandle": "Drag to reorder",
"saving": "Saving…",
"saved": "Saved",
"friendlyName": {
"edit": "Edit camera display name",
"title": "Edit Display Name",
@ -1641,6 +1649,7 @@
"addStream": "Add stream",
"addStreamDesc": "Enter a name for the new stream. This name will be used to reference the stream in your camera configuration.",
"addUrl": "Add URL",
"streamNumber": "Stream {{index}}",
"streamName": "Stream name",
"streamNamePlaceholder": "e.g., front_door",
"streamUrlPlaceholder": "e.g., rtsp://user:pass@192.168.1.100/stream",
@ -1674,7 +1683,15 @@
"audioMp3": "Transcode to MP3",
"audioExclude": "Exclude",
"hardwareNone": "No hardware acceleration",
"hardwareAuto": "Automatic hardware acceleration"
"hardwareAuto": "Automatic (recommended)",
"hardwareVaapi": "VAAPI",
"hardwareCuda": "CUDA",
"hardwareV4l2m2m": "V4L2 M2M",
"hardwareDxva2": "DXVA2",
"hardwareVideotoolbox": "VideoToolbox",
"addVideoCodec": "Add video codec",
"addAudioCodec": "Add audio codec",
"removeCodec": "Remove codec"
}
},
"birdseye": {
@ -1682,6 +1699,13 @@
"objects": "Objects",
"motion": "Motion",
"continuous": "Continuous"
},
"cameraOrder": {
"label": "Camera order",
"description": "Drag cameras to set their order in the Birdseye layout.",
"reorderHandle": "Drag to reorder",
"saving": "Saving…",
"saved": "Saved"
}
},
"retainMode": {

View File

@ -154,7 +154,8 @@
"gl": "Galego (Gallego)",
"id": "Bahasa Indonesia (Indonesio)",
"ur": "اردو (Urdu)",
"hr": "Hrvatski (Croata)"
"hr": "Hrvatski (Croata)",
"bs": "Bosanski (Bosnio)"
},
"appearance": "Apariencia",
"darkMode": {
@ -196,7 +197,10 @@
"uiPlayground": "Zona de pruebas de la interfaz de usuario",
"faceLibrary": "Biblioteca de rostros",
"classification": "Clasificación",
"profiles": "Perfiles"
"profiles": "Perfiles",
"actions": "Acciones",
"features": "Funciones",
"chat": "Chat"
},
"unit": {
"speed": {
@ -252,7 +256,19 @@
"saving": "Guardando…",
"exitFullscreen": "Salir de pantalla completa",
"on": "ENCENDIDO",
"continue": "Continuar"
"continue": "Continuar",
"add": "Añadir",
"applying": "Aplicando…",
"undo": "Deshacer",
"copiedToClipboard": "Copiado al portapapeles",
"modified": "Modificado",
"overridden": "Sobrescrito",
"resetToGlobal": "Restablecer a global",
"resetToDefault": "Restablecer valores predeterminados",
"saveAll": "Guardar todo",
"savingAll": "Guardando todo…",
"undoAll": "Deshacer todo",
"retry": "Reintentar"
},
"toast": {
"save": {
@ -260,7 +276,8 @@
"noMessage": "No se pudieron guardar los cambios de configuración",
"title": "No se pudieron guardar los cambios de configuración: {{errorMessage}}"
},
"title": "Guardar"
"title": "Guardar",
"success": "Cambios de configuración guardados correctamente."
},
"copyUrlToClipboard": "URL copiada al portapapeles."
},
@ -314,5 +331,7 @@
"field": {
"optional": "Opcional",
"internalID": "La ID interna que usa Frigate en la configuración y en la base de datos"
}
},
"no_items": "No hay elementos",
"validation_errors": "Errores de validación"
}

View File

@ -71,16 +71,77 @@
"endTimeMustAfterStartTime": "La hora de finalización debe ser posterior a la hora de inicio"
},
"success": "Exportación iniciada con éxito. Ver el archivo en la página exportaciones.",
"view": "Ver"
"view": "Ver",
"queued": "Exportación en cola. Consulta el progreso en la página de exportaciones.",
"batchSuccess_one": "Se inició 1 exportación. Abriendo el caso ahora.",
"batchSuccess_many": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.",
"batchSuccess_other": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.",
"batchPartial": "Se iniciaron {{successful}} de {{total}} exportaciones. Cámaras fallidas: {{failedCameras}}",
"batchFailed": "No se pudieron iniciar {{total}} exportaciones. Cámaras fallidas: {{failedCameras}}",
"batchQueuedSuccess_one": "1 exportación en cola. Abriendo el caso ahora.",
"batchQueuedSuccess_many": "{{count}} exportaciones en cola. Abriendo el caso ahora.",
"batchQueuedSuccess_other": "{{count}} exportaciones en cola. Abriendo el caso ahora.",
"batchQueuedPartial": "{{successful}} de {{total}} exportaciones en cola. Cámaras fallidas: {{failedCameras}}",
"batchQueueFailed": "No se pudieron poner en cola {{total}} exportaciones. Cámaras fallidas: {{failedCameras}}"
},
"fromTimeline": {
"saveExport": "Guardar exportación",
"previewExport": "Vista previa de la exportación"
"previewExport": "Vista previa de la exportación",
"queueingExport": "Poniendo exportación en cola...",
"useThisRange": "Usar este intervalo"
},
"selectOrExport": "Seleccionar o exportar",
"case": {
"label": "Caso",
"newCaseDescriptionPlaceholder": "Descripción de caso"
"newCaseDescriptionPlaceholder": "Descripción de caso",
"newCaseOption": "Crear nuevo caso",
"newCaseNamePlaceholder": "Nombre del nuevo caso",
"nonAdminHelp": "Se creará un nuevo caso para estas exportaciones.",
"placeholder": "Selecciona un caso"
},
"queueing": "Poniendo la exportación en cola…",
"tabs": {
"export": "Cámara única",
"multiCamera": "Multicámara"
},
"multiCamera": {
"timeRange": "Intervalo de tiempo",
"selectFromTimeline": "Seleccionar desde la línea de tiempo",
"cameraSelection": "Cámaras",
"cameraSelectionHelp": "Las cámaras con objetos detectados en este intervalo de tiempo están preseleccionadas",
"checkingActivity": "Comprobando actividad de las cámaras...",
"noCameras": "No hay cámaras disponibles",
"detectionCount_one": "1 objeto detectado",
"detectionCount_many": "{{count}} objetos detectados",
"detectionCount_other": "{{count}} objetos detectados",
"nameLabel": "Nombre de la exportación",
"namePlaceholder": "Nombre base opcional para estas exportaciones",
"queueingButton": "Poniendo exportaciones en cola...",
"exportButton_one": "Exportar 1 cámara",
"exportButton_many": "Exportar {{count}} cámaras",
"exportButton_other": "Exportar {{count}} cámaras"
},
"multi": {
"title_one": "Exportar 1 revisión",
"title_many": "Exportar {{count}} revisiones",
"title_other": "Exportar {{count}} revisiones",
"description": "Exportar cada revisión seleccionada. Todas las exportaciones se agruparán en un único caso.",
"descriptionNoCase": "Exportar cada revisión seleccionada.",
"caseNamePlaceholder": "Exportación de revisión - {{date}}",
"exportButton_one": "Exportar 1 revisión",
"exportButton_many": "Exportar {{count}} revisiones",
"exportButton_other": "Exportar {{count}} revisiones",
"exportingButton": "Exportando...",
"toast": {
"started_one": "Se inició 1 exportación. Abriendo el caso ahora.",
"started_many": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.",
"started_other": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.",
"startedNoCase_one": "Se inició 1 exportación.",
"startedNoCase_many": "Se iniciaron {{count}} exportaciones.",
"startedNoCase_other": "Se iniciaron {{count}} exportaciones.",
"partial": "Se iniciaron {{successful}} de {{total}} exportaciones. Fallidas: {{failedItems}}",
"failed": "No se pudieron iniciar {{total}} exportaciones. Fallidas: {{failedItems}}"
}
}
},
"streaming": {
@ -130,7 +191,12 @@
"markAsUnreviewed": "Marcar como no revisado"
},
"shareTimestamp": {
"description": "Comparta una URL con marca de tiempo de la posición actual del reproductor o elija una marca de tiempo personalizada. Tenga en cuenta que esta no es una URL pública para compartir y solo es accesible para los usuarios que tienen acceso a Frigate y a esta cámara."
"description": "Comparta una URL con marca de tiempo de la posición actual del reproductor o elija una marca de tiempo personalizada. Tenga en cuenta que esta no es una URL pública para compartir y solo es accesible para los usuarios que tienen acceso a Frigate y a esta cámara.",
"label": "Compartir marca de tiempo",
"title": "Compartir marca de tiempo",
"custom": "Marca de tiempo personalizada",
"button": "Compartir URL de la marca de tiempo",
"shareTitle": "Marca de tiempo de revisión de Frigate: {{camera}}"
}
},
"imagePicker": {

View File

@ -8,7 +8,7 @@
"description": "Habilitado"
},
"audio": {
"label": "Eventos de audio",
"label": "Detección de audio",
"description": "Configuración para la detección de eventos basada en audio para esta cámara.",
"enabled": {
"label": "Habilitar la detección de audio",
@ -28,14 +28,19 @@
},
"filters": {
"label": "Filtros de audio",
"description": "Ajustes de filtrado por tipo de audio, como umbrales de confianza utilizados para reducir los falsos positivos."
"description": "Ajustes de filtrado por tipo de audio, como umbrales de confianza utilizados para reducir los falsos positivos.",
"threshold": {
"label": "Confianza mínima de audio",
"description": "Umbral mínimo de confianza para que se cuente el evento de audio."
}
},
"enabled_in_config": {
"description": "Indica si la detección de audio estaba habilitada originalmente en el archivo de configuración estática.",
"label": "Estado original del audio"
},
"num_threads": {
"label": "Hilos de detección"
"label": "Hilos de detección",
"description": "Número de hilos que se utilizarán para el procesamiento de la detección de audio."
}
},
"friendly_name": {
@ -50,29 +55,79 @@
},
"autotracking": {
"zoom_factor": {
"description": "Controla el nivel de zoom en los objetos rastreados. Los valores más bajos mantienen una mayor parte de la escena a la vista; los valores más altos acercan la imagen, pero pueden provocar la pérdida del rastreo. Valores entre 0.1 y 0.75."
"description": "Controla el nivel de zoom en los objetos rastreados. Los valores más bajos mantienen una mayor parte de la escena a la vista; los valores más altos acercan la imagen, pero pueden provocar la pérdida del rastreo. Valores entre 0.1 y 0.75.",
"label": "Factor de zoom"
},
"calibrate_on_startup": {
"description": "Mida la velocidad de los motores PTZ al encenderlos para mejorar la precisión del seguimiento. Frigate actualizará la configuración con los `movement_weights` tras la calibración."
"description": "Mida la velocidad de los motores PTZ al encenderlos para mejorar la precisión del seguimiento. Frigate actualizará la configuración con los `movement_weights` tras la calibración.",
"label": "Calibrar al iniciar"
},
"description": "Realice un seguimiento automático de objetos en movimiento y manténgalos centrados en el encuadre mediante movimientos de cámara PTZ.",
"zooming": {
"description": "Control del comportamiento del zoom: deshabilitado (solo panorámica/inclinación), absoluto (mayor compatibilidad) o relativo (panorámica/inclinación/zoom simultáneos)."
"description": "Control del comportamiento del zoom: deshabilitado (solo panorámica/inclinación), absoluto (mayor compatibilidad) o relativo (panorámica/inclinación/zoom simultáneos).",
"label": "Modo de zoom"
},
"return_preset": {
"description": "Nombre del preajuste ONVIF configurado en el firmware de la cámara al que regresar una vez finalizado el seguimiento."
"description": "Nombre del preajuste ONVIF configurado en el firmware de la cámara al que regresar una vez finalizado el seguimiento.",
"label": "Preajuste de retorno"
},
"timeout": {
"description": "Espere esta cantidad de segundos después de perder el seguimiento antes de devolver la cámara a la posición preestablecida."
"description": "Espere esta cantidad de segundos después de perder el seguimiento antes de devolver la cámara a la posición preestablecida.",
"label": "Tiempo de espera de retorno"
},
"label": "Seguimiento automático",
"enabled": {
"label": "Habilitar seguimiento automático",
"description": "Habilita o deshabilita el seguimiento automático con cámara PTZ de objetos detectados."
},
"track": {
"label": "Objetos rastreados",
"description": "Lista de tipos de objetos que deben activar el seguimiento automático."
},
"required_zones": {
"label": "Zonas requeridas",
"description": "Los objetos deben entrar en una de estas zonas antes de que comience el seguimiento automático."
},
"movement_weights": {
"label": "Pesos de movimiento",
"description": "Valores de calibración generados automáticamente por la calibración de la cámara. No los modifiques manualmente."
},
"enabled_in_config": {
"label": "Estado original de autoseguimiento",
"description": "Campo interno para rastrear si el seguimiento automático estaba habilitado en la configuración."
}
},
"tls_insecure": {
"description": "Omitir la verificación TLS y deshabilitar la autenticación digest para ONVIF (no seguro; usar solo en redes seguras)."
"description": "Omitir la verificación TLS y deshabilitar la autenticación digest para ONVIF (no seguro; usar solo en redes seguras).",
"label": "Deshabilitar verificación TLS"
},
"label": "ONVIF",
"description": "Ajustes de conexión ONVIF y seguimiento automático PTZ para esta cámara.",
"host": {
"label": "Host ONVIF",
"description": "Host (y esquema opcional) para el servicio ONVIF de esta cámara."
},
"port": {
"label": "Puerto ONVIF",
"description": "Número de puerto del servicio ONVIF."
},
"user": {
"label": "Nombre de usuario ONVIF",
"description": "Nombre de usuario para la autenticación ONVIF; algunos dispositivos requieren un usuario administrador para ONVIF."
},
"password": {
"label": "Contraseña ONVIF",
"description": "Contraseña para la autenticación ONVIF."
},
"ignore_time_mismatch": {
"label": "Ignorar discrepancia horaria",
"description": "Ignora las diferencias de sincronización horaria entre la cámara y el servidor Frigate para la comunicación ONVIF."
}
},
"zones": {
"distances": {
"label": "Distancias reales"
"label": "Distancias reales",
"description": "Distancias reales opcionales para cada lado del cuadrilátero de la zona, usadas para cálculos de velocidad o distancia. Debe tener exactamente 4 valores si se establece."
},
"coordinates": {
"description": "Coordenadas del polígono que definen el área de la zona. Puede ser una cadena separada por comas o una lista de cadenas de coordenadas. Las coordenadas deben ser relativas (0-1) o absolutas (heredadas).",
@ -106,23 +161,41 @@
"description": "Área máxima del cuadro delimitador (píxeles o porcentaje) permitida para este tipo de objeto. Puede expresarse en píxeles (entero) o como porcentaje (decimal entre 0,000001 y 0,99).",
"label": "Área máxima del objeto"
},
"description": "Filtros para aplicar a los objetos dentro de esta zona. Se utilizan para reducir los falsos positivos o restringir qué objetos se consideran presentes en la zona."
"description": "Filtros para aplicar a los objetos dentro de esta zona. Se utilizan para reducir los falsos positivos o restringir qué objetos se consideran presentes en la zona.",
"label": "Filtros de zona",
"min_area": {
"label": "Área mínima de objeto",
"description": "Área mínima del cuadro delimitador (píxeles o porcentaje) necesaria para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)."
}
},
"objects": {
"description": "Lista de tipos de objetos (del mapa de etiquetas) que pueden activar esta zona. Puede ser una cadena de texto o una lista de cadenas. Si está vacío, se consideran todos los objetos."
"description": "Lista de tipos de objetos (del mapa de etiquetas) que pueden activar esta zona. Puede ser una cadena de texto o una lista de cadenas. Si está vacío, se consideran todos los objetos.",
"label": "Objetos activadores"
},
"description": "Las zonas le permiten definir un área específica del fotograma, de modo que pueda determinar si un objeto se encuentra o no dentro de un área determinada.",
"speed_threshold": {
"description": "Velocidad mínima (en unidades del mundo real, si se han configurado distancias) requerida para que un objeto se considere presente en la zona. Se utiliza para los disparadores de zona basados en la velocidad."
"description": "Velocidad mínima (en unidades del mundo real, si se han configurado distancias) requerida para que un objeto se considere presente en la zona. Se utiliza para los disparadores de zona basados en la velocidad.",
"label": "Velocidad mínima"
},
"friendly_name": {
"description": "Un nombre fácil de usar para la zona, que se muestra en la interfaz de usuario de Frigate. Si no se especifica, se utilizará una versión formateada del nombre de la zona."
"description": "Un nombre fácil de usar para la zona, que se muestra en la interfaz de usuario de Frigate. Si no se especifica, se utilizará una versión formateada del nombre de la zona.",
"label": "Nombre de zona"
},
"inertia": {
"description": "Número de fotogramas consecutivos en los que se debe detectar un objeto dentro de la zona antes de considerarlo presente. Ayuda a filtrar las detecciones transitorias."
"description": "Número de fotogramas consecutivos en los que se debe detectar un objeto dentro de la zona antes de considerarlo presente. Ayuda a filtrar las detecciones transitorias.",
"label": "Fotogramas de inercia"
},
"loitering_time": {
"description": "Número de segundos que un objeto debe permanecer en la zona para ser considerado como merodeo. Establezca en 0 para desactivar la detección de merodeo."
"description": "Número de segundos que un objeto debe permanecer en la zona para ser considerado como merodeo. Establezca en 0 para desactivar la detección de merodeo.",
"label": "Segundos de permanencia"
},
"label": "Zonas",
"enabled": {
"label": "Habilitado",
"description": "Habilita o deshabilita esta zona. Las zonas deshabilitadas se ignoran en tiempo de ejecución."
},
"enabled_in_config": {
"label": "Mantiene el registro del estado original de la zona."
}
},
"objects": {
@ -142,148 +215,739 @@
},
"send_triggers": {
"after_significant_updates": {
"description": "Envía una solicitud a GenAI tras un número especificado de actualizaciones significativas del objeto rastreado."
"description": "Envía una solicitud a GenAI tras un número especificado de actualizaciones significativas del objeto rastreado.",
"label": "Activador temprano de GenAI"
},
"description": "Define cuándo se deben enviar los fotogramas a GenAI (al finalizar, después de las actualizaciones, etc.)."
"description": "Define cuándo se deben enviar los fotogramas a GenAI (al finalizar, después de las actualizaciones, etc.).",
"label": "Activadores de GenAI",
"tracked_object_end": {
"label": "Enviar al finalizar",
"description": "Envía una solicitud a GenAI cuando finaliza el objeto rastreado."
}
},
"required_zones": {
"description": "Zonas en las que deben ubicarse los objetos para ser elegibles para la generación de descripciones con GenAI."
"description": "Zonas en las que deben ubicarse los objetos para ser elegibles para la generación de descripciones con GenAI.",
"label": "Zonas requeridas"
},
"prompt": {
"label": "Prompt de descripción",
"description": "Plantilla de prompt predeterminada usada al generar descripciones con GenAI."
},
"object_prompts": {
"label": "Prompts de objetos",
"description": "Prompts por objeto para personalizar las salidas de GenAI para etiquetas concretas."
},
"objects": {
"label": "Objetos de GenAI",
"description": "Lista de etiquetas de objetos que se enviarán a GenAI de forma predeterminada."
},
"debug_save_thumbnails": {
"label": "Guardar miniaturas",
"description": "Guarda las miniaturas enviadas a GenAI para depuración y revisión."
},
"enabled_in_config": {
"label": "Estado original de GenAI",
"description": "Indica si GenAI estaba habilitado en la configuración estática original."
}
},
"label": "Objetos",
"description": "Valores predeterminados de seguimiento de objetos, incluidas las etiquetas que se rastrean y los filtros por objeto.",
"track": {
"label": "Objetos a rastrear",
"description": "Lista de etiquetas de objetos a rastrear para esta cámara."
},
"filters": {
"label": "Filtros de objetos",
"description": "Filtros aplicados a los objetos detectados para reducir falsos positivos (área, relación, confianza).",
"min_area": {
"label": "Área mínima de objeto",
"description": "Área mínima del cuadro delimitador (píxeles o porcentaje) necesaria para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)."
},
"max_area": {
"label": "Área máxima de objeto",
"description": "Área máxima del cuadro delimitador (píxeles o porcentaje) permitida para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)."
},
"min_ratio": {
"label": "Relación de aspecto mínima",
"description": "Relación mínima anchura/altura necesaria para que el cuadro delimitador sea válido."
},
"max_ratio": {
"label": "Relación de aspecto máxima",
"description": "Relación máxima anchura/altura permitida para que el cuadro delimitador sea válido."
},
"threshold": {
"label": "Umbral de confianza",
"description": "Umbral medio de confianza de detección necesario para que el objeto se considere un positivo verdadero."
},
"min_score": {
"label": "Confianza mínima",
"description": "Confianza mínima de detección en un único fotograma necesaria para que el objeto se contabilice."
},
"mask": {
"label": "Máscara de filtro",
"description": "Coordenadas del polígono que definen dónde se aplica este filtro dentro del fotograma."
},
"raw_mask": {
"label": "Máscara sin procesar"
}
},
"mask": {
"label": "Máscara de objeto",
"description": "Polígono de máscara usado para evitar la detección de objetos en áreas especificadas."
}
},
"mqtt": {
"label": "MQTT",
"required_zones": {
"description": "Zonas en las que debe entrar un objeto para que se publique una imagen MQTT."
"description": "Zonas en las que debe entrar un objeto para que se publique una imagen MQTT.",
"label": "Zonas requeridas"
},
"description": "Ajustes de publicación de imágenes MQTT.",
"enabled": {
"label": "Enviar imagen",
"description": "Habilita la publicación de instantáneas de objetos en temas MQTT para esta cámara."
},
"timestamp": {
"label": "Añadir marca de tiempo",
"description": "Superpone una marca de tiempo en las imágenes publicadas en MQTT."
},
"bounding_box": {
"label": "Añadir cuadro delimitador",
"description": "Dibuja cuadros delimitadores en las imágenes publicadas mediante MQTT."
},
"crop": {
"label": "Recortar imagen",
"description": "Recorta las imágenes publicadas en MQTT al cuadro delimitador del objeto detectado."
},
"height": {
"label": "Altura de imagen",
"description": "Altura (píxeles) a la que redimensionar las imágenes publicadas mediante MQTT."
},
"quality": {
"label": "Calidad JPEG",
"description": "Calidad JPEG de las imágenes publicadas en MQTT (0-100)."
}
},
"notifications": {
"email": {
"label": "Email de notificacion"
"label": "Email de notificacion",
"description": "Dirección de correo electrónico usada para notificaciones push o requerida por ciertos proveedores de notificaciones."
},
"label": "Notificaciones",
"description": "Ajustes para habilitar y controlar las notificaciones de esta cámara.",
"enabled": {
"label": "Habilitar notificaciones",
"description": "Habilita o deshabilita las notificaciones para esta cámara."
},
"cooldown": {
"label": "Periodo de enfriamiento",
"description": "Periodo de enfriamiento (segundos) entre notificaciones para evitar saturar a los destinatarios."
},
"enabled_in_config": {
"label": "Estado original de notificaciones",
"description": "Indica si las notificaciones estaban habilitadas en la configuración estática original."
}
},
"audio_transcription": {
"description": "Configuración para la transcripción de audio en vivo y de voz, utilizada para eventos y subtítulos en tiempo real.",
"enabled": {
"label": "Habilitar transcripción"
"label": "Habilitar transcripción",
"description": "Activar o desactivar la transcripción de eventos de audio activados manualmente."
},
"label": "Transcripción de audio",
"enabled_in_config": {
"label": "Estado original de la transcripción"
},
"live_enabled": {
"label": "Transcripción en directo",
"description": "Activar la transcripción en directo del audio a medida que se recibe."
}
},
"motion": {
"skip_motion_threshold": {
"description": "Si se establece en un valor entre 0,0 y 1,0, y más de esta fracción de la imagen cambia en un solo fotograma, el detector no devolverá cuadros de movimiento y se recalibrará inmediatamente. Esto puede ahorrar recursos de CPU y reducir los falsos positivos durante tormentas eléctricas, tempestades, etc., aunque podría pasar por alto eventos reales, como el seguimiento automático de un objeto por parte de una cámara PTZ. La disyuntiva está entre descartar unos cuantos megabytes de grabaciones o revisar un par de clips cortos. Deje este parámetro sin establecer (None) para desactivar esta función."
"description": "Si se establece en un valor entre 0,0 y 1,0, y más de esta fracción de la imagen cambia en un solo fotograma, el detector no devolverá cuadros de movimiento y se recalibrará inmediatamente. Esto puede ahorrar recursos de CPU y reducir los falsos positivos durante tormentas eléctricas, tempestades, etc., aunque podría pasar por alto eventos reales, como el seguimiento automático de un objeto por parte de una cámara PTZ. La disyuntiva está entre descartar unos cuantos megabytes de grabaciones o revisar un par de clips cortos. Deje este parámetro sin establecer (None) para desactivar esta función.",
"label": "Omitir umbral de movimiento"
},
"lightning_threshold": {
"description": "Umbral para detectar e ignorar breves picos de luz (un valor menor indica mayor sensibilidad; valores entre 0,3 y 1,0). Esto no impide por completo la detección de movimiento; Simplemente provoca que el detector deje de analizar fotogramas adicionales una vez que se supera el umbral. Durante estos eventos aún se realizan grabaciones basadas en el movimiento."
"description": "Umbral para detectar e ignorar breves picos de luz (un valor menor indica mayor sensibilidad; valores entre 0,3 y 1,0). Esto no impide por completo la detección de movimiento; Simplemente provoca que el detector deje de analizar fotogramas adicionales una vez que se supera el umbral. Durante estos eventos aún se realizan grabaciones basadas en el movimiento.",
"label": "Umbral de iluminación"
},
"threshold": {
"description": "Umbral de diferencia de píxeles utilizado por el detector de movimiento; los valores más altos reducen la sensibilidad (rango 1-255)."
"description": "Umbral de diferencia de píxeles utilizado por el detector de movimiento; los valores más altos reducen la sensibilidad (rango 1-255).",
"label": "Umbral de movimiento"
},
"label": "Detección de movimiento",
"description": "Ajustes predeterminados de detección de movimiento para esta cámara.",
"enabled": {
"label": "Habilitar detección de movimiento",
"description": "Habilita o deshabilita la detección de movimiento para esta cámara."
},
"improve_contrast": {
"label": "Mejorar contraste",
"description": "Aplica una mejora de contraste a los fotogramas antes del análisis de movimiento para ayudar a la detección."
},
"contour_area": {
"label": "Área de contorno",
"description": "Área mínima de contorno en píxeles necesaria para que se cuente un contorno de movimiento."
},
"delta_alpha": {
"label": "Delta alfa",
"description": "Factor de mezcla alfa usado en la diferencia entre fotogramas para calcular el movimiento."
},
"frame_alpha": {
"label": "Alfa del fotograma",
"description": "Valor alfa usado al mezclar fotogramas para el preprocesamiento de movimiento."
},
"frame_height": {
"label": "Altura del fotograma",
"description": "Altura en píxeles a la que escalar los fotogramas al calcular el movimiento."
},
"mask": {
"label": "Coordenadas de máscara",
"description": "Coordenadas x,y ordenadas que definen el polígono de máscara de movimiento usado para incluir/excluir áreas."
},
"mqtt_off_delay": {
"label": "Retraso de apagado MQTT",
"description": "Segundos a esperar tras el último movimiento antes de publicar un estado MQTT 'off'."
},
"enabled_in_config": {
"label": "Estado de movimiento original",
"description": "Indica si la detección de movimiento estaba habilitada en la configuración estática original."
},
"raw_mask": {
"label": "Máscara sin procesar"
}
},
"lpr": {
"enhancement": {
"description": "Nivel de mejora (0-10) que se aplicará a los recortes de matrículas antes del OCR; los valores más altos no siempre mejoran los resultados, y los niveles superiores a 5 podrían funcionar únicamente con matrículas capturadas de noche, por lo que deben utilizarse con precaución."
"description": "Nivel de mejora (0-10) que se aplicará a los recortes de matrículas antes del OCR; los valores más altos no siempre mejoran los resultados, y los niveles superiores a 5 podrían funcionar únicamente con matrículas capturadas de noche, por lo que deben utilizarse con precaución.",
"label": "Nivel de mejora"
},
"expire_time": {
"description": "Tiempo en segundos tras el cual una matrícula no detectada caduca en el sistema de seguimiento (solo para cámaras LPR dedicadas)."
"description": "Tiempo en segundos tras el cual una matrícula no detectada caduca en el sistema de seguimiento (solo para cámaras LPR dedicadas).",
"label": "Segundos hasta caducar"
},
"label": "Reconocimiento de matrículas",
"description": "Ajustes de reconocimiento de matrículas, incluidos umbrales de detección, formato y matrículas conocidas.",
"enabled": {
"label": "Habilitar LPR",
"description": "Habilita o deshabilita LPR en esta cámara."
},
"min_area": {
"label": "Área mínima de matrícula",
"description": "Área mínima de matrícula (píxeles) necesaria para intentar el reconocimiento."
}
},
"detect": {
"fps": {
"description": "Fotogramas por segundo deseados para ejecutar la detección; los valores más bajos reducen el uso de la CPU (el valor recomendado es 5; establezca un valor superior —como máximo de 10— únicamente si realiza el seguimiento de objetos que se mueven con extrema rapidez)."
"description": "Fotogramas por segundo deseados para ejecutar la detección; los valores más bajos reducen el uso de la CPU (el valor recomendado es 5; establezca un valor superior —como máximo de 10— únicamente si realiza el seguimiento de objetos que se mueven con extrema rapidez).",
"label": "FPS de detección"
},
"min_initialized": {
"description": "Número de detecciones consecutivas requeridas antes de crear un objeto rastreado. Auméntelo para reducir las inicializaciones falsas. El valor predeterminado es los FPS divididos por 2."
"description": "Número de detecciones consecutivas requeridas antes de crear un objeto rastreado. Auméntelo para reducir las inicializaciones falsas. El valor predeterminado es los FPS divididos por 2.",
"label": "Fotogramas mínimos de inicialización"
},
"height": {
"description": "Altura (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión."
"description": "Altura (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión.",
"label": "Altura de detección"
},
"width": {
"description": "Ancho (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión."
"description": "Ancho (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión.",
"label": "Anchura de detección"
},
"stationary": {
"description": "Configuración para detectar y gestionar objetos que permanecen inmóviles durante un periodo de tiempo."
"description": "Configuración para detectar y gestionar objetos que permanecen inmóviles durante un periodo de tiempo.",
"label": "Configuración de objetos estacionarios",
"interval": {
"label": "Intervalo estacionario",
"description": "Frecuencia (en fotogramas) con la que se ejecuta una comprobación de detección para confirmar un objeto estacionario."
},
"threshold": {
"label": "Umbral estacionario",
"description": "Número de fotogramas sin cambio de posición necesarios para marcar un objeto como estacionario."
},
"max_frames": {
"label": "Fotogramas máximos",
"description": "Limita durante cuánto tiempo se rastrean los objetos estacionarios antes de descartarlos.",
"default": {
"label": "Fotogramas máximos predeterminados",
"description": "Número máximo predeterminado de fotogramas para rastrear un objeto estacionario antes de detenerse."
},
"objects": {
"label": "Fotogramas máximos por objeto",
"description": "Sobrescrituras por objeto para el número máximo de fotogramas en los que rastrear objetos estacionarios."
}
},
"classifier": {
"label": "Habilitar clasificador visual",
"description": "Usa un clasificador visual para detectar objetos realmente estacionarios incluso cuando los cuadros delimitadores oscilan."
}
},
"label": "Detección de objetos",
"description": "Ajustes del rol de detección/detect usado para ejecutar la detección de objetos e inicializar los rastreadores.",
"enabled": {
"label": "Habilitar detección de objetos",
"description": "Habilita o deshabilita la detección de objetos para esta cámara."
},
"max_disappeared": {
"label": "Fotogramas máximos desaparecido",
"description": "Número de fotogramas sin detección antes de que un objeto rastreado se considere desaparecido."
},
"annotation_offset": {
"label": "Desplazamiento de anotaciones",
"description": "Milisegundos para desplazar las anotaciones de detección y alinear mejor los cuadros delimitadores de la línea de tiempo con las grabaciones; puede ser positivo o negativo."
}
},
"record": {
"motion": {
"description": "Número de días para conservar las grabaciones activadas por movimiento, independientemente de los objetos rastreados. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones."
"description": "Número de días para conservar las grabaciones activadas por movimiento, independientemente de los objetos rastreados. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones.",
"label": "Retención de movimiento",
"days": {
"label": "Días de retención",
"description": "Días durante los que conservar las grabaciones."
}
},
"continuous": {
"description": "Número de días para conservar las grabaciones, independientemente de los objetos rastreados o del movimiento. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones."
"description": "Número de días para conservar las grabaciones, independientemente de los objetos rastreados o del movimiento. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones.",
"label": "Retención continua",
"days": {
"label": "Días de retención",
"description": "Días durante los que conservar las grabaciones."
}
},
"detections": {
"pre_capture": {
"description": "Número de segundos antes del evento de detección que se incluirán en la grabación."
"description": "Número de segundos antes del evento de detección que se incluirán en la grabación.",
"label": "Segundos de captura previa"
},
"post_capture": {
"description": "Número de segundos después del evento de detección que se incluirán en la grabación."
"description": "Número de segundos después del evento de detección que se incluirán en la grabación.",
"label": "Segundos de captura posterior"
},
"label": "Retención de detección",
"description": "Ajustes de retención de grabaciones para eventos de detección, incluidas las duraciones de captura previa/posterior.",
"retain": {
"label": "Retención de eventos",
"description": "Ajustes de retención para grabaciones de eventos de detección.",
"days": {
"label": "Días de retención",
"description": "Número de días durante los que conservar grabaciones de eventos de detección."
},
"mode": {
"label": "Modo de retención",
"description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)."
}
}
},
"alerts": {
"pre_capture": {
"description": "Número de segundos antes del evento de detección que se incluirán en la grabación."
"description": "Número de segundos antes del evento de detección que se incluirán en la grabación.",
"label": "Segundos de captura previa"
},
"post_capture": {
"description": "Número de segundos después del evento de detección que se incluirán en la grabación."
"description": "Número de segundos después del evento de detección que se incluirán en la grabación.",
"label": "Segundos de captura posterior"
},
"label": "Retención de alertas",
"description": "Ajustes de retención de grabaciones para eventos de alerta, incluidas las duraciones de captura previa/posterior.",
"retain": {
"label": "Retención de eventos",
"description": "Ajustes de retención para grabaciones de eventos de detección.",
"days": {
"label": "Días de retención",
"description": "Número de días durante los que conservar grabaciones de eventos de detección."
},
"mode": {
"label": "Modo de retención",
"description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)."
}
}
},
"label": "Grabación",
"description": "Ajustes de grabación y retención para esta cámara.",
"enabled": {
"label": "Habilitar grabación",
"description": "Habilita o deshabilita la grabación para esta cámara."
},
"expire_interval": {
"label": "Intervalo de limpieza de grabaciones",
"description": "Minutos entre pasadas de limpieza que eliminan segmentos de grabación caducados."
},
"export": {
"label": "Configuración de exportación",
"description": "Ajustes usados al exportar grabaciones, como timelapse y aceleración por hardware.",
"hwaccel_args": {
"label": "Argumentos hwaccel de exportación",
"description": "Argumentos de aceleración por hardware que se usarán en operaciones de exportación/transcodificación."
},
"max_concurrent": {
"label": "Exportaciones simultáneas máximas",
"description": "Número máximo de trabajos de exportación que se procesarán al mismo tiempo."
}
},
"preview": {
"label": "Configuración de vista previa",
"description": "Ajustes que controlan la calidad de las vistas previas de grabaciones mostradas en la interfaz.",
"quality": {
"label": "Calidad de vista previa",
"description": "Nivel de calidad de vista previa (very_low, low, medium, high, very_high)."
}
},
"enabled_in_config": {
"label": "Estado de grabación original",
"description": "Indica si la grabación estaba habilitada en la configuración estática original."
}
},
"ui": {
"dashboard": {
"description": "Alterna si esta cámara es visible en toda la interfaz de usuario de Frigate. Desactivar esta opción requerirá editar manualmente la configuración para volver a visualizar esta cámara en la interfaz."
"description": "Alterna si esta cámara es visible en toda la interfaz de usuario de Frigate. Desactivar esta opción requerirá editar manualmente la configuración para volver a visualizar esta cámara en la interfaz.",
"label": "Mostrar en la interfaz"
},
"label": "Interfaz de cámara",
"description": "Orden de visualización y visibilidad de esta cámara en la interfaz. El orden afecta al panel predeterminado. Para un control más granular, usa grupos de cámaras.",
"order": {
"label": "Orden en la interfaz",
"description": "Orden numérico usado para ordenar la cámara en la interfaz (panel predeterminado y listas); los números más altos aparecen más tarde."
}
},
"live": {
"height": {
"description": "Altura (en píxeles) para renderizar la transmisión en vivo de jsmpeg en la interfaz web; debe ser <= a la altura de la transmisión de detección."
"description": "Altura (en píxeles) para renderizar la transmisión en vivo de jsmpeg en la interfaz web; debe ser <= a la altura de la transmisión de detección.",
"label": "Altura en directo"
},
"description": "Configuraciones utilizadas por la interfaz web para controlar la selección, la resolución y la calidad de transmisiónes en vivo."
"description": "Configuraciones utilizadas por la interfaz web para controlar la selección, la resolución y la calidad de transmisiónes en vivo.",
"label": "Reproducción en directo",
"streams": {
"label": "Nombres de flujos en directo",
"description": "Asignación de nombres de flujos configurados a nombres de restream/go2rtc usados para la reproducción en directo."
},
"quality": {
"label": "Calidad en directo",
"description": "Calidad de codificación para el flujo jsmpeg (1 la más alta, 31 la más baja)."
}
},
"review": {
"description": "Configuraciones que controlan las alertas, las detecciones y los resúmenes de revisión de GenAI utilizados por la interfaz de usuario y el almacenamiento de esta cámara.",
"alerts": {
"required_zones": {
"description": "Zonas en las que debe entrar un objeto para ser considerado una alerta; dejar vacío para permitir cualquier zona."
"description": "Zonas en las que debe entrar un objeto para ser considerado una alerta; dejar vacío para permitir cualquier zona.",
"label": "Zonas requeridas"
},
"labels": {
"description": "Lista de etiquetas de objetos que califican como alertas (por ejemplo: car, person)."
"description": "Lista de etiquetas de objetos que califican como alertas (por ejemplo: car, person).",
"label": "Etiquetas de alerta"
},
"label": "Configuración de alertas",
"description": "Ajustes sobre qué objetos rastreados generan alertas y cómo se conservan las alertas.",
"enabled": {
"label": "Habilitar alertas",
"description": "Habilita o deshabilita la generación de alertas para esta cámara."
},
"enabled_in_config": {
"label": "Estado original de alertas",
"description": "Rastrea si las alertas estaban habilitadas originalmente en la configuración estática."
},
"cutoff_time": {
"label": "Tiempo de corte de alertas",
"description": "Segundos que se esperarán tras dejar de haber actividad causante de alerta antes de cortar una alerta."
}
},
"detections": {
"required_zones": {
"description": "Zonas en las que debe entrar un objeto para ser considerado detectado; dejar vacío para permitir cualquier zona."
"description": "Zonas en las que debe entrar un objeto para ser considerado detectado; dejar vacío para permitir cualquier zona.",
"label": "Zonas requeridas"
},
"description": "Configuración para determinar qué objetos rastreados generan detecciones (no alertas) y cómo se retienen dichas detecciones."
"description": "Configuración para determinar qué objetos rastreados generan detecciones (no alertas) y cómo se retienen dichas detecciones.",
"label": "Configuración de detecciones",
"enabled": {
"label": "Habilitar detecciones",
"description": "Habilita o deshabilita los eventos de detección para esta cámara."
},
"labels": {
"label": "Etiquetas de detección",
"description": "Lista de etiquetas de objetos que cuentan como eventos de detección."
},
"cutoff_time": {
"label": "Tiempo de corte de detecciones",
"description": "Segundos que se esperarán tras dejar de haber actividad causante de detección antes de cortar una detección."
},
"enabled_in_config": {
"label": "Estado original de detecciones",
"description": "Rastrea si las detecciones estaban habilitadas originalmente en la configuración estática."
}
},
"genai": {
"image_source": {
"description": "Fuente de las imágenes enviadas a GenAI ('preview' o 'recordings'); La opción 'recordings' utiliza fotogramas de mayor calidad, pero requiere más tokens."
"description": "Fuente de las imágenes enviadas a GenAI ('preview' o 'recordings'); La opción 'recordings' utiliza fotogramas de mayor calidad, pero requiere más tokens.",
"label": "Origen de imagen de revisión"
},
"additional_concerns": {
"description": "Una lista de preocupaciones o notas adicionales que GenAI debería tener en cuenta al evaluar la actividad en esta cámara."
"description": "Una lista de preocupaciones o notas adicionales que GenAI debería tener en cuenta al evaluar la actividad en esta cámara.",
"label": "Consideraciones adicionales"
},
"activity_context_prompt": {
"description": "Instrucción personalizada que describe qué constituye y qué no una actividad sospechosa, con el fin de proporcionar contexto para los resúmenes generados por GenAI."
"description": "Instrucción personalizada que describe qué constituye y qué no una actividad sospechosa, con el fin de proporcionar contexto para los resúmenes generados por GenAI.",
"label": "Prompt de contexto de actividad"
},
"description": "Controla el uso de IA generativa (GenAI) para la elaboración de descripciones y resúmenes de elementos de revisión.",
"debug_save_thumbnails": {
"description": "Guarde las miniaturas que se envían al proveedor de GenAI para su depuración y revisión."
"description": "Guarde las miniaturas que se envían al proveedor de GenAI para su depuración y revisión.",
"label": "Guardar miniaturas"
},
"label": "Configuración de GenAI",
"enabled": {
"label": "Habilitar descripciones de GenAI",
"description": "Habilita o deshabilita las descripciones y resúmenes generados por GenAI para los elementos de revisión."
},
"alerts": {
"label": "Habilitar GenAI para alertas",
"description": "Usa GenAI para generar descripciones de elementos de alerta."
},
"detections": {
"label": "Habilitar GenAI para detecciones",
"description": "Usa GenAI para generar descripciones de elementos de detección."
},
"enabled_in_config": {
"label": "Estado original de GenAI",
"description": "Rastrea si la revisión de GenAI estaba habilitada originalmente en la configuración estática."
},
"preferred_language": {
"label": "Idioma preferido",
"description": "Idioma preferido que se solicitará al proveedor de GenAI para las respuestas generadas."
}
}
},
"label": "Revisión"
},
"birdseye": {
"description": "Configuración para la vista compuesta Birdseye, que combina las transmisiones de múltiples cámaras en una sola vista."
"description": "Configuración para la vista compuesta Birdseye, que combina las transmisiones de múltiples cámaras en una sola vista.",
"label": "Vista general",
"enabled": {
"label": "Habilitar Birdseye",
"description": "Habilita o deshabilita la función de vista Birdseye."
},
"mode": {
"label": "Modo de seguimiento",
"description": "Modo para incluir cámaras en Birdseye: 'objects', 'motion' o 'continuous'."
},
"order": {
"label": "Posición",
"description": "Posición numérica que controla el orden de la cámara en el diseño de Birdseye."
}
},
"ffmpeg": {
"retry_interval": {
"description": "Segundos de espera antes de intentar reconectar la transmisión de una cámara tras un fallo. El valor predeterminado es 10."
"description": "Segundos de espera antes de intentar reconectar la transmisión de una cámara tras un fallo. El valor predeterminado es 10.",
"label": "Tiempo de reintento de FFmpeg"
},
"path": {
"description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"5.0\" o \"7.0\")."
"description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"5.0\" o \"7.0\").",
"label": "Ruta de FFmpeg"
},
"output_args": {
"description": "Argumentos de salida predeterminados utilizados para diferentes roles de FFmpeg, tales como detección y grabación."
"description": "Argumentos de salida predeterminados utilizados para diferentes roles de FFmpeg, tales como detección y grabación.",
"label": "Argumentos de salida",
"detect": {
"label": "Argumentos de salida de detección",
"description": "Argumentos de salida predeterminados para los flujos con rol de detección."
},
"record": {
"label": "Argumentos de salida de grabación",
"description": "Argumentos de salida predeterminados para los flujos con rol de grabación."
}
},
"description": "Configuración de FFmpeg, incluyendo la ruta del binario, argumentos, opciones de aceleración por hardware y argumentos de salida por rol."
"description": "Configuración de FFmpeg, incluyendo la ruta del binario, argumentos, opciones de aceleración por hardware y argumentos de salida por rol.",
"label": "FFmpeg",
"global_args": {
"label": "Argumentos globales de FFmpeg",
"description": "Argumentos globales pasados a los procesos de FFmpeg."
},
"hwaccel_args": {
"label": "Argumentos de aceleración por hardware",
"description": "Argumentos de aceleración por hardware para FFmpeg. Se recomiendan preajustes específicos del proveedor."
},
"input_args": {
"label": "Argumentos de entrada",
"description": "Argumentos de entrada aplicados a los flujos de entrada de FFmpeg."
},
"apple_compatibility": {
"label": "Compatibilidad con Apple",
"description": "Habilita el etiquetado HEVC para mejorar la compatibilidad con reproductores de Apple al grabar H.265."
},
"gpu": {
"label": "Índice de GPU",
"description": "Índice de GPU predeterminado usado para la aceleración por hardware si está disponible."
},
"inputs": {
"label": "Entradas de cámara",
"description": "Lista de definiciones de flujos de entrada (rutas y roles) para esta cámara.",
"path": {
"label": "Ruta de entrada",
"description": "URL o ruta del flujo de entrada de la cámara."
},
"roles": {
"label": "Roles de entrada",
"description": "Roles para este flujo de entrada."
},
"global_args": {
"label": "Argumentos globales de FFmpeg",
"description": "Argumentos globales de FFmpeg para este flujo de entrada."
},
"hwaccel_args": {
"label": "Argumentos de aceleración por hardware",
"description": "Argumentos de aceleración por hardware para este flujo de entrada."
},
"input_args": {
"label": "Argumentos de entrada",
"description": "Argumentos de entrada específicos para este flujo."
}
}
},
"face_recognition": {
"label": "Reconocimiento facial",
"description": "Ajustes de detección y reconocimiento facial para esta cámara.",
"enabled": {
"label": "Habilitar reconocimiento facial",
"description": "Habilita o deshabilita el reconocimiento facial."
},
"min_area": {
"label": "Área mínima de rostro",
"description": "Área mínima (píxeles) del cuadro de un rostro detectado necesaria para intentar el reconocimiento."
}
},
"semantic_search": {
"label": "Búsqueda semántica",
"description": "Ajustes de búsqueda semántica, que crea y consulta embeddings de objetos para encontrar elementos similares.",
"triggers": {
"label": "Activadores",
"description": "Acciones y criterios de coincidencia para activadores de búsqueda semántica específicos de la cámara.",
"friendly_name": {
"label": "Nombre descriptivo",
"description": "Nombre descriptivo opcional mostrado en la interfaz para este activador."
},
"enabled": {
"label": "Habilitar este activador",
"description": "Habilita o deshabilita este activador de búsqueda semántica."
},
"type": {
"label": "Tipo de activador",
"description": "Tipo de activador: 'thumbnail' (coincidir con imagen) o 'description' (coincidir con texto)."
},
"data": {
"label": "Contenido del activador",
"description": "Frase de texto o ID de miniatura que se comparará con objetos rastreados."
},
"threshold": {
"label": "Umbral del activador",
"description": "Puntuación mínima de similitud (0-1) necesaria para activar este activador."
},
"actions": {
"label": "Acciones del activador",
"description": "Lista de acciones que se ejecutarán cuando el activador coincida (notification, sub_label, attribute)."
}
}
},
"snapshots": {
"label": "Instantáneas",
"description": "Ajustes de instantáneas generadas por la API de objetos rastreados para esta cámara.",
"enabled": {
"label": "Habilitar instantáneas",
"description": "Habilita o deshabilita el guardado de instantáneas para esta cámara."
},
"timestamp": {
"label": "Superposición de marca de tiempo",
"description": "Superpone una marca de tiempo en las instantáneas de la API."
},
"bounding_box": {
"label": "Superposición de cuadro delimitador",
"description": "Dibuja cuadros delimitadores para los objetos rastreados en las instantáneas de la API."
},
"crop": {
"label": "Recortar instantánea",
"description": "Recorta las instantáneas de la API al cuadro delimitador del objeto detectado."
},
"required_zones": {
"label": "Zonas requeridas",
"description": "Zonas en las que debe entrar un objeto para que se guarde una instantánea."
},
"height": {
"label": "Altura de instantánea",
"description": "Altura (píxeles) a la que redimensionar las instantáneas de la API; déjalo vacío para conservar el tamaño original."
},
"retain": {
"label": "Retención de instantáneas",
"description": "Ajustes de retención de instantáneas, incluidos días predeterminados y sobrescrituras por objeto.",
"default": {
"label": "Retención predeterminada",
"description": "Número predeterminado de días durante los que conservar instantáneas."
},
"mode": {
"label": "Modo de retención",
"description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)."
},
"objects": {
"label": "Retención por objeto",
"description": "Sobrescrituras por objeto para los días de retención de instantáneas."
}
},
"quality": {
"label": "Calidad de instantánea",
"description": "Calidad de codificación de las instantáneas guardadas (0-100)."
}
},
"timestamp_style": {
"label": "Estilo de marca de tiempo",
"description": "Opciones de estilo para marcas de tiempo integradas aplicadas a grabaciones e instantáneas.",
"position": {
"label": "Posición de marca de tiempo",
"description": "Posición de la marca de tiempo en la imagen (tl/tr/bl/br)."
},
"format": {
"label": "Formato de marca de tiempo",
"description": "Cadena de formato de fecha y hora usada para las marcas de tiempo (códigos de formato datetime de Python)."
},
"color": {
"label": "Color de marca de tiempo",
"description": "Valores de color RGB para el texto de la marca de tiempo (todos los valores 0-255).",
"red": {
"label": "Rojo",
"description": "Componente rojo (0-255) para el color de la marca de tiempo."
},
"green": {
"label": "Verde",
"description": "Componente verde (0-255) para el color de la marca de tiempo."
},
"blue": {
"label": "Azul",
"description": "Componente azul (0-255) para el color de la marca de tiempo."
}
},
"thickness": {
"label": "Grosor de marca de tiempo",
"description": "Grosor de línea del texto de la marca de tiempo."
},
"effect": {
"label": "Efecto de marca de tiempo",
"description": "Efecto visual para el texto de la marca de tiempo (none, solid, shadow)."
}
},
"best_image_timeout": {
"label": "Tiempo de espera de mejor imagen",
"description": "Tiempo que se esperará la imagen con la puntuación de confianza más alta."
},
"type": {
"label": "Tipo de cámara",
"description": "Tipo de cámara"
},
"webui_url": {
"label": "URL de la cámara",
"description": "URL para visitar la cámara directamente desde la página del sistema"
},
"profiles": {
"label": "Perfiles",
"description": "Perfiles de configuración con nombre y sobrescrituras parciales que pueden activarse en tiempo de ejecución."
},
"enabled_in_config": {
"label": "Estado original de cámara",
"description": "Mantiene el registro del estado original de la cámara."
}
}

File diff suppressed because it is too large Load Diff

View File

@ -116,5 +116,15 @@
"animal": "Animal",
"postnord": "PostNord",
"usps": "USPS",
"gls": "GLS"
"gls": "GLS",
"canada_post": "Canada Post",
"royal_mail": "Royal Mail",
"school_bus": "Autobús escolar",
"skunk": "Mofeta",
"kangaroo": "Canguro",
"baby": "Bebé",
"baby_stroller": "Cochecito de bebé",
"rickshaw": "Rickshaw",
"Rodent": "Roedor",
"rodent": "Roedor"
}

View File

@ -1 +1,69 @@
{}
{
"documentTitle": "Chat - Frigate",
"title": "Frigate Chat",
"subtitle": "Tu asistente de IA para la gestión de cámaras y análisis",
"placeholder": "Pregunta cualquier cosa...",
"error": "Algo salió mal. Por favor, inténtalo de nuevo.",
"processing": "Procesando...",
"toolsUsed": "Usado: {{tools}}",
"showTools": "Mostrar herramientas ({{count}})",
"hideTools": "Ocultar herramientas",
"call": "Llamar",
"result": "Resultado",
"arguments": "Argumentos:",
"response": "Respuesta:",
"attachment_chip_label": "{{label}} en {{camera}}",
"attachment_chip_remove": "Eliminar adjunto",
"open_in_explore": "Abrir en Explorar",
"attach_event_aria": "Adjuntar evento {{eventId}}",
"attachment_picker_paste_label": "O pega el ID del evento",
"attachment_picker_attach": "Adjuntar",
"attachment_picker_placeholder": "Adjuntar un evento",
"quick_reply_find_similar": "Buscar avistamientos similares",
"quick_reply_tell_me_more": "Cuéntame más sobre esto",
"quick_reply_when_else": "¿Cuándo más se vio?",
"quick_reply_find_similar_text": "Buscar avistamientos similares a este.",
"quick_reply_tell_me_more_text": "Cuéntame más sobre este.",
"quick_reply_when_else_text": "¿Cuándo más se vio esto?",
"anchor": "Referencia",
"similarity_score": "Similitud",
"no_similar_objects_found": "No se encontraron objetos similares.",
"semantic_search_required": "La búsqueda semántica debe estar activada para encontrar objetos similares.",
"send": "Enviar",
"suggested_requests": "Prueba preguntando:",
"starting_requests": {
"show_recent_events": "Mostrar eventos recientes",
"show_camera_status": "Mostrar estado de la cámara",
"recap": "¿Qué ha pasado mientras estaba fuera?",
"watch_camera": "Vigilar una cámara en busca de actividad"
},
"starting_requests_prompts": {
"show_recent_events": "Muéstrame los eventos recientes de la última hora",
"show_camera_status": "¿Cuál es el estado actual de mis cámaras?",
"recap": "¿Qué ha pasado mientras estaba fuera?",
"watch_camera": "Vigila la puerta principal y avísame si aparece alguien"
},
"new_chat": "Nuevo chat",
"settings": {
"title": "Ajustes del chat",
"show_stats": {
"title": "Mostrar estadísticas",
"desc": "Mostrar la velocidad de generación y el tamaño del contexto en las respuestas del chat.",
"while_generating": "Durante la generación",
"always": "Siempre"
},
"auto_scroll": {
"title": "Desplazamiento automático",
"desc": "Seguir los mensajes nuevos a medida que llegan."
}
},
"stats": {
"context": "{{tokens}} tokens",
"tokens_per_second": "{{rate}} t/s"
},
"reasoning": {
"active": "Razonando…",
"show": "Mostrar razonamiento",
"hide": "Ocultar razonamiento"
}
}

View File

@ -146,7 +146,7 @@
"generateSuccess": "Imágenes de ejemplo generadas correctamente",
"missingStatesWarning": {
"title": "Faltan Ejemplos de Estado",
"description": "Se recomienda seleccionar ejemplos para todos los estados para obtener mejores resultados. Puede continuar sin seleccionar todos los estados, pero el modelo no se entrenará hasta que todos los estados tengan imágenes. Después de continuar, use la vista \"Clasificaciones recientes\" para clasificar las imágenes de los estados faltantes y luego entrene el modelo."
"description": "No todas las clases tienen ejemplos. Prueba a generar nuevos ejemplos para encontrar la clase que falta, o continúa y usa la vista de Clasificaciones recientes para añadir imágenes más tarde."
},
"allImagesRequired_one": "Por favor clasifique todas las imágenes. Queda {{count}} imagen.",
"allImagesRequired_many": "Por favor clasifique todas las imágenes. Quedan {{count}} imágenes.",

View File

@ -32,7 +32,9 @@
},
"camera": "Cámara",
"recordings": {
"documentTitle": "Grabaciones - Frigate"
"documentTitle": "Grabaciones - Frigate",
"invalidSharedLink": "No se puede abrir el enlace de la grabación con marca de tiempo debido a un error de análisis.",
"invalidSharedCamera": "No se puede abrir el enlace de la grabación con marca de tiempo debido a una cámara desconocida o no autorizada."
},
"calendarFilter": {
"last24Hours": "Últimas 24 horas"
@ -66,5 +68,28 @@
"select_all": "Todas",
"normalActivity": "Normal",
"needsReview": "Necesita revisión",
"securityConcern": "Aviso de seguridad"
"securityConcern": "Aviso de seguridad",
"motionSearch": {
"menuItem": "Búsqueda de movimiento",
"openMenu": "Opciones de cámara"
},
"motionPreviews": {
"menuItem": "Ver vistas previas de movimiento",
"title": "Vistas previas de movimiento: {{camera}}",
"mobileSettingsTitle": "Ajustes de vistas previas de movimiento",
"mobileSettingsDesc": "Ajusta la velocidad de reproducción y el atenuado, y elige una fecha para revisar clips solo de movimiento.",
"dim": "Atenuar",
"dimAria": "Ajustar intensidad de atenuado",
"dimDesc": "Aumenta el atenuado para mejorar la visibilidad de las áreas con movimiento.",
"speed": "Velocidad",
"speedAria": "Seleccionar velocidad de reproducción de las vistas previas",
"speedDesc": "Elige la velocidad a la que se reproducen los clips de vista previa.",
"back": "Atrás",
"empty": "No hay vistas previas disponibles",
"noPreview": "Vista previa no disponible",
"seekAria": "Mover el reproductor de {{camera}} a {{time}}",
"filter": "Filtrar",
"filterDesc": "Selecciona áreas para mostrar solo clips con movimiento en esas regiones.",
"filterClear": "Limpiar"
}
}

View File

@ -226,6 +226,10 @@
},
"more": {
"aria": "Más"
},
"debugReplay": {
"label": "Reproducción de depuración",
"aria": "Ver este objeto rastreado en la reproducción de depuración"
}
},
"dialog": {
@ -282,7 +286,10 @@
"zones": "Zonas",
"area": "Área",
"score": "Puntuación",
"ratio": "Ratio(proporción)"
"ratio": "Ratio(proporción)",
"computedScore": "Puntuación calculada",
"topScore": "Puntuación más alta",
"toggleAdvancedScores": "Alternar puntuaciones avanzadas"
},
"entered_zone": "{{label}} ha entrado en {{zones}}"
},

View File

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

View File

@ -30,7 +30,11 @@
"title": "Reconocimientos Recientes",
"aria": "Seleccionar reconocimientos recientes",
"empty": "No hay intentos recientes de reconocimiento facial",
"titleShort": "Reciente"
"titleShort": "Reciente",
"emptyNoLibrary": {
"title": "Subir una cara",
"description": "Debes añadir al menos una cara a la biblioteca para que el reconocimiento facial funcione."
}
},
"selectItem": "Seleccionar {{item}}",
"selectFace": "Seleccionar rostro",

View File

@ -69,7 +69,8 @@
},
"recording": {
"enable": "Habilitar grabación",
"disable": "Deshabilitar grabación"
"disable": "Deshabilitar grabación",
"disabledInConfig": "La grabación debe activarse primero en Ajustes para esta cámara."
},
"snapshots": {
"enable": "Habilitar capturas de pantalla",

View File

@ -1 +1,77 @@
{}
{
"documentTitle": "Búsqueda por movimiento - Frigate",
"title": "Búsqueda por movimiento",
"description": "Dibuja un polígono para definir la región de interés y especifica un intervalo de tiempo para buscar cambios de movimiento dentro de esa región.",
"selectCamera": "Búsqueda por movimiento se está cargando",
"startSearch": "Iniciar búsqueda",
"searchStarted": "Búsqueda iniciada",
"searchCancelled": "Búsqueda cancelada",
"cancelSearch": "Cancelar",
"searching": "Búsqueda en progreso.",
"searchComplete": "Búsqueda completada",
"noResultsYet": "Ejecuta una búsqueda para encontrar cambios de movimiento en la región seleccionada",
"noChangesFound": "No se detectaron cambios de píxeles en la región seleccionada",
"changesFound_one": "Encontrado {{count}} cambio de movimiento",
"changesFound_many": "Encontrados {{count}} cambios de movimiento",
"changesFound_other": "Encontrados {{count}} cambios de movimiento",
"framesProcessed": "{{count}} fotogramas procesados",
"jumpToTime": "Saltar a este tiempo",
"results": "Resultados",
"showSegmentHeatmap": "Mapa de calor",
"newSearch": "Nueva búsqueda",
"clearResults": "Borrar resultados",
"clearROI": "Borrar polígono",
"polygonControls": {
"points_one": "{{count}} punto",
"points_many": "{{count}} puntos",
"points_other": "{{count}} puntos",
"undo": "Deshacer el último punto",
"reset": "Restablecer polígono"
},
"motionHeatmapLabel": "Mapa de calor de movimiento",
"dialog": {
"title": "Búsqueda de movimiento",
"cameraLabel": "Cámara",
"previewAlt": "Vista previa de la cámara {{camera}}"
},
"timeRange": {
"title": "Rango de búsqueda",
"start": "Hora de inicio",
"end": "Hora de finalización"
},
"settings": {
"title": "Ajustes de búsqueda",
"parallelMode": "Modo paralelo",
"parallelModeDesc": "Analiza varios segmentos de grabación al mismo tiempo (más rápido, pero consume significativamente más CPU)",
"threshold": "Umbral de sensibilidad",
"thresholdDesc": "Los valores más bajos detectan cambios más pequeños (1-255)",
"minArea": "Área mínima de cambio",
"minAreaDesc": "Porcentaje mínimo de la región de interés que debe cambiar para considerarse significativo",
"frameSkip": "Salto de fotogramas",
"frameSkipDesc": "Procesa cada N fotogramas. Establécelo según la tasa de FPS de tu cámara para procesar un fotograma por segundo (p. ej., 5 para una cámara de 5 FPS, 30 para una cámara de 30 FPS). Los valores más altos serán más rápidos, pero pueden omitir eventos de movimiento breves.",
"maxResults": "Resultados máximos",
"maxResultsDesc": "Detener después de esta cantidad de marcas de tiempo coincidentes"
},
"errors": {
"noCamera": "Selecciona una cámara",
"noROI": "Dibuja una región de interés",
"noTimeRange": "Selecciona un rango de tiempo",
"invalidTimeRange": "La hora de fin debe ser posterior a la hora de inicio",
"searchFailed": "La búsqueda falló: {{message}}",
"polygonTooSmall": "El polígono debe tener al menos 3 puntos",
"unknown": "Error desconocido"
},
"changePercentage": "{{percentage}}% cambiado",
"metrics": {
"title": "Métricas de búsqueda",
"segmentsScanned": "Segmentos analizados",
"segmentsProcessed": "Procesado",
"segmentsSkippedInactive": "Omitido (sin actividad)",
"segmentsSkippedHeatmap": "Omitido (sin superposición de ROI)",
"fallbackFullRange": "Análisis completo de respaldo",
"framesDecoded": "Fotogramas decodificados",
"wallTime": "Tiempo de búsqueda",
"segmentErrors": "Errores de segmento",
"seconds": "{{seconds}} s"
}
}

View File

@ -1 +1,59 @@
{}
{
"title": "Depuración de reproducción",
"description": "Reproducir grabaciones de cámara para depuración. La lista de objetos muestra un resumen con retraso temporal de los objetos detectados y la pestaña Mensajes muestra un flujo de los mensajes internos de Frigate de la grabación reproducida.",
"websocket_messages": "Mensajes",
"dialog": {
"title": "Iniciar depuración de reproducción",
"description": "Crea una cámara de reproducción temporal que reproduzca en bucle imágenes históricas para depurar problemas de detección y seguimiento de objetos. La cámara de reproducción tendrá la misma configuración de detección que la cámara de origen. Elige un intervalo de tiempo para comenzar.",
"camera": "Cámara de origen",
"timeRange": "Intervalo de tiempo",
"preset": {
"1m": "Último 1 minuto",
"5m": "Últimos 5 minutos",
"timeline": "Desde la línea de tiempo",
"custom": "Personalizado"
},
"startButton": "Iniciar reproducción",
"selectFromTimeline": "Seleccionar",
"starting": "Iniciando reproducción...",
"startLabel": "Iniciar",
"endLabel": "Fin",
"toast": {
"error": "No se pudo iniciar la reproducción de depuración: {{error}}",
"alreadyActive": "Ya hay una sesión de reproducción activa",
"stopError": "No se pudo detener la reproducción de depuración: {{error}}",
"goToReplay": "Ir a la reproducción"
}
},
"page": {
"noSession": "No hay ninguna sesión activa de reproducción de depuración",
"noSessionDesc": "Inicia una reproducción de depuración desde la vista Historial haciendo clic en el botón Acciones de la barra de herramientas y seleccionando Reproducción de depuración.",
"goToRecordings": "Ir al historial",
"preparingClip": "Preparando clip…",
"preparingClipDesc": "Frigate está uniendo las grabaciones del intervalo de tiempo seleccionado. Esto puede tardar un minuto en intervalos más largos.",
"startingCamera": "Iniciando reproducción de depuración…",
"startError": {
"title": "No se pudo iniciar la reproducción de depuración",
"back": "Volver al historial"
},
"sourceCamera": "Cámara de origen",
"replayCamera": "Cámara de reproducción",
"initializingReplay": "Inicializando reproducción de depuración…",
"stoppingReplay": "Deteniendo repetición de depuración...",
"stopReplay": "Detener repetición",
"confirmStop": {
"title": "¿Detener repetición de depuración?",
"description": "Esto detendrá la sesión y eliminará todos los datos temporales. ¿Estás seguro?",
"confirm": "Detener repetición",
"cancel": "Cancelar"
},
"activity": "Actividad",
"objects": "Lista de objetos",
"audioDetections": "Detecciones de audio",
"noActivity": "No se detectó actividad",
"activeTracking": "Seguimiento activo",
"noActiveTracking": "No hay seguimiento activo",
"configuration": "Configuración",
"configurationDesc": "Ajusta con precisión la detección de movimiento y los ajustes de seguimiento de objetos para la cámara de repetición de depuración. No se guardará ningún cambio en el archivo de configuración de Frigate."
}
}

View File

@ -16,7 +16,8 @@
"globalConfig": "Configuración Global - Frigate",
"cameraConfig": "Configuración de Cámara - Frigate",
"maintenance": "Mantenimiento - Frigate",
"profiles": "Perfiles - Frigate"
"profiles": "Perfiles - Frigate",
"detectorsAndModel": "Detectores y modelo - Frigate"
},
"menu": {
"cameras": "Configuración de Cámara",
@ -42,7 +43,7 @@
"globalDetect": "Detección de Objetos",
"globalRecording": "Grabación",
"globalSnapshots": "Instantáneas",
"globalFfmpeg": "FFmpeg",
"globalFfmpeg": "arguments,Introduce",
"globalMotion": "Detección de Movimiento",
"globalObjects": "Objetos",
"globalReview": "Revisión",
@ -50,7 +51,49 @@
"globalLivePlayback": "Reproducción en Vivo",
"globalTimestampStyle": "Estilo de Marca de Tiempo",
"systemDatabase": "Base de Datos",
"systemAuthentication": "Autenticación"
"systemAuthentication": "Autenticación",
"systemTls": "TLS",
"systemNetworking": "Red",
"systemProxy": "Proxy",
"systemUi": "Interfaz",
"systemLogging": "Registro",
"systemEnvironmentVariables": "Variables de entorno",
"systemTelemetry": "Telemetría",
"systemBirdseye": "Birdseye",
"systemFfmpeg": "FFmpeg",
"systemDetectorHardware": "Hardware del detector",
"systemDetectionModel": "Modelo de detección",
"systemMqtt": "MQTT",
"systemGo2rtcStreams": "Flujos go2rtc",
"integrationSemanticSearch": "Búsqueda semántica",
"integrationGenerativeAi": "IA generativa",
"integrationFaceRecognition": "Reconocimiento facial",
"integrationLpr": "Reconocimiento de matrículas",
"integrationObjectClassification": "Clasificación de objetos",
"integrationAudioTranscription": "Transcripción de audio",
"cameraDetect": "Detección de objetos",
"cameraFfmpeg": "FFmpeg",
"cameraRecording": "Grabación",
"cameraSnapshots": "Instantáneas",
"cameraMotion": "Detección de movimiento",
"cameraObjects": "Objetos",
"cameraConfigReview": "Revisión",
"cameraAudioEvents": "Detección de audio",
"cameraAudioTranscription": "Transcripción de audio",
"cameraNotifications": "Notificaciones",
"cameraLivePlayback": "Reproducción en directo",
"cameraBirdseye": "Birdseye",
"cameraFaceRecognition": "Reconocimiento facial",
"cameraLpr": "Reconocimiento de matrículas",
"cameraMqttConfig": "MQTT",
"cameraOnvif": "ONVIF",
"cameraUi": "Interfaz de cámara",
"cameraTimestampStyle": "Estilo de marca de tiempo",
"cameraMqtt": "MQTT de cámara",
"maintenance": "Mantenimiento",
"mediaSync": "Sincronización de medios",
"regionGrid": "Cuadrícula de regiones",
"systemDetectorsAndModel": "Detectores y modelo"
},
"dialog": {
"unsavedChanges": {
@ -59,7 +102,7 @@
}
},
"cameraSetting": {
"camera": "Cámara",
"camera": "Overrides,Sobrescrituras",
"noCamera": "Sin cámara"
},
"general": {
@ -303,6 +346,10 @@
"zone": "zona",
"motion_mask": "máscara de movimiento",
"object_mask": "máscara de objeto"
},
"revertOverride": {
"title": "Revertir a la configuración base",
"desc": "Esto eliminará la sobrescritura del perfil para {{type}} <em>{{name}}</em> y revertirá a la configuración base."
}
},
"speed": {
@ -314,6 +361,12 @@
"error": {
"mustNotBeEmpty": "El nombre no puede estar vacío."
}
},
"id": {
"error": {
"mustNotBeEmpty": "El ID no puede estar vacío.",
"alreadyExists": "Ya existe una máscara con este ID para esta cámara."
}
}
},
"zones": {
@ -370,7 +423,8 @@
"success": "La zona ({{zoneName}}) ha sido guardada."
},
"enabled": {
"description": "Indica si esta zona está activa y habilitada en la configuración. Si está deshabilitado, no puede ser habilitado por MQTT. Las zonas deshabilitadas se ignoran durante la ejecución."
"description": "Indica si esta zona está activa y habilitada en la configuración. Si está deshabilitado, no puede ser habilitado por MQTT. Las zonas deshabilitadas se ignoran durante la ejecución.",
"title": "Habilitado"
}
},
"toast": {
@ -411,7 +465,13 @@
"documentTitle": "Editar Máscara de Movimiento - Frigate",
"point_one": "{{count}} punto",
"point_many": "{{count}} puntos",
"point_other": "{{count}} puntos"
"point_other": "{{count}} puntos",
"defaultName": "Máscara de movimiento {{number}}",
"name": {
"title": "Nombre",
"description": "Un nombre descriptivo opcional para esta máscara de movimiento.",
"placeholder": "Introduce un nombre..."
}
},
"objectMasks": {
"label": "Máscaras de Objetos",
@ -437,11 +497,26 @@
"point_one": "{{count}} punto",
"point_many": "{{count}} puntos",
"point_other": "{{count}} puntos",
"clickDrawPolygon": "Haz clic para dibujar un polígono en la imagen."
"clickDrawPolygon": "Haz clic para dibujar un polígono en la imagen.",
"name": {
"title": "Nombre",
"description": "Un nombre descriptivo opcional para esta máscara de objeto.",
"placeholder": "Introduce un nombre..."
}
},
"restart_required": "Es necesario reiniciar (se han cambiado las máscaras/zonas)",
"motionMaskLabel": "Máscara de movimiento {{number}}",
"objectMaskLabel": "Máscara de objeto {{number}}"
"objectMaskLabel": "Máscara de objeto {{number}}",
"disabledInConfig": "El elemento está deshabilitado en el archivo de configuración",
"addDisabledProfile": "Añádelo primero a la configuración base y luego sobrescríbelo en el perfil",
"profileBase": "(base)",
"profileOverride": "(sobrescritura)",
"masks": {
"enabled": {
"title": "Habilitado",
"description": "Indica si esta máscara está habilitada en el archivo de configuración. Si está deshabilitada, no se puede habilitar mediante MQTT. Las máscaras deshabilitadas se ignoran en tiempo de ejecución."
}
}
},
"motionDetectionTuner": {
"title": "Sintonizador de Detección de Movimiento",
@ -714,7 +789,7 @@
"snapshots": "Instantáneas",
"cleanCopySnapshots": "<code>clean_copy</code> Instantáneas"
},
"desc": "Enviar a Frigate+ requiere que tanto las capturas instantáneas como las capturas <code>clean_copy</code> estén habilitadas en tu configuración.",
"desc": "Enviar a Frigate+ requiere que las instantáneas estén habilitadas en tu configuración.",
"cleanCopyWarning": "Algunas cámaras tienen las instantáneas deshabilitadas"
},
"modelInfo": {
@ -726,13 +801,21 @@
"cameras": "Cámaras",
"loading": "Cargando información del modelo…",
"error": "No se pudo cargar la información del modelo",
"availableModels": "Modelos disponibles",
"availableModels": "Modelos de Frigate+ disponibles",
"loadingAvailableModels": "Cargando modelos disponibles…",
"modelSelect": "Tus modelos disponibles en Frigate+ se pueden seleccionar aquí. Ten en cuenta que solo se pueden seleccionar modelos compatibles con tu configuración actual de detectores.",
"trainDate": "Fecha de entrenamiento",
"plusModelType": {
"baseModel": "Modelo Base",
"userModel": "Ajustado Finamente"
},
"noModelLoaded": "Actualmente no hay ningún modelo de Frigate+ cargado.",
"selectModel": "Selecciona un modelo",
"noModelsAvailable": "No hay modelos disponibles",
"filter": {
"ariaLabel": "Filtrar modelos por tipo",
"baseModels": "Modelos base",
"fineTunedModels": "Modelos ajustados"
}
},
"toast": {
@ -741,7 +824,14 @@
},
"restart_required": "Es necesario reiniciar (se ha cambiado el modelo Frigate+)",
"unsavedChanges": "Cambios en la configuración de Frigate+ no guardados",
"description": "Frigate+ es un servicio de suscripción que proporciona acceso a funciones y capacidades adicionales para su instancia de Frigate, incluida la posibilidad de utilizar modelos de detección de objetos personalizados entrenados con sus propios datos. Puede gestionar la configuración de sus modelos de Frigate+ aquí."
"description": "Frigate+ es un servicio de suscripción que proporciona acceso a funciones y capacidades adicionales para su instancia de Frigate, incluida la posibilidad de utilizar modelos de detección de objetos personalizados entrenados con sus propios datos. Puede gestionar la configuración de sus modelos de Frigate+ aquí.",
"cardTitles": {
"api": "API",
"currentModel": "Modelo actual",
"otherModels": "Otros modelos",
"configuration": "Configuración"
},
"changeInDetectorsAndModel": "Cambiar modelo"
},
"enrichments": {
"title": "Configuración de Enriquecimientos",
@ -767,11 +857,11 @@
"modelSize": {
"label": "Tamaño del Modelo",
"small": {
"title": "pequeño",
"title": "size",
"desc": "Usar la opción <em>small</em> emplea una versión cuantizada del modelo que consume menos memoria RAM y se ejecuta más rápido en la CPU, con una diferencia muy pequeña o casi imperceptible en la calidad de las representaciones (embeddings)."
},
"large": {
"title": "grande",
"title": "model",
"desc": "Usar la opción <em>large</em> emplea el modelo completo de Jina y se ejecutará automáticamente en la GPU, si está disponible."
},
"desc": "Tamaño del modelo usado para la búsqueda semántica."
@ -1157,7 +1247,8 @@
},
"hikvision": {
"substreamWarning": "La subtransmisión 1 está limitada a una resolución baja. Muchas cámaras Hikvision admiten subtransmisiones adicionales que deben habilitarse en la configuración de la cámara. Se recomienda comprobar y utilizar dichas transmisiones si están disponibles."
}
},
"resolutionUnknown": "No se pudo detectar la resolución de este flujo. Debes establecer manualmente la resolución de detección en Ajustes o en tu configuración."
}
},
"title": "Añadir cámara",
@ -1192,7 +1283,20 @@
"streams": {
"title": "Habilitar/deshabilitar cámaras",
"desc": "Desactiva temporalmente una cámara hasta que Frigate se reinicie. Desactivar una cámara detiene por completo el procesamiento de las transmisiones de Frigate. La detección, la grabación y la depuración no estarán disponibles.<br /> <em>Nota: Esto no desactiva las retransmisiones de go2rtc.</em>",
"enableDesc": "Deshabilita temporalmente una cámara habilitada hasta que Frigate se reinicie. Deshabilitar una cámara detiene por completo el procesamiento de las transmisiones de esa cámara por parte de Frigate. La detección, la grabación y la depuración no estarán disponibles.<br /> <em>Nota: Esto no deshabilita las retransmisiones de go2rtc.</em>"
"enableDesc": "Deshabilita temporalmente una cámara habilitada hasta que Frigate se reinicie. Deshabilitar una cámara detiene completamente el procesamiento de los flujos de esa cámara por parte de Frigate. La detección, la grabación y la depuración no estarán disponibles. Nota: Esto no deshabilita las retransmisiones de go2rtc.Arrastra el controlador para reordenar las cámaras tal y como aparecen en la interfaz. El orden de las cámaras habilitadas se reflejará en toda la interfaz, incluido el panel en directo y los menús desplegables de selección de cámaras.",
"enableLabel": "Cámaras habilitadas",
"disableLabel": "Cámaras deshabilitadas",
"disableDesc": "Habilita una cámara que actualmente no está visible en la interfaz y está deshabilitada en la configuración. Es necesario reiniciar Frigate después de habilitarla.",
"enableSuccess": "{{cameraName}} se ha habilitado en la configuración. Reinicia Frigate para aplicar los cambios.",
"friendlyName": {
"edit": "Editar nombre visible de la cámara",
"title": "Editar nombre visible",
"description": "Establece el nombre descriptivo que se mostrará para esta cámara en toda la interfaz de Frigate. Déjalo en blanco para usar el ID de la cámara.",
"rename": "Renombrar"
},
"reorderHandle": "Arrastrar para reordenar",
"saving": "Guardando…",
"saved": "Guardado"
},
"cameraConfig": {
"add": "Añadir cámara",
@ -1224,8 +1328,34 @@
}
},
"deleteCameraDialog": {
"description": "Eliminar una cámara borrará permanentemente todas las grabaciones, los objetos rastreados y la configuración de esa cámara. Es posible que sea necesario eliminar manualmente cualquier transmisión go2rtc asociada a esta cámara."
}
"description": "Eliminar una cámara borrará permanentemente todas las grabaciones, los objetos rastreados y la configuración de esa cámara. Es posible que sea necesario eliminar manualmente cualquier transmisión go2rtc asociada a esta cámara.",
"title": "Eliminar cámara",
"selectPlaceholder": "Elegir cámara...",
"confirmTitle": "¿Estás seguro?",
"confirmWarning": "Eliminar <strong>{{cameraName}}</strong> no se puede deshacer.",
"deleteExports": "Eliminar también las exportaciones de esta cámara",
"confirmButton": "Eliminar permanentemente",
"success": "La cámara {{cameraName}} se ha eliminado correctamente",
"error": "No se pudo eliminar la cámara {{cameraName}}"
},
"deleteCamera": "Eliminar cámara",
"profiles": {
"title": "Sobrescrituras de cámaras del perfil",
"selectLabel": "Seleccionar perfil",
"description": "Configura qué cámaras se habilitan o deshabilitan cuando se activa un perfil. Las cámaras configuradas como \"Heredar\" conservan su estado base habilitado.",
"inherit": "Heredar",
"enabled": "Habilitado",
"disabled": "Deshabilitado"
},
"cameraType": {
"title": "Tipo de cámara",
"label": "Tipo de cámara",
"description": "Establece el tipo de cada cámara. Las cámaras LPR dedicadas son cámaras de un solo propósito con un zoom óptico potente para capturar matrículas de vehículos lejanos. La mayoría de cámaras deberían usar el tipo de cámara normal salvo que la cámara esté específicamente destinada a LPR y tenga una vista muy enfocada a matrículas.",
"normal": "Normal",
"dedicatedLpr": "LPR dedicada",
"saveSuccess": "Se ha actualizado el tipo de cámara de {{cameraName}}. Reinicia Frigate para aplicar los cambios."
},
"description": "Añade, edita y elimina cámaras, controla qué cámaras están habilitadas y configura sobrescrituras por perfil y tipo de cámara. Para configurar flujos, detección, movimiento y otros ajustes específicos de cámara, selecciona la sección correspondiente dentro de Configuración de cámara."
},
"cameraReview": {
"title": "Configuración de revisión de la cámara",
@ -1268,34 +1398,295 @@
"overriddenGlobal": "Sobrescrito (Global)",
"overriddenBaseConfigTooltip": "El perfil {{profile}} sobrescribe los ajustes de configuración de esta sección",
"overriddenGlobalTooltip": "Esta cámara sobrescribe los ajustes de configuración global en esta sección",
"overriddenBaseConfig": "Sobrescrito (Configuración Base)"
"overriddenBaseConfig": "Sobrescrito (Configuración Base)",
"overriddenInCameras": {
"label_one": "Sobrescrito en {{count}} cámara",
"label_many": "Sobrescrito en {{count}} cámaras",
"label_other": "Sobrescrito en {{count}} cámaras",
"tooltip_one": "{{count}} cámaras sobrescriben los valores de esta sección. Haz clic para ver los detalles.",
"tooltip_many": "{{count}} cámaras sobrescriben los valores de esta sección. Haz clic para ver los detalles.",
"tooltip_other": "{{count}} cámaras sobrescriben los valores de esta sección. Haz clic para ver los detalles.",
"heading_one": "This global section has fields that are overridden in {{count}} camera.",
"heading_many": "Esta sección global tiene campos que están sobrescritos en {{count}} cámaras.",
"heading_other": "Esta sección global tiene campos que están sobrescritos en {{count}} cámaras.",
"othersField_one": "{{count}} más",
"othersField_many": "{{count}} más",
"othersField_other": "{{count}} más",
"profilePrefix": "Perfil {{profile}}: {{fields}}"
},
"overriddenGlobalHeading_one": "Esta cámara sobrescribe {{count}} campo de la configuración global:",
"overriddenGlobalHeading_many": "Esta cámara sobrescribe {{count}} campos de la configuración global:",
"overriddenGlobalHeading_other": "Esta cámara sobrescribe {{count}} campos de la configuración global:",
"overriddenGlobalNoDeltas": "Esta cámara sobrescribe la configuración global, pero no hay diferencias en los valores de los campos.",
"overriddenBaseConfigHeading_one": "El perfil {{profile}} sobrescribe {{count}} campo de la configuración base:",
"overriddenBaseConfigHeading_many": "El perfil {{profile}} sobrescribe {{count}} campos de la configuración base:",
"overriddenBaseConfigHeading_other": "El perfil {{profile}} sobrescribe {{count}} campos de la configuración base:",
"overriddenBaseConfigNoDeltas": "El perfil {{profile}} sobrescribe esta sección, pero no hay diferencias en los valores de los campos respecto a la configuración base."
},
"onvif": {
"profileLoading": "Cargando perfiles..."
"profileLoading": "Cargando perfiles...",
"profileAuto": "Auto",
"autotracking": {
"zooming": {
"disabled": "Deshabilitado",
"absolute": "Absoluto",
"relative": "Relativo"
}
}
},
"maintenance": {
"sync": {
"verboseDesc": "Escribe una lista completa de archivos huérfanos en el disco para su revisión.",
"verbose": "Detallado",
"desc": "Frigate limpiará periódicamente los archivos multimedia según un cronograma regular, de acuerdo con su configuración de retención. Es normal ver algunos archivos huérfanos mientras Frigate se ejecuta. Utilice esta función para eliminar del disco los archivos multimedia huérfanos que ya no se referencian en la base de datos.",
"forceDesc": "Omitir el umbral de seguridad y completar la sincronización incluso si se eliminara más del 50% de los archivos."
"forceDesc": "Omitir el umbral de seguridad y completar la sincronización incluso si se eliminara más del 50% de los archivos.",
"title": "Sincronización de medios",
"started": "Sincronización de medios iniciada.",
"alreadyRunning": "Ya hay una tarea de sincronización en ejecución",
"error": "No se pudo iniciar la sincronización",
"currentStatus": "Estado",
"jobId": "ID de tarea",
"startTime": "Hora de inicio",
"endTime": "Hora de finalización",
"statusLabel": "Estado",
"results": "Resultados",
"errorLabel": "Error",
"mediaTypes": "Tipos de medios",
"allMedia": "Todos los medios",
"dryRun": "Simulación",
"dryRunEnabled": "No se eliminará ningún archivo",
"dryRunDisabled": "Se eliminarán archivos",
"force": "Forzar",
"running": "Sincronización en curso...",
"start": "Iniciar sincronización",
"inProgress": "La sincronización está en curso. Esta página está deshabilitada.",
"status": {
"queued": "En cola",
"running": "En ejecución",
"completed": "Completado",
"failed": "Fallido",
"notRunning": "No está en ejecución"
},
"resultsFields": {
"filesChecked": "Archivos comprobados",
"orphansFound": "Huérfanos encontrados",
"orphansDeleted": "Huérfanos eliminados",
"aborted": "Abortado. La eliminación superaría el umbral de seguridad.",
"error": "Error",
"totals": "Totales"
},
"event_snapshots": "Instantáneas de objetos rastreados",
"event_thumbnails": "Miniaturas de objetos rastreados",
"review_thumbnails": "Miniaturas de revisión",
"previews": "Vistas previas",
"exports": "Exportaciones",
"recordings": "Grabaciones"
},
"regionGrid": {
"clearConfirmDesc": "No se recomienda borrar la cuadrícula de la región a menos que haya cambiado recientemente el tamaño del modelo de su detector o la posición física de su cámara y esté experimentando problemas de seguimiento de objetos. La cuadrícula se reconstruirá automáticamente con el tiempo a medida que se realice el seguimiento de los objetos. Es necesario reiniciar Frigate para que los cambios surtan efecto.",
"desc": "La cuadrícula de regiones es una optimización que aprende dónde suelen aparecer los objetos de diferentes tamaños en el campo de visión de cada cámara. Frigate utiliza estos datos para dimensionar de forma eficiente las regiones de detección. La cuadrícula se construye automáticamente a lo largo del tiempo a partir de los datos de los objetos rastreados."
}
"desc": "La cuadrícula de regiones es una optimización que aprende dónde suelen aparecer los objetos de diferentes tamaños en el campo de visión de cada cámara. Frigate utiliza estos datos para dimensionar de forma eficiente las regiones de detección. La cuadrícula se construye automáticamente a lo largo del tiempo a partir de los datos de los objetos rastreados.",
"title": "Cuadrícula de regiones",
"clear": "Borrar cuadrícula de regiones",
"clearConfirmTitle": "Borrar cuadrícula de regiones",
"clearSuccess": "Cuadrícula de regiones borrada correctamente",
"clearError": "No se pudo borrar la cuadrícula de regiones",
"restartRequired": "Es necesario reiniciar para que los cambios de la cuadrícula de regiones surtan efecto"
},
"title": "Mantenimiento"
},
"configForm": {
"camera": {
"noCameras": "No hay cámaras disponibles",
"description": "Estos ajustes se aplican únicamente a esta cámara y anulan los ajustes globales."
"description": "Estos ajustes se aplican únicamente a esta cámara y anulan los ajustes globales.",
"title": "Ajustes de cámara"
},
"genaiModel": {
"noModels": "No hay modelos disponibles"
"noModels": "No hay modelos disponibles",
"placeholder": "Seleccionar modelo…",
"search": "Buscar modelos…"
},
"global": {
"description": "Estos ajustes se aplican a todas las cámaras, a menos que se anulen en los ajustes específicos de cada cámara."
}
"description": "Estos ajustes se aplican a todas las cámaras, a menos que se anulen en los ajustes específicos de cada cámara.",
"title": "Ajustes globales"
},
"sections": {
"go2rtc": "streams",
"detect": "Detección",
"record": "Grabación",
"snapshots": "Instantáneas",
"motion": "Movimiento",
"objects": "Objetos",
"review": "Revisión",
"audio": "Audio",
"notifications": "Notificaciones",
"live": "Vista en directo",
"timestamp_style": "Marcas de tiempo",
"mqtt": "MQTT",
"database": "Base de datos",
"telemetry": "Telemetría",
"auth": "Autenticación",
"tls": "TLS",
"proxy": "Proxy",
"ffmpeg": "FFmpeg",
"detectors": "Detectores",
"model": "Modelo",
"semantic_search": "Búsqueda semántica",
"genai": "GenAI",
"face_recognition": "Reconocimiento facial",
"lpr": "Reconocimiento de matrículas",
"birdseye": "Birdseye",
"masksAndZones": "Máscaras / zonas"
},
"advancedSettingsCount": "Ajustes avanzados ({{count}})",
"advancedCount": "Avanzado ({{count}})",
"showAdvanced": "Mostrar ajustes avanzados",
"tabs": {
"sharedDefaults": "Valores predeterminados compartidos",
"system": "Sistema",
"integrations": "Integraciones"
},
"additionalProperties": {
"keyLabel": "Clave",
"valueLabel": "Valor",
"keyPlaceholder": "Nueva clave",
"remove": "Eliminar"
},
"knownPlates": {
"namePlaceholder": "p. ej., Coche de mi mujer",
"platePlaceholder": "Número de matrícula o regex"
},
"timezone": {
"defaultOption": "Usar zona horaria del navegador"
},
"roleMap": {
"empty": "No hay asignaciones de roles",
"roleLabel": "Rol",
"groupsLabel": "Grupos",
"addMapping": "Añadir asignación de rol",
"remove": "Eliminar"
},
"ffmpegArgs": {
"preset": "Preajuste",
"manual": "Argumentos manuales",
"inherit": "Heredar del ajuste de cámara",
"none": "Ninguno",
"useGlobalSetting": "Heredar del ajuste global",
"selectPreset": "Seleccionar preajuste",
"manualPlaceholder": "Introduce argumentos de FFmpeg",
"presetLabels": {
"preset-rpi-64-h264": "Raspberry Pi (H.264)",
"preset-rpi-64-h265": "Raspberry Pi (H.265)",
"preset-vaapi": "VAAPI (GPU Intel/AMD)",
"preset-intel-qsv-h264": "Intel QuickSync (H.264)",
"preset-intel-qsv-h265": "Intel QuickSync (H.265)",
"preset-nvidia": "GPU NVIDIA",
"preset-jetson-h264": "NVIDIA Jetson (H.264)",
"preset-jetson-h265": "NVIDIA Jetson (H.265)",
"preset-rkmpp": "Rockchip RKMPP",
"preset-http-jpeg-generic": "HTTP JPEG (genérico)",
"preset-http-mjpeg-generic": "HTTP MJPEG (genérico)",
"preset-http-reolink": "HTTP - Cámaras Reolink",
"preset-rtmp-generic": "RTMP (genérico)",
"preset-rtsp-generic": "RTSP (genérico)",
"preset-rtsp-restream": "RTSP - Retransmisión desde go2rtc",
"preset-rtsp-restream-low-latency": "RTSP - Retransmisión desde go2rtc (baja latencia)",
"preset-rtsp-udp": "RTSP - UDP",
"preset-rtsp-blue-iris": "RTSP - Blue Iris",
"preset-record-generic": "Grabación (genérica, sin audio)",
"preset-record-generic-audio-copy": "Grabación (genérica + copiar audio)",
"preset-record-generic-audio-aac": "Grabación (genérica + audio a AAC)",
"preset-record-mjpeg": "Grabación - Cámaras MJPEG",
"preset-record-jpeg": "Grabación - Cámaras JPEG",
"preset-record-ubiquiti": "Grabación - Cámaras Ubiquiti"
}
},
"cameraInputs": {
"itemTitle": "Flujo {{index}}"
},
"restartRequiredField": "Reinicio necesario",
"restartRequiredFooter": "Configuración modificada - reinicio necesario",
"detect": {
"title": "Ajustes de detección"
},
"detectors": {
"title": "Ajustes de detector",
"singleType": "Solo se permite un detector {{type}}.",
"keyRequired": "El nombre del detector es obligatorio.",
"keyDuplicate": "El nombre del detector ya existe.",
"noSchema": "No hay esquemas de detector disponibles.",
"none": "No hay instancias de detector configuradas.",
"add": "Añadir detector",
"addCustomKey": "Añadir clave personalizada"
},
"record": {
"title": "Ajustes de grabación"
},
"snapshots": {
"title": "Ajustes de instantáneas"
},
"motion": {
"title": "Ajustes de movimiento"
},
"objects": {
"title": "Ajustes de objetos"
},
"audioLabels": {
"summary": "{{count}} etiquetas de audio seleccionadas",
"empty": "No hay etiquetas de audio disponibles"
},
"objectLabels": {
"summary": "{{count}} tipos de objeto seleccionados",
"empty": "No hay etiquetas de objeto disponibles"
},
"reviewLabels": {
"summary": "{{count}} etiquetas seleccionadas",
"empty": "No hay etiquetas disponibles"
},
"filters": {
"objectFieldLabel": "{{field}} para {{label}}"
},
"zoneNames": {
"summary": "{{count}} seleccionados",
"empty": "No hay zonas disponibles"
},
"inputRoles": {
"summary": "{{count}} roles seleccionados",
"empty": "No hay roles disponibles",
"options": {
"detect": "Detectar",
"record": "Grabar",
"audio": "Audio"
}
},
"genaiRoles": {
"options": {
"embeddings": "Embedding",
"descriptions": "Descripciones",
"chat": "Chat"
}
},
"semanticSearchModel": {
"placeholder": "Seleccionar modelo…",
"builtIn": "Modelos integrados",
"genaiProviders": "Proveedores de GenAI"
},
"review": {
"title": "Ajustes de revisión"
},
"audio": {
"title": "Ajustes de audio"
},
"notifications": {
"title": "Ajustes de notificaciones"
},
"live": {
"title": "Ajustes de vista en directo"
},
"timestamp_style": {
"title": "Ajustes de marcas de tiempo"
},
"searchPlaceholder": "Buscar...",
"addCustomLabel": "Añadir etiqueta personalizada..."
},
"globalConfig": {
"title": "Configuración global",
@ -1330,7 +1721,10 @@
"saveAllPartial_one": "Se ha guardado {{successCount}} de {{totalCount}} sección. {{failCount}} ha fallado.",
"saveAllPartial_many": "Se han guardado {{successCount}} de {{totalCount}} secciones. {{failCount}} han fallado.",
"saveAllPartial_other": "Se han guardado {{successCount}} de {{totalCount}} secciones. {{failCount}} han fallado.",
"saveAllFailure": "Error al guardar todas las secciones."
"saveAllFailure": "Error al guardar todas las secciones.",
"saveAllSuccessRestartRequired_one": "La sección {{count}} se ha guardado correctamente. Reinicia Frigate para aplicar los cambios.",
"saveAllSuccessRestartRequired_many": "Las {{count}} secciones se han guardado correctamente. Reinicia Frigate para aplicar los cambios.",
"saveAllSuccessRestartRequired_other": "Las {{count}} secciones se han guardado correctamente. Reinicia Frigate para aplicar los cambios."
},
"profiles": {
"title": "Perfiles",
@ -1364,26 +1758,84 @@
"renameProfile": "Renombrar perfil",
"renameSuccess": "Perfil renombrado a '{{profile}}'",
"enabledDescription": "Los perfiles están habilitados. Cree un nuevo perfil a continuación, navegue a una sección de configuración de cámara para realizar sus cambios y guarde para que estos surtan efecto.",
"disabledDescription": "Los perfiles le permiten definir conjuntos con nombre de anulaciones de configuración de la cámara (por ejemplo: armado, fuera, noche) que pueden activarse bajo demanda."
"disabledDescription": "Los perfiles le permiten definir conjuntos con nombre de anulaciones de configuración de la cámara (por ejemplo: armado, fuera, noche) que pueden activarse bajo demanda.",
"deleteProfile": "Eliminar perfil",
"deleteProfileConfirm": "¿Eliminar el perfil \"{{profile}}\" de todas las cámaras? Esta acción no se puede deshacer.",
"deleteSuccess": "Perfil '{{profile}}' eliminado",
"createSuccess": "Perfil '{{profile}}' creado",
"removeOverride": "Eliminar sobrescritura de perfil",
"deleteSection": "Eliminar sobrescrituras de sección",
"deleteSectionConfirm": "¿Eliminar las sobrescrituras de {{section}} del perfil {{profile}} en {{camera}}?",
"deleteSectionSuccess": "Sobrescrituras de {{section}} eliminadas para {{profile}}",
"enableSwitch": "Habilitar perfiles"
},
"go2rtcStreams": {
"renameStreamDesc": "Introduce un nuevo nombre para esta transmisión. Cambiar el nombre de una transmisión puede provocar fallos en las cámaras u otras transmisiones que hagan referencia a ella por su nombre.",
"addStreamDesc": "Introduce un nombre para la nueva transmisión. Este nombre se utilizará para hacer referencia a la transmisión en la configuración de su cámara.",
"description": "Gestione las configuraciones de transmisión de go2rtc para la retransmisión de cámaras. Cada transmisión tiene un nombre y una o más URL de origen.",
"deleteStreamConfirm": "¿Está seguro de que desea eliminar la transmisión \"{{streamName}}\"? Las cámaras que hagan referencia a esta transmisión podrían dejar de funcionar."
"deleteStreamConfirm": "¿Está seguro de que desea eliminar la transmisión \"{{streamName}}\"? Las cámaras que hagan referencia a esta transmisión podrían dejar de funcionar.",
"title": "Flujos go2rtc",
"addStream": "Añadir flujo",
"addUrl": "Añadir URL",
"streamName": "Nombre del flujo",
"streamNamePlaceholder": "p. ej., puerta_principal",
"streamUrlPlaceholder": "p. ej., rtsp://usuario:contraseña@192.168.1.100/stream",
"deleteStream": "Eliminar flujo",
"noStreams": "No hay flujos go2rtc configurados. Añade un flujo para empezar.",
"validation": {
"nameRequired": "El nombre del flujo es obligatorio",
"nameDuplicate": "Ya existe un flujo con este nombre",
"nameInvalid": "El nombre del flujo solo puede contener letras, números, guiones bajos y guiones",
"urlRequired": "Se requiere al menos una URL"
},
"renameStream": "Renombrar flujo",
"newStreamName": "Nuevo nombre del flujo",
"ffmpeg": {
"useFfmpegModule": "Usar modo de compatibilidad (ffmpeg)",
"video": "Vídeo",
"audio": "Audio",
"hardware": "Aceleración por hardware",
"videoCopy": "Copiar",
"videoH264": "Transcodificar a H.264",
"videoH265": "Transcodificar a H.265",
"videoExclude": "Excluir",
"audioCopy": "Copiar",
"audioAac": "Transcodificar a AAC",
"audioOpus": "Transcodificar a Opus",
"audioPcmu": "Transcodificar a PCM μ-law",
"audioPcma": "Transcodificar a PCM A-law",
"audioPcm": "Transcodificar a PCM",
"audioMp3": "Transcodificar a MP3",
"audioExclude": "Excluir",
"hardwareNone": "Sin aceleración por hardware",
"hardwareAuto": "Automático (recomendado)",
"hardwareVaapi": "VAAPI",
"hardwareCuda": "CUDA",
"hardwareV4l2m2m": "V4L2 M2M",
"hardwareDxva2": "DXVA2",
"hardwareVideotoolbox": "VideoToolbox",
"addVideoCodec": "Añadir códec de vídeo",
"addAudioCodec": "Añadir códec de audio",
"removeCodec": "Eliminar códec"
},
"streamNumber": "Flujo {{index}}"
},
"configMessages": {
"birdseye": {
"objectsModeDetectDisabled": "Birdseye está configurado en modo 'objects', pero la detección de objetos está desactivada para esta cámara. La cámara no aparecerá en Birdseye."
},
"lpr": {
"globalDisabled": "El reconocimiento de matrículas no está habilitado a nivel global. Habilítelo en la configuración global para que funcione el reconocimiento de matrículas a nivel de cámara."
"globalDisabled": "El reconocimiento de matrículas no está habilitado a nivel global. Habilítelo en la configuración global para que funcione el reconocimiento de matrículas a nivel de cámara.",
"vehicleNotTracked": "El reconocimiento de matrículas requiere rastrear 'car' o 'motorcycle'. Habilita 'car' o 'motorcycle' en Objetos para esta cámara.",
"modelSizeLarge": "El modelo 'large' está optimizado para matrículas de varias líneas. El modelo 'small' ofrece mejor rendimiento que 'large' y debería usarse salvo que tu región use formatos de matrícula de varias líneas."
},
"audio": {
"noAudioRole": "Ninguna transmisión tiene definido el rol de audio. Debe habilitar el rol de audio para que funcione la detección de audio."
},
"faceRecognition": {
"personNotTracked": "El reconocimiento facial requiere que se realice el seguimiento del objeto 'person'. Asegúrese de que 'person' se encuentre en la lista de seguimiento de objetos."
"personNotTracked": "El reconocimiento facial requiere que se realice el seguimiento del objeto 'person'. Asegúrese de que 'person' se encuentre en la lista de seguimiento de objetos.",
"globalDisabled": "El enriquecimiento de reconocimiento facial debe estar habilitado para que las funciones de reconocimiento facial funcionen en esta cámara.",
"modelSizeLarge": "El modelo 'large' requiere una GPU o NPU para ofrecer un rendimiento razonable. Usa 'small' en sistemas solo con CPU."
},
"audioTranscription": {
"audioDetectionDisabled": "La detección de audio no está habilitada para esta cámara. La transcripción de audio requiere que la detección de audio esté activa."
@ -1392,17 +1844,165 @@
"detectDisabled": "La detección de objetos está desactivada. Las instantáneas se generan a partir de los objetos rastreados y no se crearán."
},
"detectors": {
"mixedTypes": "Todos los detectores deben ser del mismo tipo. Retire los detectores existentes para utilizar un tipo diferente."
"mixedTypes": "Todos los detectores deben ser del mismo tipo. Retire los detectores existentes para utilizar un tipo diferente.",
"mixedTypesSuggestion": "Todos los detectores deben usar el mismo tipo. Elimina los detectores existentes o selecciona {{type}}."
},
"review": {
"detectDisabled": "La detección de objetos está desactivada. Los elementos de revisión requieren objetos detectados para categorizar las alertas y detecciones."
"detectDisabled": "La detección de objetos está desactivada. Los elementos de revisión requieren objetos detectados para categorizar las alertas y detecciones.",
"recordDisabled": "La grabación está deshabilitada; no se generarán elementos de revisión.",
"allNonAlertDetections": "Toda la actividad que no sea de alerta se incluirá como detecciones.",
"genaiImageSourceRecordingsRecordDisabled": "El origen de imagen está establecido en 'recordings', pero la grabación está deshabilitada. Frigate usará imágenes de vista previa como alternativa."
},
"detect": {
"fpsGreaterThanFive": "No se recomienda establecer los FPS de detección por encima de 5. Valores más altos pueden causar problemas de rendimiento y no aportarán ningún beneficio.",
"disabled": "La detección de objetos está deshabilitada. Las instantáneas, los elementos de revisión y enriquecimientos como el reconocimiento facial, el reconocimiento de matrículas y la IA generativa no funcionarán."
},
"objects": {
"genaiNoDescriptionsProvider": "Debes configurar un proveedor GenAI con el rol 'descriptions' para que se generen descripciones."
},
"record": {
"noRecordRole": "Ningún flujo tiene definido el rol de grabación. La grabación no funcionará."
},
"semanticSearch": {
"jinav2SmallModelSize": "El tamaño 'small' con el modelo Jina V2 tiene un alto consumo de RAM y coste de inferencia. Se recomienda el modelo 'large' con una GPU dedicada."
}
},
"resetToDefaultDescription": "Esto restablecerá todos los ajustes de esta sección a sus valores predeterminados. Esta acción no se puede deshacer.",
"resetToGlobalDescription": "Esto restablecerá la configuración de esta sección a los valores predeterminados globales. Esta acción no se puede deshacer.",
"detectionModel": {
"plusActive": {
"description": "Esta instancia está ejecutando un modelo de Frigate+. Seleccione o cambie su modelo en la configuración de Frigate+."
"description": "Esta instancia está ejecutando un modelo de Frigate+. Seleccione o cambie su modelo en la configuración de Frigate+.",
"title": "Gestión de modelos de Frigate+",
"label": "Origen del modelo actual",
"goToFrigatePlus": "Ir a los ajustes de Frigate+",
"showModelForm": "Configurar un modelo manualmente"
}
},
"saveAllPreview": {
"profile": {
"label": "Override,Eliminar"
},
"title": "Cambios pendientes de guardar",
"triggerLabel": "Revisar cambios pendientes",
"empty": "No hay cambios pendientes.",
"scope": {
"label": "Ámbito",
"global": "Global",
"camera": "Cámara: {{cameraName}}"
},
"field": {
"label": "Campo"
},
"value": {
"label": "Nuevo valor",
"reset": "Restablecer"
}
},
"timestampPosition": {
"tl": "Arriba a la izquierda",
"tr": "Arriba a la derecha",
"bl": "Abajo a la izquierda",
"br": "Abajo a la derecha"
},
"unsavedChanges": "Tienes cambios sin guardar",
"confirmReset": "Confirmar restablecimiento",
"birdseye": {
"trackingMode": {
"objects": "Objetos",
"motion": "Movimiento",
"continuous": "Continuo"
},
"cameraOrder": {
"label": "Orden de cámaras",
"description": "Arrastra las cámaras para establecer su orden en el diseño de Birdseye.",
"reorderHandle": "Arrastrar para reordenar",
"saving": "Guardando…",
"saved": "Guardado"
}
},
"snapshot": {
"retainMode": {
"all": "Todo",
"motion": "Movimiento",
"active_objects": "Objetos activos"
}
},
"ui": {
"timeFormat": {
"browser": "Navegador",
"12hour": "12 horas",
"24hour": "24 horas"
},
"TimeOrDateStyle": {
"full": "Completo",
"long": "Largo",
"medium": "Medio",
"short": "Corto"
},
"unitSystem": {
"metric": "Métrico",
"imperial": "Imperial"
}
},
"review": {
"imageSource": {
"recordings": "Grabaciones",
"previews": "Vistas previas"
}
},
"logger": {
"logLevel": {
"debug": "Depuración",
"info": "Información",
"warning": "Advertencia",
"error": "Error",
"critical": "Crítico"
}
},
"modelSize": {
"small": "Pequeño",
"large": "Grande"
},
"retainMode": {
"all": "Todo",
"motion": "Movimiento",
"active_objects": "Objetos activos"
},
"previewQuality": {
"very_high": "Muy alto",
"high": "Alto",
"medium": "Medio",
"low": "Bajo",
"very_low": "Muy bajo"
},
"detectorsAndModel": {
"title": "Detectores y modelo",
"description": "Configura el backend del detector que ejecuta la detección de objetos y el modelo que utiliza. Los cambios se guardan juntos para que el detector y el modelo permanezcan sincronizados.",
"cardTitles": {
"detector": "Hardware del detector",
"model": "Modelo de detección"
},
"tabs": {
"plus": "Frigate+",
"custom": "Modelo personalizado"
},
"mismatch": {
"warning": "El modelo actual de Frigate+ “{{model}}” requiere el detector {{required}}. Selecciona un modelo compatible a continuación o cambia a Modelo personalizado antes de guardar."
},
"plusModel": {
"requiresDetector": "Requiere: {{detector}}",
"noModelSelected": "Selecciona un modelo de Frigate+"
},
"toast": {
"saveSuccess": "Los ajustes de detectores y modelo se han guardado. Reinicia Frigate para aplicar los cambios.",
"saveError": "No se pudieron guardar los ajustes del detector y del modelo"
},
"unsavedChanges": "Cambios sin guardar en el detector y el modelo",
"restartRequired": "Reinicio necesario (se ha cambiado el detector o el modelo)"
},
"menuDot": {
"overrideGlobal": "Esta sección sobrescribe la configuración global",
"overrideProfile": "Esta sección está sobrescrita por el perfil {{profile}}",
"unsaved": "Esta sección tiene cambios sin guardar"
}
}

View File

@ -55,7 +55,10 @@
},
"count_other": "{{count}} mensajes",
"count_one": "{{count}} mensaje",
"empty": "No se han capturado mensaje aún"
"empty": "No se han capturado mensaje aún",
"expanded": {
"payload": "Carga útil"
}
}
},
"title": "Sistema",
@ -209,6 +212,9 @@
"unusable": "No usable",
"fair": "Normal",
"stallsLastHour": "Bloqueos (última hora)"
},
"noCameras": {
"title": "No se han encontrado cámaras"
}
},
"lastRefreshed": "Última actualización: ",

View File

@ -140,7 +140,8 @@
"gl": "Galego (galeegi keel)",
"id": "Bahasa Indonesia (indoneesia keel)",
"ur": "اردو (urdu keel)",
"hr": "Hrvatski (horvaadi keel)"
"hr": "Hrvatski (horvaadi keel)",
"bs": "Bosanski (bosnia keel)"
},
"system": "Süsteem",
"systemMetrics": "Süsteemi meetrika",

View File

@ -121,5 +121,10 @@
"royal_mail": "Royal Mail",
"school_bus": "Koolibuss",
"skunk": "Vinukloom (skunk)",
"kangaroo": "Känguru"
"kangaroo": "Känguru",
"baby": "Väikelaps",
"baby_stroller": "Lapsevanker",
"rickshaw": "Rikša",
"Rodent": "Näriline",
"rodent": "Näriline"
}

View File

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

View File

@ -16,5 +16,7 @@
"attach_event_aria": "Attacher l'événement {{eventId}}",
"attachment_picker_paste_label": "Ou coller l'event ID",
"attachment_picker_placeholder": "Attacher un événement",
"quick_reply_find_similar": "Trouver des observations similaires"
"quick_reply_find_similar": "Trouver des observations similaires",
"no_similar_objects_found": "Aucun objet similaire trouvé.",
"semantic_search_required": "La recherche sémantique doit être activée afin de trouver un objet similaire."
}

View File

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

View File

@ -221,7 +221,8 @@
"gl": "Galego (Galiziano)",
"id": "Bahasa Indonesia (Indonesiano)",
"ur": "اردو (Urdu)",
"hr": "Hrvatski (Croato)"
"hr": "Hrvatski (Croato)",
"bs": "Bosanski (Bosniaco)"
},
"darkMode": {
"label": "Modalità scura",

View File

@ -33,7 +33,11 @@
},
"filters": {
"label": "Filtri audio",
"description": "Impostazioni di filtro per ciascun tipo di audio, come le soglie di confidenza utilizzate per ridurre i falsi positivi."
"description": "Impostazioni di filtro per ciascun tipo di audio, come le soglie di confidenza utilizzate per ridurre i falsi positivi.",
"threshold": {
"label": "Affidabilità audio minima",
"description": "Soglia minima di fiducia affinché l'evento audio venga conteggiato."
}
},
"enabled_in_config": {
"label": "Stato audio originale",
@ -129,7 +133,44 @@
}
},
"detect": {
"label": "Rilevamento oggetti"
"label": "Rilevamento oggetti",
"description": "Impostazioni per il ruolo di rilevamento/rilevamento utilizzato per eseguire il rilevamento degli oggetti e inizializzare i localizzatori.",
"enabled": {
"label": "Abilita il rilevamento degli oggetti",
"description": "Abilita o disabilita il rilevamento degli oggetti per questa telecamera."
},
"height": {
"label": "Rileva altezza",
"description": "Altezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso."
},
"width": {
"label": "Rileva larghezza",
"description": "Larghezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso."
},
"fps": {
"label": "Rileva FPS",
"description": "Numero di fotogrammi al secondo desiderati per eseguire il rilevamento; valori inferiori riducono l'utilizzo della CPU (il valore consigliato è 5, impostarne uno superiore - al massimo 10 - solo se si devono tracciare oggetti in movimento estremamente rapidi)."
},
"min_initialized": {
"label": "Frame di inizializzazione minimi",
"description": "Numero di rilevamenti consecutivi necessari prima di creare un oggetto tracciato. Aumenta questo valore per ridurre le inizializzazioni errate. Il valore predefinito è FPS diviso per 2."
},
"max_disappeared": {
"label": "Numero di fotogrammi scomparsi",
"description": "Numero di fotogrammi senza rilevamento prima che un oggetto tracciato venga considerato scomparso."
},
"stationary": {
"label": "Configurazione degli oggetti stazionari",
"description": "Impostazioni per rilevare e gestire gli oggetti che rimangono fermi per un certo periodo di tempo.",
"interval": {
"label": "Intervallo stazionario",
"description": "Con quale frequenza (in fotogrammi) eseguire un controllo di rilevamento per confermare che l'oggetto sia stazionario."
},
"threshold": {
"label": "Soglia stazionaria",
"description": "Numero di fotogrammi senza cambio di posizione necessari per contrassegnare un oggetto come stazionario."
}
}
},
"face_recognition": {
"label": "Riconoscimento facciale"
@ -189,7 +230,20 @@
}
},
"birdseye": {
"label": "Birdseye"
"label": "Birdseye",
"description": "Impostazioni per la vista composita Birdseye che unisce più flussi video di telecamere in un unico formato.",
"enabled": {
"label": "Abilita Birdseye",
"description": "Abilita o disabilita la funzione di visualizzazione Birdseye."
},
"mode": {
"label": "Modalità di tracciamento",
"description": "Modalità per includere le telecamere in Birdseye: 'oggetti', 'movimento' o 'continuo'."
},
"order": {
"label": "Posizione",
"description": "Posizione numerica che controlla l'ordine delle telecamere nella disposizione Birdseye."
}
},
"semantic_search": {
"label": "Ricerca semantica",
@ -216,5 +270,9 @@
"label": "Abilitata"
},
"label": "Zone"
},
"type": {
"description": "Tipo di telecamera",
"label": "Tipo di telecamera"
}
}

View File

@ -30,7 +30,11 @@
},
"filters": {
"label": "Filtri audio",
"description": "Impostazioni di filtro per ciascun tipo di audio, come le soglie di confidenza utilizzate per ridurre i falsi positivi."
"description": "Impostazioni di filtro per ciascun tipo di audio, come le soglie di confidenza utilizzate per ridurre i falsi positivi.",
"threshold": {
"label": "Affidabilità audio minima",
"description": "Soglia minima di fiducia affinché l'evento audio venga conteggiato."
}
},
"enabled_in_config": {
"label": "Stato audio originale",
@ -254,7 +258,44 @@
}
},
"detect": {
"label": "Rilevamento oggetti"
"label": "Rilevamento oggetti",
"description": "Impostazioni per il ruolo di rilevamento/rilevamento utilizzato per eseguire il rilevamento degli oggetti e inizializzare i localizzatori.",
"enabled": {
"label": "Abilita il rilevamento degli oggetti",
"description": "Abilita o disabilita il rilevamento degli oggetti per tutte le telecamere; l'impostazione può essere modificata per ogni singola telecamera."
},
"height": {
"label": "Rileva altezza",
"description": "Altezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso."
},
"width": {
"label": "Rileva larghezza",
"description": "Larghezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso."
},
"fps": {
"label": "Rileva FPS",
"description": "Numero di fotogrammi al secondo desiderati per eseguire il rilevamento; valori inferiori riducono l'utilizzo della CPU (il valore consigliato è 5, impostarne uno superiore - al massimo 10 - solo se si devono tracciare oggetti in movimento estremamente rapidi)."
},
"min_initialized": {
"label": "Frame di inizializzazione minimi",
"description": "Numero di rilevamenti consecutivi necessari prima di creare un oggetto tracciato. Aumenta questo valore per ridurre le inizializzazioni errate. Il valore predefinito è FPS diviso per 2."
},
"max_disappeared": {
"label": "Numero di fotogrammi scomparsi",
"description": "Numero di fotogrammi senza rilevamento prima che un oggetto tracciato venga considerato scomparso."
},
"stationary": {
"label": "Configurazione degli oggetti stazionari",
"description": "Impostazioni per rilevare e gestire gli oggetti che rimangono fermi per un certo periodo di tempo.",
"interval": {
"label": "Intervallo stazionario",
"description": "Con quale frequenza (in fotogrammi) eseguire un controllo di rilevamento per confermare che l'oggetto sia stazionario."
},
"threshold": {
"label": "Soglia stazionaria",
"description": "Numero di fotogrammi senza cambio di posizione necessari per contrassegnare un oggetto come stazionario."
}
}
},
"face_recognition": {
"label": "Riconoscimento facciale",
@ -351,7 +392,7 @@
},
"notifications": {
"label": "Notifiche",
"description": "Impostazioni per abilitare e controllare le notifiche per tutte le telecamere; possono essere modificate per ogni singola telecamera.",
"description": "Impostazioni per abilitare e controllare le notifiche per tutte le telecamere; possono essere sovrascritte per ogni singola telecamera.",
"enabled": {
"label": "Abilita le notifiche",
"description": "Abilita o disabilita le notifiche per tutte le telecamere; l'impostazione può essere modificata per ogni singola telecamera."
@ -400,7 +441,20 @@
"label": "Telemetria"
},
"birdseye": {
"label": "Birdseye"
"label": "Birdseye",
"description": "Impostazioni per la vista composita Birdseye che unisce più flussi video di telecamere in un unico formato.",
"enabled": {
"label": "Abilita Birdseye",
"description": "Abilita o disabilita la funzione di visualizzazione Birdseye."
},
"mode": {
"label": "Modalità di tracciamento",
"description": "Modalità per includere le telecamere in Birdseye: 'oggetti', 'movimento' o 'continuo'."
},
"order": {
"label": "Posizione",
"description": "Posizione numerica che controlla l'ordine delle telecamere nella disposizione Birdseye."
}
},
"model": {
"label": "Modello di rilevamento"
@ -445,5 +499,8 @@
"label": "Mostra nell'interfaccia utente",
"description": "Abilita o disabilita la visualizzazione di questa telecamera in ogni punto dell'interfaccia utente di Frigate. Disabilitando questa opzione, sarà necessario modificare manualmente la configurazione per visualizzare nuovamente la telecamera nell'interfaccia utente."
}
},
"active_profile": {
"label": "Profilo attivo"
}
}

View File

@ -121,5 +121,9 @@
"royal_mail": "Royal Mail",
"school_bus": "Autobus scolastico",
"skunk": "Puzzola",
"kangaroo": "Canguro"
"kangaroo": "Canguro",
"baby": "Bambino",
"baby_stroller": "Passeggino per bambini",
"rickshaw": "Risciò",
"rodent": "Roditore"
}

View File

@ -42,5 +42,23 @@
"show_camera_status": "Qual è lo stato attuale delle mie telecamere?",
"recap": "Cosa è successo mentre ero via?",
"watch_camera": "Controlla la porta d'ingresso e fammi sapere se arriva qualcuno"
},
"new_chat": "Nuova chat",
"settings": {
"title": "Impostazioni chat",
"show_stats": {
"title": "Mostra statistiche",
"desc": "Mostra la frequenza di generazione e la dimensione del contesto per le risposte in chat.",
"while_generating": "Durante la generazione",
"always": "Sempre"
},
"auto_scroll": {
"title": "Scorrimento automatico",
"desc": "Segui i nuovi messaggi non appena arrivano."
}
},
"stats": {
"context": "{{tokens}} token",
"tokens_per_second": "{{rate}} t/s"
}
}

View File

@ -2,7 +2,7 @@
"alerts": "Avvisi",
"detections": "Rilevamenti",
"motion": {
"label": "Movimenti",
"label": "Movimento",
"only": "Solo movimenti"
},
"empty": {

View File

@ -20,7 +20,11 @@
"title": "Riconoscimenti recenti",
"aria": "Seleziona i riconoscimenti recenti",
"empty": "Non ci sono recenti tentativi di riconoscimento facciale",
"titleShort": "Recente"
"titleShort": "Recente",
"emptyNoLibrary": {
"title": "Carica un volto",
"description": "Affinché il riconoscimento facciale funzioni è necessario aggiungere almeno un volto alla libreria."
}
},
"button": {
"addFace": "Aggiungi volto",

View File

@ -158,7 +158,7 @@
},
"effectiveRetainMode": {
"modes": {
"all": "Tutto",
"all": "Tutti",
"motion": "Movimento",
"active_objects": "Oggetti attivi"
},

View File

@ -52,7 +52,15 @@
"modelType": "Tipo di modello",
"modelSelect": "Qui puoi selezionare i modelli disponibili su Frigate+. Nota: puoi selezionare solo i modelli compatibili con la configurazione attuale del tuo rilevatore.",
"title": "Informazioni sul modello",
"loading": "Caricamento informazioni sul modello…"
"loading": "Caricamento informazioni sul modello…",
"noModelsAvailable": "Nessun modello disponibile",
"noModelLoaded": "Al momento non è caricato alcun modello di Frigate+.",
"selectModel": "Seleziona un modello",
"filter": {
"ariaLabel": "Filtra i modelli per tipo",
"baseModels": "Modelli base",
"fineTunedModels": "Modelli ottimizzati"
}
},
"toast": {
"error": "Impossibile salvare le modifiche alla configurazione: {{errorMessage}}",
@ -203,6 +211,10 @@
"zone": "zona",
"motion_mask": "maschera di movimento",
"object_mask": "maschera di oggetto"
},
"revertOverride": {
"title": "Ripristina la configurazione di base",
"desc": "Questo rimuoverà la sovrascrittura del profilo per {{type}} <em>{{name}}</em> e ripristinerà la configurazione di base."
}
},
"inertia": {
@ -219,6 +231,17 @@
"error": {
"mustBeGreaterOrEqualTo": "La soglia di velocità deve essere maggiore o uguale a 0,1."
}
},
"id": {
"error": {
"mustNotBeEmpty": "L'ID non deve essere vuoto.",
"alreadyExists": "Esiste già una maschera con questo ID per questa telecamera."
}
},
"name": {
"error": {
"mustNotBeEmpty": "Il nome non deve essere vuoto."
}
}
},
"filter": {
@ -329,7 +352,11 @@
"title": "Abilitata",
"description": "Indica se questa maschera è abilitata nel file di configurazione. Se disabilitata, non può essere abilitata tramite MQTT. Le maschere disabilitate vengono ignorate in fase di esecuzione."
}
}
},
"disabledInConfig": "L'elemento è disabilitato nel file di configurazione",
"addDisabledProfile": "Aggiungi prima alla configurazione di base, poi sovrascrivi nel profilo",
"profileBase": "(base)",
"profileOverride": "(sovrascrivi)"
},
"cameraSetting": {
"camera": "Telecamera",
@ -477,7 +504,10 @@
"cameraOnvif": "ONVIF",
"cameraTimestampStyle": "Stile orario",
"cameraUi": "Interfaccia utente telecamera",
"mediaSync": "Sincronizzazione multimediale"
"mediaSync": "Sincronizzazione multimediale",
"cameraMqtt": "MQTT telecamera",
"maintenance": "Manutenzione",
"regionGrid": "Griglia di regioni"
},
"users": {
"dialog": {
@ -1359,7 +1389,8 @@
},
"hikvision": {
"substreamWarning": "Il sottoflusso 1 è bloccato a bassa risoluzione. Molte telecamere Hikvision supportano sottoflussi aggiuntivi che devono essere abilitati nelle impostazioni della telecamera. Si consiglia di controllare e utilizzare tali flussi, se disponibili."
}
},
"resolutionUnknown": "Non è stato possibile rilevare la risoluzione di questo flusso. È necessario impostare manualmente la risoluzione di rilevamento nelle Impostazioni o nella configurazione."
}
}
},
@ -1414,7 +1445,33 @@
"addGo2rtcStream": "Aggiungi flusso go2rtc"
},
"profiles": {
"enabled": "Abilitato"
"enabled": "Abilitato",
"title": "Sovrascritture della telecamera del profilo",
"selectLabel": "Seleziona il profilo",
"description": "Configura quali telecamere vengono abilitate o disabilitate all'attivazione di un profilo. Le telecamere impostate su \"Eredita\" mantengono il loro stato di abilitazione predefinito.",
"inherit": "Eredita",
"disabled": "Disabilitato"
},
"description": "Aggiungi, modifica ed elimina le telecamere, controlla quali telecamere sono abilitate e configura le impostazioni personalizzate per profilo e tipo di telecamera. Per configurare flussi video, rilevamento, movimento e altre impostazioni specifiche per ciascuna telecamera, seleziona la sezione corrispondente in Configurazione telecamera.",
"deleteCamera": "Elimina telecamera",
"deleteCameraDialog": {
"title": "Elimina telecamera",
"description": "L'eliminazione di una telecamera rimuoverà in modo permanente tutte le registrazioni, gli oggetti tracciati e la configurazione relativi a tale telecamera. Potrebbe essere comunque necessario rimuovere manualmente gli eventuali flussi go2rtc associati a questa telecamera.",
"selectPlaceholder": "Scegli telecamera...",
"confirmTitle": "Sei sicuro?",
"confirmWarning": "L'eliminazione di <strong>{{cameraName}}</strong> è irreversibile.",
"deleteExports": "Elimina anche le esportazioni per questa telecamera",
"confirmButton": "Elimina definitivamente",
"success": "Telecamera {{cameraName}} eliminata con successo",
"error": "Impossibile eliminare la telecamera {{cameraName}}"
},
"cameraType": {
"title": "Tipo di telecamera",
"label": "Tipo di telecamera",
"description": "Imposta il tipo per ogni telecamera. Le telecamere LPR dedicate sono telecamere monouso con un potente zoom ottico per acquisire le targhe dei veicoli distanti. La maggior parte delle telecamere dovrebbe utilizzare il tipo di telecamera normale, a meno che non siano specificamente progettate per il riconoscimento delle targhe e abbiano una visuale molto ravvicinata sulle targhe.",
"normal": "Normale",
"dedicatedLpr": "LPR dedicata",
"saveSuccess": "Tipo di telecamera aggiornato per {{cameraName}}. Riavviare Frigate per applicare le modifiche."
}
},
"button": {
@ -1436,7 +1493,15 @@
"othersField_many": "{{count}} altri",
"othersField_other": "{{count}} altri",
"profilePrefix": "Profilo {{profile}}: {{fields}}"
}
},
"overriddenGlobalHeading_one": "Questa telecamera sovrascrive il campo {{count}} dalla configurazione globale:",
"overriddenGlobalHeading_many": "Questa telecamera sovrascrive i campi {{count}} della configurazione globale:",
"overriddenGlobalHeading_other": "Questa telecamera sovrascrive i campi {{count}} della configurazione globale:",
"overriddenGlobalNoDeltas": "Questa telecamera sovrascrive la configurazione globale, ma nessun valore dei campi risulta diverso.",
"overriddenBaseConfigHeading_one": "Il profilo {{profile}} sovrascrive il campo {{count}} della configurazione di base:",
"overriddenBaseConfigHeading_many": "Il profilo {{profile}} sovrascrive i campi {{count}} della configurazione di base:",
"overriddenBaseConfigHeading_other": "Il profilo {{profile}} sovrascrive i campi {{count}} della configurazione di base:",
"overriddenBaseConfigNoDeltas": "Il profilo {{profile}} sovrascrive questa sezione, ma nessun valore di campo differisce dalla configurazione di base."
},
"go2rtcStreams": {
"title": "Flussi go2rtc",
@ -1447,8 +1512,38 @@
"videoCopy": "Copia",
"hardware": "Accelerazione hardware",
"hardwareNone": "Nessuna accelerazione hardware",
"hardwareAuto": "Accelerazione hardware automatica"
}
"hardwareAuto": "Accelerazione hardware automatica",
"useFfmpegModule": "Utilizza la modalità di compatibilità (ffmpeg)",
"videoH264": "Transcodifica in H.264",
"videoH265": "Transcodifica in H.265",
"videoExclude": "Escludi",
"audioAac": "Transcodifica in AAC",
"audioOpus": "Transcodifica in Opus",
"audioPcmu": "Transcodifica in PCM μ-law",
"audioPcma": "Transcodifica in PCM A-law",
"audioPcm": "Transcodifica in PCM",
"audioMp3": "Transcodifica in MP3",
"audioExclude": "Escludi"
},
"description": "Gestisci le configurazioni del flusso go2rtc per la ritrasmissione delle immagini della telecamera. Ogni flusso ha un nome e uno o più URL sorgente.",
"addStream": "Aggiungi flusso",
"addStreamDesc": "Inserisci un nome per il nuovo flusso. Questo nome verrà utilizzato per identificare il flusso nella configurazione della telecamera.",
"addUrl": "Aggiungi URL",
"streamName": "Nome flusso",
"streamNamePlaceholder": "p. es., porta_ingresso",
"streamUrlPlaceholder": "p. es., rtsp://utente:password@192.168.1.100/flusso",
"deleteStream": "Elimina flusso",
"deleteStreamConfirm": "Sei sicuro di voler eliminare il flusso \"{{streamName}}\"? Le telecamere che fanno riferimento a questo flusso potrebbero smettere di funzionare.",
"noStreams": "Nessun flusso go2rtc configurato. Aggiungi un flusso per iniziare.",
"validation": {
"nameRequired": "Il nome del flusso è obbligatorio",
"nameDuplicate": "Esiste già un flusso con questo nome",
"nameInvalid": "Il nome del flusso può contenere solo lettere, numeri, trattini bassi e trattini",
"urlRequired": "È richiesto almeno un URL"
},
"renameStream": "Rinomina flusso",
"renameStreamDesc": "Inserisci un nuovo nome per questo flusso. Rinominare un flusso potrebbe causare problemi alle telecamere o ad altri flussi che lo referenziano tramite il suo nome.",
"newStreamName": "Nuovo nome del flusso"
},
"configForm": {
"sections": {
@ -1470,7 +1565,14 @@
"face_recognition": "Riconoscimento facciale",
"masksAndZones": "Maschere / Zone",
"audio": "Audio",
"model": "Modello"
"model": "Modello",
"detect": "Rilevamento",
"motion": "Movimento",
"live": "Vista dal vivo",
"timestamp_style": "Orari",
"go2rtc": "go2rtc",
"detectors": "Rivelatori",
"genai": "GenAI"
},
"tabs": {
"system": "Sistema",
@ -1479,12 +1581,19 @@
},
"inputRoles": {
"options": {
"audio": "Audio"
}
"audio": "Audio",
"detect": "Rileva",
"record": "Registra"
},
"summary": "{{count}} ruoli selezionati",
"empty": "Nessun ruolo disponibile"
},
"roleMap": {
"roleLabel": "Ruolo",
"remove": "Rimuovi"
"remove": "Rimuovi",
"empty": "Nessuna mappatura dei ruoli",
"groupsLabel": "Gruppi",
"addMapping": "Aggiungi la mappatura dei ruoli"
},
"notifications": {
"title": "Impostazioni di notifica"
@ -1518,19 +1627,179 @@
"presetLabels": {
"preset-rpi-64-h264": "Raspberry Pi (H.264)",
"preset-rpi-64-h265": "Raspberry Pi (H.265)",
"preset-vaapi": "VAAPI (GPU Intel/AMD)"
"preset-vaapi": "VAAPI (GPU Intel/AMD)",
"preset-intel-qsv-h264": "Intel QuickSync (H.264)",
"preset-intel-qsv-h265": "Intel QuickSync (H.265)",
"preset-nvidia": "GPU NVIDIA",
"preset-jetson-h264": "NVIDIA Jetson (H.264)",
"preset-jetson-h265": "NVIDIA Jetson (H.265)",
"preset-rkmpp": "Rockchip RKMPP",
"preset-http-jpeg-generic": "HTTP JPEG (Generico)",
"preset-http-mjpeg-generic": "HTTP JPEG (Generico)",
"preset-http-reolink": "HTTP - Telecamere Reolink",
"preset-rtmp-generic": "RTMP (Generico)",
"preset-rtsp-generic": "RTSP (Generico)",
"preset-rtsp-restream": "RTSP - Ritrasmissione da go2rtc",
"preset-rtsp-restream-low-latency": "RTSP - Ritrasmissione da go2rtc (bassa latenza)",
"preset-rtsp-udp": "RTSP - UDP",
"preset-rtsp-blue-iris": "RTSP - Blue Iris",
"preset-record-generic": "Registrazione (Generica, senza audio)",
"preset-record-generic-audio-copy": "Registrazione (generica + copia audio)",
"preset-record-generic-audio-aac": "Registrazione (generica + audio in AAC)",
"preset-record-mjpeg": "Registrazione - Telecamere MJPEG",
"preset-record-jpeg": "Registrazione - Telecamere JPEG",
"preset-record-ubiquiti": "Registrazione - Telecamere Ubiquiti"
}
},
"cameraInputs": {
"itemTitle": "Stream {{index}}"
},
"restartRequiredField": "Riavvio richiesto",
"restartRequiredFooter": "Configurazione modificata - Riavvio necessario",
"detect": {
"title": "Impostazioni di rilevamento"
},
"detectors": {
"title": "Impostazioni del rilevatore",
"singleType": "È consentito un solo rilevatore di tipo {{type}}.",
"keyRequired": "Il nome del rilevatore è obbligatorio.",
"keyDuplicate": "Il nome del rilevatore esiste già.",
"noSchema": "Non sono disponibili schemi di rilevamento.",
"none": "Nessuna istanza del rilevatore configurata.",
"add": "Aggiungi rilevatore",
"addCustomKey": "Aggiungi chiave personalizzata"
},
"record": {
"title": "Impostazioni di registrazione"
},
"snapshots": {
"title": "Impostazioni istantanea"
},
"motion": {
"title": "Impostazioni di movimento"
},
"objects": {
"title": "Impostazioni oggetto"
},
"audioLabels": {
"summary": "{{count}} etichette audio selezionate",
"empty": "Nessuna etichetta audio disponibile"
},
"objectLabels": {
"summary": "{{count}} tipi di oggetto selezionati",
"empty": "Non sono disponibili etichette per gli oggetti"
},
"reviewLabels": {
"summary": "{{count}} etichette selezionate",
"empty": "Nessuna etichetta disponibile"
},
"filters": {
"objectFieldLabel": "{{field}} per {{label}}"
},
"zoneNames": {
"summary": "{{count}} selezionati",
"empty": "Nessuna zona disponibile"
},
"genaiRoles": {
"options": {
"embeddings": "Incorporamento",
"descriptions": "Descrizioni",
"chat": "Chat"
}
},
"semanticSearchModel": {
"placeholder": "Seleziona il modello…",
"builtIn": "Modelli integrati",
"genaiProviders": "Fornitori di GenAI"
},
"genaiModel": {
"placeholder": "Seleziona il modello…",
"search": "Ricerca modelli…",
"noModels": "Nessun modello disponibile"
},
"review": {
"title": "Impostazioni di revisione"
},
"audio": {
"title": "Impostazioni audio"
},
"live": {
"title": "Impostazioni della visualizzazione dal vivo"
},
"timestamp_style": {
"title": "Impostazioni orario"
},
"searchPlaceholder": "Ricerca...",
"addCustomLabel": "Aggiungi etichetta personalizzata...",
"knownPlates": {
"namePlaceholder": "p. es., auto della moglie",
"platePlaceholder": "Numero di targa o espressione regolare"
},
"timezone": {
"defaultOption": "Utilizza il fuso orario del browser"
}
},
"globalConfig": {
"title": "Configurazione globale"
"title": "Configurazione globale",
"description": "Configura le impostazioni globali che si applicano a tutte le telecamere, a meno che non vengano sovrascritte.",
"toast": {
"success": "Impostazioni globali salvate correttamente",
"error": "Impossibile salvare le impostazioni globali",
"validationError": "Validazione fallita"
}
},
"cameraConfig": {
"title": "Configurazione telecamera"
"title": "Configurazione telecamera",
"description": "Configura le impostazioni per le singole telecamere. Le impostazioni personalizzate sovrascrivono le impostazioni predefinite globali.",
"overriddenBadge": "Sovrascritto",
"resetToGlobal": "Ripristina impostazioni globali",
"toast": {
"success": "Impostazioni della telecamera salvate correttamente",
"error": "Impossibile salvare le impostazioni della telecamera"
}
},
"profiles": {
"title": "Profili",
"columnCamera": "Telecamera"
"columnCamera": "Telecamera",
"activeProfile": "Profilo attivo",
"noActiveProfile": "Nessun profilo attivo",
"active": "Attivo",
"activated": "Profilo '{{profile}}' attivato",
"activateFailed": "Impossibile impostare il profilo",
"deactivated": "Profilo disattivato",
"noProfiles": "Nessun profilo definito.",
"noOverrides": "Nessuna sovrascrittura",
"cameraCount_one": "{{count}} telecamera",
"cameraCount_many": "{{count}} telecamere",
"cameraCount_other": "{{count}} telecamere",
"columnOverrides": "Sovrascritture del profilo",
"baseConfig": "Configurazione di base",
"addProfile": "Aggiungi profilo",
"newProfile": "Nuovo profilo",
"profileNamePlaceholder": "p. es., Inserita, Assente, Modalità notturna",
"friendlyNameLabel": "Nome profilo",
"profileIdLabel": "ID profilo",
"profileIdDescription": "Identificativo interno utilizzato nella configurazione e nelle automazioni",
"nameInvalid": "Sono consentite solo lettere minuscole, numeri e trattini bassi",
"nameDuplicate": "Esiste già un profilo con questo nome",
"error": {
"mustBeAtLeastTwoCharacters": "Deve contenere almeno 2 caratteri",
"mustNotContainPeriod": "Non deve contenere punti",
"alreadyExists": "Esiste già un profilo con questo ID"
},
"renameProfile": "Rinomina profilo",
"renameSuccess": "Profilo rinominato in '{{profile}}'",
"deleteProfile": "Elimina profilo",
"deleteProfileConfirm": "Eliminare il profilo \"{{profile}}\" da tutte le telecamere? Questa operazione non può essere annullata.",
"deleteSuccess": "Profilo '{{profile}}' eliminato",
"createSuccess": "Profilo '{{profile}}' creato",
"removeOverride": "Rimuovi la sovrascrittura del profilo",
"deleteSection": "Elimina le sostituzioni della sezione",
"deleteSectionConfirm": "Rimuovere le sovrascritture {{section}} per il profilo {{profile}} su {{camera}}?",
"deleteSectionSuccess": "Rimosse le sovrascritture di {{section}} per {{profile}}",
"enableSwitch": "Abilita profili",
"enabledDescription": "I profili sono abilitati. Crea un nuovo profilo qui sotto, vai alla sezione di configurazione della telecamera per apportare le modifiche e salva affinché le modifiche abbiano effetto.",
"disabledDescription": "I profili consentono di definire insiemi denominati di impostazioni di configurazione della telecamera (p.es., inserita, assente, notturna) che possono essere attivate su richiesta."
},
"timestampPosition": {
"tl": "In alto a sinistra",
@ -1564,14 +1833,37 @@
"errorLabel": "Errore",
"resultsFields": {
"error": "Errore",
"totals": "Totali"
"totals": "Totali",
"filesChecked": "File controllati",
"orphansFound": "Orfani trovati",
"orphansDeleted": "Orfani eliminati",
"aborted": "Interrotto. La cancellazione supererebbe la soglia di sicurezza."
},
"event_snapshots": "Istantanee degli oggetti tracciati",
"event_thumbnails": "Miniature degli oggetti tracciati",
"review_thumbnails": "Anteprima delle miniature",
"previews": "Anteprime",
"exports": "Esportazioni",
"recordings": "Registrazioni"
"recordings": "Registrazioni",
"mediaTypes": "Tipi di supporto",
"allMedia": "Tutti i supporti",
"dryRun": "Prova a secco",
"dryRunEnabled": "Nessun file verrà eliminato",
"dryRunDisabled": "I file verranno eliminati",
"force": "Forza",
"forceDesc": "Ignora la soglia di sicurezza e completa la sincronizzazione anche se più del 50% dei file verrebbe eliminato.",
"verbose": "Dettagliato",
"verboseDesc": "Scrivi un elenco completo dei file orfani su disco per la revisione.",
"running": "Sincronizzazione in corso...",
"start": "Avvia sincronizzazione",
"inProgress": "Sincronizzazione in corso. Questa pagina è disabilitata.",
"status": {
"queued": "In coda",
"running": "In corso",
"completed": "Completata",
"failed": "Fallita",
"notRunning": "Non in esecuzione"
}
},
"regionGrid": {
"title": "Griglia di regioni",
@ -1583,5 +1875,158 @@
"clearError": "Impossibile pulire la griglia di regioni",
"restartRequired": "È necessario riavviare il sistema affinché le modifiche alla griglia di regioni abbiano effetto"
}
},
"retainMode": {
"motion": "Movimento",
"all": "Tutti",
"active_objects": "Oggetti attivi"
},
"birdseye": {
"trackingMode": {
"motion": "Movimento",
"objects": "Oggetti",
"continuous": "Continuo"
}
},
"toast": {
"success": "Impostazioni salvate correttamente",
"applied": "Impostazioni applicate correttamente",
"successRestartRequired": "Impostazioni salvate correttamente. Riavvia Frigate per applicare le modifiche.",
"error": "Impossibile salvare le impostazioni",
"validationError": "Validazione non riuscita: {{message}}",
"resetSuccess": "Ripristina le impostazioni predefinite globali",
"resetError": "Impossibile ripristinare le impostazioni",
"saveAllSuccess_one": "Salvata {{count}} sezione correttamente.",
"saveAllSuccess_many": "Tutte le {{count}} sezioni sono state salvate correttamente.",
"saveAllSuccess_other": "Tutte le {{count}} sezioni sono state salvate correttamente.",
"saveAllPartial_one": "{{successCount}} sezione su {{totalCount}} salvata. {{failCount}} errore.",
"saveAllPartial_many": "{{successCount}} sezioni su {{totalCount}} salvate. {{failCount}} errori.",
"saveAllPartial_other": "{{successCount}} sezioni su {{totalCount}} salvate. {{failCount}} errori.",
"saveAllFailure": "Impossibile salvare tutte le sezioni."
},
"unsavedChanges": "Hai delle modifiche non salvate",
"confirmReset": "Conferma il ripristino",
"resetToDefaultDescription": "Questa operazione ripristinerà tutte le impostazioni di questa sezione ai valori predefiniti. Tale azione è irreversibile.",
"resetToGlobalDescription": "Questa operazione ripristinerà le impostazioni di questa sezione ai valori predefiniti globali. Tale azione è irreversibile.",
"previewQuality": {
"very_high": "Molto alta",
"high": "Alta",
"medium": "Media",
"low": "Bassa",
"very_low": "Molto bassa"
},
"ui": {
"TimeOrDateStyle": {
"medium": "Medio",
"full": "Completo",
"long": "Lungo",
"short": "Corto"
},
"timeFormat": {
"browser": "Navigatore",
"12hour": "12 ore",
"24hour": "24 ore"
},
"unitSystem": {
"metric": "Metrico",
"imperial": "Imperiale"
}
},
"review": {
"imageSource": {
"recordings": "Registrazioni",
"previews": "Anteprime"
}
},
"logger": {
"logLevel": {
"debug": "Correzioni",
"info": "Informazioni",
"warning": "Avviso",
"error": "Errore",
"critical": "Critico"
}
},
"onvif": {
"profileAuto": "Automatico",
"profileLoading": "Caricamento profili...",
"autotracking": {
"zooming": {
"disabled": "Disabilitato",
"absolute": "Assoluto",
"relative": "Relativo"
}
}
},
"modelSize": {
"small": "Piccolo",
"large": "Grande"
},
"configMessages": {
"review": {
"recordDisabled": "La registrazione è disabilitata, pertanto non verranno generati elementi di revisione.",
"detectDisabled": "Il rilevamento degli oggetti è disabilitato. Gli elementi di revisione richiedono la presenza di oggetti rilevati per poter classificare avvisi e rilevamenti.",
"allNonAlertDetections": "Tutte le attività non di avviso saranno incluse tra i rilevamenti.",
"genaiImageSourceRecordingsRecordDisabled": "La sorgente dell'immagine è impostata su 'registrazioni', ma la registrazione è disabilitata. Frigate utilizzerà le immagini di anteprima."
},
"audio": {
"noAudioRole": "Nessun flusso ha il ruolo audio definito. È necessario abilitare il ruolo audio affinché il rilevamento audio funzioni."
},
"audioTranscription": {
"audioDetectionDisabled": "Il rilevamento audio non è abilitato per questa telecamera. La trascrizione audio richiede che il rilevamento audio sia attivo."
},
"detect": {
"fpsGreaterThanFive": "Impostare il valore di FPS rilevato su un valore superiore a 5 non è consigliabile. Valori più elevati potrebbero causare problemi di prestazioni e non apporteranno alcun vantaggio.",
"disabled": "Il rilevamento degli oggetti è disabilitato. Le istantanee, gli elementi di revisione e le funzionalità aggiuntive come il riconoscimento facciale, il riconoscimento delle targhe e l'intelligenza artificiale generativa non funzioneranno."
},
"objects": {
"genaiNoDescriptionsProvider": "Per generare le descrizioni è necessario configurare un provider GenAI con il ruolo 'descrizioni'."
},
"faceRecognition": {
"globalDisabled": "Perché le funzionalità di riconoscimento facciale funzionino correttamente su questa telecamera, è necessario abilitare l'arricchimento del riconoscimento facciale.",
"personNotTracked": "Il riconoscimento facciale richiede che l'oggetto 'persona' venga tracciato. Abilita 'persona' nella sezione ogggetti di questa telecamera.",
"modelSizeLarge": "Il modello 'grande' richiede una GPU o una NPU per prestazioni accettabili. Utilizzare il modello 'piccolo' su sistemi dotati solo di CPU."
},
"lpr": {
"globalDisabled": "Per il corretto funzionamento delle funzioni LPR (riconoscimento targhe) su questa telecamera, è necessario abilitare la funzione di arricchimento del riconoscimento delle targhe.",
"vehicleNotTracked": "Il riconoscimento della targa richiede che venga tracciato 'automobile' o 'moto'. Abilita 'automobile' o 'moto' nella sezione oggetti per questa telecamera.",
"modelSizeLarge": "Il modello 'grande' è ottimizzato per le targhe multilinea. Il modello 'piccolo' offre prestazioni migliori rispetto al modello 'grande' e dovrebbe essere utilizzato a meno che nella vostra regione non siano in vigore formati di targa multilinea."
},
"record": {
"noRecordRole": "Nessun flusso ha il ruolo di registrazione definito. La registrazione non funzionerà."
},
"birdseye": {
"objectsModeDetectDisabled": "Birdseye è impostato sulla modalità 'oggetti', ma il rilevamento degli oggetti è disabilitato per questa telecamera. La telecamera non verrà visualizzata in Birdseye."
},
"snapshots": {
"detectDisabled": "Il rilevamento degli oggetti è disabilitato. Le istantanee vengono generate dagli oggetti tracciati e non verranno create."
},
"detectors": {
"mixedTypes": "Tutti i rilevatori devono essere dello stesso tipo. Rimuovi i rilevatori esistenti per poter utilizzare un tipo diverso.",
"mixedTypesSuggestion": "Tutti i rilevatori devono essere dello stesso tipo. Rimuovi i rilevatori esistenti oppure seleziona {{type}}."
},
"semanticSearch": {
"jinav2SmallModelSize": "Il modello 'piccolo' Jina V2 presenta elevati consumi di RAM e di inferenza. Si consiglia il modello 'grande' con GPU dedicata."
}
},
"saveAllPreview": {
"title": "Modifiche da salvare",
"triggerLabel": "Revisione delle modifiche in sospeso",
"empty": "Nessuna modifica in sospeso.",
"scope": {
"label": "Ambito",
"global": "Globale",
"camera": "Telecamera: {{cameraName}}"
},
"profile": {
"label": "Profilo"
},
"field": {
"label": "Campo"
},
"value": {
"label": "Nuovo valore",
"reset": "Reimposta"
}
}
}

View File

@ -175,7 +175,7 @@
"framesAndDetections": "Fotogrammi / Rilevamenti",
"label": {
"camera": "telecamera",
"detect": "rilevamento",
"detect": "rileva",
"skipped": "saltati",
"ffmpeg": "FFmpeg",
"capture": "cattura",

View File

@ -216,7 +216,8 @@
"gl": "Galego (Galisisk)",
"id": "Bahasa Indonesia (Indonesisk)",
"ur": "اردو (Urdu)",
"hr": "Hrvatski (Kroatisk)"
"hr": "Hrvatski (Kroatisk)",
"bs": "Bosanski (Bosnisk)"
},
"appearance": "Utseende",
"darkMode": {
@ -241,7 +242,8 @@
"classification": "Klassifisering",
"profiles": "Profiler",
"chat": "Chat",
"actions": "Handlinger"
"actions": "Handlinger",
"features": "Funksjoner"
},
"pagination": {
"next": {

View File

@ -52,7 +52,7 @@
"description": "Innstillinger for å aktivere og kontrollere varslinger for dette kameraet."
},
"audio": {
"label": "Lydhendelser",
"label": "Lyddeteksjon",
"enabled": {
"label": "Aktiver lyddeteksjon",
"description": "Aktiver eller deaktiver deteksjon av lydhendelser for dette kameraet."
@ -71,7 +71,11 @@
},
"filters": {
"label": "Lydfiltre",
"description": "Filterinnstillinger per lydtype, som konfidensterskler for å redusere falske positive."
"description": "Filterinnstillinger per lydtype, som konfidensterskler for å redusere falske positive.",
"threshold": {
"description": "Minimum konfidens-terskel for at lydhendelsen skal bli registrert.",
"label": "Minimum konfidens for lyd"
}
},
"enabled_in_config": {
"label": "Opprinnelig lydstatus",
@ -476,6 +480,10 @@
"hwaccel_args": {
"label": "Argumenter for maskinvareakselerasjon ved eksport",
"description": "Argumenter for maskinvareakselerasjon som skal brukes ved eksport og transkoding."
},
"max_concurrent": {
"description": "Maksimalt antall eksportjobber som kan behandles samtidig.",
"label": "Maksimalt antall samtidige eksporter"
}
},
"preview": {

View File

@ -242,7 +242,7 @@
"description": "Aktiver overvåking av nettverksbåndbredde per prosess for kamera-ffmpeg-prosesser og detektorer."
},
"intel_gpu_device": {
"label": "SR-IOV-enhet",
"label": "Intel GPU-enhet",
"description": "Enhetsidentifikator som brukes når Intel-GPU-er behandles som SR-IOV for å korrigere GPU-statistikk."
}
},
@ -539,7 +539,7 @@
}
},
"audio": {
"label": "Lydhendelser",
"label": "Lyddeteksjon",
"description": "Innstillinger for lydbasert hendelsesdeteksjon for alle kameraer; kan overstyres per kamera.",
"enabled": {
"label": "Aktiver lyddeteksjon",
@ -559,7 +559,11 @@
},
"filters": {
"label": "Lydfiltre",
"description": "Filterinnstillinger per lydtype, som konfidensterskler for å redusere falske positive."
"description": "Filterinnstillinger per lydtype, som konfidensterskler for å redusere falske positive.",
"threshold": {
"description": "Minimum konfidens-terskel for at lydhendelsen skal bli registrert.",
"label": "Minimum konfidens for lyd"
}
},
"enabled_in_config": {
"label": "Opprinnelig lydstatus",
@ -1000,6 +1004,10 @@
"hwaccel_args": {
"label": "Argumenter for maskinvareakselerasjon ved eksport",
"description": "Argumenter for maskinvareakselerasjon som skal brukes ved eksport og transkoding."
},
"max_concurrent": {
"description": "Maksimalt antall eksportjobber som kan behandles samtidig.",
"label": "Maksimalt antall samtidige eksporter"
}
},
"preview": {

View File

@ -121,5 +121,10 @@
"skunk": "Skunk",
"school_bus": "Skolebuss",
"royal_mail": "Royal Mail",
"canada_post": "Canada Post"
"canada_post": "Canada Post",
"baby_stroller": "Barnevogn",
"Rodent": "Gnager",
"baby": "Baby",
"rickshaw": "Rickshaw",
"rodent": "Gnager"
}

View File

@ -1 +1,64 @@
{}
{
"documentTitle": "Chat - Frigate",
"title": "Frigate Chat",
"subtitle": "Din AI-assistent for kamerahåndtering og innsikt",
"placeholder": "Spør om hva som helst...",
"error": "Noe gikk galt. Vennligst prøv igjen.",
"processing": "Behandler...",
"toolsUsed": "Brukt: {{tools}}",
"showTools": "Vis verktøy ({{count}})",
"hideTools": "Skjul verktøy",
"call": "Kall",
"result": "Resultat",
"arguments": "Argumenter:",
"response": "Svar:",
"attachment_chip_label": "{{label}} på {{camera}}",
"attachment_chip_remove": "Fjern vedlegg",
"open_in_explore": "Åpne i Utforsk",
"attach_event_aria": "Legg ved hendelse {{eventId}}",
"attachment_picker_paste_label": "Eller lim inn hendelses-ID",
"attachment_picker_attach": "Legg ved",
"attachment_picker_placeholder": "Legg ved en hendelse",
"quick_reply_find_similar": "Finn lignende observasjoner",
"quick_reply_tell_me_more": "Fortell meg mer om dette",
"quick_reply_when_else": "Når ellers ble det sett?",
"quick_reply_find_similar_text": "Finn lignende observasjoner som denne.",
"quick_reply_tell_me_more_text": "Fortell meg mer om denne.",
"quick_reply_when_else_text": "Når ellers ble denne sett?",
"anchor": "Referanse",
"similarity_score": "Likhet",
"no_similar_objects_found": "Ingen lignende objekter funnet.",
"semantic_search_required": "Semantisk søk må være aktivert for å finne lignende objekter.",
"send": "Send",
"suggested_requests": "Prøv å spørre:",
"starting_requests": {
"show_recent_events": "Vis nylige hendelser",
"show_camera_status": "Vis kamerastatus",
"recap": "Hva skjedde mens jeg var borte?",
"watch_camera": "Overvåk et kamera for aktivitet"
},
"starting_requests_prompts": {
"show_recent_events": "Vis meg nylige hendelser fra den siste timen",
"show_camera_status": "Hva er status for kameraene mine akkurat nå?",
"recap": "Hva skjedde mens jeg var borte?",
"watch_camera": "Overvåk inngangsdøren og gi meg beskjed hvis noen dukker opp"
},
"new_chat": "Ny chat",
"settings": {
"title": "Chat-innstillinger",
"show_stats": {
"title": "Vis statistikk",
"desc": "Vis genereringshastighet og kontekststørrelse for chat-svar.",
"while_generating": "Under generering",
"always": "Alltid"
},
"auto_scroll": {
"title": "Auto-rulling",
"desc": "Følg nye meldinger etter hvert som de ankommer."
}
},
"stats": {
"context": "{{tokens}} tokens",
"tokens_per_second": "{{rate}} t/s"
}
}

View File

@ -27,7 +27,11 @@
"aria": "Velg nylige gjenkjennelser",
"title": "Nylige gjenkjennelser",
"empty": "Det er ingen nylige forsøk på ansiktsgjenkjenning",
"titleShort": "Nylige"
"titleShort": "Nylige",
"emptyNoLibrary": {
"description": "Du må legge til minst ett ansikt i biblioteket for at ansiktsgjenkjenning skal fungere.",
"title": "Last opp et ansikt"
}
},
"selectFace": "Velg ansikt",
"deleteFaceLibrary": {

View File

@ -152,7 +152,8 @@
},
"recording": {
"enable": "Aktiver opptak",
"disable": "Deaktiver opptak"
"disable": "Deaktiver opptak",
"disabledInConfig": "Opptak må først aktiveres i Innstillinger for dette kameraet."
},
"streamStats": {
"enable": "Vis Strømmestatistikk",

View File

@ -1 +1,75 @@
{}
{
"documentTitle": "Bevegelsessøk - Frigate",
"title": "Bevegelsessøk",
"description": "Tegn et polygon for å definere et interesseområde, og angi et tidsrom for å søke etter bevegelsesendringer i dette området.",
"selectCamera": "Bevegelsessøk laster",
"startSearch": "Start søk",
"searchStarted": "Søk startet",
"searchCancelled": "Søk avbrutt",
"cancelSearch": "Avbryt",
"searching": "Søk pågår...",
"searchComplete": "Søk fullført",
"noResultsYet": "Kjør et søk for å finne bevegelsesendringer i det valgte området",
"noChangesFound": "Ingen pikselendringer ble funnet i det valgte området",
"changesFound_one": "Fant {{count}} bevegelsesendring",
"changesFound_other": "Fant {{count}} bevegelsesendringer",
"framesProcessed": "{{count}} bilder behandlet",
"jumpToTime": "Gå til dette tidspunktet",
"results": "Resultater",
"showSegmentHeatmap": "Varmekart",
"newSearch": "Nytt søk",
"clearResults": "Tøm resultater",
"clearROI": "Slett polygon",
"polygonControls": {
"points_one": "{{count}} punkt",
"points_other": "{{count}} punkter",
"undo": "Angre siste punkt",
"reset": "Tilbakestill polygon"
},
"motionHeatmapLabel": "Varmekart for bevegelse",
"dialog": {
"title": "Bevegelsessøk",
"cameraLabel": "Kamera",
"previewAlt": "Forhåndsvisning av kamera for {{camera}}"
},
"timeRange": {
"title": "Søkeperiode",
"start": "Starttid",
"end": "Sluttid"
},
"settings": {
"title": "Søkeinnstillinger",
"parallelMode": "Parallellmodus",
"parallelModeDesc": "Skann flere opptakssegmenter samtidig (raskere, men betydelig mer CPU-intensivt)",
"threshold": "Følsomhetsterskel",
"thresholdDesc": "Lavere verdier detekterer mindre endringer (1255)",
"minArea": "Minimum endringsområde",
"minAreaDesc": "Minimum prosentandel av interesseområdet som må endres for å anses som betydelig",
"frameSkip": "Bilde-sprang",
"frameSkipDesc": "Behandle hvert N-te bilde. Sett denne til kameraets bildefrekvens for å behandle ett bilde i sekundet (f.eks. 5 for et 5 FPS-kamera, 30 for et 30 FPS-kamera). Høyere verdier vil være raskere, men kan gå glipp av korte bevegelseshendelser.",
"maxResults": "Maksimalt antall resultater",
"maxResultsDesc": "Stopp etter dette antallet samsvarende tidsstempler"
},
"errors": {
"noCamera": "Vennligst velg et kamera",
"noROI": "Vennligst tegn et interesseområde",
"noTimeRange": "Vennligst velg en tidsperiode",
"invalidTimeRange": "Sluttid må være etter starttid",
"searchFailed": "Søk mislyktes: {{message}}",
"polygonTooSmall": "Polygonet må ha minst 3 punkter",
"unknown": "Ukjent feil"
},
"changePercentage": "{{percentage}} % endret",
"metrics": {
"title": "Statistikk for søk",
"segmentsScanned": "Segmenter skannet",
"segmentsProcessed": "Behandlet",
"segmentsSkippedInactive": "Hoppet over (ingen aktivitet)",
"segmentsSkippedHeatmap": "Hoppet over (manglende ROI-overlapp)",
"fallbackFullRange": "Fullskanning som reserve",
"framesDecoded": "Bilder dekodet",
"wallTime": "Søketid",
"segmentErrors": "Segmentfeil",
"seconds": "{{seconds}}s"
}
}

View File

@ -1 +1,59 @@
{}
{
"title": "Feilsøkingsavspilling",
"description": "Spill av kameraopptak for feilsøking. Objektlisten viser et tidsforsinket sammendrag av detekterte objekter, og fanen Meldinger viser en strøm av Frigates interne meldinger fra avspillingen.",
"websocket_messages": "Meldinger",
"dialog": {
"title": "Start feilsøkingsavspilling",
"description": "Opprett et midlertidig avspillingskamera som repeterer historisk materiale for å feilsøke problemer med objektdeteksjon og sporing. Avspillingskameraet vil ha samme deteksjonskonfigurasjon som kildekameraet. Velg et tidsrom for å begynne.",
"camera": "Kildekamera",
"timeRange": "Tidsrom",
"preset": {
"1m": "Siste minutt",
"5m": "Siste 5 minutter",
"timeline": "Fra tidslinje",
"custom": "Egendefinert"
},
"startButton": "Start avspilling",
"selectFromTimeline": "Velg",
"starting": "Starter avspilling...",
"startLabel": "Start",
"endLabel": "Slutt",
"toast": {
"error": "Kunne ikke starte feilsøkingsavspilling: {{error}}",
"alreadyActive": "En avspillingsøkt er allerede aktiv",
"stopError": "Kunne ikke stoppe feilsøkingsavspilling: {{error}}",
"goToReplay": "Gå til avspilling"
}
},
"page": {
"noSession": "Ingen aktiv feilsøkingsøkt",
"noSessionDesc": "Start en feilsøkingsavspilling fra Historikk-visningen ved å klikke på Handlinger-knappen i verktøylinjen og velge Feilsøkingsavspilling.",
"goToRecordings": "Gå til historikk",
"preparingClip": "Forbereder klipp…",
"preparingClipDesc": "Frigate syr sammen opptak for det valgte tidsrommet. Dette kan ta et minutt for lengre perioder.",
"startingCamera": "Starter feilsøkingsavspilling…",
"startError": {
"title": "Kunne ikke starte feilsøkingsavspilling",
"back": "Tilbake til historikk"
},
"sourceCamera": "Kildekamera",
"replayCamera": "Avspillingskamera",
"initializingReplay": "Initialiserer feilsøkingsavspilling...",
"stoppingReplay": "Stopper feilsøkingsavspilling...",
"stopReplay": "Stopp avspilling",
"confirmStop": {
"title": "Stoppe feilsøkingsavspilling?",
"description": "Dette vil stoppe økten og slette alle midlertidige data. Er du sikker?",
"confirm": "Stopp avspilling",
"cancel": "Avbryt"
},
"activity": "Aktivitet",
"objects": "Objektliste",
"audioDetections": "Lyd-deteksjoner",
"noActivity": "Ingen aktivitet detektert",
"activeTracking": "Aktiv sporing",
"noActiveTracking": "Ingen aktiv sporing",
"configuration": "Konfigurasjon",
"configurationDesc": "Finjuster innstillinger for bevegelsesdeteksjon og objektsporing for feilsøkingskameraet. Ingen endringer lagres i din Frigate-konfigurasjonsfil."
}
}

View File

@ -61,8 +61,8 @@
"cameraLpr": "Kjennemerke-gjenkjenning",
"integrationLpr": "Kjennemerke-gjenkjenning",
"systemLogging": "Logging",
"cameraAudioEvents": "Lydhendelser",
"globalAudioEvents": "Lydhendelser",
"cameraAudioEvents": "Lyd-deteksjon",
"globalAudioEvents": "Lyd-deteksjon",
"cameraAudioTranscription": "Lydtranskripsjon",
"integrationAudioTranscription": "Lydtranskripsjon",
"systemDetectorHardware": "Maskinvare for detektor",
@ -791,6 +791,14 @@
"plusModelType": {
"userModel": "Finjustert",
"baseModel": "Basismodell"
},
"noModelLoaded": "Ingen Frigate+-modell er lastet inn for øyeblikket.",
"selectModel": "Velg en modell",
"noModelsAvailable": "Ingen modeller tilgjengelig",
"filter": {
"ariaLabel": "Filtrer modeller etter type",
"baseModels": "Basismodeller",
"fineTunedModels": "Finjusterte modeller"
}
},
"title": "Frigate+ Innstillinger",
@ -1341,7 +1349,8 @@
},
"hikvision": {
"substreamWarning": "Substrøm 1 er låst til lav oppløsning. Mange Hikvision-kameraer støtter flere substrømmer som må aktiveres i kameraets innstillinger. Det anbefales å sjekke og benytte disse strømmene hvis de er tilgjengelige."
}
},
"resolutionUnknown": "Oppløsningen for denne strømmen kunne ikke fastslås. Du bør angi deteksjonsoppløsningen manuelt i innstillingene eller i konfigurasjonen din."
}
}
},
@ -1358,7 +1367,13 @@
"enableSuccess": "Aktiverte {{cameraName}} i konfigurasjonen. Start Frigate på nytt for å ta i bruk endringene.",
"enableLabel": "Aktiverte kameraer",
"enableDesc": "Deaktiver et aktivert kamera midlertidig frem til Frigate starter på nytt. Deaktivering av et kamera stopper all prosessering av kameraets strømmer. Deteksjon, opptak og feilsøking vil være utilgjengelig.<br /> <em>Merk: Dette deaktiverer ikke videreformidling (restream) i go2rtc.</em>",
"disableLabel": "Deaktiverte kameraer"
"disableLabel": "Deaktiverte kameraer",
"friendlyName": {
"edit": "Rediger visningsnavn for kamera",
"title": "Rediger visningsnavn",
"description": "Angi visningsnavnet som skal brukes for dette kameraet i Frigate-grensesnittet. La feltet stå tomt for å bruke kamera-ID.",
"rename": "Omdøp"
}
},
"cameraConfig": {
"add": "Legg til kamera",
@ -1408,7 +1423,16 @@
"description": "Sletting av et kamera vil fjerne alle opptak, sporede objekter og konfigurasjon for dette kameraet permanent. Eventuelle go2rtc-strømmer tilknyttet kameraet må eventuelt fjernes manuelt.",
"selectPlaceholder": "Velg kamera..."
},
"deleteCamera": "Slett kamera"
"deleteCamera": "Slett kamera",
"cameraType": {
"title": "Kameratype",
"label": "Kameratype",
"description": "Angi type for hvert kamera. Dedikerte LPR-kameraer er spesialkameraer med kraftig optisk zoom for å fange opp kjennemerker på kjøretøy langt unna. De fleste kameraer bør bruke typen \"Normal\", med mindre kameraet er spesifikt for gjenkjenning av kjennemerker og har et snevert fokus på kjennemerker.",
"normal": "Normal",
"dedicatedLpr": "Dedikert LPR (lesing av kjennemerker)",
"saveSuccess": "Kameratype oppdatert for {{cameraName}}. Start Frigate på nytt for å bruke endringene."
},
"description": "Legg til, rediger og slett kameraer, kontroller hvilke kameraer som er aktivert, og konfigurer overstyringer for hver profil og kameratype. For å konfigurere strømmer, deteksjon, bevegelse og andre kameraspesifikke innstillinger, velg den aktuelle seksjonen under Kamerakonfigurasjon."
},
"cameraReview": {
"title": "Innstillinger for kamerainspeksjon",
@ -1572,7 +1596,9 @@
"options": {
"embeddings": "Vektorrepresentasjoner",
"tools": "Verktøy",
"vision": "Bildegjenkjenning"
"vision": "Bildegjenkjenning",
"descriptions": "Beskrivelser",
"chat": "Chat"
}
},
"additionalProperties": {
@ -1645,7 +1671,24 @@
"overriddenBaseConfigTooltip": "{{profile}}-profilen overstyrer konfigurasjonsinnstillinger i denne seksjonen",
"overriddenGlobalTooltip": "Dette kameraet overstyrer globale konfigurasjonsinnstillinger i denne seksjonen",
"overriddenBaseConfig": "Overstyrt (Basiskonfigurasjon)",
"overriddenGlobal": "Overstyrt (Global)"
"overriddenGlobal": "Overstyrt (Global)",
"overriddenInCameras": {
"label_one": "Overstyrt i {{count}} kamera",
"label_other": "Overstyrt i {{count}} kameraer",
"tooltip_one": "{{count}} kamera overstyrer verdier i denne seksjonen. Klikk for å se detaljer.",
"tooltip_other": "{{count}} kameraer overstyrer verdier i denne seksjonen. Klikk for å se detaljer.",
"heading_one": "Denne globale seksjonen har felt som er overstyrt i {{count}} kamera.",
"heading_other": "Denne globale seksjonen har felt som er overstyrt i {{count}} kameraer.",
"othersField_one": "{{count}} annen",
"othersField_other": "{{count}} andre",
"profilePrefix": "{{profile}}-profil: {{fields}}"
},
"overriddenGlobalHeading_one": "Dette kameraet overstyrer {{count}} felt fra den globale konfigurasjonen:",
"overriddenGlobalHeading_other": "Dette kameraet overstyrer {{count}} felt fra den globale konfigurasjonen:",
"overriddenGlobalNoDeltas": "Dette kameraet overstyrer den globale konfigurasjonen, men ingen feltverdier er forskjellige.",
"overriddenBaseConfigHeading_one": "{{profile}}-profilen overstyrer {{count}} felt fra basiskonfigurasjonen:",
"overriddenBaseConfigHeading_other": "{{profile}}-profilen overstyrer {{count}} felt fra basiskonfigurasjonen:",
"overriddenBaseConfigNoDeltas": "{{profile}}-profilen overstyrer denne seksjonen, men ingen feltverdier er forskjellige fra basiskonfigurasjonen."
},
"detectionModel": {
"plusActive": {
@ -1744,18 +1787,21 @@
"review": {
"allNonAlertDetections": "All aktivitet som ikke er et varsel, vil bli inkludert som deteksjoner.",
"detectDisabled": "Objektdeteksjon er deaktivert. Inspeksjonselementer krever detekterte objekter for å kategorisere varsler og deteksjoner.",
"recordDisabled": "Opptak er deaktivert, inspeksjonselementer vil ikke bli generert."
"recordDisabled": "Opptak er deaktivert, inspeksjonselementer vil ikke bli generert.",
"genaiImageSourceRecordingsRecordDisabled": "Bildekilde er satt til \"opptak\", men opptak er deaktivert. Frigate vil falle tilbake til forhåndsvisningsbilder."
},
"detectors": {
"mixedTypesSuggestion": "Alle detektorer må bruke samme type. Fjern eksisterende detektorer eller velg {{type}}.",
"mixedTypes": "Alle detektorer må bruke samme type. Fjern eksisterende detektorer for å bruke en annen type."
},
"faceRecognition": {
"globalDisabled": "Ansiktsgjenkjenning er ikke aktivert på globalt nivå. Aktiver det i globale innstillinger for at ansiktsgjenkjenning på kameranivå skal fungere.",
"personNotTracked": "Ansiktsgjenkjenning krever at objektet 'person' spores. Sørg for at 'person' er i listen over objektsporing."
"globalDisabled": "Utvidelse for ansiktsgjenkjenning må være aktivert for at funksjoner for ansiktsgjenkjenning skal fungere på dette kameraet.",
"personNotTracked": "Ansiktsgjenkjenning krever at objektet 'person' spores. Aktiver 'person' under Objekter for dette kameraet.",
"modelSizeLarge": "Den store (large) modellen krever GPU eller NPU for akseptabel ytelse. Bruk liten (small) på systemer med kun CPU."
},
"detect": {
"fpsGreaterThanFive": "Det anbefales ikke å sette FPS for deteksjon høyere enn 5."
"fpsGreaterThanFive": "Det anbefales ikke å sette FPS for deteksjon høyere enn 5. Høyere verdier kan føre til ytelsesproblemer uten å gi noen fordeler.",
"disabled": "Objektdeteksjon er deaktivert. Stillbilder, inspeksjonselementer og utvidelser som ansiktsgjenkjenning, lesing av kjennemerker og generativ AI vil ikke fungere."
},
"birdseye": {
"objectsModeDetectDisabled": "Fugleperspektiv er satt til 'objekter'-modus, men objektdeteksjon er deaktivert for dette kameraet. Kameraet vil ikke vises i Fugleperspektiv."
@ -1773,8 +1819,15 @@
"detectDisabled": "Objektdeteksjon er deaktivert. Stillbilder genereres fra sporede objekter og vil ikke bli opprettet."
},
"lpr": {
"globalDisabled": "Identifisering av kjennemerker er ikke aktivert på globalt nivå. Aktiver det i globale innstillinger for at identifisering på kameranivå skal fungere.",
"vehicleNotTracked": "Identifisering av kjnnemerker krever at 'bil' eller 'motorsykkel' spores."
"globalDisabled": "Utvidelse for identifisering av kjennemerker må være aktivert for at LPR-funksjoner skal fungere på dette kameraet.",
"vehicleNotTracked": "Identifisering av kjennemerker krever at 'bil' eller 'motorsykkel' spores. Aktiver 'bil' eller 'motorsykkel' under Objekter for dette kameraet.",
"modelSizeLarge": "Den store (large) modellen er optimalisert for kjennemerker over flere linjer. Den lille (small) modellen gir bedre ytelse og bør brukes med mindre din region bruker skiltformater med flere linjer."
},
"objects": {
"genaiNoDescriptionsProvider": "Du må konfigurere en GenAI-leverandør med rollen \"beskrivelser\" for at beskrivelser skal kunne genereres."
},
"semanticSearch": {
"jinav2SmallModelSize": "Størrelsen \"liten\" med Jina V2-modellen har høyt minnebruk og beregningskostnad. Den \"store\" modellen med en dedikert GPU anbefales."
}
},
"maintenance": {
@ -1839,7 +1892,14 @@
},
"onvif": {
"profileAuto": "Auto",
"profileLoading": "Laster profiler..."
"profileLoading": "Laster profiler...",
"autotracking": {
"zooming": {
"disabled": "Deaktivert",
"absolute": "Absolutt",
"relative": "Relativ"
}
}
},
"confirmReset": "Bekreft nullstilling",
"resetToDefaultDescription": "Dette vil nullstille alle innstillinger i denne seksjonen til standardverdiene. Denne handlingen kan ikke angres.",
@ -1903,5 +1963,67 @@
"bl": "Nederst til venstre",
"tr": "Øverst til høyre",
"tl": "Øverst til venstre"
},
"birdseye": {
"trackingMode": {
"objects": "Objekter",
"motion": "Bevegelse",
"continuous": "Kontinuerlig"
}
},
"snapshot": {
"retainMode": {
"all": "Alle",
"motion": "Bevegelse",
"active_objects": "Aktive objekter"
}
},
"ui": {
"timeFormat": {
"browser": "Nettleser",
"12hour": "12 timer",
"24hour": "24 timer"
},
"TimeOrDateStyle": {
"full": "Full",
"long": "Lang",
"medium": "Middels",
"short": "Kort"
},
"unitSystem": {
"metric": "Metrisk",
"imperial": "Imperial"
}
},
"review": {
"imageSource": {
"recordings": "Opptak",
"previews": "Forhåndsvisninger"
}
},
"logger": {
"logLevel": {
"debug": "Debug",
"info": "Info",
"warning": "Advarsel",
"error": "Feil",
"critical": "Kritisk"
}
},
"modelSize": {
"small": "Liten",
"large": "Stor"
},
"retainMode": {
"all": "Alle",
"motion": "Bevegelse",
"active_objects": "Aktive objekter"
},
"previewQuality": {
"very_high": "Svært høy",
"high": "Høy",
"medium": "Middels",
"low": "Lav",
"very_low": "Svært lav"
}
}

View File

@ -210,6 +210,9 @@
"expectedFps": "Forventet BPS",
"reconnectsLastHour": "Gjentatte tilkoblinger (siste time)",
"stallsLastHour": "Avbrudd (siste time)"
},
"noCameras": {
"title": "Ingen kameraer funnet"
}
},
"enrichments": {

View File

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

View File

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

View File

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

View File

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

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