Compare commits

..
Author SHA1 Message Date
1fa7ce5486 Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Cantonese (Traditional Han script))

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Cantonese (Traditional Han script))

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Cantonese (Traditional Han script))

Currently translated at 100.0% (179 of 179 strings)

Translated using Weblate (Cantonese (Traditional Han script))

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Cantonese (Traditional Han script))

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Cantonese (Traditional Han script))

Currently translated at 100.0% (53 of 53 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: beginner2047 <leoywng44@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/yue_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/yue_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/yue_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/yue_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/yue_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/yue_Hant/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2025-05-13 08:30:20 -06:00
bc74ba5b35 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% (50 of 50 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (179 of 179 strings)

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% (339 of 339 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (118 of 118 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (53 of 53 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/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/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-settings/nb_NO/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2025-05-13 08:30:20 -06:00
caebc583da Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 90.0% (54 of 60 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 95.8% (23 of 24 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 98.1% (52 of 53 strings)

Co-authored-by: GuoQing Liu <842607283@qq.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/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-system/zh_Hans/
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-system
2025-05-13 08:30:20 -06:00
57e933e68a Added translation using Weblate (Urdu)
Added translation using Weblate (Urdu)

Added translation using Weblate (Urdu)

Added translation using Weblate (Urdu)

Added translation using Weblate (Urdu)

Added translation using Weblate (Urdu)

Update translation files

Updated by "Squash Git commits" add-on in Weblate.

Added translation using Weblate (Urdu)

Added translation using Weblate (Urdu)

Added translation using Weblate (Urdu)

Added translation using Weblate (Urdu)

Added translation using Weblate (Urdu)

Added translation using Weblate (Urdu)

Added translation using Weblate (Urdu)

Update translation files

Updated by "Squash Git commits" add-on in Weblate.

Translated using Weblate (Urdu)

Currently translated at 4.9% (21 of 427 strings)

Translated using Weblate (Urdu)

Currently translated at 4.6% (20 of 427 strings)

Update translation files

Updated by "Squash Git commits" add-on in Weblate.

Translated using Weblate (Urdu)

Currently translated at 4.2% (18 of 427 strings)

Translated using Weblate (Urdu)

Currently translated at 2.5% (11 of 427 strings)

Added translation using Weblate (Urdu)

Translated using Weblate (Urdu)

Currently translated at 2.1% (9 of 427 strings)

Added translation using Weblate (Urdu)

Added translation using Weblate (Urdu)

Translated using Weblate (Urdu)

Currently translated at 1.8% (8 of 427 strings)

Added translation using Weblate (Urdu)

Translated using Weblate (Urdu)

Currently translated at 1.1% (5 of 427 strings)

Added translation using Weblate (Urdu)

Translated using Weblate (Urdu)

Currently translated at 0.9% (4 of 427 strings)

Added translation using Weblate (Urdu)

Added translation using Weblate (Urdu)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: yousaf465 <yousaf465@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/ur/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
2025-05-13 08:30:20 -06:00
c104913f81 Translated using Weblate (Slovenian)
Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Slovenian)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Slovenian)

Currently translated at 100.0% (118 of 118 strings)

Translated using Weblate (Slovenian)

Currently translated at 12.4% (53 of 427 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mitja Ahlin <mitja@ahlin.si>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/sl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/sl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/sl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/sl/
Translation: Frigate NVR/audio
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-configeditor
Translation: Frigate NVR/views-events
2025-05-13 08:30:20 -06:00
d38a5659a9 Added translation using Weblate (Persian)
Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Added translation using Weblate (Persian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Omid Nateghi <MasterDeveloper313@gmail.com>
2025-05-13 08:30:20 -06:00
0c54d2c47c Translated using Weblate (French)
Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (French)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (French)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (French)

Currently translated at 100.0% (179 of 179 strings)

Translated using Weblate (French)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (French)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (French)

Currently translated at 100.0% (333 of 333 strings)

Translated using Weblate (French)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (French)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (French)

Currently translated at 100.0% (333 of 333 strings)

Translated using Weblate (French)

Currently translated at 100.0% (48 of 48 strings)

Translated using Weblate (French)

Currently translated at 100.0% (177 of 177 strings)

Translated using Weblate (French)

Currently translated at 100.0% (177 of 177 strings)

Translated using Weblate (French)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (French)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (French)

Currently translated at 100.0% (53 of 53 strings)

Co-authored-by: Apocoloquintose <bertrand.moreux@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/fr/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2025-05-13 08:30:20 -06:00
4bb52f0357 Translated using Weblate (Spanish)
Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (179 of 179 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (53 of 53 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/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-facelibrary/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/es/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2025-05-13 08:30:20 -06:00
c1594b2482 Translated using Weblate (Dutch)
Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (48 of 48 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (118 of 118 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (53 of 53 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Marijn <168113859+Marijn0@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/nl/
Translation: Frigate NVR/audio
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
2025-05-13 08:30:20 -06:00
41919f9648 Translated using Weblate (Indonesian)
Currently translated at 5.3% (6 of 113 strings)

Translated using Weblate (Indonesian)

Currently translated at 1.8% (6 of 333 strings)

Translated using Weblate (Indonesian)

Currently translated at 12.5% (6 of 48 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (6 of 6 strings)

Translated using Weblate (Indonesian)

Currently translated at 7.5% (6 of 80 strings)

Translated using Weblate (Indonesian)

Currently translated at 11.3% (6 of 53 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.6% (6 of 9 strings)

Translated using Weblate (Indonesian)

Currently translated at 5.4% (6 of 110 strings)

Translated using Weblate (Indonesian)

Currently translated at 30.4% (7 of 23 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Indonesian)

Currently translated at 28.0% (7 of 25 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Indonesian)

Currently translated at 10.7% (7 of 65 strings)

Translated using Weblate (Indonesian)

Currently translated at 14.5% (7 of 48 strings)

Translated using Weblate (Indonesian)

Currently translated at 16.2% (7 of 43 strings)

Translated using Weblate (Indonesian)

Currently translated at 77.7% (7 of 9 strings)

Translated using Weblate (Indonesian)

Currently translated at 5.9% (7 of 118 strings)

Translated using Weblate (Indonesian)

Currently translated at 3.3% (6 of 177 strings)

Translated using Weblate (Indonesian)

Currently translated at 6.5% (28 of 427 strings)

Translated using Weblate (Indonesian)

Currently translated at 3.5% (4 of 113 strings)

Translated using Weblate (Indonesian)

Currently translated at 1.2% (4 of 333 strings)

Translated using Weblate (Indonesian)

Currently translated at 8.3% (4 of 48 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.6% (4 of 6 strings)

Translated using Weblate (Indonesian)

Currently translated at 5.0% (4 of 80 strings)

Translated using Weblate (Indonesian)

Currently translated at 7.5% (4 of 53 strings)

Translated using Weblate (Indonesian)

Currently translated at 55.5% (5 of 9 strings)

Translated using Weblate (Indonesian)

Currently translated at 4.5% (5 of 110 strings)

Translated using Weblate (Indonesian)

Currently translated at 21.7% (5 of 23 strings)

Translated using Weblate (Indonesian)

Currently translated at 71.4% (5 of 7 strings)

Translated using Weblate (Indonesian)

Currently translated at 20.0% (5 of 25 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Indonesian)

Currently translated at 7.6% (5 of 65 strings)

Translated using Weblate (Indonesian)

Currently translated at 10.4% (5 of 48 strings)

Translated using Weblate (Indonesian)

Currently translated at 11.6% (5 of 43 strings)

Translated using Weblate (Indonesian)

Currently translated at 55.5% (5 of 9 strings)

Translated using Weblate (Indonesian)

Currently translated at 4.2% (5 of 118 strings)

Translated using Weblate (Indonesian)

Currently translated at 2.2% (4 of 177 strings)

Translated using Weblate (Indonesian)

Currently translated at 6.0% (26 of 427 strings)

Co-authored-by: Catto <sisharyadi@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Viktor Stier <viktor-stier@gmx.de>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-icons/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-input/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-recording/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/id/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-auth
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/components-icons
Translation: Frigate NVR/components-input
Translation: Frigate NVR/components-player
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-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-recording
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2025-05-13 08:30:20 -06:00
79e0fd1343 Translated using Weblate (Italian)
Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (177 of 177 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (53 of 53 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/views-events/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/it/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2025-05-13 08:30:20 -06:00
b1b78feec2 Translated using Weblate (Polish)
Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (333 of 333 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (65 of 65 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (177 of 177 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mateusz Paś <piciuok@gmail.com>
Co-authored-by: Patryk Smoliński <smolinski.patryk@mensa.org.pl>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/pl/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2025-05-13 08:30:20 -06:00
f27538607e Translated using Weblate (Hungarian)
Currently translated at 7.5% (25 of 333 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Pintér István <thestevepappa@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/hu/
Translation: Frigate NVR/views-settings
2025-05-13 08:30:20 -06:00
2fc70c62a5 Translated using Weblate (Portuguese)
Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Portuguese)

Currently translated at 62.7% (209 of 333 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (48 of 48 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (65 of 65 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (48 of 48 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (177 of 177 strings)

Translated using Weblate (Portuguese)

Currently translated at 81.4% (92 of 113 strings)

Translated using Weblate (Portuguese)

Currently translated at 57.0% (190 of 333 strings)

Translated using Weblate (Portuguese)

Currently translated at 85.4% (41 of 48 strings)

Translated using Weblate (Portuguese)

Currently translated at 86.7% (46 of 53 strings)

Translated using Weblate (Portuguese)

Currently translated at 88.2% (98 of 111 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Portuguese)

Currently translated at 95.8% (46 of 48 strings)

Translated using Weblate (Portuguese)

Currently translated at 76.2% (135 of 177 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (118 of 118 strings)

Translated using Weblate (Portuguese)

Currently translated at 44.4% (190 of 427 strings)

Translated using Weblate (Portuguese)

Currently translated at 67.7% (120 of 177 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Interlig <haylanzinho@gmail.com>
Co-authored-by: Ivan Martins Pereira <vodikus@users.noreply.hosted.weblate.org>
Co-authored-by: Peter Williams BA Hons MBCS <hello@p-williams.com>
Co-authored-by: Артём Владимиров <artyomka71@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/pt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/pt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/pt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/pt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/pt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/pt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/pt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/pt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/pt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/pt/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/pt/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2025-05-13 08:30:20 -06:00
a3890c0304 Added translation using Weblate (Japanese)
Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Update translation files

Updated by "Squash Git commits" add-on in Weblate.

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Added translation using Weblate (Japanese)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Tomoya Hashimoto <tomoya.hashimoto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/
Translation: Frigate NVR/common
2025-05-13 08:30:20 -06:00
953d7d0428 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (179 of 179 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (333 of 333 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (333 of 333 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (48 of 48 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (43 of 43 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (177 of 177 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Sotski Eugene <jekakmail@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic4@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-icons/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/uk/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-icons
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2025-05-13 08:30:20 -06:00
1a0e2abb78 Translated using Weblate (Bulgarian)
Currently translated at 1.1% (2 of 177 strings)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Added translation using Weblate (Bulgarian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: elgratea <weblate@fastmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/bg/
Translation: Frigate NVR/common
2025-05-13 08:30:20 -06:00
5b0c7694d3 Translated using Weblate (Russian)
Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (179 of 179 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (333 of 333 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (48 of 48 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (65 of 65 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (177 of 177 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Артём Владимиров <artyomka71@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ru/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/ru/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/ru/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/ru/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/ru/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ru/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/ru/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ru/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
2025-05-13 08:30:20 -06:00
9e0b0778f8 Translated using Weblate (German)
Currently translated at 100.0% (340 of 340 strings)

Translated using Weblate (German)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (German)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (German)

Currently translated at 100.0% (339 of 339 strings)

Translated using Weblate (German)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (German)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (German)

Currently translated at 100.0% (333 of 333 strings)

Translated using Weblate (German)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (German)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (German)

Currently translated at 86.7% (289 of 333 strings)

Translated using Weblate (German)

Currently translated at 86.7% (289 of 333 strings)

Translated using Weblate (German)

Currently translated at 100.0% (48 of 48 strings)

Translated using Weblate (German)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (German)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (German)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (German)

Currently translated at 100.0% (177 of 177 strings)

Translated using Weblate (German)

Currently translated at 71.7% (239 of 333 strings)

Translated using Weblate (German)

Currently translated at 66.9% (223 of 333 strings)

Translated using Weblate (German)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (German)

Currently translated at 100.0% (177 of 177 strings)

Translated using Weblate (German)

Currently translated at 43.8% (146 of 333 strings)

Translated using Weblate (German)

Currently translated at 97.3% (110 of 113 strings)

Translated using Weblate (German)

Currently translated at 43.5% (145 of 333 strings)

Translated using Weblate (German)

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (German)

Currently translated at 39.6% (132 of 333 strings)

Translated using Weblate (German)

Currently translated at 39.6% (132 of 333 strings)

Translated using Weblate (German)

Currently translated at 97.9% (47 of 48 strings)

Translated using Weblate (German)

Currently translated at 100.0% (53 of 53 strings)

Co-authored-by: BOFH90 <michael@becker-lan.de>
Co-authored-by: Darkyputz <darkwing@gmx.li>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J <zippelman@web.de>
Co-authored-by: Mehmet Uyanik <met456@gmail.com>
Co-authored-by: Phil Jope <phil@jope.cloud>
Co-authored-by: Viktor Stier <viktor-stier@gmx.de>
Co-authored-by: engels0n <christian.engelbarts@outlook.de>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/de/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/views-configeditor
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2025-05-13 08:30:20 -06:00
120fff31a7 Translated using Weblate (Turkish)
Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (24 of 24 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (333 of 333 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (177 of 177 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kemal <kemal@korkmazlar.nl>
Co-authored-by: pcislocked <git@pcislocked.net>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/tr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/tr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/tr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/tr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/tr/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2025-05-13 08:30:20 -06:00
Martin WeineltandGitHub 4d4d54d030 Fix various typing issues (#18187)
* Fix the `Any` typing hint treewide

There has been confusion between the Any type[1] and the any function[2]
in typing hints.

[1] https://docs.python.org/3/library/typing.html#typing.Any
[2] https://docs.python.org/3/library/functions.html#any

* Fix typing for various frame_shape members

Frame shapes are most likely defined by height and width, so a single int
cannot express that.

* Wrap gpu stats functions in Optional[]

These can return `None`, so they need to be `Type | None`, which is what
`Optional` expresses very nicely.

* Fix return type in get_latest_segment_datetime

Returns a datetime object, not an integer.

* Make the return type of FrameManager.write optional

This is necessary since the SharedMemoryFrameManager.write function can
return None.

* Fix total_seconds() return type in get_tz_modifiers

The function returns a float, not an int.

https://docs.python.org/3/library/datetime.html#datetime.timedelta.total_seconds

* Account for floating point results in to_relative_box

Because the function uses division the return types may either be int or
float.

* Resolve ruff deprecation warning

The config has been split into formatter and linter, and the global
options are deprecated.
2025-05-13 08:27:20 -06:00
2c9bfaa49c Fixes (#18176)
* Add camera name tooltip to previews in recording view

* Apply face area check to cv2 face detection

* Delete review thumbnails

* Don't import hailo until it is used

* Add comment

* Clean up camera name

* Filter out empty keys when updating yaml config

HA ingress seems to randomly add an equal sign to the PUT urls for updating the config from the UI. This fix prevents empty keys from being processed, but still allows empty values.

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2025-05-13 08:27:07 -06:00
f39ddbc00d Fixes (#18139)
* Catch error and show toast when failing to delete review items

* i18n keys

* add link to speed estimation docs in zone edit pane

* Implement reset of tracked object update for each camera

* Cleanup

* register mqtt callbacks for toggling alerts and detections

* clarify snapshots docs

* clarify semantic search reindexing

* add ukrainian

* adjust date granularity for last recording time

The api endpoint only returns granularity down to the day

* Add amd hardware

* fix crash in face library on initial start after enabling

* Fix recordings view for mobile landscape

The events view incorrectly was displaying two columns on landscape view and it only took up 20% of the screen width. Additionally, in landscape view the timeline was too wide (especially on iPads of various screen sizes) and would overlap the main video

* face rec overfitting instructions

* Clarify

* face docs

* clarify

* clarify

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2025-05-11 12:03:53 -06:00
8094dd4075 Fixes (#18117)
* face library i18n fixes

* face library i18n fixes

* add ability to use ctrl/cmd S to save in the config editor

* Use datetime as ID

* Update metrics inference speed to start with 0 ms

* fix android formatted thumbnail

* ensure role is comma separated and stripped correctly

* improve face library deletion

- add a confirmation dialog
- add ability to select all / delete faces in collections

* Implement lazy loading for video previews

* Force GPU for large embedding model

* GPU is required

* settings i18n fixes

* Don't delete train tab

* webpush debugging logs

* Fix incorrectly copying zones

* copy path data

* Ensure that cache dir exists for Frigate+

* face docs update

* Add description to upload image step to clarify the image

* Clean up

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2025-05-09 07:36:44 -06:00
Felipe SantosandGitHub 52d94231c7 Avoid unhealthy container when Frigate is stopping (#18021) 2025-05-07 19:43:51 -05:00
ac8e647b92 Fixes (#18077)
* fix onvif reinitialization

* api docs: clarify usage of clip.mp4 endpoint

* Always show train tab

* Add description to API

* catch lpr model inference exceptions

* always apply motion mask when using yolov9 plate detection

* lpr faq

* fix incorrect focus when reopening search detail dialog on video tab

* only use keyboard listener in face library when train tab is active

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2025-05-07 16:31:24 -06:00
Josh HawkinsandGitHub da1fb935b4 Refactor async ONVIF (#18093)
* use async/await instead of asyncio.run()

* fix autotracking

* create cameras in same event loop that will use them

* more debug

* try using existing event loop instead of creating a new one

* merge dev

* fixes

* run get_camera_info onvifcontroller calls in dedicated loop

* move coroutine call with loop to api

* use asyncio for autotracking move queues

* clean up

* fix calibration

* improve exception logging
2025-05-07 07:53:29 -06:00
GuoQing LiuandGitHub 83188e7ea4 Add chinese docs (#17954)
* add docs chinese i18n

* fix some broken links

* update some i18n

* update chinese docs

* add chinese community docs

* Change docs i18n chinese label
2025-05-06 08:49:49 -06:00
Nicolas MowenandGitHub 3a69273f0c revert onnx runtime update (#18074)
* revert onnx runtime update

* Fix docs
2025-05-06 09:02:34 -05:00
511542eaf8 Fixes (#18055)
* frigate+ pane i18n fix

* catch more exceptions

* explore search result tooltip i18n fix

* i18n fix

* remove comments about deprecated strftime_fmt

* Catch producers exists but is None

* Formatting

* fix live camera view i18n

* Add default role config for proxy users

This allows users to specify a default role for users when using a proxy for auth. This can be useful for users who can't/don't want to define a header mapping for the remote-role header.

* update reference config and auth docs

* clarify face rec camera level config

* clarify auth docs

* Fix onnx not working with openvino

* Update openvino to fix failed npu plugin check

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2025-05-05 20:42:24 -06:00
idxlicsandGitHub 976863518b Use HF_ENDPOINT env instead of hardcoding https://huggingface.co (#18036)
* Update jina_v1_embedding.py

* Update jina_v2_embedding.py
2025-05-04 19:38:17 -05:00
Nicolas MowenandGitHub 895afcdb0e Various Fixes (#18035)
* Support multi and single core rknn npus

* Update docs config to be more clear
2025-05-04 09:33:27 -06:00
Josh HawkinsandGitHub da2636d6f7 Docs tweaks (#18025)
* face recognition usage instructions

* clarify lpr docs for motorcycles

* person must be detected before face

* add note about coral

* add note about local

* update reference config for face model size

* clarify reference config for face
2025-05-03 20:13:22 -06:00
Felipe SantosandGitHub 27d3be0356 Fix go2rtc homeassistant config dir (#18017) 2025-05-03 10:24:16 -06:00
Nicolas MowenandGitHub fa196f85a7 Opt out of OpenVINO telemetry (#18015) 2025-05-03 08:55:22 -05:00
Blake BlackshearandGitHub 9a3a64a9bd cleanup variants (#18010) 2025-05-03 06:24:30 -06:00
306 changed files with 4213 additions and 1108 deletions
+2 -2
View File
@@ -39,14 +39,14 @@ jobs:
STABLE_TAG=${BASE}:stable
PULL_TAG=${BASE}:${BUILD_TAG}
docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG} docker://${VERSION_TAG}
for variant in standard-arm64 tensorrt tensorrt-jp5 tensorrt-jp6 rk h8l rocm; do
for variant in standard-arm64 tensorrt tensorrt-jp6 rk rocm; do
docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG}-${variant} docker://${VERSION_TAG}-${variant}
done
# stable tag
if [[ "${BUILD_TYPE}" == "stable" ]]; then
docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG} docker://${STABLE_TAG}
for variant in standard-arm64 tensorrt tensorrt-jp5 tensorrt-jp6 rk h8l rocm; do
for variant in standard-arm64 tensorrt tensorrt-jp6 rk rocm; do
docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG}-${variant} docker://${STABLE_TAG}-${variant}
done
fi
+6 -6
View File
@@ -24,11 +24,9 @@
- 通过RTSP重新流传输以减少摄像头的连接数
- 支持WebRTC和MSE,实现低延迟的实时观看
## 文档(英文)
## 社区中文翻译文档
你可以在这里查看文档 https://docs.frigate.video
文档还暂时没有提供翻译,将会在未来提供。
你可以在这里查看文档 https://docs.frigate-cn.video
## 赞助
@@ -60,5 +58,7 @@
## 翻译
我们使用 [Weblate](https://hosted.weblate.org/projects/frigate-nvr/) 平台提供翻译支持,欢迎参与进来一起完善。
## 中文讨论社区
欢迎加入非官方中文讨论QQ群:1043861059
## 非官方中文讨论社区
欢迎加入中文讨论QQ群:1043861059
Bilibilihttps://space.bilibili.com/3546894915602564
+1 -1
View File
@@ -260,7 +260,7 @@ ENTRYPOINT ["/init"]
CMD []
HEALTHCHECK --start-period=300s --start-interval=5s --interval=15s --timeout=5s --retries=3 \
CMD curl --fail --silent --show-error http://127.0.0.1:5000/api/version || exit 1
CMD test -f /dev/shm/.frigate-is-stopping && exit 0; curl --fail --silent --show-error http://127.0.0.1:5000/api/version || exit 1
# Frigate deps with Node.js and NPM for devcontainer
FROM deps AS devcontainer
@@ -25,4 +25,7 @@ elif [[ "${exit_code_service}" -ne 0 ]]; then
fi
fi
# used by the docker healthcheck
touch /dev/shm/.frigate-is-stopping
exec /run/s6/basedir/bin/halt
@@ -4,6 +4,11 @@
set -o errexit -o nounset -o pipefail
# opt out of openvino telemetry
if [ -e /usr/local/bin/opt_in_out ]; then
/usr/local/bin/opt_in_out --opt_out
fi
# Logs should be sent to stdout so that s6 can collect them
# Tell S6-Overlay not to restart this service
@@ -138,5 +138,9 @@ function migrate_db_from_media_to_config() {
fi
}
# remove leftover from last run, not normally needed, but just in case
# used by the docker healthcheck
rm -f /dev/shm/.frigate-is-stopping
migrate_addon_config_dir
migrate_db_from_media_to_config
@@ -1,5 +1,6 @@
import json
import sys
from typing import Any
from ruamel.yaml import YAML
@@ -21,11 +22,11 @@ try:
raw_config = f.read()
if config_file.endswith((".yaml", ".yml")):
config: dict[str, any] = yaml.load(raw_config)
config: dict[str, Any] = yaml.load(raw_config)
elif config_file.endswith(".json"):
config: dict[str, any] = json.loads(raw_config)
config: dict[str, Any] = json.loads(raw_config)
except FileNotFoundError:
config: dict[str, any] = {}
config: dict[str, Any] = {}
path = config.get("ffmpeg", {}).get("path", "default")
if path == "default":
@@ -4,6 +4,7 @@ import json
import os
import sys
from pathlib import Path
from typing import Any
from ruamel.yaml import YAML
@@ -37,13 +38,13 @@ try:
raw_config = f.read()
if config_file.endswith((".yaml", ".yml")):
config: dict[str, any] = yaml.load(raw_config)
config: dict[str, Any] = yaml.load(raw_config)
elif config_file.endswith(".json"):
config: dict[str, any] = json.loads(raw_config)
config: dict[str, Any] = json.loads(raw_config)
except FileNotFoundError:
config: dict[str, any] = {}
config: dict[str, Any] = {}
go2rtc_config: dict[str, any] = config.get("go2rtc", {})
go2rtc_config: dict[str, Any] = config.get("go2rtc", {})
# Need to enable CORS for go2rtc so the frigate integration / card work automatically
if go2rtc_config.get("api") is None:
@@ -53,7 +54,7 @@ elif go2rtc_config["api"].get("origin") is None:
# Need to set default location for HA config
if go2rtc_config.get("hass") is None:
go2rtc_config["hass"] = {"config": "/config"}
go2rtc_config["hass"] = {"config": "/homeassistant"}
# we want to ensure that logs are easy to read
if go2rtc_config.get("log") is None:
@@ -134,7 +135,7 @@ for name in go2rtc_config.get("streams", {}):
# add birdseye restream stream if enabled
if config.get("birdseye", {}).get("restream", False):
birdseye: dict[str, any] = config.get("birdseye")
birdseye: dict[str, Any] = config.get("birdseye")
input = f"-f rawvideo -pix_fmt yuv420p -video_size {birdseye.get('width', 1280)}x{birdseye.get('height', 720)} -r 10 -i {BIRDSEYE_PIPE}"
ffmpeg_cmd = f"exec:{parse_preset_hardware_acceleration_encode(ffmpeg_path, config.get('ffmpeg', {}).get('hwaccel_args', ''), input, '-rtsp_transport tcp -f rtsp {output}')}"
@@ -2,9 +2,10 @@
import json
import os
from typing import Any
base_path = os.environ.get("FRIGATE_BASE_PATH", "")
result: dict[str, any] = {"base_path": base_path}
result: dict[str, Any] = {"base_path": base_path}
print(json.dumps(result))
@@ -2,6 +2,7 @@
import json
import sys
from typing import Any
from ruamel.yaml import YAML
@@ -19,12 +20,12 @@ try:
raw_config = f.read()
if config_file.endswith((".yaml", ".yml")):
config: dict[str, any] = yaml.load(raw_config)
config: dict[str, Any] = yaml.load(raw_config)
elif config_file.endswith(".json"):
config: dict[str, any] = json.loads(raw_config)
config: dict[str, Any] = json.loads(raw_config)
except FileNotFoundError:
config: dict[str, any] = {}
config: dict[str, Any] = {}
tls_config: dict[str, any] = config.get("tls", {"enabled": True})
tls_config: dict[str, Any] = config.get("tls", {"enabled": True})
print(json.dumps(tls_config))
+9 -1
View File
@@ -77,7 +77,7 @@ Changing the secret will invalidate current tokens.
Frigate can be configured to leverage features of common upstream authentication proxies such as Authelia, Authentik, oauth2_proxy, or traefik-forward-auth.
If you are leveraging the authentication of an upstream proxy, you likely want to disable Frigate's authentication. Optionally, if communication between the reverse proxy and Frigate is over an untrusted network, you should set an `auth_secret` in the `proxy` config and configure the proxy to send the secret value as a header named `X-Proxy-Secret`. Assuming this is an untrusted network, you will also want to [configure a real TLS certificate](tls.md) to ensure the traffic can't simply be sniffed to steal the secret.
If you are leveraging the authentication of an upstream proxy, you likely want to disable Frigate's authentication as there is no correspondence between users in Frigate's database and users authenticated via the proxy. Optionally, if communication between the reverse proxy and Frigate is over an untrusted network, you should set an `auth_secret` in the `proxy` config and configure the proxy to send the secret value as a header named `X-Proxy-Secret`. Assuming this is an untrusted network, you will also want to [configure a real TLS certificate](tls.md) to ensure the traffic can't simply be sniffed to steal the secret.
Here is an example of how to disable Frigate's authentication and also ensure the requests come only from your known proxy.
@@ -109,6 +109,14 @@ proxy:
Frigate supports both `admin` and `viewer` roles (see below). When using port `8971`, Frigate validates these headers and subsequent requests use the headers `remote-user` and `remote-role` for authorization.
A default role can be provided. Any value in the mapped `role` header will override the default.
```yaml
proxy:
...
default_role: viewer
```
#### Port Considerations
**Authenticated Port (8971)**
+35 -7
View File
@@ -3,7 +3,7 @@ id: face_recognition
title: Face Recognition
---
Face recognition identifies known individuals by matching detected faces with previously learned facial data. When a known person is recognized, their name will be added as a `sub_label`. This information is included in the UI, filters, as well as in notifications.
Face recognition identifies known individuals by matching detected faces with previously learned facial data. When a known `person` is recognized, their name will be added as a `sub_label`. This information is included in the UI, filters, as well as in notifications.
## Model Requirements
@@ -13,6 +13,12 @@ When running a Frigate+ model (or any custom model that natively detects faces)
When running a default COCO model or another model that does not include `face` as a detectable label, face detection will run via CV2 using a lightweight DNN model that runs on the CPU. In this case, you should _not_ define `face` in your list of objects to track.
:::note
Frigate needs to first detect a `person` before it can detect and recognize a face.
:::
### Face Recognition
Frigate has support for two face recognition model types:
@@ -22,11 +28,13 @@ Frigate has support for two face recognition model types:
In both cases, a lightweight face landmark detection model is also used to align faces before running recognition.
All of these features run locally on your system.
## Minimum System Requirements
The `small` model is optimized for efficiency and runs on the CPU, most CPUs should run the model efficiently.
The `large` model is optimized for accuracy, an integrated or discrete GPU is highly recommended. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation.
The `large` model is optimized for accuracy, an integrated or discrete GPU is required. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation.
## Configuration
@@ -39,7 +47,7 @@ face_recognition:
## Advanced Configuration
Fine-tune face recognition with these optional parameters:
Fine-tune face recognition with these optional parameters at the global level of your config. The only optional parameters that can be set at the camera level are `enabled` and `min_area`.
### Detection
@@ -62,6 +70,13 @@ Fine-tune face recognition with these optional parameters:
- `blur_confidence_filter`: Enables a filter that calculates how blurry the face is and adjusts the confidence based on this.
- Default: `True`.
## Usage
1. **Enable face recognition** in your configuration file and restart Frigate.
2. **Upload your face** using the **Add Face** button's wizard in the Face Library section of the Frigate UI.
3. When Frigate detects and attempts to recognize a face, it will appear in the **Train** tab of the Face Library, along with its associated recognition confidence.
4. From the **Train** tab, you can **assign the face** to a new or existing person to improve recognition accuracy for the future.
## Creating a Robust Training Set
The number of images needed for a sufficient training set for face recognition varies depending on several factors:
@@ -92,17 +107,17 @@ When choosing images to include in the face training set it is recommended to al
### Step 1 - Building a Strong Foundation
When first enabling face recognition it is important to build a foundation of strong images. It is recommended to start by uploading 1-5 "portrait" photos for each person. It is important that the person's face in the photo is straight-on and not turned which will ensure a good starting point.
When first enabling face recognition it is important to build a foundation of strong images. It is recommended to start by uploading 1-5 photos containing just this person's face. It is important that the person's face in the photo is front-facing and not turned, this will ensure a good starting point.
Then it is recommended to use the `Face Library` tab in Frigate to select and train images for each person as they are detected. When building a strong foundation it is strongly recommended to only train on images that are straight-on. Ignore images from cameras that recognize faces from an angle.
Then it is recommended to use the `Face Library` tab in Frigate to select and train images for each person as they are detected. When building a strong foundation it is strongly recommended to only train on images that are front-facing. Ignore images from cameras that recognize faces from an angle.
Aim to strike a balance between the quality of images while also having a range of conditions (day / night, different weather conditions, different times of day, etc.) in order to have diversity in the images used for each person and not have over-fitting.
Once a person starts to be consistently recognized correctly on images that are straight-on, it is time to move on to the next step.
Once a person starts to be consistently recognized correctly on images that are front-facing, it is time to move on to the next step.
### Step 2 - Expanding The Dataset
Once straight-on images are performing well, start choosing slightly off-angle images to include for training. It is important to still choose images where enough face detail is visible to recognize someone.
Once front-facing images are performing well, start choosing slightly off-angle images to include for training. It is important to still choose images where enough face detail is visible to recognize someone.
## FAQ
@@ -122,6 +137,15 @@ This can happen for a few different reasons, but this is usually an indicator th
- When you provide images with different poses, lighting, and expressions, the algorithm extracts features that are consistent across those variations.
- By training on a diverse set of images, the algorithm becomes less sensitive to minor variations and noise in the input image.
Review your face collections and remove most of the unclear or low-quality images. Then, use the **Reprocess** button on each face in the **Train** tab to evaluate how the changes affect recognition scores.
Avoid training on images that already score highly, as this can lead to over-fitting. Instead, focus on relatively clear images that score lower - ideally with different lighting, angles, and conditions—to help the model generalize more effectively.
### Frigate misidentified a face. Can I tell it that a face is "not" a specific person?
No, face recognition does not support negative training (i.e., explicitly telling it who someone is _not_). Instead, the best approach is to improve the training data by using a more diverse and representative set of images for each person.
For more guidance, refer to the section above on improving recognition accuracy.
### I see scores above the threshold in the train tab, but a sub label wasn't assigned?
The Frigate considers the recognition scores across all recognition attempts for each person object. The scores are continually weighted based on the area of the face, and a sub label will only be assigned to person if a person is confidently recognized consistently. This avoids cases where a single high confidence recognition would throw off the results.
@@ -141,3 +165,7 @@ Face recognition does not run on the recording stream, this would be suboptimal
### I get an unknown error when taking a photo directly with my iPhone
By default iOS devices will use HEIC (High Efficiency Image Container) for images, but this format is not supported for uploads. Choosing `large` as the format instead of `original` will use JPG which will work correctly.
## How can I delete the face database and start over?
Frigate does not store anything in its database related to face recognition. You can simply delete all of your faces through the Frigate UI or remove the contents of the `/media/frigate/clips/faces` directory.
@@ -24,3 +24,9 @@ Object detection and enrichments (like Semantic Search, Face Recognition, and Li
- Jetson devices will automatically be detected and used for enrichments in the `-tensorrt-jp6` Frigate image.
Utilizing a GPU for enrichments does not require you to use the same GPU for object detection. For example, you can run the `tensorrt` Docker image for enrichments and still use other dedicated hardware for object detection.
:::note
A Google Coral is a TPU (Tensor Processing Unit), not a dedicated GPU (Graphics Processing Unit) and therefore does not provide any kind of acceleration for Frigate's enrichments.
:::
@@ -3,7 +3,7 @@ id: license_plate_recognition
title: License Plate Recognition (LPR)
---
Frigate can recognize license plates on vehicles and automatically add the detected characters to the `recognized_license_plate` field or a known name as a `sub_label` to tracked objects of type `car`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street.
Frigate can recognize license plates on vehicles and automatically add the detected characters to the `recognized_license_plate` field or a known name as a `sub_label` to tracked objects of type `car` or `motorcycle`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street.
LPR works best when the license plate is clearly visible to the camera. For moving vehicles, Frigate continuously refines the recognition process, keeping the most confident result. However, LPR does not run on stationary vehicles.
@@ -13,7 +13,7 @@ When a plate is recognized, the details are:
- Viewable in the Review Item Details pane in Review (sub labels).
- Viewable in the Tracked Object Details pane in Explore (sub labels and recognized license plates).
- Filterable through the More Filters menu in Explore.
- Published via the `frigate/events` MQTT topic as a `sub_label` (known) or `recognized_license_plate` (unknown) for the `car` tracked object.
- Published via the `frigate/events` MQTT topic as a `sub_label` (known) or `recognized_license_plate` (unknown) for the `car` or `motorcycle` tracked object.
- Published via the `frigate/tracked_object_update` MQTT topic with `name` (if known) and `plate`.
## Model Requirements
@@ -24,7 +24,7 @@ Users without a model that detects license plates can still run LPR. Frigate use
:::note
In the default mode, Frigate's LPR needs to first detect a `car` before it can recognize a license plate. If you're using a dedicated LPR camera and have a zoomed-in view where a `car` will not be detected, you can still run LPR, but the configuration parameters will differ from the default mode. See the [Dedicated LPR Cameras](#dedicated-lpr-cameras) section below.
In the default mode, Frigate's LPR needs to first detect a `car` or `motorcycle` before it can recognize a license plate. If you're using a dedicated LPR camera and have a zoomed-in view where a `car` or `motorcycle` will not be detected, you can still run LPR, but the configuration parameters will differ from the default mode. See the [Dedicated LPR Cameras](#dedicated-lpr-cameras) section below.
:::
@@ -51,7 +51,7 @@ cameras:
enabled: False
```
For non-dedicated LPR cameras, ensure that your camera is configured to detect objects of type `car`, and that a car is actually being detected by Frigate. Otherwise, LPR will not run.
For non-dedicated LPR cameras, ensure that your camera is configured to detect objects of type `car` or `motorcycle`, and that a car or motorcycle is actually being detected by Frigate. Otherwise, LPR will not run.
Like the other real-time processors in Frigate, license plate recognition runs on the camera stream defined by the `detect` role in your config. To ensure optimal performance, select a suitable resolution for this stream in your camera's firmware that fits your specific scene and requirements.
@@ -87,7 +87,7 @@ Fine-tune the LPR feature using these optional parameters at the global level of
### Matching
- **`known_plates`**: List of strings or regular expressions that assign custom a `sub_label` to `car` objects when a recognized plate matches a known value.
- **`known_plates`**: List of strings or regular expressions that assign custom a `sub_label` to `car` and `motorcycle` objects when a recognized plate matches a known value.
- These labels appear in the UI, filters, and notifications.
- Unknown plates are still saved but are added to the `recognized_license_plate` field rather than the `sub_label`.
- **`match_distance`**: Allows for minor variations (missing/incorrect characters) when matching a detected plate to a known plate.
@@ -217,7 +217,7 @@ With this setup:
- Snapshots will have license plate bounding boxes on them.
- The `frigate/events` MQTT topic will publish tracked object updates.
- Debug view will display `license_plate` bounding boxes.
- If you are using a Frigate+ model and want to submit images from your dedicated LPR camera for model training and fine-tuning, annotate both the `car` and the `license_plate` in the snapshots on the Frigate+ website, even if the car is barely visible.
- If you are using a Frigate+ model and want to submit images from your dedicated LPR camera for model training and fine-tuning, annotate both the `car` / `motorcycle` and the `license_plate` in the snapshots on the Frigate+ website, even if the car is barely visible.
### Using the Secondary LPR Pipeline (Without Frigate+)
@@ -311,9 +311,9 @@ Recognized plates will show as object labels in the debug view and will appear i
If you are still having issues detecting plates, start with a basic configuration and see the debugging tips below.
### Can I run LPR without detecting `car` objects?
### Can I run LPR without detecting `car` or `motorcycle` objects?
In normal LPR mode, Frigate requires a `car` to be detected first before recognizing a license plate. If you have a dedicated LPR camera, you can change the camera `type` to `"lpr"` to use the Dedicated LPR Camera algorithm. This comes with important caveats, though. See the [Dedicated LPR Cameras](#dedicated-lpr-cameras) section above.
In normal LPR mode, Frigate requires a `car` or `motorcycle` to be detected first before recognizing a license plate. If you have a dedicated LPR camera, you can change the camera `type` to `"lpr"` to use the Dedicated LPR Camera algorithm. This comes with important caveats, though. See the [Dedicated LPR Cameras](#dedicated-lpr-cameras) section above.
### How can I improve detection accuracy?
@@ -336,8 +336,8 @@ Use `match_distance` to allow small character mismatches. Alternatively, define
### How do I debug LPR issues?
- View MQTT messages for `frigate/events` to verify detected plates.
- If you are using a Frigate+ model or a model that detects license plates, watch the debug view (Settings --> Debug) to ensure that `license_plate` is being detected with a `car`.
- Watch the debug view to see plates recognized in real-time. For non-dedicated LPR cameras, the `car` label will change to the recognized plate when LPR is enabled and working.
- If you are using a Frigate+ model or a model that detects license plates, watch the debug view (Settings --> Debug) to ensure that `license_plate` is being detected with a `car` or `motorcycle`.
- Watch the debug view to see plates recognized in real-time. For non-dedicated LPR cameras, the `car` or `motorcycle` label will change to the recognized plate when LPR is enabled and working.
- Adjust `detection_threshold` and `recognition_threshold` settings per the suggestions [above](#advanced-configuration).
- Enable `debug_save_plates` to save images of detected text on plates to the clips directory (`/media/frigate/clips/lpr`). Ensure these images are readable and the text is clear.
- Enable debug logs for LPR by adding `frigate.data_processing.common.license_plate: debug` to your `logger` configuration. These logs are _very_ verbose, so only enable this when necessary.
@@ -357,12 +357,16 @@ LPR's performance impact depends on your hardware. Ensure you have at least 4GB
The YOLOv9 license plate detector model will run (and the metric will appear) if you've enabled LPR but haven't defined `license_plate` as an object to track, either at the global or camera level.
If you are detecting `car` on cameras where you don't want to run LPR, make sure you disable LPR it at the camera level. And if you do want to run LPR on those cameras, make sure you define `license_plate` as an object to track.
If you are detecting `car` or `motorcycle` on cameras where you don't want to run LPR, make sure you disable LPR it at the camera level. And if you do want to run LPR on those cameras, make sure you define `license_plate` as an object to track.
### It looks like Frigate picked up my camera's timestamp as the license plate. How can I prevent this?
### It looks like Frigate picked up my camera's timestamp or overlay text as the license plate. How can I prevent this?
This could happen if cars travel close to your camera's timestamp. You could either move the timestamp through your camera's firmware, or apply a mask to it in Frigate.
This could happen if cars or motorcycles travel close to your camera's timestamp or overlay text. You could either move the text through your camera's firmware, or apply a mask to it in Frigate.
If you are using a model that natively detects `license_plate`, add an _object mask_ of type `license_plate` and a _motion mask_ over your timestamp.
If you are using a model that natively detects `license_plate`, add an _object mask_ of type `license_plate` and a _motion mask_ over your text.
If you are using dedicated LPR camera mode, only a _motion mask_ over your timestamp is required.
If you are not using a model that natively detects `license_plate` or you are using dedicated LPR camera mode, only a _motion mask_ over your text is required.
### I see "Error running ... model" in my logs. How can I fix this?
This usually happens when your GPU is unable to compile or use one of the LPR models. Set your `device` to `CPU` and try again. GPU acceleration only provides a slight performance increase, and the models are lightweight enough to run without issue on most CPUs.
+4 -4
View File
@@ -152,7 +152,7 @@ Use this configuration for YOLO-based models. When no custom model path or URL i
```yaml
detectors:
hailo8l:
hailo:
type: hailo8l
device: PCIe
@@ -185,7 +185,7 @@ For SSD-based models, provide either a model path or URL to your compiled SSD mo
```yaml
detectors:
hailo8l:
hailo:
type: hailo8l
device: PCIe
@@ -209,7 +209,7 @@ The Hailo detector supports all YOLO models compiled for Hailo hardware that inc
```yaml
detectors:
hailo8l:
hailo:
type: hailo8l
device: PCIe
@@ -1069,5 +1069,5 @@ wget -O yolov9-t.pt "https://github.com/WongKinYiu/yolov9/releases/download/v0.1
# prepare and run export script
sed -i "s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g" ./models/experimental.py
python3 export.py --weights ./yolov9-t.pt --imgsz 320 --simplify --include onnx
bin/python3 export.py --weights ./yolov9-t.pt --imgsz 320 --simplify --include onnx
```
+8 -3
View File
@@ -78,16 +78,19 @@ proxy:
# Optional: Mapping for headers from upstream proxies. Only used if Frigate's auth
# is disabled.
# NOTE: Many authentication proxies pass a header downstream with the authenticated
# user name. Not all values are supported. It must be a whitelisted header.
# user name and role. Not all values are supported. It must be a whitelisted header.
# See the docs for more info.
header_map:
user: x-forwarded-user
role: x-forwarded-role
# Optional: Url for logging out a user. This sets the location of the logout url in
# the UI.
logout_url: /api/logout
# Optional: Auth secret that is checked against the X-Proxy-Secret header sent from
# the proxy. If not set, all requests are trusted regardless of origin.
auth_secret: None
# Optional: The default role to use for proxy auth. Must be "admin" or "viewer"
default_role: viewer
# Optional: Authentication configuration
auth:
@@ -543,9 +546,9 @@ semantic_search:
model_size: "small"
# Optional: Configuration for face recognition capability
# NOTE: Can (enabled, min_area) be overridden at the camera level
# NOTE: enabled, min_area can be overridden at the camera level
face_recognition:
# Optional: Enable semantic search (default: shown below)
# Optional: Enable face recognition (default: shown below)
enabled: False
# Optional: Minimum face distance score required to mark as a potential match (default: shown below)
unknown_score: 0.8
@@ -560,6 +563,8 @@ face_recognition:
save_attempts: 100
# Optional: Apply a blur quality filter to adjust confidence based on the blur level of the image (default: shown below)
blur_confidence_filter: True
# Optional: Set the model size used face recognition. (default: shown below)
model_size: small
# Optional: Configuration for license plate recognition capability
# NOTE: enabled, min_area, and enhancement can be overridden at the camera level
+4 -4
View File
@@ -19,7 +19,7 @@ For best performance, 16GB or more of RAM and a dedicated GPU are recommended.
## Configuration
Semantic Search is disabled by default, and must be enabled in your config file or in the UI's Settings page before it can be used. Semantic Search is a global configuration setting.
Semantic Search is disabled by default, and must be enabled in your config file or in the UI's Classification Settings page before it can be used. Semantic Search is a global configuration setting.
```yaml
semantic_search:
@@ -29,9 +29,9 @@ semantic_search:
:::tip
The embeddings database can be re-indexed from the existing tracked objects in your database by adding `reindex: True` to your `semantic_search` configuration or by toggling the switch on the Search Settings page in the UI and restarting Frigate. Depending on the number of tracked objects you have, it can take a long while to complete and may max out your CPU while indexing. Make sure to turn the UI's switch off or set the config back to `False` before restarting Frigate again.
The embeddings database can be re-indexed from the existing tracked objects in your database by pressing the "Reindex" button in the Classification Settings in the UI or by adding `reindex: True` to your `semantic_search` configuration and restarting Frigate. Depending on the number of tracked objects you have, it can take a long while to complete and may max out your CPU while indexing.
If you are enabling Semantic Search for the first time, be advised that Frigate does not automatically index older tracked objects. You will need to enable the `reindex` feature in order to do that.
If you are enabling Semantic Search for the first time, be advised that Frigate does not automatically index older tracked objects. You will need to reindex as described above.
:::
@@ -72,7 +72,7 @@ For most users, especially native English speakers, the V1 model remains the rec
:::note
Switching between V1 and V2 requires reindexing your embeddings. To do this, set `reindex: True` in your Semantic Search configuration and restart Frigate. The embeddings from V1 and V2 are incompatible, and failing to reindex will result in incorrect search results.
Switching between V1 and V2 requires reindexing your embeddings. The embeddings from V1 and V2 are incompatible, and failing to reindex will result in incorrect search results.
:::
+1 -1
View File
@@ -5,7 +5,7 @@ title: Snapshots
Frigate can save a snapshot image to `/media/frigate/clips` for each object that is detected named as `<camera>-<id>.jpg`. They are also accessible [via the api](../integrations/api/event-snapshot-events-event-id-snapshot-jpg-get.api.mdx)
For users with Frigate+ enabled, snapshots are accessible in the UI in the Frigate+ pane to allow for quick submission to the Frigate+ service.
Snapshots are accessible in the UI in the Explore pane. This allows for quick submission to the Frigate+ service.
To only save snapshots for objects that enter a specific zone, [see the zone docs](./zones.md#restricting-snapshots-to-specific-zones)
+4 -3
View File
@@ -143,9 +143,10 @@ Inference speeds will vary greatly depending on the GPU and the model used.
With the [rocm](../configuration/object_detectors.md#amdrocm-gpu-detector) detector Frigate can take advantage of many discrete AMD GPUs.
| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time |
| -------- | --------------------- | ------------------------- |
| AMD 780M | ~ 14 ms | 320: ~ 30 ms 640: ~ 60 ms |
| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time |
| --------- | --------------------- | ------------------------- |
| AMD 780M | ~ 14 ms | 320: ~ 30 ms 640: ~ 60 ms |
| AMD 8700G | | 320: ~ 20 ms 640: ~ 40 ms |
## Community Supported Detectors
+19
View File
@@ -17,6 +17,15 @@ const config: Config = {
markdown: {
mermaid: true,
},
i18n: {
defaultLocale: 'en',
locales: ['en'],
localeConfigs: {
en: {
label: 'English',
}
},
},
themeConfig: {
algolia: {
appId: 'WIURGBNBPY',
@@ -82,6 +91,16 @@ const config: Config = {
label: 'Demo',
position: 'right',
},
{
type: 'localeDropdown',
position: 'right',
dropdownItemsAfter: [
{
label: '简体中文(社区翻译)',
href: 'https://docs.frigate-cn.video',
}
]
},
{
href: 'https://github.com/blakeblackshear/frigate',
label: 'GitHub',
@@ -0,0 +1,25 @@
import React, { useEffect, useState } from 'react';
import { useLocation } from '@docusaurus/router';
import styles from './styles.module.css';
export default function LanguageAlert() {
const [showAlert, setShowAlert] = useState(false);
const { pathname } = useLocation();
useEffect(() => {
const userLanguage = navigator?.language || 'en';
const isChineseUser = userLanguage.includes('zh');
setShowAlert(isChineseUser);
}, [pathname]);
if (!showAlert) return null;
return (
<div className={styles.alert}>
<span>检测到您的主要语言为中文您可以访问由中文社区翻译的</span>
<a href={'https://docs.frigate-cn.video'+pathname}>中文文档</a>
<span> 以获得更好的体验</span>
</div>
);
}
@@ -0,0 +1,13 @@
.alert {
padding: 12px;
background: #fff8e6;
border-bottom: 1px solid #ffd166;
text-align: center;
font-size: 15px;
}
.alert a {
color: #1890ff;
font-weight: 500;
margin-left: 6px;
}
+15
View File
@@ -0,0 +1,15 @@
import React from 'react';
import NavbarLayout from '@theme/Navbar/Layout';
import NavbarContent from '@theme/Navbar/Content';
import LanguageAlert from '../../components/LanguageAlert';
export default function Navbar() {
return (
<>
<NavbarLayout>
<NavbarContent />
</NavbarLayout>
<LanguageAlert />
</>
);
}
+2
View File
@@ -2926,6 +2926,8 @@ paths:
tags:
- Media
summary: Recording Clip
description: >-
For iOS devices, use the master.m3u8 HLS link instead of clip.mp4. Safari does not reliably process progressive mp4 files.
operationId: recording_clip__camera_name__start__start_ts__end__end_ts__clip_mp4_get
parameters:
- name: camera_name
+5 -5
View File
@@ -74,7 +74,7 @@ def go2rtc_streams():
)
stream_data = r.json()
for data in stream_data.values():
for producer in data.get("producers", []):
for producer in data.get("producers") or []:
producer["url"] = clean_camera_user_pass(producer.get("url", ""))
return JSONResponse(content=stream_data)
@@ -131,7 +131,7 @@ def metrics(request: Request):
@router.get("/config")
def config(request: Request):
config_obj: FrigateConfig = request.app.frigate_config
config: dict[str, dict[str, any]] = config_obj.model_dump(
config: dict[str, dict[str, Any]] = config_obj.model_dump(
mode="json", warnings="none", exclude_none=True
)
@@ -158,7 +158,7 @@ def config(request: Request):
camera_dict["zones"][zone_name]["color"] = zone.color
# remove go2rtc stream passwords
go2rtc: dict[str, any] = config_obj.go2rtc.model_dump(
go2rtc: dict[str, Any] = config_obj.go2rtc.model_dump(
mode="json", warnings="none", exclude_none=True
)
for stream_name, stream in go2rtc.get("streams", {}).items():
@@ -648,7 +648,7 @@ def plusModels(request: Request, filterByCurrentModelDetector: bool = False):
status_code=400,
)
models: dict[any, any] = request.app.frigate_config.plus_api.get_models()
models: dict[Any, Any] = request.app.frigate_config.plus_api.get_models()
if not models["list"]:
return JSONResponse(
@@ -801,7 +801,7 @@ def hourly_timeline(params: AppTimelineHourlyQueryParameters = Depends()):
count = 0
start = 0
end = 0
hours: dict[str, list[dict[str, any]]] = {}
hours: dict[str, list[dict[str, Any]]] = {}
for t in timeline:
if count == 0:
+6 -4
View File
@@ -261,14 +261,16 @@ def auth(request: Request):
role_header = proxy_config.header_map.role
role = (
request.headers.get(role_header, default="viewer")
request.headers.get(role_header, default=proxy_config.default_role)
if role_header
else "viewer"
else proxy_config.default_role
)
# if comma-separated with "admin", use "admin", else "viewer"
# if comma-separated with "admin", use "admin", else use default role
success_response.headers["remote-role"] = (
"admin" if role and "admin" in role else "viewer"
"admin"
if role and "admin" in [r.strip() for r in role.split(",")]
else proxy_config.default_role
)
return success_response
+6 -7
View File
@@ -1,10 +1,10 @@
"""Object classification APIs."""
import datetime
import logging
import os
import random
import shutil
import string
from typing import Any
import cv2
from fastapi import APIRouter, Depends, Request, UploadFile
@@ -59,7 +59,7 @@ def reclassify_face(request: Request, body: dict = None):
content={"message": "Face recognition is not enabled.", "success": False},
)
json: dict[str, any] = body or {}
json: dict[str, Any] = body or {}
training_file = os.path.join(
FACE_DIR, f"train/{sanitize_filename(json.get('training_file', ''))}"
)
@@ -92,7 +92,7 @@ def train_face(request: Request, name: str, body: dict = None):
content={"message": "Face recognition is not enabled.", "success": False},
)
json: dict[str, any] = body or {}
json: dict[str, Any] = body or {}
training_file_name = sanitize_filename(json.get("training_file", ""))
training_file = os.path.join(FACE_DIR, f"train/{training_file_name}")
event_id = json.get("event_id")
@@ -120,8 +120,7 @@ def train_face(request: Request, name: str, body: dict = None):
)
sanitized_name = sanitize_filename(name)
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
new_name = f"{sanitized_name}-{rand_id}.webp"
new_name = f"{sanitized_name}-{datetime.datetime.now().timestamp()}.webp"
new_file_folder = os.path.join(FACE_DIR, f"{sanitized_name}")
if not os.path.exists(new_file_folder):
@@ -248,7 +247,7 @@ def deregister_faces(request: Request, name: str, body: dict = None):
content={"message": "Face recognition is not enabled.", "success": False},
)
json: dict[str, any] = body or {}
json: dict[str, Any] = body or {}
list_of_ids = json.get("ids", "")
context: EmbeddingsContext = request.app.embeddings
+13 -5
View File
@@ -1,5 +1,6 @@
"""Image and video apis."""
import asyncio
import glob
import logging
import math
@@ -8,6 +9,7 @@ import subprocess as sp
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path as FilePath
from typing import Any
from urllib.parse import unquote
import cv2
@@ -88,7 +90,7 @@ def imagestream(
camera_name: str,
fps: int,
height: int,
draw_options: dict[str, any],
draw_options: dict[str, Any],
):
while True:
# max out at specified FPS
@@ -110,9 +112,12 @@ def imagestream(
@router.get("/{camera_name}/ptz/info")
async def camera_ptz_info(request: Request, camera_name: str):
if camera_name in request.app.frigate_config.cameras:
return JSONResponse(
content=await request.app.onvif.get_camera_info(camera_name),
# Schedule get_camera_info in the OnvifController's event loop
future = asyncio.run_coroutine_threadsafe(
request.app.onvif.get_camera_info(camera_name), request.app.onvif.loop
)
result = future.result()
return JSONResponse(content=result)
else:
return JSONResponse(
content={"success": False, "message": "Camera not found"},
@@ -537,7 +542,10 @@ def recordings(
return JSONResponse(content=list(recordings))
@router.get("/{camera_name}/start/{start_ts}/end/{end_ts}/clip.mp4")
@router.get(
"/{camera_name}/start/{start_ts}/end/{end_ts}/clip.mp4",
description="For iOS devices, use the master.m3u8 HLS link instead of clip.mp4. Safari does not reliably process progressive mp4 files.",
)
def recording_clip(
request: Request,
camera_name: str,
@@ -902,7 +910,7 @@ def event_thumbnail(
elif extension == "webp":
quality_params = [int(cv2.IMWRITE_WEBP_QUALITY), 60]
_, img = cv2.imencode(f".{img}", thumbnail, quality_params)
_, img = cv2.imencode(f".{extension}", thumbnail, quality_params)
thumbnail_bytes = img.tobytes()
return Response(
+2 -1
View File
@@ -2,6 +2,7 @@
import logging
import os
from typing import Any
from cryptography.hazmat.primitives import serialization
from fastapi import APIRouter, Request
@@ -41,7 +42,7 @@ def register_notifications(request: Request, body: dict = None):
else:
username = "admin"
json: dict[str, any] = body or {}
json: dict[str, Any] = body or {}
sub = json.get("sub")
if not sub:
+4
View File
@@ -699,6 +699,10 @@ class FrigateApp:
self.audio_process.terminate()
self.audio_process.join()
# stop the onvif controller
if self.onvif_controller:
self.onvif_controller.close()
# ensure the capture processes are done
for camera, metrics in self.camera_metrics.items():
capture_process = metrics.capture_process
+6 -6
View File
@@ -1,18 +1,18 @@
"""Manage camera activity and updating listeners."""
from collections import Counter
from typing import Callable
from typing import Any, Callable
from frigate.config.config import FrigateConfig
class CameraActivityManager:
def __init__(
self, config: FrigateConfig, publish: Callable[[str, any], None]
self, config: FrigateConfig, publish: Callable[[str, Any], None]
) -> None:
self.config = config
self.publish = publish
self.last_camera_activity: dict[str, dict[str, any]] = {}
self.last_camera_activity: dict[str, dict[str, Any]] = {}
self.camera_all_object_counts: dict[str, Counter] = {}
self.camera_active_object_counts: dict[str, Counter] = {}
self.zone_all_object_counts: dict[str, Counter] = {}
@@ -39,8 +39,8 @@ class CameraActivityManager:
else camera_config.objects.track
)
def update_activity(self, new_activity: dict[str, dict[str, any]]) -> None:
all_objects: list[dict[str, any]] = []
def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None:
all_objects: list[dict[str, Any]] = []
for camera in new_activity.keys():
new_objects = new_activity[camera].get("objects", [])
@@ -93,7 +93,7 @@ class CameraActivityManager:
self.last_camera_activity = new_activity
def compare_camera_activity(
self, camera: str, new_activity: dict[str, any]
self, camera: str, new_activity: dict[str, Any]
) -> None:
all_objects = Counter(
obj["label"].replace("-verified", "") for obj in new_activity
+2 -2
View File
@@ -239,7 +239,7 @@ class CameraState:
self,
frame_name: str,
frame_time: float,
current_detections: dict[str, dict[str, any]],
current_detections: dict[str, dict[str, Any]],
motion_boxes: list[tuple[int, int, int, int]],
regions: list[tuple[int, int, int, int]],
):
@@ -337,7 +337,7 @@ class CameraState:
# TODO: can i switch to looking this up and only changing when an event ends?
# maintain best objects
camera_activity: dict[str, list[any]] = {
camera_activity: dict[str, list[Any]] = {
"motion": len(motion_boxes) > 0,
"objects": [],
}
+3 -3
View File
@@ -2,7 +2,7 @@
import multiprocessing as mp
from multiprocessing.synchronize import Event as MpEvent
from typing import Optional
from typing import Any, Optional
import zmq
@@ -18,7 +18,7 @@ class ConfigPublisher:
self.socket.bind(SOCKET_PUB_SUB)
self.stop_event: MpEvent = mp.Event()
def publish(self, topic: str, payload: any) -> None:
def publish(self, topic: str, payload: Any) -> None:
"""There is no communication back to the processes."""
self.socket.send_string(topic, flags=zmq.SNDMORE)
self.socket.send_pyobj(payload)
@@ -40,7 +40,7 @@ class ConfigSubscriber:
self.socket.setsockopt_string(zmq.SUBSCRIBE, topic)
self.socket.connect(SOCKET_PUB_SUB)
def check_for_update(self) -> Optional[tuple[str, any]]:
def check_for_update(self) -> Optional[tuple[str, Any]]:
"""Returns updated config or None if no update."""
try:
topic = self.socket.recv_string(flags=zmq.NOBLOCK)
+3 -3
View File
@@ -1,7 +1,7 @@
"""Facilitates communication between processes."""
from enum import Enum
from typing import Optional
from typing import Any, Optional
from .zmq_proxy import Publisher, Subscriber
@@ -35,10 +35,10 @@ class DetectionSubscriber(Subscriber):
def check_for_update(
self, timeout: float = None
) -> Optional[tuple[DetectionTypeEnum, any]]:
) -> Optional[tuple[DetectionTypeEnum, Any]]:
return super().check_for_update(timeout)
def _return_object(self, topic: str, payload: any) -> any:
def _return_object(self, topic: str, payload: Any) -> Any:
if payload is None:
return (None, None)
return (DetectionTypeEnum[topic[len(self.topic_base) :]], payload)
+2 -2
View File
@@ -1,7 +1,7 @@
"""Facilitates communication between processes."""
from enum import Enum
from typing import Callable
from typing import Any, Callable
import zmq
@@ -58,7 +58,7 @@ class EmbeddingsRequestor:
self.socket = self.context.socket(zmq.REQ)
self.socket.connect(SOCKET_REP_REQ)
def send_data(self, topic: str, data: any) -> str:
def send_data(self, topic: str, data: Any) -> str:
"""Sends data and then waits for reply."""
try:
self.socket.send_json((topic, data))
+2 -1
View File
@@ -2,6 +2,7 @@
import logging
from enum import Enum
from typing import Any
from .zmq_proxy import Publisher, Subscriber
@@ -27,7 +28,7 @@ class EventMetadataPublisher(Publisher):
def __init__(self) -> None:
super().__init__()
def publish(self, topic: EventMetadataTypeEnum, payload: any) -> None:
def publish(self, topic: EventMetadataTypeEnum, payload: Any) -> None:
super().publish(payload, topic.value)
+4 -2
View File
@@ -1,5 +1,7 @@
"""Facilitates communication between processes."""
from typing import Any
from frigate.events.types import EventStateEnum, EventTypeEnum
from .zmq_proxy import Publisher, Subscriber
@@ -14,7 +16,7 @@ class EventUpdatePublisher(Publisher):
super().__init__("update")
def publish(
self, payload: tuple[EventTypeEnum, EventStateEnum, str, str, dict[str, any]]
self, payload: tuple[EventTypeEnum, EventStateEnum, str, str, dict[str, Any]]
) -> None:
super().publish(payload)
@@ -37,7 +39,7 @@ class EventEndPublisher(Publisher):
super().__init__("finalized")
def publish(
self, payload: tuple[EventTypeEnum, EventStateEnum, str, dict[str, any]]
self, payload: tuple[EventTypeEnum, EventStateEnum, str, dict[str, Any]]
) -> None:
super().publish(payload)
+2 -2
View File
@@ -3,7 +3,7 @@
import multiprocessing as mp
import threading
from multiprocessing.synchronize import Event as MpEvent
from typing import Callable
from typing import Any, Callable
import zmq
@@ -63,7 +63,7 @@ class InterProcessRequestor:
self.socket = self.context.socket(zmq.REQ)
self.socket.connect(SOCKET_REP_REQ)
def send_data(self, topic: str, data: any) -> any:
def send_data(self, topic: str, data: Any) -> Any:
"""Sends data and then waits for reply."""
try:
self.socket.send_json((topic, data))
+2
View File
@@ -213,6 +213,8 @@ class MqttClient(Communicator): # type: ignore[misc]
"motion_contour_area",
"birdseye",
"birdseye_mode",
"review_alerts",
"review_detections",
]
for name in self.config.cameras.keys():
+5
View File
@@ -303,6 +303,9 @@ class WebPushClient(Communicator): # type: ignore[misc]
and len(payload["before"]["data"]["zones"])
== len(payload["after"]["data"]["zones"])
):
logger.debug(
f"Skipping notification for {camera} - message is an update and important fields don't have an update"
)
return
self.last_camera_notification_time[camera] = current_time
@@ -325,6 +328,8 @@ class WebPushClient(Communicator): # type: ignore[misc]
direct_url = f"/review?id={reviewId}" if state == "end" else f"/#{camera}"
ttl = 3600 if state == "end" else 0
logger.debug(f"Sending push notification for {camera}, review ID {reviewId}")
for user in self.web_pushers:
self.send_push_notification(
user=user,
+4 -4
View File
@@ -2,7 +2,7 @@
import json
import threading
from typing import Optional
from typing import Any, Optional
import zmq
@@ -58,7 +58,7 @@ class Publisher:
self.socket = self.context.socket(zmq.PUB)
self.socket.connect(SOCKET_PUB)
def publish(self, payload: any, sub_topic: str = "") -> None:
def publish(self, payload: Any, sub_topic: str = "") -> None:
"""Publish message."""
self.socket.send_string(f"{self.topic}{sub_topic} {json.dumps(payload)}")
@@ -81,7 +81,7 @@ class Subscriber:
def check_for_update(
self, timeout: float = FAST_QUEUE_TIMEOUT
) -> Optional[tuple[str, any]]:
) -> Optional[tuple[str, Any]]:
"""Returns message or None if no update."""
try:
has_update, _, _ = zmq.select([self.socket], [], [], timeout)
@@ -98,5 +98,5 @@ class Subscriber:
self.socket.close()
self.context.destroy()
def _return_object(self, topic: str, payload: any) -> any:
def _return_object(self, topic: str, payload: Any) -> Any:
return payload
+3
View File
@@ -30,3 +30,6 @@ class ProxyConfig(FrigateBaseModel):
default=None,
title="Secret value for proxy authentication.",
)
default_role: Optional[str] = Field(
default="viewer", title="Default role for proxy users."
)
@@ -10,7 +10,7 @@ import random
import re
import string
from pathlib import Path
from typing import List, Optional, Tuple
from typing import Any, List, Optional, Tuple
import cv2
import numpy as np
@@ -25,7 +25,7 @@ from frigate.comms.event_metadata_updater import (
from frigate.const import CLIPS_DIR
from frigate.embeddings.onnx.lpr_embedding import LPR_EMBEDDING_SIZE
from frigate.types import TrackedObjectUpdateTypesEnum
from frigate.util.builtin import EventsPerSecond
from frigate.util.builtin import EventsPerSecond, InferenceSpeed
from frigate.util.image import area
logger = logging.getLogger(__name__)
@@ -36,8 +36,10 @@ WRITE_DEBUG_IMAGES = False
class LicensePlateProcessingMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.plate_rec_speed = InferenceSpeed(self.metrics.alpr_speed)
self.plates_rec_second = EventsPerSecond()
self.plates_rec_second.start()
self.plate_det_speed = InferenceSpeed(self.metrics.yolov9_lpr_speed)
self.plates_det_second = EventsPerSecond()
self.plates_det_second.start()
self.event_metadata_publisher = EventMetadataPublisher()
@@ -79,7 +81,12 @@ class LicensePlateProcessingMixin:
resized_image,
)
outputs = self.model_runner.detection_model([normalized_image])[0]
try:
outputs = self.model_runner.detection_model([normalized_image])[0]
except Exception as e:
logger.warning(f"Error running LPR box detection model: {e}")
return []
outputs = outputs[0, :, :]
if False:
@@ -115,7 +122,11 @@ class LicensePlateProcessingMixin:
norm_img = norm_img[np.newaxis, :]
norm_images.append(norm_img)
outputs = self.model_runner.classification_model(norm_images)
try:
outputs = self.model_runner.classification_model(norm_images)
except Exception as e:
logger.warning(f"Error running LPR classification model: {e}")
return
return self._process_classification_output(images, outputs)
@@ -152,7 +163,10 @@ class LicensePlateProcessingMixin:
norm_image = norm_image[np.newaxis, :]
norm_images.append(norm_image)
outputs = self.model_runner.recognition_model(norm_images)
try:
outputs = self.model_runner.recognition_model(norm_images)
except Exception as e:
logger.warning(f"Error running LPR recognition model: {e}")
return self.ctc_decoder(outputs)
def _process_license_plate(
@@ -968,7 +982,11 @@ class LicensePlateProcessingMixin:
Return the dimensions of the detected plate as [x1, y1, x2, y2].
"""
predictions = self.model_runner.yolov9_detection_model(input)
try:
predictions = self.model_runner.yolov9_detection_model(input)
except Exception as e:
logger.warning(f"Error running YOLOv9 license plate detection model: {e}")
return None
confidence_threshold = self.lpr_config.detection_threshold
@@ -1141,22 +1159,6 @@ class LicensePlateProcessingMixin:
# 5. Return True if previous plate scores higher
return prev_score > curr_score
def __update_yolov9_metrics(self, duration: float) -> None:
"""
Update inference metrics.
"""
self.metrics.yolov9_lpr_speed.value = (
self.metrics.yolov9_lpr_speed.value * 9 + duration
) / 10
def __update_lpr_metrics(self, duration: float) -> None:
"""
Update inference metrics.
"""
self.metrics.alpr_speed.value = (
self.metrics.alpr_speed.value * 9 + duration
) / 10
def _generate_plate_event(self, camera: str, plate: str, plate_score: float) -> str:
"""Generate a unique ID for a plate event based on camera and text."""
now = datetime.datetime.now().timestamp()
@@ -1179,7 +1181,7 @@ class LicensePlateProcessingMixin:
return event_id
def lpr_process(
self, obj_data: dict[str, any], frame: np.ndarray, dedicated_lpr: bool = False
self, obj_data: dict[str, Any], frame: np.ndarray, dedicated_lpr: bool = False
):
"""Look for license plates in image."""
self.metrics.alpr_pps.value = self.plates_rec_second.eps()
@@ -1212,7 +1214,7 @@ class LicensePlateProcessingMixin:
f"{camera}: YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms"
)
self.plates_det_second.update()
self.__update_yolov9_metrics(
self.plate_det_speed.update(
datetime.datetime.now().timestamp() - yolov9_start
)
@@ -1270,7 +1272,7 @@ class LicensePlateProcessingMixin:
)
return
license_plate: Optional[dict[str, any]] = None
license_plate: Optional[dict[str, Any]] = None
if "license_plate" not in self.config.cameras[camera].objects.track:
logger.debug(f"{camera}: Running manual license_plate detection.")
@@ -1281,6 +1283,10 @@ class LicensePlateProcessingMixin:
return
rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
# apply motion mask
rgb[self.config.cameras[camera].motion.mask == 0] = [0, 0, 0]
left, top, right, bottom = car_box
car = rgb[top:bottom, left:right]
@@ -1299,7 +1305,7 @@ class LicensePlateProcessingMixin:
f"{camera}: YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms"
)
self.plates_det_second.update()
self.__update_yolov9_metrics(
self.plate_det_speed.update(
datetime.datetime.now().timestamp() - yolov9_start
)
@@ -1335,7 +1341,7 @@ class LicensePlateProcessingMixin:
return
if obj_data.get("label") in ["car", "motorcycle"]:
attributes: list[dict[str, any]] = obj_data.get(
attributes: list[dict[str, Any]] = obj_data.get(
"current_attributes", []
)
for attr in attributes:
@@ -1413,7 +1419,7 @@ class LicensePlateProcessingMixin:
camera, id, license_plate_frame
)
self.plates_rec_second.update()
self.__update_lpr_metrics(datetime.datetime.now().timestamp() - start)
self.plate_rec_speed.update(datetime.datetime.now().timestamp() - start)
if license_plates:
for plate, confidence, text_area in zip(license_plates, confidences, areas):
@@ -1546,6 +1552,12 @@ class LicensePlateProcessingMixin:
(base64.b64encode(encoded_img).decode("ASCII"), id, camera),
)
if id not in self.detected_license_plates:
if camera not in self.camera_current_cars:
self.camera_current_cars[camera] = []
self.camera_current_cars[camera].append(id)
self.detected_license_plates[id] = {
"plate": top_plate,
"char_confidences": top_char_confidences,
@@ -1555,10 +1567,10 @@ class LicensePlateProcessingMixin:
"last_seen": current_time if dedicated_lpr else None,
}
def handle_request(self, topic, request_data) -> dict[str, any] | None:
def handle_request(self, topic, request_data) -> dict[str, Any] | None:
return
def expire_object(self, object_id: str):
def expire_object(self, object_id: str, camera: str):
if object_id in self.detected_license_plates:
self.detected_license_plates.pop(object_id)
+3 -2
View File
@@ -2,6 +2,7 @@
import logging
from abc import ABC, abstractmethod
from typing import Any
from frigate.config import FrigateConfig
@@ -25,7 +26,7 @@ class PostProcessorApi(ABC):
@abstractmethod
def process_data(
self, data: dict[str, any], data_type: PostProcessDataEnum
self, data: dict[str, Any], data_type: PostProcessDataEnum
) -> None:
"""Processes the data of data type.
Args:
@@ -38,7 +39,7 @@ class PostProcessorApi(ABC):
pass
@abstractmethod
def handle_request(self, request_data: dict[str, any]) -> dict[str, any] | None:
def handle_request(self, request_data: dict[str, Any]) -> dict[str, Any] | None:
"""Handle metadata requests.
Args:
request_data (dict): containing data about requested change to process.
@@ -2,6 +2,7 @@
import datetime
import logging
from typing import Any
import cv2
import numpy as np
@@ -36,7 +37,7 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi):
sub_label_publisher: EventMetadataPublisher,
metrics: DataProcessorMetrics,
model_runner: LicensePlateModelRunner,
detected_license_plates: dict[str, dict[str, any]],
detected_license_plates: dict[str, dict[str, Any]],
):
self.requestor = requestor
self.detected_license_plates = detected_license_plates
@@ -47,7 +48,7 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi):
super().__init__(config, metrics, model_runner)
def process_data(
self, data: dict[str, any], data_type: PostProcessDataEnum
self, data: dict[str, Any], data_type: PostProcessDataEnum
) -> None:
"""Look for license plates in recording stream image
Args:
@@ -214,7 +215,7 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi):
logger.debug(f"Post processing plate: {event_id}, {frame_time}")
self.lpr_process(keyframe_obj_data, frame)
def handle_request(self, topic, request_data) -> dict[str, any] | None:
def handle_request(self, topic, request_data) -> dict[str, Any] | None:
if topic == EmbeddingsRequestEnum.reprocess_plate.value:
event = request_data["event"]
+6 -4
View File
@@ -2,6 +2,7 @@
import logging
from abc import ABC, abstractmethod
from typing import Any
import numpy as np
@@ -24,7 +25,7 @@ class RealTimeProcessorApi(ABC):
pass
@abstractmethod
def process_frame(self, obj_data: dict[str, any], frame: np.ndarray) -> None:
def process_frame(self, obj_data: dict[str, Any], frame: np.ndarray) -> None:
"""Processes the frame with object data.
Args:
obj_data (dict): containing data about focused object in frame.
@@ -37,8 +38,8 @@ class RealTimeProcessorApi(ABC):
@abstractmethod
def handle_request(
self, topic: str, request_data: dict[str, any]
) -> dict[str, any] | None:
self, topic: str, request_data: dict[str, Any]
) -> dict[str, Any] | None:
"""Handle metadata requests.
Args:
topic (str): topic that dictates what work is requested.
@@ -50,10 +51,11 @@ class RealTimeProcessorApi(ABC):
pass
@abstractmethod
def expire_object(self, object_id: str) -> None:
def expire_object(self, object_id: str, camera: str) -> None:
"""Handle objects that are no longer detected.
Args:
object_id (str): id of object that is no longer detected.
camera (str): name of camera that object was detected on.
Returns:
None.
+4 -3
View File
@@ -2,6 +2,7 @@
import logging
import os
from typing import Any
import cv2
import numpy as np
@@ -35,8 +36,8 @@ class BirdRealTimeProcessor(RealTimeProcessorApi):
super().__init__(config, metrics)
self.interpreter: Interpreter = None
self.sub_label_publisher = sub_label_publisher
self.tensor_input_details: dict[str, any] = None
self.tensor_output_details: dict[str, any] = None
self.tensor_input_details: dict[str, Any] = None
self.tensor_output_details: dict[str, Any] = None
self.detected_birds: dict[str, float] = {}
self.labelmap: dict[int, str] = {}
@@ -152,6 +153,6 @@ class BirdRealTimeProcessor(RealTimeProcessorApi):
def handle_request(self, topic, request_data):
return None
def expire_object(self, object_id):
def expire_object(self, object_id, camera):
if object_id in self.detected_birds:
self.detected_birds.pop(object_id)
+39 -17
View File
@@ -5,10 +5,8 @@ import datetime
import json
import logging
import os
import random
import shutil
import string
from typing import Optional
from typing import Any, Optional
import cv2
import numpy as np
@@ -27,7 +25,7 @@ from frigate.data_processing.common.face.model import (
FaceRecognizer,
)
from frigate.types import TrackedObjectUpdateTypesEnum
from frigate.util.builtin import EventsPerSecond
from frigate.util.builtin import EventsPerSecond, InferenceSpeed
from frigate.util.image import area
from ..types import DataProcessorMetrics
@@ -56,8 +54,10 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
self.face_detector: cv2.FaceDetectorYN = None
self.requires_face_detection = "face" not in self.config.objects.all_objects
self.person_face_history: dict[str, list[tuple[str, float, int]]] = {}
self.camera_current_people: dict[str, list[str]] = {}
self.recognizer: FaceRecognizer | None = None
self.faces_per_second = EventsPerSecond()
self.inference_speed = InferenceSpeed(self.metrics.face_rec_speed)
download_path = os.path.join(MODEL_CACHE_DIR, "facedet")
self.model_files = {
@@ -155,11 +155,9 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
def __update_metrics(self, duration: float) -> None:
self.faces_per_second.update()
self.metrics.face_rec_speed.value = (
self.metrics.face_rec_speed.value * 9 + duration
) / 10
self.inference_speed.update(duration)
def process_frame(self, obj_data: dict[str, any], frame: np.ndarray):
def process_frame(self, obj_data: dict[str, Any], frame: np.ndarray):
"""Look for faces in image."""
self.metrics.face_rec_fps.value = self.faces_per_second.eps()
camera = obj_data["camera"]
@@ -200,7 +198,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
logger.debug("Not processing due to hitting max rec attempts.")
return
face: Optional[dict[str, any]] = None
face: Optional[dict[str, Any]] = None
if self.requires_face_detection:
logger.debug("Running manual face detection.")
@@ -223,6 +221,13 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
max(0, face_box[0]) : min(frame.shape[1], face_box[2]),
]
# check that face is correct size
if area(face_box) < self.config.cameras[camera].face_recognition.min_area:
logger.debug(
f"Detected face that is smaller than the min_area {face} < {self.config.cameras[camera].face_recognition.min_area}"
)
return
try:
face_frame = cv2.cvtColor(face_frame, cv2.COLOR_RGB2BGR)
except Exception:
@@ -233,7 +238,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
logger.debug("No attributes to parse.")
return
attributes: list[dict[str, any]] = obj_data.get("current_attributes", [])
attributes: list[dict[str, Any]] = obj_data.get("current_attributes", [])
for attr in attributes:
if attr.get("label") != "face":
continue
@@ -285,9 +290,13 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
if id not in self.person_face_history:
self.person_face_history[id] = []
if camera not in self.camera_current_people:
self.camera_current_people[camera] = []
self.person_face_history[id].append(
(sub_label, score, face_frame.shape[0] * face_frame.shape[1])
)
self.camera_current_people[camera].append(id)
(weighted_sub_label, weighted_score) = self.weighted_average(
self.person_face_history[id]
)
@@ -314,7 +323,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
self.__update_metrics(datetime.datetime.now().timestamp() - start)
def handle_request(self, topic, request_data) -> dict[str, any] | None:
def handle_request(self, topic, request_data) -> dict[str, Any] | None:
if topic == EmbeddingsRequestEnum.clear_face_classifier.value:
self.recognizer.clear()
elif topic == EmbeddingsRequestEnum.recognize_face.value:
@@ -343,11 +352,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
return {"success": True, "score": score, "face_name": sub_label}
elif topic == EmbeddingsRequestEnum.register_face.value:
rand_id = "".join(
random.choices(string.ascii_lowercase + string.digits, k=6)
)
label = request_data["face_name"]
id = f"{label}-{rand_id}"
if request_data.get("cropped"):
thumbnail = request_data["image"]
@@ -376,7 +381,9 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
# write face to library
folder = os.path.join(FACE_DIR, label)
file = os.path.join(folder, f"{id}.webp")
file = os.path.join(
folder, f"{label}_{datetime.datetime.now().timestamp()}.webp"
)
os.makedirs(folder, exist_ok=True)
# save face image
@@ -425,10 +432,25 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
)
shutil.move(current_file, new_file)
def expire_object(self, object_id: str):
def expire_object(self, object_id: str, camera: str):
if object_id in self.person_face_history:
self.person_face_history.pop(object_id)
if object_id in self.camera_current_people.get(camera, []):
self.camera_current_people[camera].remove(object_id)
if len(self.camera_current_people[camera]) == 0:
self.requestor.send_data(
"tracked_object_update",
json.dumps(
{
"type": TrackedObjectUpdateTypesEnum.face,
"name": None,
"camera": camera,
}
),
)
def weighted_average(
self, results_list: list[tuple[str, float, int]], max_weight: int = 4000
):
@@ -1,6 +1,8 @@
"""Handle processing images for face detection and recognition."""
import json
import logging
from typing import Any
import numpy as np
@@ -13,6 +15,7 @@ from frigate.data_processing.common.license_plate.mixin import (
from frigate.data_processing.common.license_plate.model import (
LicensePlateModelRunner,
)
from frigate.types import TrackedObjectUpdateTypesEnum
from ..types import DataProcessorMetrics
from .api import RealTimeProcessorApi
@@ -28,7 +31,7 @@ class LicensePlateRealTimeProcessor(LicensePlateProcessingMixin, RealTimeProcess
sub_label_publisher: EventMetadataPublisher,
metrics: DataProcessorMetrics,
model_runner: LicensePlateModelRunner,
detected_license_plates: dict[str, dict[str, any]],
detected_license_plates: dict[str, dict[str, Any]],
):
self.requestor = requestor
self.detected_license_plates = detected_license_plates
@@ -36,20 +39,37 @@ class LicensePlateRealTimeProcessor(LicensePlateProcessingMixin, RealTimeProcess
self.lpr_config = config.lpr
self.config = config
self.sub_label_publisher = sub_label_publisher
self.camera_current_cars: dict[str, list[str]] = {}
super().__init__(config, metrics)
def process_frame(
self,
obj_data: dict[str, any],
obj_data: dict[str, Any],
frame: np.ndarray,
dedicated_lpr: bool | None = False,
):
"""Look for license plates in image."""
self.lpr_process(obj_data, frame, dedicated_lpr)
def handle_request(self, topic, request_data) -> dict[str, any] | None:
def handle_request(self, topic, request_data) -> dict[str, Any] | None:
return
def expire_object(self, object_id: str):
def expire_object(self, object_id: str, camera: str):
if object_id in self.detected_license_plates:
self.detected_license_plates.pop(object_id)
if object_id in self.camera_current_cars.get(camera, []):
self.camera_current_cars[camera].remove(object_id)
if len(self.camera_current_cars[camera]) == 0:
self.requestor.send_data(
"tracked_object_update",
json.dumps(
{
"type": TrackedObjectUpdateTypesEnum.lpr,
"name": None,
"plate": None,
"camera": camera,
}
),
)
+7 -5
View File
@@ -7,7 +7,9 @@ from multiprocessing.sharedctypes import Synchronized
class DataProcessorMetrics:
image_embeddings_speed: Synchronized
image_embeddings_eps: Synchronized
text_embeddings_speed: Synchronized
text_embeddings_eps: Synchronized
face_rec_speed: Synchronized
face_rec_fps: Synchronized
alpr_speed: Synchronized
@@ -16,15 +18,15 @@ class DataProcessorMetrics:
yolov9_lpr_pps: Synchronized
def __init__(self):
self.image_embeddings_speed = mp.Value("d", 0.01)
self.image_embeddings_speed = mp.Value("d", 0.0)
self.image_embeddings_eps = mp.Value("d", 0.0)
self.text_embeddings_speed = mp.Value("d", 0.01)
self.text_embeddings_speed = mp.Value("d", 0.0)
self.text_embeddings_eps = mp.Value("d", 0.0)
self.face_rec_speed = mp.Value("d", 0.01)
self.face_rec_speed = mp.Value("d", 0.0)
self.face_rec_fps = mp.Value("d", 0.0)
self.alpr_speed = mp.Value("d", 0.01)
self.alpr_speed = mp.Value("d", 0.0)
self.alpr_pps = mp.Value("d", 0.0)
self.yolov9_lpr_speed = mp.Value("d", 0.01)
self.yolov9_lpr_speed = mp.Value("d", 0.0)
self.yolov9_lpr_pps = mp.Value("d", 0.0)
+5 -2
View File
@@ -3,7 +3,7 @@ import json
import logging
import os
from enum import Enum
from typing import Dict, Optional, Tuple
from typing import Any, Dict, Optional, Tuple
import requests
from pydantic import BaseModel, ConfigDict, Field
@@ -126,6 +126,9 @@ class ModelConfig(BaseModel):
if not self.path or not self.path.startswith("plus://"):
return
# ensure that model cache dir exists
os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
model_id = self.path[7:]
self.path = os.path.join(MODEL_CACHE_DIR, model_id)
model_info_path = f"{self.path}.json"
@@ -144,7 +147,7 @@ class ModelConfig(BaseModel):
json.dump(model_info, f)
else:
with open(model_info_path, "r") as f:
model_info: dict[str, any] = json.load(f)
model_info: dict[str, Any] = json.load(f)
if detector and detector not in model_info["supportedDetectors"]:
raise ValueError(f"Model does not support detector type of {detector}")
+20 -24
View File
@@ -8,17 +8,6 @@ from typing import Dict, List, Optional, Tuple
import cv2
import numpy as np
try:
from hailo_platform import (
HEF,
FormatType,
HailoSchedulingAlgorithm,
VDevice,
)
except ModuleNotFoundError:
pass
from pydantic import Field
from typing_extensions import Literal
@@ -102,6 +91,18 @@ class HailoAsyncInference:
output_type: Optional[Dict[str, str]] = None,
send_original_frame: bool = False,
) -> None:
# when importing hailo it activates the driver
# which leaves processes running even though it may not be used.
try:
from hailo_platform import (
HEF,
FormatType,
HailoSchedulingAlgorithm,
VDevice,
)
except ModuleNotFoundError:
pass
self.input_store = input_store
self.output_store = output_store
@@ -112,24 +113,19 @@ class HailoAsyncInference:
self.target = VDevice(params)
self.infer_model = self.target.create_infer_model(hef_path)
self.infer_model.set_batch_size(batch_size)
if input_type is not None:
self._set_input_type(input_type)
self.infer_model.input().set_format_type(getattr(FormatType, input_type))
if output_type is not None:
self._set_output_type(output_type)
for output_name, output_type in output_type.items():
self.infer_model.output(output_name).set_format_type(
getattr(FormatType, output_type)
)
self.output_type = output_type
self.send_original_frame = send_original_frame
def _set_input_type(self, input_type: Optional[str] = None) -> None:
self.infer_model.input().set_format_type(getattr(FormatType, input_type))
def _set_output_type(
self, output_type_dict: Optional[Dict[str, str]] = None
) -> None:
for output_name, output_type in output_type_dict.items():
self.infer_model.output(output_name).set_format_type(
getattr(FormatType, output_type)
)
def callback(
self,
completion_info,
+7 -7
View File
@@ -9,7 +9,7 @@ import re
import signal
import threading
from types import FrameType
from typing import Optional, Union
from typing import Any, Optional, Union
from pathvalidate import ValidationError, sanitize_filename
from setproctitle import setproctitle
@@ -190,7 +190,7 @@ class EmbeddingsContext:
return results
def register_face(self, face_name: str, image_data: bytes) -> dict[str, any]:
def register_face(self, face_name: str, image_data: bytes) -> dict[str, Any]:
return self.requestor.send_data(
EmbeddingsRequestEnum.register_face.value,
{
@@ -199,7 +199,7 @@ class EmbeddingsContext:
},
)
def recognize_face(self, image_data: bytes) -> dict[str, any]:
def recognize_face(self, image_data: bytes) -> dict[str, Any]:
return self.requestor.send_data(
EmbeddingsRequestEnum.recognize_face.value,
{
@@ -217,7 +217,7 @@ class EmbeddingsContext:
return self.db.execute_sql(sql_query).fetchall()
def reprocess_face(self, face_file: str) -> dict[str, any]:
def reprocess_face(self, face_file: str) -> dict[str, Any]:
return self.requestor.send_data(
EmbeddingsRequestEnum.reprocess_face.value, {"image_file": face_file}
)
@@ -235,7 +235,7 @@ class EmbeddingsContext:
if os.path.isfile(file_path):
os.unlink(file_path)
if len(os.listdir(folder)) == 0:
if face != "train" and len(os.listdir(folder)) == 0:
os.rmdir(folder)
self.requestor.send_data(
@@ -284,10 +284,10 @@ class EmbeddingsContext:
{"id": event_id, "description": description},
)
def reprocess_plate(self, event: dict[str, any]) -> dict[str, any]:
def reprocess_plate(self, event: dict[str, Any]) -> dict[str, Any]:
return self.requestor.send_data(
EmbeddingsRequestEnum.reprocess_plate.value, {"event": event}
)
def reindex_embeddings(self) -> dict[str, any]:
def reindex_embeddings(self) -> dict[str, Any]:
return self.requestor.send_data(EmbeddingsRequestEnum.reindex.value, {})
+7 -16
View File
@@ -21,7 +21,7 @@ from frigate.data_processing.types import DataProcessorMetrics
from frigate.db.sqlitevecq import SqliteVecQueueDatabase
from frigate.models import Event
from frigate.types import ModelStatusTypesEnum
from frigate.util.builtin import EventsPerSecond, serialize
from frigate.util.builtin import EventsPerSecond, InferenceSpeed, serialize
from frigate.util.path import get_event_thumbnail_bytes
from .onnx.jina_v1_embedding import JinaV1ImageEmbedding, JinaV1TextEmbedding
@@ -75,8 +75,10 @@ class Embeddings:
self.metrics = metrics
self.requestor = InterProcessRequestor()
self.image_inference_speed = InferenceSpeed(self.metrics.image_embeddings_speed)
self.image_eps = EventsPerSecond()
self.image_eps.start()
self.text_inference_speed = InferenceSpeed(self.metrics.text_embeddings_speed)
self.text_eps = EventsPerSecond()
self.text_eps.start()
@@ -183,10 +185,7 @@ class Embeddings:
(event_id, serialize(embedding)),
)
duration = datetime.datetime.now().timestamp() - start
self.metrics.image_embeddings_speed.value = (
self.metrics.image_embeddings_speed.value * 9 + duration
) / 10
self.image_inference_speed.update(datetime.datetime.now().timestamp() - start)
self.image_eps.update()
return embedding
@@ -220,9 +219,7 @@ class Embeddings:
)
duration = datetime.datetime.now().timestamp() - start
self.metrics.text_embeddings_speed.value = (
self.metrics.text_embeddings_speed.value * 9 + (duration / len(ids))
) / 10
self.text_inference_speed.update(duration / len(ids))
return embeddings
@@ -241,10 +238,7 @@ class Embeddings:
(event_id, serialize(embedding)),
)
duration = datetime.datetime.now().timestamp() - start
self.metrics.text_embeddings_speed.value = (
self.metrics.text_embeddings_speed.value * 9 + duration
) / 10
self.text_inference_speed.update(datetime.datetime.now().timestamp() - start)
self.text_eps.update()
return embedding
@@ -276,10 +270,7 @@ class Embeddings:
items,
)
duration = datetime.datetime.now().timestamp() - start
self.metrics.text_embeddings_speed.value = (
self.metrics.text_embeddings_speed.value * 9 + (duration / len(ids))
) / 10
self.text_inference_speed.update(datetime.datetime.now().timestamp() - start)
return embeddings
+5 -5
View File
@@ -7,7 +7,7 @@ import os
import threading
from multiprocessing.synchronize import Event as MpEvent
from pathlib import Path
from typing import Optional
from typing import Any, Optional
import cv2
import numpy as np
@@ -104,7 +104,7 @@ class EmbeddingMaintainer(threading.Thread):
self.embeddings_responder = EmbeddingsResponder()
self.frame_manager = SharedMemoryFrameManager()
self.detected_license_plates: dict[str, dict[str, any]] = {}
self.detected_license_plates: dict[str, dict[str, Any]] = {}
# model runners to share between realtime and post processors
if self.config.lpr.enabled:
@@ -159,7 +159,7 @@ class EmbeddingMaintainer(threading.Thread):
)
self.stop_event = stop_event
self.tracked_events: dict[str, list[any]] = {}
self.tracked_events: dict[str, list[Any]] = {}
self.early_request_sent: dict[str, bool] = {}
self.genai_client = get_genai_client(config)
@@ -190,7 +190,7 @@ class EmbeddingMaintainer(threading.Thread):
def _process_requests(self) -> None:
"""Process embeddings requests"""
def _handle_request(topic: str, data: dict[str, any]) -> str:
def _handle_request(topic: str, data: dict[str, Any]) -> str:
try:
# First handle the embedding-specific topics when semantic search is enabled
if self.config.semantic_search.enabled:
@@ -359,7 +359,7 @@ class EmbeddingMaintainer(threading.Thread):
# expire in realtime processors
for processor in self.realtime_processors:
processor.expire_object(event_id)
processor.expire_object(event_id, camera)
if updated_db:
try:
+4 -3
View File
@@ -5,6 +5,7 @@ import os
from abc import ABC, abstractmethod
from enum import Enum
from io import BytesIO
from typing import Any
import numpy as np
import requests
@@ -59,7 +60,7 @@ class BaseEmbedding(ABC):
pass
@abstractmethod
def _preprocess_inputs(self, raw_inputs: any) -> any:
def _preprocess_inputs(self, raw_inputs: Any) -> Any:
pass
def _process_image(self, image, output: str = "RGB") -> Image.Image:
@@ -74,7 +75,7 @@ class BaseEmbedding(ABC):
return image
def _postprocess_outputs(self, outputs: any) -> any:
def _postprocess_outputs(self, outputs: Any) -> Any:
return outputs
def __call__(
@@ -84,7 +85,7 @@ class BaseEmbedding(ABC):
processed = self._preprocess_inputs(inputs)
input_names = self.runner.get_input_names()
onnx_inputs = {name: [] for name in input_names}
input: dict[str, any]
input: dict[str, Any]
for input in processed:
for key, value in input.items():
if key in input_names:
+3 -11
View File
@@ -23,10 +23,7 @@ FACENET_INPUT_SIZE = 160
class FaceNetEmbedding(BaseEmbedding):
def __init__(
self,
device: str = "AUTO",
):
def __init__(self):
super().__init__(
model_name="facedet",
model_file="facenet.tflite",
@@ -34,7 +31,6 @@ class FaceNetEmbedding(BaseEmbedding):
"facenet.tflite": "https://github.com/NickM-27/facenet-onnx/releases/download/v1.0/facenet.tflite",
},
)
self.device = device
self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name)
self.tokenizer = None
self.feature_extractor = None
@@ -113,10 +109,7 @@ class FaceNetEmbedding(BaseEmbedding):
class ArcfaceEmbedding(BaseEmbedding):
def __init__(
self,
device: str = "AUTO",
):
def __init__(self):
super().__init__(
model_name="facedet",
model_file="arcface.onnx",
@@ -124,7 +117,6 @@ class ArcfaceEmbedding(BaseEmbedding):
"arcface.onnx": "https://github.com/NickM-27/facenet-onnx/releases/download/v1.0/arcface.onnx",
},
)
self.device = device
self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name)
self.tokenizer = None
self.feature_extractor = None
@@ -154,7 +146,7 @@ class ArcfaceEmbedding(BaseEmbedding):
self.runner = ONNXModelRunner(
os.path.join(self.download_path, self.model_file),
self.device,
"GPU",
)
def _preprocess_inputs(self, raw_inputs):
+5 -3
View File
@@ -36,11 +36,12 @@ class JinaV1TextEmbedding(BaseEmbedding):
requestor: InterProcessRequestor,
device: str = "AUTO",
):
HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co")
super().__init__(
model_name="jinaai/jina-clip-v1",
model_file="text_model_fp16.onnx",
download_urls={
"text_model_fp16.onnx": "https://huggingface.co/jinaai/jina-clip-v1/resolve/main/onnx/text_model_fp16.onnx",
"text_model_fp16.onnx": f"{HF_ENDPOINT}/jinaai/jina-clip-v1/resolve/main/onnx/text_model_fp16.onnx",
},
)
self.tokenizer_file = "tokenizer"
@@ -156,12 +157,13 @@ class JinaV1ImageEmbedding(BaseEmbedding):
if model_size == "large"
else "vision_model_quantized.onnx"
)
HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co")
super().__init__(
model_name="jinaai/jina-clip-v1",
model_file=model_file,
download_urls={
model_file: f"https://huggingface.co/jinaai/jina-clip-v1/resolve/main/onnx/{model_file}",
"preprocessor_config.json": "https://huggingface.co/jinaai/jina-clip-v1/resolve/main/preprocessor_config.json",
model_file: f"{HF_ENDPOINT}/jinaai/jina-clip-v1/resolve/main/onnx/{model_file}",
"preprocessor_config.json": f"{HF_ENDPOINT}/jinaai/jina-clip-v1/resolve/main/preprocessor_config.json",
},
)
self.requestor = requestor
+3 -2
View File
@@ -34,12 +34,13 @@ class JinaV2Embedding(BaseEmbedding):
model_file = (
"model_fp16.onnx" if model_size == "large" else "model_quantized.onnx"
)
HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co")
super().__init__(
model_name="jinaai/jina-clip-v2",
model_file=model_file,
download_urls={
model_file: f"https://huggingface.co/jinaai/jina-clip-v2/resolve/main/onnx/{model_file}",
"preprocessor_config.json": "https://huggingface.co/jinaai/jina-clip-v2/resolve/main/preprocessor_config.json",
model_file: f"{HF_ENDPOINT}/jinaai/jina-clip-v2/resolve/main/onnx/{model_file}",
"preprocessor_config.json": f"{HF_ENDPOINT}/jinaai/jina-clip-v2/resolve/main/preprocessor_config.json",
},
)
self.tokenizer_file = "tokenizer"
+2 -2
View File
@@ -6,7 +6,7 @@ import random
import string
import threading
import time
from typing import Tuple
from typing import Any, Tuple
import numpy as np
@@ -126,7 +126,7 @@ class AudioEventMaintainer(threading.Thread):
self.config = camera
self.camera_metrics = camera_metrics
self.detections: dict[dict[str, any]] = {}
self.detections: dict[dict[str, Any]] = {}
self.stop_event = stop_event
self.detector = AudioTfl(stop_event, self.config.audio.num_threads)
self.shape = (int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE)),)
+2 -1
View File
@@ -6,6 +6,7 @@ import os
import threading
from multiprocessing.synchronize import Event as MpEvent
from pathlib import Path
from typing import Any
from frigate.config import FrigateConfig
from frigate.const import CLIPS_DIR
@@ -29,7 +30,7 @@ class EventCleanup(threading.Thread):
self.db = db
self.camera_keys = list(self.config.cameras.keys())
self.removed_camera_labels: list[str] = None
self.camera_labels: dict[str, dict[str, any]] = {}
self.camera_labels: dict[str, dict[str, Any]] = {}
def get_removed_camera_labels(self) -> list[Event]:
"""Get a list of distinct labels for removed cameras."""
+5 -5
View File
@@ -10,7 +10,7 @@ import queue
import subprocess as sp
import threading
import traceback
from typing import Optional
from typing import Any, Optional
import cv2
import numpy as np
@@ -542,10 +542,10 @@ class BirdsEyeFrameManager:
self,
cameras_to_add: list[str],
coefficient: float,
) -> tuple[any]:
) -> tuple[Any]:
"""Calculate the optimal layout for 2+ cameras."""
def map_layout(camera_layout: list[list[any]], row_height: int):
def map_layout(camera_layout: list[list[Any]], row_height: int):
"""Map the calculated layout."""
candidate_layout = []
starting_x = 0
@@ -588,7 +588,7 @@ class BirdsEyeFrameManager:
return max_width, y, candidate_layout
canvas_aspect_x, canvas_aspect_y = self.canvas.get_aspect(coefficient)
camera_layout: list[list[any]] = []
camera_layout: list[list[Any]] = []
camera_layout.append([])
starting_x = 0
x = starting_x
@@ -786,7 +786,7 @@ class Birdseye:
def write_data(
self,
camera: str,
current_tracked_objects: list[dict[str, any]],
current_tracked_objects: list[dict[str, Any]],
motion_boxes: list[list[int]],
frame_time: float,
frame: np.ndarray,
+3 -2
View File
@@ -8,6 +8,7 @@ import subprocess as sp
import threading
import time
from pathlib import Path
from typing import Any
import cv2
import numpy as np
@@ -255,7 +256,7 @@ class PreviewRecorder:
def should_write_frame(
self,
current_tracked_objects: list[dict[str, any]],
current_tracked_objects: list[dict[str, Any]],
motion_boxes: list[list[int]],
frame_time: float,
) -> bool:
@@ -315,7 +316,7 @@ class PreviewRecorder:
def write_data(
self,
current_tracked_objects: list[dict[str, any]],
current_tracked_objects: list[dict[str, Any]],
motion_boxes: list[list[int]],
frame_time: float,
frame: np.ndarray,
+64 -61
View File
@@ -3,12 +3,11 @@
import asyncio
import copy
import logging
import queue
import threading
import time
from collections import deque
from functools import partial
from multiprocessing.synchronize import Event as MpEvent
from typing import Any
import cv2
import numpy as np
@@ -61,7 +60,7 @@ class PtzMotionEstimator:
def motion_estimator(
self,
detections: list[dict[str, any]],
detections: list[dict[str, Any]],
frame_name: str,
frame_time: float,
camera: str,
@@ -169,7 +168,12 @@ class PtzAutoTrackerThread(threading.Thread):
continue
if camera_config.onvif.autotracking.enabled:
self.ptz_autotracker.camera_maintenance(camera)
future = asyncio.run_coroutine_threadsafe(
self.ptz_autotracker.camera_maintenance(camera),
self.ptz_autotracker.onvif.loop,
)
# Wait for the coroutine to complete
future.result()
else:
# disabled dynamically by mqtt
if self.ptz_autotracker.tracked_object.get(camera):
@@ -219,9 +223,13 @@ class PtzAutoTracker:
camera_config.onvif.autotracking.enabled
and camera_config.onvif.autotracking.enabled_in_config
):
self._autotracker_setup(camera_config, camera)
future = asyncio.run_coroutine_threadsafe(
self._autotracker_setup(camera_config, camera), self.onvif.loop
)
# Wait for the coroutine to complete
future.result()
def _autotracker_setup(self, camera_config: CameraConfig, camera: str):
async def _autotracker_setup(self, camera_config: CameraConfig, camera: str):
logger.debug(f"{camera}: Autotracker init")
self.object_types[camera] = camera_config.onvif.autotracking.track
@@ -242,8 +250,8 @@ class PtzAutoTracker:
self.intercept[camera] = None
self.move_coefficients[camera] = []
self.move_queues[camera] = queue.Queue()
self.move_queue_locks[camera] = threading.Lock()
self.move_queues[camera] = asyncio.Queue()
self.move_queue_locks[camera] = asyncio.Lock()
# handle onvif constructor failing due to no connection
if camera not in self.onvif.cams:
@@ -255,7 +263,7 @@ class PtzAutoTracker:
return
if not self.onvif.cams[camera]["init"]:
if not asyncio.run(self.onvif._init_onvif(camera)):
if not await self.onvif._init_onvif(camera):
logger.warning(
f"Disabling autotracking for {camera}: Unable to initialize onvif"
)
@@ -271,7 +279,7 @@ class PtzAutoTracker:
self.ptz_metrics[camera].autotracker_enabled.value = False
return
move_status_supported = self.onvif.get_service_capabilities(camera)
move_status_supported = await self.onvif.get_service_capabilities(camera)
if not (
isinstance(move_status_supported, bool) and move_status_supported
@@ -287,15 +295,12 @@ class PtzAutoTracker:
return
if self.onvif.cams[camera]["init"]:
self.onvif.get_camera_status(camera)
await self.onvif.get_camera_status(camera)
# movement thread per camera
self.move_threads[camera] = threading.Thread(
name=f"ptz_move_thread_{camera}",
target=partial(self._process_move_queue, camera),
# movement queue with asyncio on OnvifController loop
asyncio.run_coroutine_threadsafe(
self._process_move_queue(camera), self.onvif.loop
)
self.move_threads[camera].daemon = True
self.move_threads[camera].start()
if camera_config.onvif.autotracking.movement_weights:
if len(camera_config.onvif.autotracking.movement_weights) == 6:
@@ -330,7 +335,7 @@ class PtzAutoTracker:
)
if camera_config.onvif.autotracking.calibrate_on_startup:
self._calibrate_camera(camera)
await self._calibrate_camera(camera)
self.ptz_metrics[camera].tracking_active.clear()
self.dispatcher.publish(f"{camera}/ptz_autotracker/active", "OFF", retain=False)
@@ -349,7 +354,7 @@ class PtzAutoTracker:
self.config.cameras[camera].onvif.autotracking.movement_weights,
)
def _calibrate_camera(self, camera):
async def _calibrate_camera(self, camera):
# move the camera from the preset in steps and measure the time it takes to move that amount
# this will allow us to predict movement times with a simple linear regression
# start with 0 so we can determine a baseline (to be used as the intercept in the regression calc)
@@ -373,25 +378,25 @@ class PtzAutoTracker:
for i in range(2):
# absolute move to 0 - fully zoomed out
self.onvif._zoom_absolute(
await self.onvif._zoom_absolute(
camera,
self.onvif.cams[camera]["absolute_zoom_range"]["XRange"]["Min"],
1,
)
while not self.ptz_metrics[camera].motor_stopped.is_set():
self.onvif.get_camera_status(camera)
await self.onvif.get_camera_status(camera)
zoom_out_values.append(self.ptz_metrics[camera].zoom_level.value)
self.onvif._zoom_absolute(
await self.onvif._zoom_absolute(
camera,
self.onvif.cams[camera]["absolute_zoom_range"]["XRange"]["Max"],
1,
)
while not self.ptz_metrics[camera].motor_stopped.is_set():
self.onvif.get_camera_status(camera)
await self.onvif.get_camera_status(camera)
zoom_in_values.append(self.ptz_metrics[camera].zoom_level.value)
@@ -400,7 +405,7 @@ class PtzAutoTracker:
== ZoomingModeEnum.relative
):
# relative move to -0.01
self.onvif._move_relative(
await self.onvif._move_relative(
camera,
0,
0,
@@ -409,13 +414,13 @@ class PtzAutoTracker:
)
while not self.ptz_metrics[camera].motor_stopped.is_set():
self.onvif.get_camera_status(camera)
await self.onvif.get_camera_status(camera)
zoom_out_values.append(self.ptz_metrics[camera].zoom_level.value)
zoom_start_time = time.time()
# relative move to 0.01
self.onvif._move_relative(
await self.onvif._move_relative(
camera,
0,
0,
@@ -424,13 +429,13 @@ class PtzAutoTracker:
)
while not self.ptz_metrics[camera].motor_stopped.is_set():
self.onvif.get_camera_status(camera)
await self.onvif.get_camera_status(camera)
zoom_stop_time = time.time()
full_relative_start_time = time.time()
self.onvif._move_relative(
await self.onvif._move_relative(
camera,
-1,
-1,
@@ -439,11 +444,11 @@ class PtzAutoTracker:
)
while not self.ptz_metrics[camera].motor_stopped.is_set():
self.onvif.get_camera_status(camera)
await self.onvif.get_camera_status(camera)
full_relative_stop_time = time.time()
self.onvif._move_relative(
await self.onvif._move_relative(
camera,
1,
1,
@@ -452,7 +457,7 @@ class PtzAutoTracker:
)
while not self.ptz_metrics[camera].motor_stopped.is_set():
self.onvif.get_camera_status(camera)
await self.onvif.get_camera_status(camera)
self.zoom_time[camera] = (
full_relative_stop_time - full_relative_start_time
@@ -471,7 +476,7 @@ class PtzAutoTracker:
self.ptz_metrics[camera].max_zoom.value = 1
self.ptz_metrics[camera].min_zoom.value = 0
self.onvif._move_to_preset(
await self.onvif._move_to_preset(
camera,
self.config.cameras[camera].onvif.autotracking.return_preset.lower(),
)
@@ -480,18 +485,18 @@ class PtzAutoTracker:
# Wait until the camera finishes moving
while not self.ptz_metrics[camera].motor_stopped.is_set():
self.onvif.get_camera_status(camera)
await self.onvif.get_camera_status(camera)
for step in range(num_steps):
pan = step_sizes[step]
tilt = step_sizes[step]
start_time = time.time()
self.onvif._move_relative(camera, pan, tilt, 0, 1)
await self.onvif._move_relative(camera, pan, tilt, 0, 1)
# Wait until the camera finishes moving
while not self.ptz_metrics[camera].motor_stopped.is_set():
self.onvif.get_camera_status(camera)
await self.onvif.get_camera_status(camera)
stop_time = time.time()
self.move_metrics[camera].append(
@@ -503,7 +508,7 @@ class PtzAutoTracker:
}
)
self.onvif._move_to_preset(
await self.onvif._move_to_preset(
camera,
self.config.cameras[camera].onvif.autotracking.return_preset.lower(),
)
@@ -512,7 +517,7 @@ class PtzAutoTracker:
# Wait until the camera finishes moving
while not self.ptz_metrics[camera].motor_stopped.is_set():
self.onvif.get_camera_status(camera)
await self.onvif.get_camera_status(camera)
logger.info(
f"Calibration for {camera} in progress: {round((step / num_steps) * 100)}% complete"
@@ -709,18 +714,17 @@ class PtzAutoTracker:
centroid_distance < self.tracked_object_metrics[camera]["distance"]
)
def _process_move_queue(self, camera):
camera_config = self.config.cameras[camera]
camera_config.frame_shape[1]
camera_config.frame_shape[0]
async def _process_move_queue(self, camera):
move_queue = self.move_queues[camera]
while not self.stop_event.is_set():
try:
move_data = self.move_queues[camera].get(True, 0.1)
except queue.Empty:
# Asynchronously wait for move data with a timeout
move_data = await asyncio.wait_for(move_queue.get(), timeout=0.1)
except asyncio.TimeoutError:
continue
with self.move_queue_locks[camera]:
async with self.move_queue_locks[camera]:
frame_time, pan, tilt, zoom = move_data
# if we're receiving move requests during a PTZ move, ignore them
@@ -729,8 +733,6 @@ class PtzAutoTracker:
self.ptz_metrics[camera].start_time.value,
self.ptz_metrics[camera].stop_time.value,
):
# instead of dequeueing this might be a good place to preemptively move based
# on an estimate - for fast moving objects, etc.
logger.debug(
f"{camera}: Move queue: PTZ moving, dequeueing move request - frame time: {frame_time}, final pan: {pan}, final tilt: {tilt}, final zoom: {zoom}"
)
@@ -741,25 +743,24 @@ class PtzAutoTracker:
self.config.cameras[camera].onvif.autotracking.zooming
== ZoomingModeEnum.relative
):
self.onvif._move_relative(camera, pan, tilt, zoom, 1)
await self.onvif._move_relative(camera, pan, tilt, zoom, 1)
else:
if pan != 0 or tilt != 0:
self.onvif._move_relative(camera, pan, tilt, 0, 1)
await self.onvif._move_relative(camera, pan, tilt, 0, 1)
# Wait until the camera finishes moving
while not self.ptz_metrics[camera].motor_stopped.is_set():
self.onvif.get_camera_status(camera)
await self.onvif.get_camera_status(camera)
if (
zoom > 0
and self.ptz_metrics[camera].zoom_level.value != zoom
):
self.onvif._zoom_absolute(camera, zoom, 1)
await self.onvif._zoom_absolute(camera, zoom, 1)
# Wait until the camera finishes moving
while not self.ptz_metrics[camera].motor_stopped.is_set():
self.onvif.get_camera_status(camera)
await self.onvif.get_camera_status(camera)
if self.config.cameras[camera].onvif.autotracking.movement_weights:
logger.debug(
@@ -796,6 +797,10 @@ class PtzAutoTracker:
# calculate new coefficients if we have enough data
self._calculate_move_coefficients(camera)
# Clean up the queue on exit
while not move_queue.empty():
await move_queue.get()
def _enqueue_move(self, camera, frame_time, pan, tilt, zoom):
def split_value(value, suppress_diff=True):
clipped = np.clip(value, -1, 1)
@@ -824,7 +829,9 @@ class PtzAutoTracker:
f"{camera}: Enqueue movement for frame time: {frame_time} pan: {pan}, tilt: {tilt}, zoom: {zoom}"
)
move_data = (frame_time, pan, tilt, zoom)
self.move_queues[camera].put(move_data)
self.onvif.loop.call_soon_threadsafe(
self.move_queues[camera].put_nowait, move_data
)
# reset values to not split up large movements
pan = 0
@@ -1420,7 +1427,7 @@ class PtzAutoTracker:
** (1 / self.zoom_factor[camera])
}
def camera_maintenance(self, camera):
async def camera_maintenance(self, camera):
# bail and don't check anything if we're calibrating or tracking an object
if (
not self.autotracker_init[camera]
@@ -1437,7 +1444,7 @@ class PtzAutoTracker:
self._autotracker_setup(self.config.cameras[camera], camera)
# regularly update camera status
if not self.ptz_metrics[camera].motor_stopped.is_set():
self.onvif.get_camera_status(camera)
await self.onvif.get_camera_status(camera)
# return to preset if tracking is over
if (
@@ -1455,22 +1462,18 @@ class PtzAutoTracker:
self.tracked_object[camera] = None
self.tracked_object_history[camera].clear()
# empty move queue
while not self.move_queues[camera].empty():
self.move_queues[camera].get()
self.ptz_metrics[camera].motor_stopped.wait()
logger.debug(
f"{camera}: Time is {self.ptz_metrics[camera].frame_time.value}, returning to preset: {autotracker_config.return_preset}"
)
self.onvif._move_to_preset(
await self.onvif._move_to_preset(
camera,
autotracker_config.return_preset.lower(),
)
# update stored zoom level from preset
if not self.ptz_metrics[camera].motor_stopped.is_set():
self.onvif.get_camera_status(camera)
await self.onvif.get_camera_status(camera)
self.ptz_metrics[camera].tracking_active.clear()
self.dispatcher.publish(
+144 -80
View File
@@ -2,10 +2,12 @@
import asyncio
import logging
import threading
import time
from enum import Enum
from importlib.util import find_spec
from pathlib import Path
from typing import Any
import numpy
from onvif import ONVIFCamera, ONVIFError, ONVIFService
@@ -39,27 +41,56 @@ class OnvifController:
def __init__(
self, config: FrigateConfig, ptz_metrics: dict[str, PTZMetrics]
) -> None:
self.cams: dict[str, ONVIFCamera] = {}
self.cams: dict[str, dict] = {}
self.failed_cams: dict[str, dict] = {}
self.max_retries = 5
self.reset_timeout = 900 # 15 minutes
self.config = config
self.ptz_metrics = ptz_metrics
# Create a dedicated event loop and run it in a separate thread
self.loop = asyncio.new_event_loop()
self.loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
self.loop_thread.start()
self.camera_configs = {}
for cam_name, cam in config.cameras.items():
if not cam.enabled:
continue
if cam.onvif.host:
result = self._create_onvif_camera(cam_name, cam)
if result:
self.cams[cam_name] = result
self.camera_configs[cam_name] = cam
def _create_onvif_camera(self, cam_name: str, cam) -> dict | None:
"""Create an ONVIF camera instance and handle failures."""
asyncio.run_coroutine_threadsafe(self._init_cameras(), self.loop)
def _run_event_loop(self) -> None:
"""Run the event loop in a separate thread."""
asyncio.set_event_loop(self.loop)
try:
return {
self.loop.run_forever()
except Exception as e:
logger.error(f"Onvif event loop terminated unexpectedly: {e}")
async def _init_cameras(self) -> None:
"""Initialize all configured cameras."""
for cam_name in self.camera_configs:
await self._init_single_camera(cam_name)
async def _init_single_camera(self, cam_name: str) -> bool:
"""Initialize a single camera by name.
Args:
cam_name: The name of the camera to initialize
Returns:
bool: True if initialization succeeded, False otherwise
"""
if cam_name not in self.camera_configs:
logger.error(f"No configuration found for camera {cam_name}")
return False
cam = self.camera_configs[cam_name]
try:
self.cams[cam_name] = {
"onvif": ONVIFCamera(
cam.onvif.host,
cam.onvif.port,
@@ -74,7 +105,8 @@ class OnvifController:
"features": [],
"presets": {},
}
except ONVIFError as e:
return True
except (Fault, ONVIFError, TransportError, Exception) as e:
logger.error(f"Failed to create ONVIF camera instance for {cam_name}: {e}")
# track initial failures
self.failed_cams[cam_name] = {
@@ -82,7 +114,7 @@ class OnvifController:
"last_error": str(e),
"last_attempt": time.time(),
}
return None
return False
async def _init_onvif(self, camera_name: str) -> bool:
onvif: ONVIFCamera = self.cams[camera_name]["onvif"]
@@ -100,7 +132,7 @@ class OnvifController:
# this will fire an exception if camera is not a ptz
capabilities = onvif.get_definition("ptz")
logger.debug(f"Onvif capabilities for {camera_name}: {capabilities}")
except (ONVIFError, Fault, TransportError) as e:
except (Fault, ONVIFError, TransportError, Exception) as e:
logger.error(
f"Unable to get Onvif capabilities for camera: {camera_name}: {e}"
)
@@ -109,7 +141,7 @@ class OnvifController:
try:
profiles = await media.GetProfiles()
logger.debug(f"Onvif profiles for {camera_name}: {profiles}")
except (ONVIFError, Fault, TransportError) as e:
except (Fault, ONVIFError, TransportError, Exception) as e:
logger.error(
f"Unable to get Onvif media profiles for camera: {camera_name}: {e}"
)
@@ -240,12 +272,12 @@ class OnvifController:
logger.debug(
f"{camera_name}: Relative move request after deleting zoom: {move_request}"
)
except Exception:
except Exception as e:
self.config.cameras[
camera_name
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
logger.warning(
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported"
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}"
)
if move_request.Speed is None:
@@ -263,7 +295,7 @@ class OnvifController:
# setup existing presets
try:
presets: list[dict] = await ptz.GetPresets({"ProfileToken": profile.token})
except ONVIFError as e:
except (Fault, ONVIFError, TransportError, Exception) as e:
logger.warning(f"Unable to get presets from camera: {camera_name}: {e}")
presets = []
@@ -295,7 +327,7 @@ class OnvifController:
self.cams[camera_name]["relative_zoom_range"] = (
ptz_config.Spaces.RelativeZoomTranslationSpace[0]
)
except Exception:
except Exception as e:
if (
self.config.cameras[camera_name].onvif.autotracking.zooming
== ZoomingModeEnum.relative
@@ -304,7 +336,7 @@ class OnvifController:
camera_name
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
logger.warning(
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported"
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}"
)
if configs.DefaultAbsoluteZoomPositionSpace:
@@ -319,13 +351,13 @@ class OnvifController:
ptz_config.Spaces.AbsoluteZoomPositionSpace[0]
)
self.cams[camera_name]["zoom_limits"] = configs.ZoomLimits
except Exception:
except Exception as e:
if self.config.cameras[camera_name].onvif.autotracking.zooming:
self.config.cameras[
camera_name
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
logger.warning(
f"Disabling autotracking zooming for {camera_name}: Absolute zoom not supported"
f"Disabling autotracking zooming for {camera_name}: Absolute zoom not supported. Exception: {e}"
)
# set relative pan/tilt space for autotracker
@@ -344,25 +376,23 @@ class OnvifController:
self.cams[camera_name]["init"] = True
return True
def _stop(self, camera_name: str) -> None:
async def _stop(self, camera_name: str) -> None:
move_request = self.cams[camera_name]["move_request"]
asyncio.run(
self.cams[camera_name]["ptz"].Stop(
{
"ProfileToken": move_request.ProfileToken,
"PanTilt": True,
"Zoom": True,
}
)
await self.cams[camera_name]["ptz"].Stop(
{
"ProfileToken": move_request.ProfileToken,
"PanTilt": True,
"Zoom": True,
}
)
self.cams[camera_name]["active"] = False
def _move(self, camera_name: str, command: OnvifCommandEnum) -> None:
async def _move(self, camera_name: str, command: OnvifCommandEnum) -> None:
if self.cams[camera_name]["active"]:
logger.warning(
f"{camera_name} is already performing an action, stopping..."
)
self._stop(camera_name)
await self._stop(camera_name)
if "pt" not in self.cams[camera_name]["features"]:
logger.error(f"{camera_name} does not support ONVIF pan/tilt movement.")
@@ -391,11 +421,11 @@ class OnvifController:
}
try:
asyncio.run(self.cams[camera_name]["ptz"].ContinuousMove(move_request))
except ONVIFError as e:
await self.cams[camera_name]["ptz"].ContinuousMove(move_request)
except (Fault, ONVIFError, TransportError, Exception) as e:
logger.warning(f"Onvif sending move request to {camera_name} failed: {e}")
def _move_relative(self, camera_name: str, pan, tilt, zoom, speed) -> None:
async def _move_relative(self, camera_name: str, pan, tilt, zoom, speed) -> None:
if "pt-r-fov" not in self.cams[camera_name]["features"]:
logger.error(f"{camera_name} does not support ONVIF RelativeMove (FOV).")
return
@@ -464,7 +494,7 @@ class OnvifController:
}
move_request.Translation.Zoom.x = zoom
asyncio.run(self.cams[camera_name]["ptz"].RelativeMove(move_request))
await self.cams[camera_name]["ptz"].RelativeMove(move_request)
# reset after the move request
move_request.Translation.PanTilt.x = 0
@@ -479,7 +509,7 @@ class OnvifController:
self.cams[camera_name]["active"] = False
def _move_to_preset(self, camera_name: str, preset: str) -> None:
async def _move_to_preset(self, camera_name: str, preset: str) -> None:
if preset not in self.cams[camera_name]["presets"]:
logger.error(f"{preset} is not a valid preset for {camera_name}")
return
@@ -489,23 +519,22 @@ class OnvifController:
self.ptz_metrics[camera_name].stop_time.value = 0
move_request = self.cams[camera_name]["move_request"]
preset_token = self.cams[camera_name]["presets"][preset]
asyncio.run(
self.cams[camera_name]["ptz"].GotoPreset(
{
"ProfileToken": move_request.ProfileToken,
"PresetToken": preset_token,
}
)
await self.cams[camera_name]["ptz"].GotoPreset(
{
"ProfileToken": move_request.ProfileToken,
"PresetToken": preset_token,
}
)
self.cams[camera_name]["active"] = False
def _zoom(self, camera_name: str, command: OnvifCommandEnum) -> None:
async def _zoom(self, camera_name: str, command: OnvifCommandEnum) -> None:
if self.cams[camera_name]["active"]:
logger.warning(
f"{camera_name} is already performing an action, stopping..."
)
self._stop(camera_name)
await self._stop(camera_name)
if "zoom" not in self.cams[camera_name]["features"]:
logger.error(f"{camera_name} does not support ONVIF zooming.")
@@ -519,9 +548,9 @@ class OnvifController:
elif command == OnvifCommandEnum.zoom_out:
move_request.Velocity = {"Zoom": {"x": -0.5}}
asyncio.run(self.cams[camera_name]["ptz"].ContinuousMove(move_request))
await self.cams[camera_name]["ptz"].ContinuousMove(move_request)
def _zoom_absolute(self, camera_name: str, zoom, speed) -> None:
async def _zoom_absolute(self, camera_name: str, zoom, speed) -> None:
if "zoom-a" not in self.cams[camera_name]["features"]:
logger.error(f"{camera_name} does not support ONVIF AbsoluteMove zooming.")
return
@@ -560,19 +589,20 @@ class OnvifController:
logger.debug(f"{camera_name}: Absolute zoom: {zoom}")
asyncio.run(self.cams[camera_name]["ptz"].AbsoluteMove(move_request))
await self.cams[camera_name]["ptz"].AbsoluteMove(move_request)
self.cams[camera_name]["active"] = False
def handle_command(
async def handle_command_async(
self, camera_name: str, command: OnvifCommandEnum, param: str = ""
) -> None:
"""Handle ONVIF commands asynchronously"""
if camera_name not in self.cams.keys():
logger.error(f"ONVIF is not configured for {camera_name}")
return
if not self.cams[camera_name]["init"]:
if not asyncio.run(self._init_onvif(camera_name)):
if not await self._init_onvif(camera_name):
return
try:
@@ -580,23 +610,44 @@ class OnvifController:
# already init
return
elif command == OnvifCommandEnum.stop:
self._stop(camera_name)
await self._stop(camera_name)
elif command == OnvifCommandEnum.preset:
self._move_to_preset(camera_name, param)
await self._move_to_preset(camera_name, param)
elif command == OnvifCommandEnum.move_relative:
_, pan, tilt = param.split("_")
self._move_relative(camera_name, float(pan), float(tilt), 0, 1)
await self._move_relative(camera_name, float(pan), float(tilt), 0, 1)
elif (
command == OnvifCommandEnum.zoom_in
or command == OnvifCommandEnum.zoom_out
):
self._zoom(camera_name, command)
await self._zoom(camera_name, command)
else:
self._move(camera_name, command)
except ONVIFError as e:
await self._move(camera_name, command)
except (Fault, ONVIFError, TransportError, Exception) as e:
logger.error(f"Unable to handle onvif command: {e}")
async def get_camera_info(self, camera_name: str) -> dict[str, any]:
def handle_command(
self, camera_name: str, command: OnvifCommandEnum, param: str = ""
) -> None:
"""
Handle ONVIF commands by scheduling them in the event loop.
This is the synchronous interface that schedules async work.
"""
future = asyncio.run_coroutine_threadsafe(
self.handle_command_async(camera_name, command, param), self.loop
)
try:
# Wait with a timeout to prevent blocking indefinitely
future.result(timeout=10)
except asyncio.TimeoutError:
logger.error(f"Command {command} timed out for camera {camera_name}")
except Exception as e:
logger.error(
f"Error executing command {command} for camera {camera_name}: {e}"
)
async def get_camera_info(self, camera_name: str) -> dict[str, Any]:
"""
Get ptz capabilities and presets, attempting to reconnect if ONVIF is configured
but not initialized.
@@ -609,26 +660,23 @@ class OnvifController:
)
return {}
if camera_name not in self.cams and (
if camera_name not in self.cams.keys() and (
camera_name not in self.config.cameras
or not self.config.cameras[camera_name].onvif.host
):
logger.debug(f"ONVIF is not configured for {camera_name}")
return {}
if camera_name in self.cams and self.cams[camera_name]["init"]:
if camera_name in self.cams.keys() and self.cams[camera_name]["init"]:
return {
"name": camera_name,
"features": self.cams[camera_name]["features"],
"presets": list(self.cams[camera_name]["presets"].keys()),
}
if camera_name not in self.cams and camera_name in self.config.cameras:
cam = self.config.cameras[camera_name]
result = self._create_onvif_camera(camera_name, cam)
if result:
self.cams[camera_name] = result
else:
if camera_name not in self.cams.keys() and camera_name in self.config.cameras:
success = await self._init_single_camera(camera_name)
if not success:
return {}
# Reset retry count after timeout
@@ -681,23 +729,21 @@ class OnvifController:
logger.debug(f"Could not initialize ONVIF for {camera_name}")
return {}
def get_service_capabilities(self, camera_name: str) -> None:
async def get_service_capabilities(self, camera_name: str) -> None:
if camera_name not in self.cams.keys():
logger.error(f"ONVIF is not configured for {camera_name}")
return {}
if not self.cams[camera_name]["init"]:
asyncio.run(self._init_onvif(camera_name))
await self._init_onvif(camera_name)
service_capabilities_request = self.cams[camera_name][
"service_capabilities_request"
]
try:
service_capabilities = asyncio.run(
self.cams[camera_name]["ptz"].GetServiceCapabilities(
service_capabilities_request
)
)
service_capabilities = await self.cams[camera_name][
"ptz"
].GetServiceCapabilities(service_capabilities_request)
logger.debug(
f"Onvif service capabilities for {camera_name}: {service_capabilities}"
@@ -705,25 +751,24 @@ class OnvifController:
# MoveStatus is required for autotracking - should return "true" if supported
return find_by_key(vars(service_capabilities), "MoveStatus")
except Exception:
except Exception as e:
logger.warning(
f"Camera {camera_name} does not support the ONVIF GetServiceCapabilities method. Autotracking will not function correctly and must be disabled in your config."
f"Camera {camera_name} does not support the ONVIF GetServiceCapabilities method. Autotracking will not function correctly and must be disabled in your config. Exception: {e}"
)
return False
def get_camera_status(self, camera_name: str) -> None:
async def get_camera_status(self, camera_name: str) -> None:
if camera_name not in self.cams.keys():
logger.error(f"ONVIF is not configured for {camera_name}")
return {}
return
if not self.cams[camera_name]["init"]:
asyncio.run(self._init_onvif(camera_name))
if not await self._init_onvif(camera_name):
return
status_request = self.cams[camera_name]["status_request"]
try:
status = asyncio.run(
self.cams[camera_name]["ptz"].GetStatus(status_request)
)
status = await self.cams[camera_name]["ptz"].GetStatus(status_request)
except Exception:
pass # We're unsupported, that'll be reported in the next check.
@@ -807,3 +852,22 @@ class OnvifController:
camera_name
].frame_time.value
logger.warning(f"Camera {camera_name} is still in ONVIF 'MOVING' status.")
def close(self) -> None:
"""Gracefully shut down the ONVIF controller."""
if not hasattr(self, "loop") or self.loop.is_closed():
logger.debug("ONVIF controller already closed")
return
logger.info("Exiting ONVIF controller...")
def stop_and_cleanup():
try:
self.loop.stop()
except Exception as e:
logger.error(f"Error during loop cleanup: {e}")
# Schedule stop and cleanup in the loop thread
self.loop.call_soon_threadsafe(stop_and_cleanup)
self.loop_thread.join()
+5 -1
View File
@@ -69,7 +69,7 @@ class RecordingCleanup(threading.Thread):
now - datetime.timedelta(days=config.record.detections.retain.days)
).timestamp()
expired_reviews: ReviewSegment = (
ReviewSegment.select(ReviewSegment.id)
ReviewSegment.select(ReviewSegment.id, ReviewSegment.thumb_path)
.where(ReviewSegment.camera == config.name)
.where(
(
@@ -84,6 +84,10 @@ class RecordingCleanup(threading.Thread):
.namedtuples()
)
thumbs_to_delete = list(map(lambda x: x[1], expired_reviews))
for thumb_path in thumbs_to_delete:
Path(thumb_path).unlink(missing_ok=True)
max_deletes = 100000
deleted_reviews_list = list(map(lambda x: x[0], expired_reviews))
for i in range(0, len(deleted_reviews_list), max_deletes):
+1 -1
View File
@@ -242,7 +242,7 @@ class RecordingMaintainer(threading.Thread):
self.end_time_cache.pop(cache_path, None)
async def validate_and_move_segment(
self, camera: str, reviews: list[ReviewSegment], recording: dict[str, any]
self, camera: str, reviews: list[ReviewSegment], recording: dict[str, Any]
) -> None:
cache_path: str = recording["cache_path"]
start_time: datetime.datetime = recording["start_time"]
+22 -19
View File
@@ -1,5 +1,6 @@
"""Maintain review segments in db."""
import copy
import json
import logging
import os
@@ -9,7 +10,7 @@ import sys
import threading
from multiprocessing.synchronize import Event as MpEvent
from pathlib import Path
from typing import Optional
from typing import Any, Optional
import cv2
import numpy as np
@@ -119,21 +120,23 @@ class PendingReviewSegment:
)
def get_data(self, ended: bool) -> dict:
return {
ReviewSegment.id.name: self.id,
ReviewSegment.camera.name: self.camera,
ReviewSegment.start_time.name: self.start_time,
ReviewSegment.end_time.name: self.last_update if ended else None,
ReviewSegment.severity.name: self.severity.value,
ReviewSegment.thumb_path.name: self.frame_path,
ReviewSegment.data.name: {
"detections": list(set(self.detections.keys())),
"objects": list(set(self.detections.values())),
"sub_labels": list(self.sub_labels.values()),
"zones": self.zones,
"audio": list(self.audio),
},
}.copy()
return copy.deepcopy(
{
ReviewSegment.id.name: self.id,
ReviewSegment.camera.name: self.camera,
ReviewSegment.start_time.name: self.start_time,
ReviewSegment.end_time.name: self.last_update if ended else None,
ReviewSegment.severity.name: self.severity.value,
ReviewSegment.thumb_path.name: self.frame_path,
ReviewSegment.data.name: {
"detections": list(set(self.detections.keys())),
"objects": list(set(self.detections.values())),
"sub_labels": list(self.sub_labels.values()),
"zones": self.zones,
"audio": list(self.audio),
},
}
)
class ReviewSegmentMaintainer(threading.Thread):
@@ -153,7 +156,7 @@ class ReviewSegmentMaintainer(threading.Thread):
self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all)
# manual events
self.indefinite_events: dict[str, dict[str, any]] = {}
self.indefinite_events: dict[str, dict[str, Any]] = {}
# ensure dirs
Path(os.path.join(CLIPS_DIR, "review")).mkdir(exist_ok=True)
@@ -191,7 +194,7 @@ class ReviewSegmentMaintainer(threading.Thread):
camera_config: CameraConfig,
frame,
objects: list[TrackedObject],
prev_data: dict[str, any],
prev_data: dict[str, Any],
) -> None:
"""Update segment."""
if frame is not None:
@@ -216,7 +219,7 @@ class ReviewSegmentMaintainer(threading.Thread):
def _publish_segment_end(
self,
segment: PendingReviewSegment,
prev_data: dict[str, any],
prev_data: dict[str, Any],
) -> None:
"""End segment."""
final_data = segment.get_data(ended=True)
+5 -5
View File
@@ -6,7 +6,7 @@ import logging
import threading
import time
from multiprocessing.synchronize import Event as MpEvent
from typing import Optional
from typing import Any, Optional
from frigate.comms.inter_process import InterProcessRequestor
from frigate.config import FrigateConfig
@@ -33,12 +33,12 @@ class StatsEmitter(threading.Thread):
self.stats_tracking = stats_tracking
self.stop_event = stop_event
self.hwaccel_errors: list[str] = []
self.stats_history: list[dict[str, any]] = []
self.stats_history: list[dict[str, Any]] = []
# create communication for stats
self.requestor = InterProcessRequestor()
def get_latest_stats(self) -> dict[str, any]:
def get_latest_stats(self) -> dict[str, Any]:
"""Get latest stats."""
if len(self.stats_history) > 0:
return self.stats_history[-1]
@@ -51,12 +51,12 @@ class StatsEmitter(threading.Thread):
def get_stats_history(
self, keys: Optional[list[str]] = None
) -> list[dict[str, any]]:
) -> list[dict[str, Any]]:
"""Get stats history."""
if not keys:
return self.stats_history
selected_stats: list[dict[str, any]] = []
selected_stats: list[dict[str, Any]] = []
for s in self.stats_history:
selected = {}
+8 -7
View File
@@ -5,6 +5,7 @@ import queue
import threading
from multiprocessing import Queue
from multiprocessing.synchronize import Event as MpEvent
from typing import Any
from frigate.config import FrigateConfig
from frigate.events.maintainer import EventStateEnum, EventTypeEnum
@@ -27,7 +28,7 @@ class TimelineProcessor(threading.Thread):
self.config = config
self.queue = queue
self.stop_event = stop_event
self.pre_event_cache: dict[str, list[dict[str, any]]] = {}
self.pre_event_cache: dict[str, list[dict[str, Any]]] = {}
def run(self) -> None:
while not self.stop_event.is_set():
@@ -55,9 +56,9 @@ class TimelineProcessor(threading.Thread):
def insert_or_save(
self,
entry: dict[str, any],
prev_event_data: dict[any, any],
event_data: dict[any, any],
entry: dict[str, Any],
prev_event_data: dict[Any, Any],
event_data: dict[Any, Any],
) -> None:
"""Insert into db or cache."""
id = entry[Timeline.source_id]
@@ -81,8 +82,8 @@ class TimelineProcessor(threading.Thread):
self,
camera: str,
event_type: str,
prev_event_data: dict[any, any],
event_data: dict[any, any],
prev_event_data: dict[Any, Any],
event_data: dict[Any, Any],
) -> bool:
"""Handle object detection."""
save = False
@@ -153,7 +154,7 @@ class TimelineProcessor(threading.Thread):
self,
camera: str,
event_type: str,
event_data: dict[any, any],
event_data: dict[Any, Any],
) -> bool:
if event_type != "new":
return False
+2 -1
View File
@@ -1,4 +1,5 @@
from abc import ABC, abstractmethod
from typing import Any
from frigate.config import DetectConfig
@@ -10,6 +11,6 @@ class ObjectTracker(ABC):
@abstractmethod
def match_and_update(
self, frame_name: str, frame_time: float, detections: list[dict[str, any]]
self, frame_name: str, frame_time: float, detections: list[dict[str, Any]]
) -> None:
pass
+2 -2
View File
@@ -1,7 +1,7 @@
import logging
import random
import string
from typing import Sequence
from typing import Any, Sequence
import cv2
import numpy as np
@@ -460,7 +460,7 @@ class NorfairTracker(ObjectTracker):
self.match_and_update(frame_name, frame_time, detections=detections)
def match_and_update(
self, frame_name: str, frame_time: float, detections: list[dict[str, any]]
self, frame_name: str, frame_time: float, detections: list[dict[str, Any]]
):
# Group detections by object type
detections_by_type = {}
+3 -2
View File
@@ -7,6 +7,7 @@ import threading
from collections import defaultdict
from enum import Enum
from multiprocessing.synchronize import Event as MpEvent
from typing import Any
import cv2
import numpy as np
@@ -70,7 +71,7 @@ class TrackedObjectProcessor(threading.Thread):
self.event_end_subscriber = EventEndSubscriber()
self.sub_label_subscriber = EventMetadataSubscriber(EventMetadataTypeEnum.all)
self.camera_activity: dict[str, dict[str, any]] = {}
self.camera_activity: dict[str, dict[str, Any]] = {}
self.ongoing_manual_events: dict[str, str] = {}
# {
@@ -301,7 +302,7 @@ class TrackedObjectProcessor(threading.Thread):
return {}
def get_current_frame(
self, camera: str, draw_options: dict[str, any] = {}
self, camera: str, draw_options: dict[str, Any] = {}
) -> np.ndarray | None:
if camera == "birdseye":
return self.frame_manager.get(
+6 -6
View File
@@ -5,7 +5,7 @@ import math
import os
from collections import defaultdict
from statistics import median
from typing import Optional
from typing import Any, Optional
import cv2
import numpy as np
@@ -38,7 +38,7 @@ class TrackedObject:
camera_config: CameraConfig,
ui_config: UIConfig,
frame_cache,
obj_data: dict[str, any],
obj_data: dict[str, Any],
):
# set the score history then remove as it is not part of object state
self.score_history = obj_data["score_history"]
@@ -154,7 +154,7 @@ class TrackedObject:
"attributes": obj_data["attributes"],
"current_estimated_speed": self.current_estimated_speed,
"velocity_angle": self.velocity_angle,
"path_data": self.path_data,
"path_data": self.path_data.copy(),
"recognized_license_plate": obj_data.get(
"recognized_license_plate"
),
@@ -378,7 +378,7 @@ class TrackedObject:
"current_estimated_speed": self.current_estimated_speed,
"average_estimated_speed": self.average_estimated_speed,
"velocity_angle": self.velocity_angle,
"path_data": self.path_data,
"path_data": self.path_data.copy(),
"recognized_license_plate": self.obj_data.get("recognized_license_plate"),
}
@@ -621,7 +621,7 @@ class TrackedObjectAttribute:
self.ratio = raw_data[4]
self.region = raw_data[5]
def get_tracking_data(self) -> dict[str, any]:
def get_tracking_data(self) -> dict[str, Any]:
"""Return data saved to the object."""
return {
"label": self.label,
@@ -629,7 +629,7 @@ class TrackedObjectAttribute:
"box": self.box,
}
def find_best_object(self, objects: list[dict[str, any]]) -> Optional[str]:
def find_best_object(self, objects: list[dict[str, Any]]) -> Optional[str]:
"""Find the best attribute for each object and return its ID."""
best_object_area = None
best_object_id = None
+28 -7
View File
@@ -11,6 +11,7 @@ import shlex
import struct
import urllib.parse
from collections.abc import Mapping
from multiprocessing.sharedctypes import Synchronized
from pathlib import Path
from typing import Any, Optional, Tuple, Union
from zoneinfo import ZoneInfoNotFoundError
@@ -26,16 +27,16 @@ logger = logging.getLogger(__name__)
class EventsPerSecond:
def __init__(self, max_events=1000, last_n_seconds=10):
def __init__(self, max_events=1000, last_n_seconds=10) -> None:
self._start = None
self._max_events = max_events
self._last_n_seconds = last_n_seconds
self._timestamps = []
def start(self):
def start(self) -> None:
self._start = datetime.datetime.now().timestamp()
def update(self):
def update(self) -> None:
now = datetime.datetime.now().timestamp()
if self._start is None:
self._start = now
@@ -45,7 +46,7 @@ class EventsPerSecond:
self._timestamps = self._timestamps[(1 - self._max_events) :]
self.expire_timestamps(now)
def eps(self):
def eps(self) -> float:
now = datetime.datetime.now().timestamp()
if self._start is None:
self._start = now
@@ -58,12 +59,29 @@ class EventsPerSecond:
return len(self._timestamps) / seconds
# remove aged out timestamps
def expire_timestamps(self, now):
def expire_timestamps(self, now: float) -> None:
threshold = now - self._last_n_seconds
while self._timestamps and self._timestamps[0] < threshold:
del self._timestamps[0]
class InferenceSpeed:
def __init__(self, metric: Synchronized) -> None:
self.__metric = metric
self.__initialized = False
def update(self, inference_time: float) -> None:
if not self.__initialized:
self.__metric.value = inference_time
self.__initialized = True
return
self.__metric.value = (self.__metric.value * 9 + inference_time) / 10
def current(self) -> float:
return self.__metric.value
def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dict:
"""
:param dct1: First dict to merge
@@ -138,7 +156,7 @@ def load_labels(path: Optional[str], encoding="utf-8", prefill=91):
return labels
def get_tz_modifiers(tz_name: str) -> Tuple[str, str, int]:
def get_tz_modifiers(tz_name: str) -> Tuple[str, str, float]:
seconds_offset = (
datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds()
)
@@ -151,7 +169,7 @@ def get_tz_modifiers(tz_name: str) -> Tuple[str, str, int]:
def to_relative_box(
width: int, height: int, box: Tuple[int, int, int, int]
) -> Tuple[int, int, int, int]:
) -> Tuple[int | float, int | float, int | float, int | float]:
return (
box[0] / width, # x
box[1] / height, # y
@@ -169,6 +187,9 @@ def update_yaml_from_url(file_path, url):
parsed_url = urllib.parse.urlparse(url)
query_string = urllib.parse.parse_qs(parsed_url.query, keep_blank_values=True)
# Filter out empty keys but keep blank values for non-empty keys
query_string = {k: v for k, v in query_string.items() if k}
for key_path_str, new_value_list in query_string.items():
key_path = key_path_str.split(".")
for i in range(len(key_path)):
+10 -10
View File
@@ -4,7 +4,7 @@ import asyncio
import logging
import os
import shutil
from typing import Optional, Union
from typing import Any, Optional, Union
from ruamel.yaml import YAML
@@ -37,7 +37,7 @@ def migrate_frigate_config(config_file: str):
yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
with open(config_file, "r") as f:
config: dict[str, dict[str, any]] = yaml.load(f)
config: dict[str, dict[str, Any]] = yaml.load(f)
if config is None:
logger.error(f"Failed to load config at {config_file}")
@@ -94,7 +94,7 @@ def migrate_frigate_config(config_file: str):
logger.info("Finished frigate config migration...")
def migrate_014(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]]:
def migrate_014(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
"""Handle migrating frigate config to 0.14"""
# migrate record.events.required_zones to review.alerts.required_zones
new_config = config.copy()
@@ -142,7 +142,7 @@ def migrate_014(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]]:
del new_config["rtmp"]
for name, camera in config.get("cameras", {}).items():
camera_config: dict[str, dict[str, any]] = camera.copy()
camera_config: dict[str, dict[str, Any]] = camera.copy()
required_zones = (
camera_config.get("record", {}).get("events", {}).get("required_zones", [])
)
@@ -181,7 +181,7 @@ def migrate_014(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]]:
return new_config
def migrate_015_0(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]]:
def migrate_015_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
"""Handle migrating frigate config to 0.15-0"""
new_config = config.copy()
@@ -232,9 +232,9 @@ def migrate_015_0(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]
del new_config["record"]["events"]
for name, camera in config.get("cameras", {}).items():
camera_config: dict[str, dict[str, any]] = camera.copy()
camera_config: dict[str, dict[str, Any]] = camera.copy()
record_events: dict[str, any] = camera_config.get("record", {}).get("events")
record_events: dict[str, Any] = camera_config.get("record", {}).get("events")
if record_events:
alerts_retention = {"retain": {}}
@@ -281,7 +281,7 @@ def migrate_015_0(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]
return new_config
def migrate_015_1(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]]:
def migrate_015_1(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
"""Handle migrating frigate config to 0.15-1"""
new_config = config.copy()
@@ -296,7 +296,7 @@ def migrate_015_1(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]
return new_config
def migrate_016_0(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]]:
def migrate_016_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
"""Handle migrating frigate config to 0.16-0"""
new_config = config.copy()
@@ -307,7 +307,7 @@ def migrate_016_0(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]
new_config["detect"] = detect_config
for name, camera in config.get("cameras", {}).items():
camera_config: dict[str, dict[str, any]] = camera.copy()
camera_config: dict[str, dict[str, Any]] = camera.copy()
live_config = camera_config.get("live", {})
if "stream_name" in live_config:
+4 -4
View File
@@ -8,7 +8,7 @@ from abc import ABC, abstractmethod
from multiprocessing import resource_tracker as _mprt
from multiprocessing import shared_memory as _mpshm
from string import printable
from typing import AnyStr, Optional
from typing import Any, AnyStr, Optional
import cv2
import numpy as np
@@ -766,7 +766,7 @@ class FrameManager(ABC):
pass
@abstractmethod
def write(self, name: str) -> memoryview:
def write(self, name: str) -> Optional[memoryview]:
pass
@abstractmethod
@@ -847,7 +847,7 @@ class SharedMemoryFrameManager(FrameManager):
self.shm_store[name] = shm
return shm.buf
def write(self, name: str) -> memoryview:
def write(self, name: str) -> Optional[memoryview]:
try:
if name in self.shm_store:
shm = self.shm_store[name]
@@ -944,7 +944,7 @@ def get_image_from_recording(
relative_frame_time: float,
codec: str,
height: Optional[int] = None,
) -> Optional[any]:
) -> Optional[Any]:
"""retrieve a frame from given time in recording file."""
ffmpeg_cmd = [
+2 -2
View File
@@ -2,6 +2,7 @@
import logging
import os
from typing import Any
import cv2
import numpy as np
@@ -284,7 +285,7 @@ def post_process_yolox(
def get_ort_providers(
force_cpu: bool = False, device: str = "AUTO", requires_fp16: bool = False
) -> tuple[list[str], list[dict[str, any]]]:
) -> tuple[list[str], list[dict[str, Any]]]:
if force_cpu:
return (
["CPUExecutionProvider"],
@@ -340,7 +341,6 @@ def get_ort_providers(
providers.append(provider)
options.append(
{
"arena_extend_strategy": "kSameAsRequested",
"cache_dir": os.path.join(MODEL_CACHE_DIR, "openvino/ort"),
"device_type": device,
}
+11 -10
View File
@@ -4,6 +4,7 @@ import datetime
import logging
import math
from collections import defaultdict
from typing import Any
import cv2
import numpy as np
@@ -38,7 +39,7 @@ def get_camera_regions_grid(
name: str,
detect: DetectConfig,
min_region_size: int,
) -> list[list[dict[str, any]]]:
) -> list[list[dict[str, Any]]]:
"""Build a grid of expected region sizes for a camera."""
# get grid from db if available
try:
@@ -163,10 +164,10 @@ def get_cluster_region_from_grid(frame_shape, min_region, cluster, boxes, region
def get_region_from_grid(
frame_shape: tuple[int],
frame_shape: tuple[int, int],
cluster: list[int],
min_region: int,
region_grid: list[list[dict[str, any]]],
region_grid: list[list[dict[str, Any]]],
) -> list[int]:
"""Get a region for a box based on the region grid."""
box = calculate_region(
@@ -446,9 +447,9 @@ def get_cluster_region(frame_shape, min_region, cluster, boxes):
def get_startup_regions(
frame_shape: tuple[int],
frame_shape: tuple[int, int],
region_min_size: int,
region_grid: list[list[dict[str, any]]],
region_grid: list[list[dict[str, Any]]],
) -> list[list[int]]:
"""Get a list of regions to run on startup."""
# return 8 most popular regions for the camera
@@ -480,12 +481,12 @@ def get_startup_regions(
def reduce_detections(
frame_shape: tuple[int],
all_detections: list[tuple[any]],
) -> list[tuple[any]]:
frame_shape: tuple[int, int],
all_detections: list[tuple[Any]],
) -> list[tuple[Any]]:
"""Take a list of detections and reduce overlaps to create a list of confident detections."""
def reduce_overlapping_detections(detections: list[tuple[any]]) -> list[tuple[any]]:
def reduce_overlapping_detections(detections: list[tuple[Any]]) -> list[tuple[Any]]:
"""apply non-maxima suppression to suppress weak, overlapping bounding boxes."""
detected_object_groups = defaultdict(lambda: [])
for detection in detections:
@@ -524,7 +525,7 @@ def reduce_detections(
# set the detections list to only include top objects
return selected_objects
def get_consolidated_object_detections(detections: list[tuple[any]]):
def get_consolidated_object_detections(detections: list[tuple[Any]]):
"""Drop detections that overlap too much."""
detected_object_groups = defaultdict(lambda: [])
for detection in detections:
+16 -10
View File
@@ -9,7 +9,7 @@ import signal
import subprocess as sp
import traceback
from datetime import datetime
from typing import List, Optional, Tuple
from typing import Any, List, Optional, Tuple
import cv2
import psutil
@@ -230,7 +230,7 @@ def is_vaapi_amd_driver() -> bool:
return any("AMD Radeon Graphics" in line for line in output)
def get_amd_gpu_stats() -> dict[str, str]:
def get_amd_gpu_stats() -> Optional[dict[str, str]]:
"""Get stats using radeontop."""
radeontop_command = ["radeontop", "-d", "-", "-l", "1"]
@@ -256,7 +256,7 @@ def get_amd_gpu_stats() -> dict[str, str]:
return results
def get_intel_gpu_stats(sriov: bool) -> dict[str, str]:
def get_intel_gpu_stats(sriov: bool) -> Optional[dict[str, str]]:
"""Get stats using intel_gpu_top."""
def get_stats_manually(output: str) -> dict[str, str]:
@@ -382,7 +382,7 @@ def get_intel_gpu_stats(sriov: bool) -> dict[str, str]:
return results
def get_rockchip_gpu_stats() -> dict[str, str]:
def get_rockchip_gpu_stats() -> Optional[dict[str, str]]:
"""Get GPU stats using rk."""
try:
with open("/sys/kernel/debug/rkrga/load", "r") as f:
@@ -403,12 +403,18 @@ def get_rockchip_gpu_stats() -> dict[str, str]:
return {"gpu": average_load, "mem": "-"}
def get_rockchip_npu_stats() -> dict[str, str]:
def get_rockchip_npu_stats() -> Optional[dict[str, float | str]]:
"""Get NPU stats using rk."""
try:
with open("/sys/kernel/debug/rknpu/load", "r") as f:
npu_output = f.read()
core_loads = re.findall(r"Core\d+:\s*(\d+)%", npu_output)
if "Core0:" in npu_output:
# multi core NPU
core_loads = re.findall(r"Core\d+:\s*(\d+)%", npu_output)
else:
# single core NPU
core_loads = re.findall(r"NPU load:\s+(\d+)%", npu_output)
except FileNotFoundError:
core_loads = None
@@ -488,7 +494,7 @@ def get_nvidia_gpu_stats() -> dict[int, dict]:
return results
def get_jetson_stats() -> dict[int, dict]:
def get_jetson_stats() -> Optional[dict[int, dict]]:
results = {}
try:
@@ -531,7 +537,7 @@ def vainfo_hwaccel(device_name: Optional[str] = None) -> sp.CompletedProcess:
return sp.run(ffprobe_cmd, capture_output=True)
def get_nvidia_driver_info() -> dict[str, any]:
def get_nvidia_driver_info() -> dict[str, Any]:
"""Get general hardware info for nvidia GPU."""
results = {}
try:
@@ -590,8 +596,8 @@ def auto_detect_hwaccel() -> str:
async def get_video_properties(
ffmpeg, url: str, get_duration: bool = False
) -> dict[str, any]:
async def calculate_duration(video: Optional[any]) -> float:
) -> dict[str, Any]:
async def calculate_duration(video: Optional[Any]) -> float:
duration = None
if video is not None:
+5 -3
View File
@@ -184,7 +184,7 @@ class CameraWatchdog(threading.Thread):
self.capture_thread = None
self.ffmpeg_detect_process = None
self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect")
self.ffmpeg_other_processes: list[dict[str, any]] = []
self.ffmpeg_other_processes: list[dict[str, Any]] = []
self.camera_fps = camera_fps
self.skipped_fps = skipped_fps
self.ffmpeg_pid = ffmpeg_pid
@@ -371,7 +371,9 @@ class CameraWatchdog(threading.Thread):
p["logpipe"].close()
self.ffmpeg_other_processes.clear()
def get_latest_segment_datetime(self, latest_segment: datetime.datetime) -> int:
def get_latest_segment_datetime(
self, latest_segment: datetime.datetime
) -> datetime.datetime:
"""Checks if ffmpeg is still writing recording segments to cache."""
cache_files = sorted(
[
@@ -859,7 +861,7 @@ def process_frames(
detections[obj["id"]] = {**obj, "attributes": []}
# find the best object for each attribute to be assigned to
all_objects: list[dict[str, any]] = object_tracker.tracked_objects.values()
all_objects: list[dict[str, Any]] = object_tracker.tracked_objects.values()
for attributes in attribute_detections.values():
for attribute in attributes:
filtered_objects = filter(
+2 -2
View File
@@ -1,3 +1,3 @@
[tool.ruff]
[tool.ruff.lint]
ignore = ["E501","E711","E712"]
extend-select = ["I"]
extend-select = ["I"]
+1
View File
@@ -0,0 +1 @@
{}
+6
View File
@@ -0,0 +1,6 @@
{
"time": {
"today": "Днес",
"yesterday": "Вчера"
}
}
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}

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