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 STABLE_TAG=${BASE}:stable
PULL_TAG=${BASE}:${BUILD_TAG} 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} 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} 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 done
# stable tag # stable tag
if [[ "${BUILD_TYPE}" == "stable" ]]; then 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} 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} 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 done
fi fi
+6 -6
View File
@@ -24,11 +24,9 @@
- 通过RTSP重新流传输以减少摄像头的连接数 - 通过RTSP重新流传输以减少摄像头的连接数
- 支持WebRTC和MSE,实现低延迟的实时观看 - 支持WebRTC和MSE,实现低延迟的实时观看
## 文档(英文) ## 社区中文翻译文档
你可以在这里查看文档 https://docs.frigate.video 你可以在这里查看文档 https://docs.frigate-cn.video
文档还暂时没有提供翻译,将会在未来提供。
## 赞助 ## 赞助
@@ -60,5 +58,7 @@
## 翻译 ## 翻译
我们使用 [Weblate](https://hosted.weblate.org/projects/frigate-nvr/) 平台提供翻译支持,欢迎参与进来一起完善。 我们使用 [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 [] CMD []
HEALTHCHECK --start-period=300s --start-interval=5s --interval=15s --timeout=5s --retries=3 \ 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 # Frigate deps with Node.js and NPM for devcontainer
FROM deps AS devcontainer FROM deps AS devcontainer
@@ -25,4 +25,7 @@ elif [[ "${exit_code_service}" -ne 0 ]]; then
fi fi
fi fi
# used by the docker healthcheck
touch /dev/shm/.frigate-is-stopping
exec /run/s6/basedir/bin/halt exec /run/s6/basedir/bin/halt
@@ -4,6 +4,11 @@
set -o errexit -o nounset -o pipefail 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 # Logs should be sent to stdout so that s6 can collect them
# Tell S6-Overlay not to restart this service # Tell S6-Overlay not to restart this service
@@ -138,5 +138,9 @@ function migrate_db_from_media_to_config() {
fi 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_addon_config_dir
migrate_db_from_media_to_config migrate_db_from_media_to_config
@@ -1,5 +1,6 @@
import json import json
import sys import sys
from typing import Any
from ruamel.yaml import YAML from ruamel.yaml import YAML
@@ -21,11 +22,11 @@ try:
raw_config = f.read() raw_config = f.read()
if config_file.endswith((".yaml", ".yml")): 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"): elif config_file.endswith(".json"):
config: dict[str, any] = json.loads(raw_config) config: dict[str, Any] = json.loads(raw_config)
except FileNotFoundError: except FileNotFoundError:
config: dict[str, any] = {} config: dict[str, Any] = {}
path = config.get("ffmpeg", {}).get("path", "default") path = config.get("ffmpeg", {}).get("path", "default")
if path == "default": if path == "default":
@@ -4,6 +4,7 @@ import json
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any
from ruamel.yaml import YAML from ruamel.yaml import YAML
@@ -37,13 +38,13 @@ try:
raw_config = f.read() raw_config = f.read()
if config_file.endswith((".yaml", ".yml")): 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"): elif config_file.endswith(".json"):
config: dict[str, any] = json.loads(raw_config) config: dict[str, Any] = json.loads(raw_config)
except FileNotFoundError: 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 # Need to enable CORS for go2rtc so the frigate integration / card work automatically
if go2rtc_config.get("api") is None: 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 # Need to set default location for HA config
if go2rtc_config.get("hass") is None: 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 # we want to ensure that logs are easy to read
if go2rtc_config.get("log") is None: if go2rtc_config.get("log") is None:
@@ -134,7 +135,7 @@ for name in go2rtc_config.get("streams", {}):
# add birdseye restream stream if enabled # add birdseye restream stream if enabled
if config.get("birdseye", {}).get("restream", False): 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}" 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}')}" 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 json
import os import os
from typing import Any
base_path = os.environ.get("FRIGATE_BASE_PATH", "") 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)) print(json.dumps(result))
@@ -2,6 +2,7 @@
import json import json
import sys import sys
from typing import Any
from ruamel.yaml import YAML from ruamel.yaml import YAML
@@ -19,12 +20,12 @@ try:
raw_config = f.read() raw_config = f.read()
if config_file.endswith((".yaml", ".yml")): 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"): elif config_file.endswith(".json"):
config: dict[str, any] = json.loads(raw_config) config: dict[str, Any] = json.loads(raw_config)
except FileNotFoundError: 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)) 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. 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. 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. 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 #### Port Considerations
**Authenticated Port (8971)** **Authenticated Port (8971)**
+35 -7
View File
@@ -3,7 +3,7 @@ id: face_recognition
title: 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 ## 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. 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 ### Face Recognition
Frigate has support for two face recognition model types: 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. 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 ## Minimum System Requirements
The `small` model is optimized for efficiency and runs on the CPU, most CPUs should run the model efficiently. 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 ## Configuration
@@ -39,7 +47,7 @@ face_recognition:
## Advanced Configuration ## 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 ### 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. - `blur_confidence_filter`: Enables a filter that calculates how blurry the face is and adjusts the confidence based on this.
- Default: `True`. - 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 ## Creating a Robust Training Set
The number of images needed for a sufficient training set for face recognition varies depending on several factors: 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 ### 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. 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 ### 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 ## 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. - 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. - 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? ### 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. 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 ### 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. 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. - 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. 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) 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. 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 Review Item Details pane in Review (sub labels).
- Viewable in the Tracked Object Details pane in Explore (sub labels and recognized license plates). - Viewable in the Tracked Object Details pane in Explore (sub labels and recognized license plates).
- Filterable through the More Filters menu in Explore. - 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`. - Published via the `frigate/tracked_object_update` MQTT topic with `name` (if known) and `plate`.
## Model Requirements ## Model Requirements
@@ -24,7 +24,7 @@ Users without a model that detects license plates can still run LPR. Frigate use
:::note :::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 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. 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 ### 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. - 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`. - 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. - **`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. - Snapshots will have license plate bounding boxes on them.
- The `frigate/events` MQTT topic will publish tracked object updates. - The `frigate/events` MQTT topic will publish tracked object updates.
- Debug view will display `license_plate` bounding boxes. - 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+) ### 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. 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? ### 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? ### How do I debug LPR issues?
- View MQTT messages for `frigate/events` to verify detected plates. - 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`. - 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` label will change to the recognized plate when LPR is enabled and working. - 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). - 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_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. - 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. 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 ```yaml
detectors: detectors:
hailo8l: hailo:
type: hailo8l type: hailo8l
device: PCIe device: PCIe
@@ -185,7 +185,7 @@ For SSD-based models, provide either a model path or URL to your compiled SSD mo
```yaml ```yaml
detectors: detectors:
hailo8l: hailo:
type: hailo8l type: hailo8l
device: PCIe device: PCIe
@@ -209,7 +209,7 @@ The Hailo detector supports all YOLO models compiled for Hailo hardware that inc
```yaml ```yaml
detectors: detectors:
hailo8l: hailo:
type: hailo8l type: hailo8l
device: PCIe 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 # 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 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 # Optional: Mapping for headers from upstream proxies. Only used if Frigate's auth
# is disabled. # is disabled.
# NOTE: Many authentication proxies pass a header downstream with the authenticated # 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. # See the docs for more info.
header_map: header_map:
user: x-forwarded-user user: x-forwarded-user
role: x-forwarded-role
# Optional: Url for logging out a user. This sets the location of the logout url in # Optional: Url for logging out a user. This sets the location of the logout url in
# the UI. # the UI.
logout_url: /api/logout logout_url: /api/logout
# Optional: Auth secret that is checked against the X-Proxy-Secret header sent from # 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. # the proxy. If not set, all requests are trusted regardless of origin.
auth_secret: None auth_secret: None
# Optional: The default role to use for proxy auth. Must be "admin" or "viewer"
default_role: viewer
# Optional: Authentication configuration # Optional: Authentication configuration
auth: auth:
@@ -543,9 +546,9 @@ semantic_search:
model_size: "small" model_size: "small"
# Optional: Configuration for face recognition capability # 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: face_recognition:
# Optional: Enable semantic search (default: shown below) # Optional: Enable face recognition (default: shown below)
enabled: False enabled: False
# Optional: Minimum face distance score required to mark as a potential match (default: shown below) # Optional: Minimum face distance score required to mark as a potential match (default: shown below)
unknown_score: 0.8 unknown_score: 0.8
@@ -560,6 +563,8 @@ face_recognition:
save_attempts: 100 save_attempts: 100
# Optional: Apply a blur quality filter to adjust confidence based on the blur level of the image (default: shown below) # Optional: Apply a blur quality filter to adjust confidence based on the blur level of the image (default: shown below)
blur_confidence_filter: True 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 # Optional: Configuration for license plate recognition capability
# NOTE: enabled, min_area, and enhancement can be overridden at the camera level # 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 ## 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 ```yaml
semantic_search: semantic_search:
@@ -29,9 +29,9 @@ semantic_search:
:::tip :::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 :::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) 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) 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. 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 | | Name | YOLOv9 Inference Time | YOLO-NAS Inference Time |
| -------- | --------------------- | ------------------------- | | --------- | --------------------- | ------------------------- |
| AMD 780M | ~ 14 ms | 320: ~ 30 ms 640: ~ 60 ms | | AMD 780M | ~ 14 ms | 320: ~ 30 ms 640: ~ 60 ms |
| AMD 8700G | | 320: ~ 20 ms 640: ~ 40 ms |
## Community Supported Detectors ## Community Supported Detectors
+19
View File
@@ -17,6 +17,15 @@ const config: Config = {
markdown: { markdown: {
mermaid: true, mermaid: true,
}, },
i18n: {
defaultLocale: 'en',
locales: ['en'],
localeConfigs: {
en: {
label: 'English',
}
},
},
themeConfig: { themeConfig: {
algolia: { algolia: {
appId: 'WIURGBNBPY', appId: 'WIURGBNBPY',
@@ -82,6 +91,16 @@ const config: Config = {
label: 'Demo', label: 'Demo',
position: 'right', position: 'right',
}, },
{
type: 'localeDropdown',
position: 'right',
dropdownItemsAfter: [
{
label: '简体中文(社区翻译)',
href: 'https://docs.frigate-cn.video',
}
]
},
{ {
href: 'https://github.com/blakeblackshear/frigate', href: 'https://github.com/blakeblackshear/frigate',
label: 'GitHub', 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: tags:
- Media - Media
summary: Recording Clip 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 operationId: recording_clip__camera_name__start__start_ts__end__end_ts__clip_mp4_get
parameters: parameters:
- name: camera_name - name: camera_name
+5 -5
View File
@@ -74,7 +74,7 @@ def go2rtc_streams():
) )
stream_data = r.json() stream_data = r.json()
for data in stream_data.values(): 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", "")) producer["url"] = clean_camera_user_pass(producer.get("url", ""))
return JSONResponse(content=stream_data) return JSONResponse(content=stream_data)
@@ -131,7 +131,7 @@ def metrics(request: Request):
@router.get("/config") @router.get("/config")
def config(request: Request): def config(request: Request):
config_obj: FrigateConfig = request.app.frigate_config 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 mode="json", warnings="none", exclude_none=True
) )
@@ -158,7 +158,7 @@ def config(request: Request):
camera_dict["zones"][zone_name]["color"] = zone.color camera_dict["zones"][zone_name]["color"] = zone.color
# remove go2rtc stream passwords # 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 mode="json", warnings="none", exclude_none=True
) )
for stream_name, stream in go2rtc.get("streams", {}).items(): for stream_name, stream in go2rtc.get("streams", {}).items():
@@ -648,7 +648,7 @@ def plusModels(request: Request, filterByCurrentModelDetector: bool = False):
status_code=400, 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"]: if not models["list"]:
return JSONResponse( return JSONResponse(
@@ -801,7 +801,7 @@ def hourly_timeline(params: AppTimelineHourlyQueryParameters = Depends()):
count = 0 count = 0
start = 0 start = 0
end = 0 end = 0
hours: dict[str, list[dict[str, any]]] = {} hours: dict[str, list[dict[str, Any]]] = {}
for t in timeline: for t in timeline:
if count == 0: if count == 0:
+6 -4
View File
@@ -261,14 +261,16 @@ def auth(request: Request):
role_header = proxy_config.header_map.role role_header = proxy_config.header_map.role
role = ( role = (
request.headers.get(role_header, default="viewer") request.headers.get(role_header, default=proxy_config.default_role)
if role_header 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"] = ( 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 return success_response
+6 -7
View File
@@ -1,10 +1,10 @@
"""Object classification APIs.""" """Object classification APIs."""
import datetime
import logging import logging
import os import os
import random
import shutil import shutil
import string from typing import Any
import cv2 import cv2
from fastapi import APIRouter, Depends, Request, UploadFile 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}, 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( training_file = os.path.join(
FACE_DIR, f"train/{sanitize_filename(json.get('training_file', ''))}" 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}, 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_name = sanitize_filename(json.get("training_file", ""))
training_file = os.path.join(FACE_DIR, f"train/{training_file_name}") training_file = os.path.join(FACE_DIR, f"train/{training_file_name}")
event_id = json.get("event_id") 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) sanitized_name = sanitize_filename(name)
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) new_name = f"{sanitized_name}-{datetime.datetime.now().timestamp()}.webp"
new_name = f"{sanitized_name}-{rand_id}.webp"
new_file_folder = os.path.join(FACE_DIR, f"{sanitized_name}") new_file_folder = os.path.join(FACE_DIR, f"{sanitized_name}")
if not os.path.exists(new_file_folder): 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}, 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", "") list_of_ids = json.get("ids", "")
context: EmbeddingsContext = request.app.embeddings context: EmbeddingsContext = request.app.embeddings
+13 -5
View File
@@ -1,5 +1,6 @@
"""Image and video apis.""" """Image and video apis."""
import asyncio
import glob import glob
import logging import logging
import math import math
@@ -8,6 +9,7 @@ import subprocess as sp
import time import time
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path as FilePath from pathlib import Path as FilePath
from typing import Any
from urllib.parse import unquote from urllib.parse import unquote
import cv2 import cv2
@@ -88,7 +90,7 @@ def imagestream(
camera_name: str, camera_name: str,
fps: int, fps: int,
height: int, height: int,
draw_options: dict[str, any], draw_options: dict[str, Any],
): ):
while True: while True:
# max out at specified FPS # max out at specified FPS
@@ -110,9 +112,12 @@ def imagestream(
@router.get("/{camera_name}/ptz/info") @router.get("/{camera_name}/ptz/info")
async def camera_ptz_info(request: Request, camera_name: str): async def camera_ptz_info(request: Request, camera_name: str):
if camera_name in request.app.frigate_config.cameras: if camera_name in request.app.frigate_config.cameras:
return JSONResponse( # Schedule get_camera_info in the OnvifController's event loop
content=await request.app.onvif.get_camera_info(camera_name), 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: else:
return JSONResponse( return JSONResponse(
content={"success": False, "message": "Camera not found"}, content={"success": False, "message": "Camera not found"},
@@ -537,7 +542,10 @@ def recordings(
return JSONResponse(content=list(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( def recording_clip(
request: Request, request: Request,
camera_name: str, camera_name: str,
@@ -902,7 +910,7 @@ def event_thumbnail(
elif extension == "webp": elif extension == "webp":
quality_params = [int(cv2.IMWRITE_WEBP_QUALITY), 60] 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() thumbnail_bytes = img.tobytes()
return Response( return Response(
+2 -1
View File
@@ -2,6 +2,7 @@
import logging import logging
import os import os
from typing import Any
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from fastapi import APIRouter, Request from fastapi import APIRouter, Request
@@ -41,7 +42,7 @@ def register_notifications(request: Request, body: dict = None):
else: else:
username = "admin" username = "admin"
json: dict[str, any] = body or {} json: dict[str, Any] = body or {}
sub = json.get("sub") sub = json.get("sub")
if not sub: if not sub:
+4
View File
@@ -699,6 +699,10 @@ class FrigateApp:
self.audio_process.terminate() self.audio_process.terminate()
self.audio_process.join() self.audio_process.join()
# stop the onvif controller
if self.onvif_controller:
self.onvif_controller.close()
# ensure the capture processes are done # ensure the capture processes are done
for camera, metrics in self.camera_metrics.items(): for camera, metrics in self.camera_metrics.items():
capture_process = metrics.capture_process capture_process = metrics.capture_process
+6 -6
View File
@@ -1,18 +1,18 @@
"""Manage camera activity and updating listeners.""" """Manage camera activity and updating listeners."""
from collections import Counter from collections import Counter
from typing import Callable from typing import Any, Callable
from frigate.config.config import FrigateConfig from frigate.config.config import FrigateConfig
class CameraActivityManager: class CameraActivityManager:
def __init__( def __init__(
self, config: FrigateConfig, publish: Callable[[str, any], None] self, config: FrigateConfig, publish: Callable[[str, Any], None]
) -> None: ) -> None:
self.config = config self.config = config
self.publish = publish 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_all_object_counts: dict[str, Counter] = {}
self.camera_active_object_counts: dict[str, Counter] = {} self.camera_active_object_counts: dict[str, Counter] = {}
self.zone_all_object_counts: dict[str, Counter] = {} self.zone_all_object_counts: dict[str, Counter] = {}
@@ -39,8 +39,8 @@ class CameraActivityManager:
else camera_config.objects.track else camera_config.objects.track
) )
def update_activity(self, new_activity: dict[str, dict[str, any]]) -> None: def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None:
all_objects: list[dict[str, any]] = [] all_objects: list[dict[str, Any]] = []
for camera in new_activity.keys(): for camera in new_activity.keys():
new_objects = new_activity[camera].get("objects", []) new_objects = new_activity[camera].get("objects", [])
@@ -93,7 +93,7 @@ class CameraActivityManager:
self.last_camera_activity = new_activity self.last_camera_activity = new_activity
def compare_camera_activity( def compare_camera_activity(
self, camera: str, new_activity: dict[str, any] self, camera: str, new_activity: dict[str, Any]
) -> None: ) -> None:
all_objects = Counter( all_objects = Counter(
obj["label"].replace("-verified", "") for obj in new_activity obj["label"].replace("-verified", "") for obj in new_activity
+2 -2
View File
@@ -239,7 +239,7 @@ class CameraState:
self, self,
frame_name: str, frame_name: str,
frame_time: float, 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]], motion_boxes: list[tuple[int, int, int, int]],
regions: 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? # TODO: can i switch to looking this up and only changing when an event ends?
# maintain best objects # maintain best objects
camera_activity: dict[str, list[any]] = { camera_activity: dict[str, list[Any]] = {
"motion": len(motion_boxes) > 0, "motion": len(motion_boxes) > 0,
"objects": [], "objects": [],
} }
+3 -3
View File
@@ -2,7 +2,7 @@
import multiprocessing as mp import multiprocessing as mp
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from typing import Optional from typing import Any, Optional
import zmq import zmq
@@ -18,7 +18,7 @@ class ConfigPublisher:
self.socket.bind(SOCKET_PUB_SUB) self.socket.bind(SOCKET_PUB_SUB)
self.stop_event: MpEvent = mp.Event() 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.""" """There is no communication back to the processes."""
self.socket.send_string(topic, flags=zmq.SNDMORE) self.socket.send_string(topic, flags=zmq.SNDMORE)
self.socket.send_pyobj(payload) self.socket.send_pyobj(payload)
@@ -40,7 +40,7 @@ class ConfigSubscriber:
self.socket.setsockopt_string(zmq.SUBSCRIBE, topic) self.socket.setsockopt_string(zmq.SUBSCRIBE, topic)
self.socket.connect(SOCKET_PUB_SUB) 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.""" """Returns updated config or None if no update."""
try: try:
topic = self.socket.recv_string(flags=zmq.NOBLOCK) topic = self.socket.recv_string(flags=zmq.NOBLOCK)
+3 -3
View File
@@ -1,7 +1,7 @@
"""Facilitates communication between processes.""" """Facilitates communication between processes."""
from enum import Enum from enum import Enum
from typing import Optional from typing import Any, Optional
from .zmq_proxy import Publisher, Subscriber from .zmq_proxy import Publisher, Subscriber
@@ -35,10 +35,10 @@ class DetectionSubscriber(Subscriber):
def check_for_update( def check_for_update(
self, timeout: float = None self, timeout: float = None
) -> Optional[tuple[DetectionTypeEnum, any]]: ) -> Optional[tuple[DetectionTypeEnum, Any]]:
return super().check_for_update(timeout) 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: if payload is None:
return (None, None) return (None, None)
return (DetectionTypeEnum[topic[len(self.topic_base) :]], payload) return (DetectionTypeEnum[topic[len(self.topic_base) :]], payload)
+2 -2
View File
@@ -1,7 +1,7 @@
"""Facilitates communication between processes.""" """Facilitates communication between processes."""
from enum import Enum from enum import Enum
from typing import Callable from typing import Any, Callable
import zmq import zmq
@@ -58,7 +58,7 @@ class EmbeddingsRequestor:
self.socket = self.context.socket(zmq.REQ) self.socket = self.context.socket(zmq.REQ)
self.socket.connect(SOCKET_REP_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.""" """Sends data and then waits for reply."""
try: try:
self.socket.send_json((topic, data)) self.socket.send_json((topic, data))
+2 -1
View File
@@ -2,6 +2,7 @@
import logging import logging
from enum import Enum from enum import Enum
from typing import Any
from .zmq_proxy import Publisher, Subscriber from .zmq_proxy import Publisher, Subscriber
@@ -27,7 +28,7 @@ class EventMetadataPublisher(Publisher):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
def publish(self, topic: EventMetadataTypeEnum, payload: any) -> None: def publish(self, topic: EventMetadataTypeEnum, payload: Any) -> None:
super().publish(payload, topic.value) super().publish(payload, topic.value)
+4 -2
View File
@@ -1,5 +1,7 @@
"""Facilitates communication between processes.""" """Facilitates communication between processes."""
from typing import Any
from frigate.events.types import EventStateEnum, EventTypeEnum from frigate.events.types import EventStateEnum, EventTypeEnum
from .zmq_proxy import Publisher, Subscriber from .zmq_proxy import Publisher, Subscriber
@@ -14,7 +16,7 @@ class EventUpdatePublisher(Publisher):
super().__init__("update") super().__init__("update")
def publish( def publish(
self, payload: tuple[EventTypeEnum, EventStateEnum, str, str, dict[str, any]] self, payload: tuple[EventTypeEnum, EventStateEnum, str, str, dict[str, Any]]
) -> None: ) -> None:
super().publish(payload) super().publish(payload)
@@ -37,7 +39,7 @@ class EventEndPublisher(Publisher):
super().__init__("finalized") super().__init__("finalized")
def publish( def publish(
self, payload: tuple[EventTypeEnum, EventStateEnum, str, dict[str, any]] self, payload: tuple[EventTypeEnum, EventStateEnum, str, dict[str, Any]]
) -> None: ) -> None:
super().publish(payload) super().publish(payload)
+2 -2
View File
@@ -3,7 +3,7 @@
import multiprocessing as mp import multiprocessing as mp
import threading import threading
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from typing import Callable from typing import Any, Callable
import zmq import zmq
@@ -63,7 +63,7 @@ class InterProcessRequestor:
self.socket = self.context.socket(zmq.REQ) self.socket = self.context.socket(zmq.REQ)
self.socket.connect(SOCKET_REP_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.""" """Sends data and then waits for reply."""
try: try:
self.socket.send_json((topic, data)) self.socket.send_json((topic, data))
+2
View File
@@ -213,6 +213,8 @@ class MqttClient(Communicator): # type: ignore[misc]
"motion_contour_area", "motion_contour_area",
"birdseye", "birdseye",
"birdseye_mode", "birdseye_mode",
"review_alerts",
"review_detections",
] ]
for name in self.config.cameras.keys(): 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"]) and len(payload["before"]["data"]["zones"])
== len(payload["after"]["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 return
self.last_camera_notification_time[camera] = current_time 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}" direct_url = f"/review?id={reviewId}" if state == "end" else f"/#{camera}"
ttl = 3600 if state == "end" else 0 ttl = 3600 if state == "end" else 0
logger.debug(f"Sending push notification for {camera}, review ID {reviewId}")
for user in self.web_pushers: for user in self.web_pushers:
self.send_push_notification( self.send_push_notification(
user=user, user=user,
+4 -4
View File
@@ -2,7 +2,7 @@
import json import json
import threading import threading
from typing import Optional from typing import Any, Optional
import zmq import zmq
@@ -58,7 +58,7 @@ class Publisher:
self.socket = self.context.socket(zmq.PUB) self.socket = self.context.socket(zmq.PUB)
self.socket.connect(SOCKET_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.""" """Publish message."""
self.socket.send_string(f"{self.topic}{sub_topic} {json.dumps(payload)}") self.socket.send_string(f"{self.topic}{sub_topic} {json.dumps(payload)}")
@@ -81,7 +81,7 @@ class Subscriber:
def check_for_update( def check_for_update(
self, timeout: float = FAST_QUEUE_TIMEOUT self, timeout: float = FAST_QUEUE_TIMEOUT
) -> Optional[tuple[str, any]]: ) -> Optional[tuple[str, Any]]:
"""Returns message or None if no update.""" """Returns message or None if no update."""
try: try:
has_update, _, _ = zmq.select([self.socket], [], [], timeout) has_update, _, _ = zmq.select([self.socket], [], [], timeout)
@@ -98,5 +98,5 @@ class Subscriber:
self.socket.close() self.socket.close()
self.context.destroy() self.context.destroy()
def _return_object(self, topic: str, payload: any) -> any: def _return_object(self, topic: str, payload: Any) -> Any:
return payload return payload
+3
View File
@@ -30,3 +30,6 @@ class ProxyConfig(FrigateBaseModel):
default=None, default=None,
title="Secret value for proxy authentication.", 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 re
import string import string
from pathlib import Path from pathlib import Path
from typing import List, Optional, Tuple from typing import Any, List, Optional, Tuple
import cv2 import cv2
import numpy as np import numpy as np
@@ -25,7 +25,7 @@ from frigate.comms.event_metadata_updater import (
from frigate.const import CLIPS_DIR from frigate.const import CLIPS_DIR
from frigate.embeddings.onnx.lpr_embedding import LPR_EMBEDDING_SIZE from frigate.embeddings.onnx.lpr_embedding import LPR_EMBEDDING_SIZE
from frigate.types import TrackedObjectUpdateTypesEnum 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 frigate.util.image import area
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -36,8 +36,10 @@ WRITE_DEBUG_IMAGES = False
class LicensePlateProcessingMixin: class LicensePlateProcessingMixin:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.plate_rec_speed = InferenceSpeed(self.metrics.alpr_speed)
self.plates_rec_second = EventsPerSecond() self.plates_rec_second = EventsPerSecond()
self.plates_rec_second.start() self.plates_rec_second.start()
self.plate_det_speed = InferenceSpeed(self.metrics.yolov9_lpr_speed)
self.plates_det_second = EventsPerSecond() self.plates_det_second = EventsPerSecond()
self.plates_det_second.start() self.plates_det_second.start()
self.event_metadata_publisher = EventMetadataPublisher() self.event_metadata_publisher = EventMetadataPublisher()
@@ -79,7 +81,12 @@ class LicensePlateProcessingMixin:
resized_image, 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, :, :] outputs = outputs[0, :, :]
if False: if False:
@@ -115,7 +122,11 @@ class LicensePlateProcessingMixin:
norm_img = norm_img[np.newaxis, :] norm_img = norm_img[np.newaxis, :]
norm_images.append(norm_img) 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) return self._process_classification_output(images, outputs)
@@ -152,7 +163,10 @@ class LicensePlateProcessingMixin:
norm_image = norm_image[np.newaxis, :] norm_image = norm_image[np.newaxis, :]
norm_images.append(norm_image) 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) return self.ctc_decoder(outputs)
def _process_license_plate( def _process_license_plate(
@@ -968,7 +982,11 @@ class LicensePlateProcessingMixin:
Return the dimensions of the detected plate as [x1, y1, x2, y2]. 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 confidence_threshold = self.lpr_config.detection_threshold
@@ -1141,22 +1159,6 @@ class LicensePlateProcessingMixin:
# 5. Return True if previous plate scores higher # 5. Return True if previous plate scores higher
return prev_score > curr_score 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: 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.""" """Generate a unique ID for a plate event based on camera and text."""
now = datetime.datetime.now().timestamp() now = datetime.datetime.now().timestamp()
@@ -1179,7 +1181,7 @@ class LicensePlateProcessingMixin:
return event_id return event_id
def lpr_process( 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.""" """Look for license plates in image."""
self.metrics.alpr_pps.value = self.plates_rec_second.eps() 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" f"{camera}: YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms"
) )
self.plates_det_second.update() self.plates_det_second.update()
self.__update_yolov9_metrics( self.plate_det_speed.update(
datetime.datetime.now().timestamp() - yolov9_start datetime.datetime.now().timestamp() - yolov9_start
) )
@@ -1270,7 +1272,7 @@ class LicensePlateProcessingMixin:
) )
return 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: if "license_plate" not in self.config.cameras[camera].objects.track:
logger.debug(f"{camera}: Running manual license_plate detection.") logger.debug(f"{camera}: Running manual license_plate detection.")
@@ -1281,6 +1283,10 @@ class LicensePlateProcessingMixin:
return return
rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) 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 left, top, right, bottom = car_box
car = rgb[top:bottom, left:right] 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" f"{camera}: YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms"
) )
self.plates_det_second.update() self.plates_det_second.update()
self.__update_yolov9_metrics( self.plate_det_speed.update(
datetime.datetime.now().timestamp() - yolov9_start datetime.datetime.now().timestamp() - yolov9_start
) )
@@ -1335,7 +1341,7 @@ class LicensePlateProcessingMixin:
return return
if obj_data.get("label") in ["car", "motorcycle"]: 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", [] "current_attributes", []
) )
for attr in attributes: for attr in attributes:
@@ -1413,7 +1419,7 @@ class LicensePlateProcessingMixin:
camera, id, license_plate_frame camera, id, license_plate_frame
) )
self.plates_rec_second.update() 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: if license_plates:
for plate, confidence, text_area in zip(license_plates, confidences, areas): 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), (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] = { self.detected_license_plates[id] = {
"plate": top_plate, "plate": top_plate,
"char_confidences": top_char_confidences, "char_confidences": top_char_confidences,
@@ -1555,10 +1567,10 @@ class LicensePlateProcessingMixin:
"last_seen": current_time if dedicated_lpr else None, "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 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: if object_id in self.detected_license_plates:
self.detected_license_plates.pop(object_id) self.detected_license_plates.pop(object_id)
+3 -2
View File
@@ -2,6 +2,7 @@
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
@@ -25,7 +26,7 @@ class PostProcessorApi(ABC):
@abstractmethod @abstractmethod
def process_data( def process_data(
self, data: dict[str, any], data_type: PostProcessDataEnum self, data: dict[str, Any], data_type: PostProcessDataEnum
) -> None: ) -> None:
"""Processes the data of data type. """Processes the data of data type.
Args: Args:
@@ -38,7 +39,7 @@ class PostProcessorApi(ABC):
pass pass
@abstractmethod @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. """Handle metadata requests.
Args: Args:
request_data (dict): containing data about requested change to process. request_data (dict): containing data about requested change to process.
@@ -2,6 +2,7 @@
import datetime import datetime
import logging import logging
from typing import Any
import cv2 import cv2
import numpy as np import numpy as np
@@ -36,7 +37,7 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi):
sub_label_publisher: EventMetadataPublisher, sub_label_publisher: EventMetadataPublisher,
metrics: DataProcessorMetrics, metrics: DataProcessorMetrics,
model_runner: LicensePlateModelRunner, model_runner: LicensePlateModelRunner,
detected_license_plates: dict[str, dict[str, any]], detected_license_plates: dict[str, dict[str, Any]],
): ):
self.requestor = requestor self.requestor = requestor
self.detected_license_plates = detected_license_plates self.detected_license_plates = detected_license_plates
@@ -47,7 +48,7 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi):
super().__init__(config, metrics, model_runner) super().__init__(config, metrics, model_runner)
def process_data( def process_data(
self, data: dict[str, any], data_type: PostProcessDataEnum self, data: dict[str, Any], data_type: PostProcessDataEnum
) -> None: ) -> None:
"""Look for license plates in recording stream image """Look for license plates in recording stream image
Args: Args:
@@ -214,7 +215,7 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi):
logger.debug(f"Post processing plate: {event_id}, {frame_time}") logger.debug(f"Post processing plate: {event_id}, {frame_time}")
self.lpr_process(keyframe_obj_data, frame) 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: if topic == EmbeddingsRequestEnum.reprocess_plate.value:
event = request_data["event"] event = request_data["event"]
+6 -4
View File
@@ -2,6 +2,7 @@
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any
import numpy as np import numpy as np
@@ -24,7 +25,7 @@ class RealTimeProcessorApi(ABC):
pass pass
@abstractmethod @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. """Processes the frame with object data.
Args: Args:
obj_data (dict): containing data about focused object in frame. obj_data (dict): containing data about focused object in frame.
@@ -37,8 +38,8 @@ class RealTimeProcessorApi(ABC):
@abstractmethod @abstractmethod
def handle_request( def handle_request(
self, topic: str, request_data: dict[str, any] self, topic: str, request_data: dict[str, Any]
) -> dict[str, any] | None: ) -> dict[str, Any] | None:
"""Handle metadata requests. """Handle metadata requests.
Args: Args:
topic (str): topic that dictates what work is requested. topic (str): topic that dictates what work is requested.
@@ -50,10 +51,11 @@ class RealTimeProcessorApi(ABC):
pass pass
@abstractmethod @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. """Handle objects that are no longer detected.
Args: Args:
object_id (str): id of object that is no longer detected. object_id (str): id of object that is no longer detected.
camera (str): name of camera that object was detected on.
Returns: Returns:
None. None.
+4 -3
View File
@@ -2,6 +2,7 @@
import logging import logging
import os import os
from typing import Any
import cv2 import cv2
import numpy as np import numpy as np
@@ -35,8 +36,8 @@ class BirdRealTimeProcessor(RealTimeProcessorApi):
super().__init__(config, metrics) super().__init__(config, metrics)
self.interpreter: Interpreter = None self.interpreter: Interpreter = None
self.sub_label_publisher = sub_label_publisher self.sub_label_publisher = sub_label_publisher
self.tensor_input_details: dict[str, any] = None self.tensor_input_details: dict[str, Any] = None
self.tensor_output_details: dict[str, any] = None self.tensor_output_details: dict[str, Any] = None
self.detected_birds: dict[str, float] = {} self.detected_birds: dict[str, float] = {}
self.labelmap: dict[int, str] = {} self.labelmap: dict[int, str] = {}
@@ -152,6 +153,6 @@ class BirdRealTimeProcessor(RealTimeProcessorApi):
def handle_request(self, topic, request_data): def handle_request(self, topic, request_data):
return None return None
def expire_object(self, object_id): def expire_object(self, object_id, camera):
if object_id in self.detected_birds: if object_id in self.detected_birds:
self.detected_birds.pop(object_id) self.detected_birds.pop(object_id)
+39 -17
View File
@@ -5,10 +5,8 @@ import datetime
import json import json
import logging import logging
import os import os
import random
import shutil import shutil
import string from typing import Any, Optional
from typing import Optional
import cv2 import cv2
import numpy as np import numpy as np
@@ -27,7 +25,7 @@ from frigate.data_processing.common.face.model import (
FaceRecognizer, FaceRecognizer,
) )
from frigate.types import TrackedObjectUpdateTypesEnum 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 frigate.util.image import area
from ..types import DataProcessorMetrics from ..types import DataProcessorMetrics
@@ -56,8 +54,10 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
self.face_detector: cv2.FaceDetectorYN = None self.face_detector: cv2.FaceDetectorYN = None
self.requires_face_detection = "face" not in self.config.objects.all_objects self.requires_face_detection = "face" not in self.config.objects.all_objects
self.person_face_history: dict[str, list[tuple[str, float, int]]] = {} 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.recognizer: FaceRecognizer | None = None
self.faces_per_second = EventsPerSecond() self.faces_per_second = EventsPerSecond()
self.inference_speed = InferenceSpeed(self.metrics.face_rec_speed)
download_path = os.path.join(MODEL_CACHE_DIR, "facedet") download_path = os.path.join(MODEL_CACHE_DIR, "facedet")
self.model_files = { self.model_files = {
@@ -155,11 +155,9 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
def __update_metrics(self, duration: float) -> None: def __update_metrics(self, duration: float) -> None:
self.faces_per_second.update() self.faces_per_second.update()
self.metrics.face_rec_speed.value = ( self.inference_speed.update(duration)
self.metrics.face_rec_speed.value * 9 + duration
) / 10
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.""" """Look for faces in image."""
self.metrics.face_rec_fps.value = self.faces_per_second.eps() self.metrics.face_rec_fps.value = self.faces_per_second.eps()
camera = obj_data["camera"] camera = obj_data["camera"]
@@ -200,7 +198,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
logger.debug("Not processing due to hitting max rec attempts.") logger.debug("Not processing due to hitting max rec attempts.")
return return
face: Optional[dict[str, any]] = None face: Optional[dict[str, Any]] = None
if self.requires_face_detection: if self.requires_face_detection:
logger.debug("Running manual 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]), 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: try:
face_frame = cv2.cvtColor(face_frame, cv2.COLOR_RGB2BGR) face_frame = cv2.cvtColor(face_frame, cv2.COLOR_RGB2BGR)
except Exception: except Exception:
@@ -233,7 +238,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
logger.debug("No attributes to parse.") logger.debug("No attributes to parse.")
return 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: for attr in attributes:
if attr.get("label") != "face": if attr.get("label") != "face":
continue continue
@@ -285,9 +290,13 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
if id not in self.person_face_history: if id not in self.person_face_history:
self.person_face_history[id] = [] self.person_face_history[id] = []
if camera not in self.camera_current_people:
self.camera_current_people[camera] = []
self.person_face_history[id].append( self.person_face_history[id].append(
(sub_label, score, face_frame.shape[0] * face_frame.shape[1]) (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( (weighted_sub_label, weighted_score) = self.weighted_average(
self.person_face_history[id] self.person_face_history[id]
) )
@@ -314,7 +323,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
self.__update_metrics(datetime.datetime.now().timestamp() - start) 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: if topic == EmbeddingsRequestEnum.clear_face_classifier.value:
self.recognizer.clear() self.recognizer.clear()
elif topic == EmbeddingsRequestEnum.recognize_face.value: elif topic == EmbeddingsRequestEnum.recognize_face.value:
@@ -343,11 +352,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
return {"success": True, "score": score, "face_name": sub_label} return {"success": True, "score": score, "face_name": sub_label}
elif topic == EmbeddingsRequestEnum.register_face.value: elif topic == EmbeddingsRequestEnum.register_face.value:
rand_id = "".join(
random.choices(string.ascii_lowercase + string.digits, k=6)
)
label = request_data["face_name"] label = request_data["face_name"]
id = f"{label}-{rand_id}"
if request_data.get("cropped"): if request_data.get("cropped"):
thumbnail = request_data["image"] thumbnail = request_data["image"]
@@ -376,7 +381,9 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
# write face to library # write face to library
folder = os.path.join(FACE_DIR, label) 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) os.makedirs(folder, exist_ok=True)
# save face image # save face image
@@ -425,10 +432,25 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
) )
shutil.move(current_file, new_file) 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: if object_id in self.person_face_history:
self.person_face_history.pop(object_id) 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( def weighted_average(
self, results_list: list[tuple[str, float, int]], max_weight: int = 4000 self, results_list: list[tuple[str, float, int]], max_weight: int = 4000
): ):
@@ -1,6 +1,8 @@
"""Handle processing images for face detection and recognition.""" """Handle processing images for face detection and recognition."""
import json
import logging import logging
from typing import Any
import numpy as np 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 ( from frigate.data_processing.common.license_plate.model import (
LicensePlateModelRunner, LicensePlateModelRunner,
) )
from frigate.types import TrackedObjectUpdateTypesEnum
from ..types import DataProcessorMetrics from ..types import DataProcessorMetrics
from .api import RealTimeProcessorApi from .api import RealTimeProcessorApi
@@ -28,7 +31,7 @@ class LicensePlateRealTimeProcessor(LicensePlateProcessingMixin, RealTimeProcess
sub_label_publisher: EventMetadataPublisher, sub_label_publisher: EventMetadataPublisher,
metrics: DataProcessorMetrics, metrics: DataProcessorMetrics,
model_runner: LicensePlateModelRunner, model_runner: LicensePlateModelRunner,
detected_license_plates: dict[str, dict[str, any]], detected_license_plates: dict[str, dict[str, Any]],
): ):
self.requestor = requestor self.requestor = requestor
self.detected_license_plates = detected_license_plates self.detected_license_plates = detected_license_plates
@@ -36,20 +39,37 @@ class LicensePlateRealTimeProcessor(LicensePlateProcessingMixin, RealTimeProcess
self.lpr_config = config.lpr self.lpr_config = config.lpr
self.config = config self.config = config
self.sub_label_publisher = sub_label_publisher self.sub_label_publisher = sub_label_publisher
self.camera_current_cars: dict[str, list[str]] = {}
super().__init__(config, metrics) super().__init__(config, metrics)
def process_frame( def process_frame(
self, self,
obj_data: dict[str, any], obj_data: dict[str, Any],
frame: np.ndarray, frame: np.ndarray,
dedicated_lpr: bool | None = False, dedicated_lpr: bool | None = False,
): ):
"""Look for license plates in image.""" """Look for license plates in image."""
self.lpr_process(obj_data, frame, dedicated_lpr) 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 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: if object_id in self.detected_license_plates:
self.detected_license_plates.pop(object_id) 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: class DataProcessorMetrics:
image_embeddings_speed: Synchronized image_embeddings_speed: Synchronized
image_embeddings_eps: Synchronized
text_embeddings_speed: Synchronized text_embeddings_speed: Synchronized
text_embeddings_eps: Synchronized
face_rec_speed: Synchronized face_rec_speed: Synchronized
face_rec_fps: Synchronized face_rec_fps: Synchronized
alpr_speed: Synchronized alpr_speed: Synchronized
@@ -16,15 +18,15 @@ class DataProcessorMetrics:
yolov9_lpr_pps: Synchronized yolov9_lpr_pps: Synchronized
def __init__(self): 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.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.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.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.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) self.yolov9_lpr_pps = mp.Value("d", 0.0)
+5 -2
View File
@@ -3,7 +3,7 @@ import json
import logging import logging
import os import os
from enum import Enum from enum import Enum
from typing import Dict, Optional, Tuple from typing import Any, Dict, Optional, Tuple
import requests import requests
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
@@ -126,6 +126,9 @@ class ModelConfig(BaseModel):
if not self.path or not self.path.startswith("plus://"): if not self.path or not self.path.startswith("plus://"):
return return
# ensure that model cache dir exists
os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
model_id = self.path[7:] model_id = self.path[7:]
self.path = os.path.join(MODEL_CACHE_DIR, model_id) self.path = os.path.join(MODEL_CACHE_DIR, model_id)
model_info_path = f"{self.path}.json" model_info_path = f"{self.path}.json"
@@ -144,7 +147,7 @@ class ModelConfig(BaseModel):
json.dump(model_info, f) json.dump(model_info, f)
else: else:
with open(model_info_path, "r") as f: 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"]: if detector and detector not in model_info["supportedDetectors"]:
raise ValueError(f"Model does not support detector type of {detector}") 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 cv2
import numpy as np import numpy as np
try:
from hailo_platform import (
HEF,
FormatType,
HailoSchedulingAlgorithm,
VDevice,
)
except ModuleNotFoundError:
pass
from pydantic import Field from pydantic import Field
from typing_extensions import Literal from typing_extensions import Literal
@@ -102,6 +91,18 @@ class HailoAsyncInference:
output_type: Optional[Dict[str, str]] = None, output_type: Optional[Dict[str, str]] = None,
send_original_frame: bool = False, send_original_frame: bool = False,
) -> None: ) -> 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.input_store = input_store
self.output_store = output_store self.output_store = output_store
@@ -112,24 +113,19 @@ class HailoAsyncInference:
self.target = VDevice(params) self.target = VDevice(params)
self.infer_model = self.target.create_infer_model(hef_path) self.infer_model = self.target.create_infer_model(hef_path)
self.infer_model.set_batch_size(batch_size) self.infer_model.set_batch_size(batch_size)
if input_type is not None: 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: 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.output_type = output_type
self.send_original_frame = send_original_frame 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( def callback(
self, self,
completion_info, completion_info,
+7 -7
View File
@@ -9,7 +9,7 @@ import re
import signal import signal
import threading import threading
from types import FrameType from types import FrameType
from typing import Optional, Union from typing import Any, Optional, Union
from pathvalidate import ValidationError, sanitize_filename from pathvalidate import ValidationError, sanitize_filename
from setproctitle import setproctitle from setproctitle import setproctitle
@@ -190,7 +190,7 @@ class EmbeddingsContext:
return results 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( return self.requestor.send_data(
EmbeddingsRequestEnum.register_face.value, 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( return self.requestor.send_data(
EmbeddingsRequestEnum.recognize_face.value, EmbeddingsRequestEnum.recognize_face.value,
{ {
@@ -217,7 +217,7 @@ class EmbeddingsContext:
return self.db.execute_sql(sql_query).fetchall() 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( return self.requestor.send_data(
EmbeddingsRequestEnum.reprocess_face.value, {"image_file": face_file} EmbeddingsRequestEnum.reprocess_face.value, {"image_file": face_file}
) )
@@ -235,7 +235,7 @@ class EmbeddingsContext:
if os.path.isfile(file_path): if os.path.isfile(file_path):
os.unlink(file_path) os.unlink(file_path)
if len(os.listdir(folder)) == 0: if face != "train" and len(os.listdir(folder)) == 0:
os.rmdir(folder) os.rmdir(folder)
self.requestor.send_data( self.requestor.send_data(
@@ -284,10 +284,10 @@ class EmbeddingsContext:
{"id": event_id, "description": description}, {"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( return self.requestor.send_data(
EmbeddingsRequestEnum.reprocess_plate.value, {"event": event} 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, {}) 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.db.sqlitevecq import SqliteVecQueueDatabase
from frigate.models import Event from frigate.models import Event
from frigate.types import ModelStatusTypesEnum 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 frigate.util.path import get_event_thumbnail_bytes
from .onnx.jina_v1_embedding import JinaV1ImageEmbedding, JinaV1TextEmbedding from .onnx.jina_v1_embedding import JinaV1ImageEmbedding, JinaV1TextEmbedding
@@ -75,8 +75,10 @@ class Embeddings:
self.metrics = metrics self.metrics = metrics
self.requestor = InterProcessRequestor() self.requestor = InterProcessRequestor()
self.image_inference_speed = InferenceSpeed(self.metrics.image_embeddings_speed)
self.image_eps = EventsPerSecond() self.image_eps = EventsPerSecond()
self.image_eps.start() self.image_eps.start()
self.text_inference_speed = InferenceSpeed(self.metrics.text_embeddings_speed)
self.text_eps = EventsPerSecond() self.text_eps = EventsPerSecond()
self.text_eps.start() self.text_eps.start()
@@ -183,10 +185,7 @@ class Embeddings:
(event_id, serialize(embedding)), (event_id, serialize(embedding)),
) )
duration = datetime.datetime.now().timestamp() - start self.image_inference_speed.update(datetime.datetime.now().timestamp() - start)
self.metrics.image_embeddings_speed.value = (
self.metrics.image_embeddings_speed.value * 9 + duration
) / 10
self.image_eps.update() self.image_eps.update()
return embedding return embedding
@@ -220,9 +219,7 @@ class Embeddings:
) )
duration = datetime.datetime.now().timestamp() - start duration = datetime.datetime.now().timestamp() - start
self.metrics.text_embeddings_speed.value = ( self.text_inference_speed.update(duration / len(ids))
self.metrics.text_embeddings_speed.value * 9 + (duration / len(ids))
) / 10
return embeddings return embeddings
@@ -241,10 +238,7 @@ class Embeddings:
(event_id, serialize(embedding)), (event_id, serialize(embedding)),
) )
duration = datetime.datetime.now().timestamp() - start self.text_inference_speed.update(datetime.datetime.now().timestamp() - start)
self.metrics.text_embeddings_speed.value = (
self.metrics.text_embeddings_speed.value * 9 + duration
) / 10
self.text_eps.update() self.text_eps.update()
return embedding return embedding
@@ -276,10 +270,7 @@ class Embeddings:
items, items,
) )
duration = datetime.datetime.now().timestamp() - start self.text_inference_speed.update(datetime.datetime.now().timestamp() - start)
self.metrics.text_embeddings_speed.value = (
self.metrics.text_embeddings_speed.value * 9 + (duration / len(ids))
) / 10
return embeddings return embeddings
+5 -5
View File
@@ -7,7 +7,7 @@ import os
import threading import threading
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Any, Optional
import cv2 import cv2
import numpy as np import numpy as np
@@ -104,7 +104,7 @@ class EmbeddingMaintainer(threading.Thread):
self.embeddings_responder = EmbeddingsResponder() self.embeddings_responder = EmbeddingsResponder()
self.frame_manager = SharedMemoryFrameManager() 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 # model runners to share between realtime and post processors
if self.config.lpr.enabled: if self.config.lpr.enabled:
@@ -159,7 +159,7 @@ class EmbeddingMaintainer(threading.Thread):
) )
self.stop_event = stop_event 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.early_request_sent: dict[str, bool] = {}
self.genai_client = get_genai_client(config) self.genai_client = get_genai_client(config)
@@ -190,7 +190,7 @@ class EmbeddingMaintainer(threading.Thread):
def _process_requests(self) -> None: def _process_requests(self) -> None:
"""Process embeddings requests""" """Process embeddings requests"""
def _handle_request(topic: str, data: dict[str, any]) -> str: def _handle_request(topic: str, data: dict[str, Any]) -> str:
try: try:
# First handle the embedding-specific topics when semantic search is enabled # First handle the embedding-specific topics when semantic search is enabled
if self.config.semantic_search.enabled: if self.config.semantic_search.enabled:
@@ -359,7 +359,7 @@ class EmbeddingMaintainer(threading.Thread):
# expire in realtime processors # expire in realtime processors
for processor in self.realtime_processors: for processor in self.realtime_processors:
processor.expire_object(event_id) processor.expire_object(event_id, camera)
if updated_db: if updated_db:
try: try:
+4 -3
View File
@@ -5,6 +5,7 @@ import os
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from enum import Enum from enum import Enum
from io import BytesIO from io import BytesIO
from typing import Any
import numpy as np import numpy as np
import requests import requests
@@ -59,7 +60,7 @@ class BaseEmbedding(ABC):
pass pass
@abstractmethod @abstractmethod
def _preprocess_inputs(self, raw_inputs: any) -> any: def _preprocess_inputs(self, raw_inputs: Any) -> Any:
pass pass
def _process_image(self, image, output: str = "RGB") -> Image.Image: def _process_image(self, image, output: str = "RGB") -> Image.Image:
@@ -74,7 +75,7 @@ class BaseEmbedding(ABC):
return image return image
def _postprocess_outputs(self, outputs: any) -> any: def _postprocess_outputs(self, outputs: Any) -> Any:
return outputs return outputs
def __call__( def __call__(
@@ -84,7 +85,7 @@ class BaseEmbedding(ABC):
processed = self._preprocess_inputs(inputs) processed = self._preprocess_inputs(inputs)
input_names = self.runner.get_input_names() input_names = self.runner.get_input_names()
onnx_inputs = {name: [] for name in input_names} onnx_inputs = {name: [] for name in input_names}
input: dict[str, any] input: dict[str, Any]
for input in processed: for input in processed:
for key, value in input.items(): for key, value in input.items():
if key in input_names: if key in input_names:
+3 -11
View File
@@ -23,10 +23,7 @@ FACENET_INPUT_SIZE = 160
class FaceNetEmbedding(BaseEmbedding): class FaceNetEmbedding(BaseEmbedding):
def __init__( def __init__(self):
self,
device: str = "AUTO",
):
super().__init__( super().__init__(
model_name="facedet", model_name="facedet",
model_file="facenet.tflite", 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", "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.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name)
self.tokenizer = None self.tokenizer = None
self.feature_extractor = None self.feature_extractor = None
@@ -113,10 +109,7 @@ class FaceNetEmbedding(BaseEmbedding):
class ArcfaceEmbedding(BaseEmbedding): class ArcfaceEmbedding(BaseEmbedding):
def __init__( def __init__(self):
self,
device: str = "AUTO",
):
super().__init__( super().__init__(
model_name="facedet", model_name="facedet",
model_file="arcface.onnx", 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", "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.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name)
self.tokenizer = None self.tokenizer = None
self.feature_extractor = None self.feature_extractor = None
@@ -154,7 +146,7 @@ class ArcfaceEmbedding(BaseEmbedding):
self.runner = ONNXModelRunner( self.runner = ONNXModelRunner(
os.path.join(self.download_path, self.model_file), os.path.join(self.download_path, self.model_file),
self.device, "GPU",
) )
def _preprocess_inputs(self, raw_inputs): def _preprocess_inputs(self, raw_inputs):
+5 -3
View File
@@ -36,11 +36,12 @@ class JinaV1TextEmbedding(BaseEmbedding):
requestor: InterProcessRequestor, requestor: InterProcessRequestor,
device: str = "AUTO", device: str = "AUTO",
): ):
HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co")
super().__init__( super().__init__(
model_name="jinaai/jina-clip-v1", model_name="jinaai/jina-clip-v1",
model_file="text_model_fp16.onnx", model_file="text_model_fp16.onnx",
download_urls={ 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" self.tokenizer_file = "tokenizer"
@@ -156,12 +157,13 @@ class JinaV1ImageEmbedding(BaseEmbedding):
if model_size == "large" if model_size == "large"
else "vision_model_quantized.onnx" else "vision_model_quantized.onnx"
) )
HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co")
super().__init__( super().__init__(
model_name="jinaai/jina-clip-v1", model_name="jinaai/jina-clip-v1",
model_file=model_file, model_file=model_file,
download_urls={ download_urls={
model_file: f"https://huggingface.co/jinaai/jina-clip-v1/resolve/main/onnx/{model_file}", model_file: f"{HF_ENDPOINT}/jinaai/jina-clip-v1/resolve/main/onnx/{model_file}",
"preprocessor_config.json": "https://huggingface.co/jinaai/jina-clip-v1/resolve/main/preprocessor_config.json", "preprocessor_config.json": f"{HF_ENDPOINT}/jinaai/jina-clip-v1/resolve/main/preprocessor_config.json",
}, },
) )
self.requestor = requestor self.requestor = requestor
+3 -2
View File
@@ -34,12 +34,13 @@ class JinaV2Embedding(BaseEmbedding):
model_file = ( model_file = (
"model_fp16.onnx" if model_size == "large" else "model_quantized.onnx" "model_fp16.onnx" if model_size == "large" else "model_quantized.onnx"
) )
HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co")
super().__init__( super().__init__(
model_name="jinaai/jina-clip-v2", model_name="jinaai/jina-clip-v2",
model_file=model_file, model_file=model_file,
download_urls={ download_urls={
model_file: f"https://huggingface.co/jinaai/jina-clip-v2/resolve/main/onnx/{model_file}", model_file: f"{HF_ENDPOINT}/jinaai/jina-clip-v2/resolve/main/onnx/{model_file}",
"preprocessor_config.json": "https://huggingface.co/jinaai/jina-clip-v2/resolve/main/preprocessor_config.json", "preprocessor_config.json": f"{HF_ENDPOINT}/jinaai/jina-clip-v2/resolve/main/preprocessor_config.json",
}, },
) )
self.tokenizer_file = "tokenizer" self.tokenizer_file = "tokenizer"
+2 -2
View File
@@ -6,7 +6,7 @@ import random
import string import string
import threading import threading
import time import time
from typing import Tuple from typing import Any, Tuple
import numpy as np import numpy as np
@@ -126,7 +126,7 @@ class AudioEventMaintainer(threading.Thread):
self.config = camera self.config = camera
self.camera_metrics = camera_metrics self.camera_metrics = camera_metrics
self.detections: dict[dict[str, any]] = {} self.detections: dict[dict[str, Any]] = {}
self.stop_event = stop_event self.stop_event = stop_event
self.detector = AudioTfl(stop_event, self.config.audio.num_threads) self.detector = AudioTfl(stop_event, self.config.audio.num_threads)
self.shape = (int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE)),) self.shape = (int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE)),)
+2 -1
View File
@@ -6,6 +6,7 @@ import os
import threading import threading
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from pathlib import Path from pathlib import Path
from typing import Any
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.const import CLIPS_DIR from frigate.const import CLIPS_DIR
@@ -29,7 +30,7 @@ class EventCleanup(threading.Thread):
self.db = db self.db = db
self.camera_keys = list(self.config.cameras.keys()) self.camera_keys = list(self.config.cameras.keys())
self.removed_camera_labels: list[str] = None 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]: def get_removed_camera_labels(self) -> list[Event]:
"""Get a list of distinct labels for removed cameras.""" """Get a list of distinct labels for removed cameras."""
+5 -5
View File
@@ -10,7 +10,7 @@ import queue
import subprocess as sp import subprocess as sp
import threading import threading
import traceback import traceback
from typing import Optional from typing import Any, Optional
import cv2 import cv2
import numpy as np import numpy as np
@@ -542,10 +542,10 @@ class BirdsEyeFrameManager:
self, self,
cameras_to_add: list[str], cameras_to_add: list[str],
coefficient: float, coefficient: float,
) -> tuple[any]: ) -> tuple[Any]:
"""Calculate the optimal layout for 2+ cameras.""" """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.""" """Map the calculated layout."""
candidate_layout = [] candidate_layout = []
starting_x = 0 starting_x = 0
@@ -588,7 +588,7 @@ class BirdsEyeFrameManager:
return max_width, y, candidate_layout return max_width, y, candidate_layout
canvas_aspect_x, canvas_aspect_y = self.canvas.get_aspect(coefficient) canvas_aspect_x, canvas_aspect_y = self.canvas.get_aspect(coefficient)
camera_layout: list[list[any]] = [] camera_layout: list[list[Any]] = []
camera_layout.append([]) camera_layout.append([])
starting_x = 0 starting_x = 0
x = starting_x x = starting_x
@@ -786,7 +786,7 @@ class Birdseye:
def write_data( def write_data(
self, self,
camera: str, camera: str,
current_tracked_objects: list[dict[str, any]], current_tracked_objects: list[dict[str, Any]],
motion_boxes: list[list[int]], motion_boxes: list[list[int]],
frame_time: float, frame_time: float,
frame: np.ndarray, frame: np.ndarray,
+3 -2
View File
@@ -8,6 +8,7 @@ import subprocess as sp
import threading import threading
import time import time
from pathlib import Path from pathlib import Path
from typing import Any
import cv2 import cv2
import numpy as np import numpy as np
@@ -255,7 +256,7 @@ class PreviewRecorder:
def should_write_frame( def should_write_frame(
self, self,
current_tracked_objects: list[dict[str, any]], current_tracked_objects: list[dict[str, Any]],
motion_boxes: list[list[int]], motion_boxes: list[list[int]],
frame_time: float, frame_time: float,
) -> bool: ) -> bool:
@@ -315,7 +316,7 @@ class PreviewRecorder:
def write_data( def write_data(
self, self,
current_tracked_objects: list[dict[str, any]], current_tracked_objects: list[dict[str, Any]],
motion_boxes: list[list[int]], motion_boxes: list[list[int]],
frame_time: float, frame_time: float,
frame: np.ndarray, frame: np.ndarray,
+64 -61
View File
@@ -3,12 +3,11 @@
import asyncio import asyncio
import copy import copy
import logging import logging
import queue
import threading import threading
import time import time
from collections import deque from collections import deque
from functools import partial
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from typing import Any
import cv2 import cv2
import numpy as np import numpy as np
@@ -61,7 +60,7 @@ class PtzMotionEstimator:
def motion_estimator( def motion_estimator(
self, self,
detections: list[dict[str, any]], detections: list[dict[str, Any]],
frame_name: str, frame_name: str,
frame_time: float, frame_time: float,
camera: str, camera: str,
@@ -169,7 +168,12 @@ class PtzAutoTrackerThread(threading.Thread):
continue continue
if camera_config.onvif.autotracking.enabled: 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: else:
# disabled dynamically by mqtt # disabled dynamically by mqtt
if self.ptz_autotracker.tracked_object.get(camera): if self.ptz_autotracker.tracked_object.get(camera):
@@ -219,9 +223,13 @@ class PtzAutoTracker:
camera_config.onvif.autotracking.enabled camera_config.onvif.autotracking.enabled
and camera_config.onvif.autotracking.enabled_in_config 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") logger.debug(f"{camera}: Autotracker init")
self.object_types[camera] = camera_config.onvif.autotracking.track self.object_types[camera] = camera_config.onvif.autotracking.track
@@ -242,8 +250,8 @@ class PtzAutoTracker:
self.intercept[camera] = None self.intercept[camera] = None
self.move_coefficients[camera] = [] self.move_coefficients[camera] = []
self.move_queues[camera] = queue.Queue() self.move_queues[camera] = asyncio.Queue()
self.move_queue_locks[camera] = threading.Lock() self.move_queue_locks[camera] = asyncio.Lock()
# handle onvif constructor failing due to no connection # handle onvif constructor failing due to no connection
if camera not in self.onvif.cams: if camera not in self.onvif.cams:
@@ -255,7 +263,7 @@ class PtzAutoTracker:
return return
if not self.onvif.cams[camera]["init"]: 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( logger.warning(
f"Disabling autotracking for {camera}: Unable to initialize onvif" f"Disabling autotracking for {camera}: Unable to initialize onvif"
) )
@@ -271,7 +279,7 @@ class PtzAutoTracker:
self.ptz_metrics[camera].autotracker_enabled.value = False self.ptz_metrics[camera].autotracker_enabled.value = False
return return
move_status_supported = self.onvif.get_service_capabilities(camera) move_status_supported = await self.onvif.get_service_capabilities(camera)
if not ( if not (
isinstance(move_status_supported, bool) and move_status_supported isinstance(move_status_supported, bool) and move_status_supported
@@ -287,15 +295,12 @@ class PtzAutoTracker:
return return
if self.onvif.cams[camera]["init"]: if self.onvif.cams[camera]["init"]:
self.onvif.get_camera_status(camera) await self.onvif.get_camera_status(camera)
# movement thread per camera # movement queue with asyncio on OnvifController loop
self.move_threads[camera] = threading.Thread( asyncio.run_coroutine_threadsafe(
name=f"ptz_move_thread_{camera}", self._process_move_queue(camera), self.onvif.loop
target=partial(self._process_move_queue, camera),
) )
self.move_threads[camera].daemon = True
self.move_threads[camera].start()
if camera_config.onvif.autotracking.movement_weights: if camera_config.onvif.autotracking.movement_weights:
if len(camera_config.onvif.autotracking.movement_weights) == 6: if len(camera_config.onvif.autotracking.movement_weights) == 6:
@@ -330,7 +335,7 @@ class PtzAutoTracker:
) )
if camera_config.onvif.autotracking.calibrate_on_startup: 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.ptz_metrics[camera].tracking_active.clear()
self.dispatcher.publish(f"{camera}/ptz_autotracker/active", "OFF", retain=False) 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, 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 # 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 # 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) # 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): for i in range(2):
# absolute move to 0 - fully zoomed out # absolute move to 0 - fully zoomed out
self.onvif._zoom_absolute( await self.onvif._zoom_absolute(
camera, camera,
self.onvif.cams[camera]["absolute_zoom_range"]["XRange"]["Min"], self.onvif.cams[camera]["absolute_zoom_range"]["XRange"]["Min"],
1, 1,
) )
while not self.ptz_metrics[camera].motor_stopped.is_set(): 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_out_values.append(self.ptz_metrics[camera].zoom_level.value)
self.onvif._zoom_absolute( await self.onvif._zoom_absolute(
camera, camera,
self.onvif.cams[camera]["absolute_zoom_range"]["XRange"]["Max"], self.onvif.cams[camera]["absolute_zoom_range"]["XRange"]["Max"],
1, 1,
) )
while not self.ptz_metrics[camera].motor_stopped.is_set(): 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) zoom_in_values.append(self.ptz_metrics[camera].zoom_level.value)
@@ -400,7 +405,7 @@ class PtzAutoTracker:
== ZoomingModeEnum.relative == ZoomingModeEnum.relative
): ):
# relative move to -0.01 # relative move to -0.01
self.onvif._move_relative( await self.onvif._move_relative(
camera, camera,
0, 0,
0, 0,
@@ -409,13 +414,13 @@ class PtzAutoTracker:
) )
while not self.ptz_metrics[camera].motor_stopped.is_set(): 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_out_values.append(self.ptz_metrics[camera].zoom_level.value)
zoom_start_time = time.time() zoom_start_time = time.time()
# relative move to 0.01 # relative move to 0.01
self.onvif._move_relative( await self.onvif._move_relative(
camera, camera,
0, 0,
0, 0,
@@ -424,13 +429,13 @@ class PtzAutoTracker:
) )
while not self.ptz_metrics[camera].motor_stopped.is_set(): 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() zoom_stop_time = time.time()
full_relative_start_time = time.time() full_relative_start_time = time.time()
self.onvif._move_relative( await self.onvif._move_relative(
camera, camera,
-1, -1,
-1, -1,
@@ -439,11 +444,11 @@ class PtzAutoTracker:
) )
while not self.ptz_metrics[camera].motor_stopped.is_set(): 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() full_relative_stop_time = time.time()
self.onvif._move_relative( await self.onvif._move_relative(
camera, camera,
1, 1,
1, 1,
@@ -452,7 +457,7 @@ class PtzAutoTracker:
) )
while not self.ptz_metrics[camera].motor_stopped.is_set(): 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] = ( self.zoom_time[camera] = (
full_relative_stop_time - full_relative_start_time 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].max_zoom.value = 1
self.ptz_metrics[camera].min_zoom.value = 0 self.ptz_metrics[camera].min_zoom.value = 0
self.onvif._move_to_preset( await self.onvif._move_to_preset(
camera, camera,
self.config.cameras[camera].onvif.autotracking.return_preset.lower(), self.config.cameras[camera].onvif.autotracking.return_preset.lower(),
) )
@@ -480,18 +485,18 @@ class PtzAutoTracker:
# Wait until the camera finishes moving # Wait until the camera finishes moving
while not self.ptz_metrics[camera].motor_stopped.is_set(): 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): for step in range(num_steps):
pan = step_sizes[step] pan = step_sizes[step]
tilt = step_sizes[step] tilt = step_sizes[step]
start_time = time.time() 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 # Wait until the camera finishes moving
while not self.ptz_metrics[camera].motor_stopped.is_set(): 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() stop_time = time.time()
self.move_metrics[camera].append( self.move_metrics[camera].append(
@@ -503,7 +508,7 @@ class PtzAutoTracker:
} }
) )
self.onvif._move_to_preset( await self.onvif._move_to_preset(
camera, camera,
self.config.cameras[camera].onvif.autotracking.return_preset.lower(), self.config.cameras[camera].onvif.autotracking.return_preset.lower(),
) )
@@ -512,7 +517,7 @@ class PtzAutoTracker:
# Wait until the camera finishes moving # Wait until the camera finishes moving
while not self.ptz_metrics[camera].motor_stopped.is_set(): 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( logger.info(
f"Calibration for {camera} in progress: {round((step / num_steps) * 100)}% complete" 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"] centroid_distance < self.tracked_object_metrics[camera]["distance"]
) )
def _process_move_queue(self, camera): async def _process_move_queue(self, camera):
camera_config = self.config.cameras[camera] move_queue = self.move_queues[camera]
camera_config.frame_shape[1]
camera_config.frame_shape[0]
while not self.stop_event.is_set(): while not self.stop_event.is_set():
try: try:
move_data = self.move_queues[camera].get(True, 0.1) # Asynchronously wait for move data with a timeout
except queue.Empty: move_data = await asyncio.wait_for(move_queue.get(), timeout=0.1)
except asyncio.TimeoutError:
continue continue
with self.move_queue_locks[camera]: async with self.move_queue_locks[camera]:
frame_time, pan, tilt, zoom = move_data frame_time, pan, tilt, zoom = move_data
# if we're receiving move requests during a PTZ move, ignore them # 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].start_time.value,
self.ptz_metrics[camera].stop_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( logger.debug(
f"{camera}: Move queue: PTZ moving, dequeueing move request - frame time: {frame_time}, final pan: {pan}, final tilt: {tilt}, final zoom: {zoom}" 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 self.config.cameras[camera].onvif.autotracking.zooming
== ZoomingModeEnum.relative == ZoomingModeEnum.relative
): ):
self.onvif._move_relative(camera, pan, tilt, zoom, 1) await self.onvif._move_relative(camera, pan, tilt, zoom, 1)
else: else:
if pan != 0 or tilt != 0: 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 # Wait until the camera finishes moving
while not self.ptz_metrics[camera].motor_stopped.is_set(): while not self.ptz_metrics[camera].motor_stopped.is_set():
self.onvif.get_camera_status(camera) await self.onvif.get_camera_status(camera)
if ( if (
zoom > 0 zoom > 0
and self.ptz_metrics[camera].zoom_level.value != zoom 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 # Wait until the camera finishes moving
while not self.ptz_metrics[camera].motor_stopped.is_set(): 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: if self.config.cameras[camera].onvif.autotracking.movement_weights:
logger.debug( logger.debug(
@@ -796,6 +797,10 @@ class PtzAutoTracker:
# calculate new coefficients if we have enough data # calculate new coefficients if we have enough data
self._calculate_move_coefficients(camera) 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 _enqueue_move(self, camera, frame_time, pan, tilt, zoom):
def split_value(value, suppress_diff=True): def split_value(value, suppress_diff=True):
clipped = np.clip(value, -1, 1) 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}" f"{camera}: Enqueue movement for frame time: {frame_time} pan: {pan}, tilt: {tilt}, zoom: {zoom}"
) )
move_data = (frame_time, pan, tilt, 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 # reset values to not split up large movements
pan = 0 pan = 0
@@ -1420,7 +1427,7 @@ class PtzAutoTracker:
** (1 / self.zoom_factor[camera]) ** (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 # bail and don't check anything if we're calibrating or tracking an object
if ( if (
not self.autotracker_init[camera] not self.autotracker_init[camera]
@@ -1437,7 +1444,7 @@ class PtzAutoTracker:
self._autotracker_setup(self.config.cameras[camera], camera) self._autotracker_setup(self.config.cameras[camera], camera)
# regularly update camera status # regularly update camera status
if not self.ptz_metrics[camera].motor_stopped.is_set(): 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 # return to preset if tracking is over
if ( if (
@@ -1455,22 +1462,18 @@ class PtzAutoTracker:
self.tracked_object[camera] = None self.tracked_object[camera] = None
self.tracked_object_history[camera].clear() 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() self.ptz_metrics[camera].motor_stopped.wait()
logger.debug( logger.debug(
f"{camera}: Time is {self.ptz_metrics[camera].frame_time.value}, returning to preset: {autotracker_config.return_preset}" 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, camera,
autotracker_config.return_preset.lower(), autotracker_config.return_preset.lower(),
) )
# update stored zoom level from preset # update stored zoom level from preset
if not self.ptz_metrics[camera].motor_stopped.is_set(): 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.ptz_metrics[camera].tracking_active.clear()
self.dispatcher.publish( self.dispatcher.publish(
+144 -80
View File
@@ -2,10 +2,12 @@
import asyncio import asyncio
import logging import logging
import threading
import time import time
from enum import Enum from enum import Enum
from importlib.util import find_spec from importlib.util import find_spec
from pathlib import Path from pathlib import Path
from typing import Any
import numpy import numpy
from onvif import ONVIFCamera, ONVIFError, ONVIFService from onvif import ONVIFCamera, ONVIFError, ONVIFService
@@ -39,27 +41,56 @@ class OnvifController:
def __init__( def __init__(
self, config: FrigateConfig, ptz_metrics: dict[str, PTZMetrics] self, config: FrigateConfig, ptz_metrics: dict[str, PTZMetrics]
) -> None: ) -> None:
self.cams: dict[str, ONVIFCamera] = {} self.cams: dict[str, dict] = {}
self.failed_cams: dict[str, dict] = {} self.failed_cams: dict[str, dict] = {}
self.max_retries = 5 self.max_retries = 5
self.reset_timeout = 900 # 15 minutes self.reset_timeout = 900 # 15 minutes
self.config = config self.config = config
self.ptz_metrics = ptz_metrics 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(): for cam_name, cam in config.cameras.items():
if not cam.enabled: if not cam.enabled:
continue continue
if cam.onvif.host: if cam.onvif.host:
result = self._create_onvif_camera(cam_name, cam) self.camera_configs[cam_name] = cam
if result:
self.cams[cam_name] = result
def _create_onvif_camera(self, cam_name: str, cam) -> dict | None: asyncio.run_coroutine_threadsafe(self._init_cameras(), self.loop)
"""Create an ONVIF camera instance and handle failures."""
def _run_event_loop(self) -> None:
"""Run the event loop in a separate thread."""
asyncio.set_event_loop(self.loop)
try: 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( "onvif": ONVIFCamera(
cam.onvif.host, cam.onvif.host,
cam.onvif.port, cam.onvif.port,
@@ -74,7 +105,8 @@ class OnvifController:
"features": [], "features": [],
"presets": {}, "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}") logger.error(f"Failed to create ONVIF camera instance for {cam_name}: {e}")
# track initial failures # track initial failures
self.failed_cams[cam_name] = { self.failed_cams[cam_name] = {
@@ -82,7 +114,7 @@ class OnvifController:
"last_error": str(e), "last_error": str(e),
"last_attempt": time.time(), "last_attempt": time.time(),
} }
return None return False
async def _init_onvif(self, camera_name: str) -> bool: async def _init_onvif(self, camera_name: str) -> bool:
onvif: ONVIFCamera = self.cams[camera_name]["onvif"] onvif: ONVIFCamera = self.cams[camera_name]["onvif"]
@@ -100,7 +132,7 @@ class OnvifController:
# this will fire an exception if camera is not a ptz # this will fire an exception if camera is not a ptz
capabilities = onvif.get_definition("ptz") capabilities = onvif.get_definition("ptz")
logger.debug(f"Onvif capabilities for {camera_name}: {capabilities}") logger.debug(f"Onvif capabilities for {camera_name}: {capabilities}")
except (ONVIFError, Fault, TransportError) as e: except (Fault, ONVIFError, TransportError, Exception) as e:
logger.error( logger.error(
f"Unable to get Onvif capabilities for camera: {camera_name}: {e}" f"Unable to get Onvif capabilities for camera: {camera_name}: {e}"
) )
@@ -109,7 +141,7 @@ class OnvifController:
try: try:
profiles = await media.GetProfiles() profiles = await media.GetProfiles()
logger.debug(f"Onvif profiles for {camera_name}: {profiles}") logger.debug(f"Onvif profiles for {camera_name}: {profiles}")
except (ONVIFError, Fault, TransportError) as e: except (Fault, ONVIFError, TransportError, Exception) as e:
logger.error( logger.error(
f"Unable to get Onvif media profiles for camera: {camera_name}: {e}" f"Unable to get Onvif media profiles for camera: {camera_name}: {e}"
) )
@@ -240,12 +272,12 @@ class OnvifController:
logger.debug( logger.debug(
f"{camera_name}: Relative move request after deleting zoom: {move_request}" f"{camera_name}: Relative move request after deleting zoom: {move_request}"
) )
except Exception: except Exception as e:
self.config.cameras[ self.config.cameras[
camera_name camera_name
].onvif.autotracking.zooming = ZoomingModeEnum.disabled ].onvif.autotracking.zooming = ZoomingModeEnum.disabled
logger.warning( 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: if move_request.Speed is None:
@@ -263,7 +295,7 @@ class OnvifController:
# setup existing presets # setup existing presets
try: try:
presets: list[dict] = await ptz.GetPresets({"ProfileToken": profile.token}) 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}") logger.warning(f"Unable to get presets from camera: {camera_name}: {e}")
presets = [] presets = []
@@ -295,7 +327,7 @@ class OnvifController:
self.cams[camera_name]["relative_zoom_range"] = ( self.cams[camera_name]["relative_zoom_range"] = (
ptz_config.Spaces.RelativeZoomTranslationSpace[0] ptz_config.Spaces.RelativeZoomTranslationSpace[0]
) )
except Exception: except Exception as e:
if ( if (
self.config.cameras[camera_name].onvif.autotracking.zooming self.config.cameras[camera_name].onvif.autotracking.zooming
== ZoomingModeEnum.relative == ZoomingModeEnum.relative
@@ -304,7 +336,7 @@ class OnvifController:
camera_name camera_name
].onvif.autotracking.zooming = ZoomingModeEnum.disabled ].onvif.autotracking.zooming = ZoomingModeEnum.disabled
logger.warning( 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: if configs.DefaultAbsoluteZoomPositionSpace:
@@ -319,13 +351,13 @@ class OnvifController:
ptz_config.Spaces.AbsoluteZoomPositionSpace[0] ptz_config.Spaces.AbsoluteZoomPositionSpace[0]
) )
self.cams[camera_name]["zoom_limits"] = configs.ZoomLimits self.cams[camera_name]["zoom_limits"] = configs.ZoomLimits
except Exception: except Exception as e:
if self.config.cameras[camera_name].onvif.autotracking.zooming: if self.config.cameras[camera_name].onvif.autotracking.zooming:
self.config.cameras[ self.config.cameras[
camera_name camera_name
].onvif.autotracking.zooming = ZoomingModeEnum.disabled ].onvif.autotracking.zooming = ZoomingModeEnum.disabled
logger.warning( 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 # set relative pan/tilt space for autotracker
@@ -344,25 +376,23 @@ class OnvifController:
self.cams[camera_name]["init"] = True self.cams[camera_name]["init"] = True
return 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"] move_request = self.cams[camera_name]["move_request"]
asyncio.run( await self.cams[camera_name]["ptz"].Stop(
self.cams[camera_name]["ptz"].Stop( {
{ "ProfileToken": move_request.ProfileToken,
"ProfileToken": move_request.ProfileToken, "PanTilt": True,
"PanTilt": True, "Zoom": True,
"Zoom": True, }
}
)
) )
self.cams[camera_name]["active"] = False 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"]: if self.cams[camera_name]["active"]:
logger.warning( logger.warning(
f"{camera_name} is already performing an action, stopping..." 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"]: if "pt" not in self.cams[camera_name]["features"]:
logger.error(f"{camera_name} does not support ONVIF pan/tilt movement.") logger.error(f"{camera_name} does not support ONVIF pan/tilt movement.")
@@ -391,11 +421,11 @@ class OnvifController:
} }
try: try:
asyncio.run(self.cams[camera_name]["ptz"].ContinuousMove(move_request)) await self.cams[camera_name]["ptz"].ContinuousMove(move_request)
except ONVIFError as e: except (Fault, ONVIFError, TransportError, Exception) as e:
logger.warning(f"Onvif sending move request to {camera_name} failed: {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"]: if "pt-r-fov" not in self.cams[camera_name]["features"]:
logger.error(f"{camera_name} does not support ONVIF RelativeMove (FOV).") logger.error(f"{camera_name} does not support ONVIF RelativeMove (FOV).")
return return
@@ -464,7 +494,7 @@ class OnvifController:
} }
move_request.Translation.Zoom.x = zoom 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 # reset after the move request
move_request.Translation.PanTilt.x = 0 move_request.Translation.PanTilt.x = 0
@@ -479,7 +509,7 @@ class OnvifController:
self.cams[camera_name]["active"] = False 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"]: if preset not in self.cams[camera_name]["presets"]:
logger.error(f"{preset} is not a valid preset for {camera_name}") logger.error(f"{preset} is not a valid preset for {camera_name}")
return return
@@ -489,23 +519,22 @@ class OnvifController:
self.ptz_metrics[camera_name].stop_time.value = 0 self.ptz_metrics[camera_name].stop_time.value = 0
move_request = self.cams[camera_name]["move_request"] move_request = self.cams[camera_name]["move_request"]
preset_token = self.cams[camera_name]["presets"][preset] preset_token = self.cams[camera_name]["presets"][preset]
asyncio.run(
self.cams[camera_name]["ptz"].GotoPreset( await self.cams[camera_name]["ptz"].GotoPreset(
{ {
"ProfileToken": move_request.ProfileToken, "ProfileToken": move_request.ProfileToken,
"PresetToken": preset_token, "PresetToken": preset_token,
} }
)
) )
self.cams[camera_name]["active"] = False 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"]: if self.cams[camera_name]["active"]:
logger.warning( logger.warning(
f"{camera_name} is already performing an action, stopping..." 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"]: if "zoom" not in self.cams[camera_name]["features"]:
logger.error(f"{camera_name} does not support ONVIF zooming.") logger.error(f"{camera_name} does not support ONVIF zooming.")
@@ -519,9 +548,9 @@ class OnvifController:
elif command == OnvifCommandEnum.zoom_out: elif command == OnvifCommandEnum.zoom_out:
move_request.Velocity = {"Zoom": {"x": -0.5}} 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"]: if "zoom-a" not in self.cams[camera_name]["features"]:
logger.error(f"{camera_name} does not support ONVIF AbsoluteMove zooming.") logger.error(f"{camera_name} does not support ONVIF AbsoluteMove zooming.")
return return
@@ -560,19 +589,20 @@ class OnvifController:
logger.debug(f"{camera_name}: Absolute zoom: {zoom}") 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 self.cams[camera_name]["active"] = False
def handle_command( async def handle_command_async(
self, camera_name: str, command: OnvifCommandEnum, param: str = "" self, camera_name: str, command: OnvifCommandEnum, param: str = ""
) -> None: ) -> None:
"""Handle ONVIF commands asynchronously"""
if camera_name not in self.cams.keys(): if camera_name not in self.cams.keys():
logger.error(f"ONVIF is not configured for {camera_name}") logger.error(f"ONVIF is not configured for {camera_name}")
return return
if not self.cams[camera_name]["init"]: 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 return
try: try:
@@ -580,23 +610,44 @@ class OnvifController:
# already init # already init
return return
elif command == OnvifCommandEnum.stop: elif command == OnvifCommandEnum.stop:
self._stop(camera_name) await self._stop(camera_name)
elif command == OnvifCommandEnum.preset: elif command == OnvifCommandEnum.preset:
self._move_to_preset(camera_name, param) await self._move_to_preset(camera_name, param)
elif command == OnvifCommandEnum.move_relative: elif command == OnvifCommandEnum.move_relative:
_, pan, tilt = param.split("_") _, 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 ( elif (
command == OnvifCommandEnum.zoom_in command == OnvifCommandEnum.zoom_in
or command == OnvifCommandEnum.zoom_out or command == OnvifCommandEnum.zoom_out
): ):
self._zoom(camera_name, command) await self._zoom(camera_name, command)
else: else:
self._move(camera_name, command) await self._move(camera_name, command)
except ONVIFError as e: except (Fault, ONVIFError, TransportError, Exception) as e:
logger.error(f"Unable to handle onvif command: {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 Get ptz capabilities and presets, attempting to reconnect if ONVIF is configured
but not initialized. but not initialized.
@@ -609,26 +660,23 @@ class OnvifController:
) )
return {} 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 camera_name not in self.config.cameras
or not self.config.cameras[camera_name].onvif.host or not self.config.cameras[camera_name].onvif.host
): ):
logger.debug(f"ONVIF is not configured for {camera_name}") logger.debug(f"ONVIF is not configured for {camera_name}")
return {} 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 { return {
"name": camera_name, "name": camera_name,
"features": self.cams[camera_name]["features"], "features": self.cams[camera_name]["features"],
"presets": list(self.cams[camera_name]["presets"].keys()), "presets": list(self.cams[camera_name]["presets"].keys()),
} }
if camera_name not in self.cams and camera_name in self.config.cameras: if camera_name not in self.cams.keys() and camera_name in self.config.cameras:
cam = self.config.cameras[camera_name] success = await self._init_single_camera(camera_name)
result = self._create_onvif_camera(camera_name, cam) if not success:
if result:
self.cams[camera_name] = result
else:
return {} return {}
# Reset retry count after timeout # Reset retry count after timeout
@@ -681,23 +729,21 @@ class OnvifController:
logger.debug(f"Could not initialize ONVIF for {camera_name}") logger.debug(f"Could not initialize ONVIF for {camera_name}")
return {} 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(): if camera_name not in self.cams.keys():
logger.error(f"ONVIF is not configured for {camera_name}") logger.error(f"ONVIF is not configured for {camera_name}")
return {} return {}
if not self.cams[camera_name]["init"]: 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 = self.cams[camera_name][
"service_capabilities_request" "service_capabilities_request"
] ]
try: try:
service_capabilities = asyncio.run( service_capabilities = await self.cams[camera_name][
self.cams[camera_name]["ptz"].GetServiceCapabilities( "ptz"
service_capabilities_request ].GetServiceCapabilities(service_capabilities_request)
)
)
logger.debug( logger.debug(
f"Onvif service capabilities for {camera_name}: {service_capabilities}" 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 # MoveStatus is required for autotracking - should return "true" if supported
return find_by_key(vars(service_capabilities), "MoveStatus") return find_by_key(vars(service_capabilities), "MoveStatus")
except Exception: except Exception as e:
logger.warning( 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 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(): if camera_name not in self.cams.keys():
logger.error(f"ONVIF is not configured for {camera_name}") logger.error(f"ONVIF is not configured for {camera_name}")
return {} return
if not self.cams[camera_name]["init"]: 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"] status_request = self.cams[camera_name]["status_request"]
try: try:
status = asyncio.run( status = await self.cams[camera_name]["ptz"].GetStatus(status_request)
self.cams[camera_name]["ptz"].GetStatus(status_request)
)
except Exception: except Exception:
pass # We're unsupported, that'll be reported in the next check. pass # We're unsupported, that'll be reported in the next check.
@@ -807,3 +852,22 @@ class OnvifController:
camera_name camera_name
].frame_time.value ].frame_time.value
logger.warning(f"Camera {camera_name} is still in ONVIF 'MOVING' status.") 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) now - datetime.timedelta(days=config.record.detections.retain.days)
).timestamp() ).timestamp()
expired_reviews: ReviewSegment = ( expired_reviews: ReviewSegment = (
ReviewSegment.select(ReviewSegment.id) ReviewSegment.select(ReviewSegment.id, ReviewSegment.thumb_path)
.where(ReviewSegment.camera == config.name) .where(ReviewSegment.camera == config.name)
.where( .where(
( (
@@ -84,6 +84,10 @@ class RecordingCleanup(threading.Thread):
.namedtuples() .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 max_deletes = 100000
deleted_reviews_list = list(map(lambda x: x[0], expired_reviews)) deleted_reviews_list = list(map(lambda x: x[0], expired_reviews))
for i in range(0, len(deleted_reviews_list), max_deletes): 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) self.end_time_cache.pop(cache_path, None)
async def validate_and_move_segment( 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: ) -> None:
cache_path: str = recording["cache_path"] cache_path: str = recording["cache_path"]
start_time: datetime.datetime = recording["start_time"] start_time: datetime.datetime = recording["start_time"]
+22 -19
View File
@@ -1,5 +1,6 @@
"""Maintain review segments in db.""" """Maintain review segments in db."""
import copy
import json import json
import logging import logging
import os import os
@@ -9,7 +10,7 @@ import sys
import threading import threading
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Any, Optional
import cv2 import cv2
import numpy as np import numpy as np
@@ -119,21 +120,23 @@ class PendingReviewSegment:
) )
def get_data(self, ended: bool) -> dict: def get_data(self, ended: bool) -> dict:
return { return copy.deepcopy(
ReviewSegment.id.name: self.id, {
ReviewSegment.camera.name: self.camera, ReviewSegment.id.name: self.id,
ReviewSegment.start_time.name: self.start_time, ReviewSegment.camera.name: self.camera,
ReviewSegment.end_time.name: self.last_update if ended else None, ReviewSegment.start_time.name: self.start_time,
ReviewSegment.severity.name: self.severity.value, ReviewSegment.end_time.name: self.last_update if ended else None,
ReviewSegment.thumb_path.name: self.frame_path, ReviewSegment.severity.name: self.severity.value,
ReviewSegment.data.name: { ReviewSegment.thumb_path.name: self.frame_path,
"detections": list(set(self.detections.keys())), ReviewSegment.data.name: {
"objects": list(set(self.detections.values())), "detections": list(set(self.detections.keys())),
"sub_labels": list(self.sub_labels.values()), "objects": list(set(self.detections.values())),
"zones": self.zones, "sub_labels": list(self.sub_labels.values()),
"audio": list(self.audio), "zones": self.zones,
}, "audio": list(self.audio),
}.copy() },
}
)
class ReviewSegmentMaintainer(threading.Thread): class ReviewSegmentMaintainer(threading.Thread):
@@ -153,7 +156,7 @@ class ReviewSegmentMaintainer(threading.Thread):
self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all) self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all)
# manual events # manual events
self.indefinite_events: dict[str, dict[str, any]] = {} self.indefinite_events: dict[str, dict[str, Any]] = {}
# ensure dirs # ensure dirs
Path(os.path.join(CLIPS_DIR, "review")).mkdir(exist_ok=True) Path(os.path.join(CLIPS_DIR, "review")).mkdir(exist_ok=True)
@@ -191,7 +194,7 @@ class ReviewSegmentMaintainer(threading.Thread):
camera_config: CameraConfig, camera_config: CameraConfig,
frame, frame,
objects: list[TrackedObject], objects: list[TrackedObject],
prev_data: dict[str, any], prev_data: dict[str, Any],
) -> None: ) -> None:
"""Update segment.""" """Update segment."""
if frame is not None: if frame is not None:
@@ -216,7 +219,7 @@ class ReviewSegmentMaintainer(threading.Thread):
def _publish_segment_end( def _publish_segment_end(
self, self,
segment: PendingReviewSegment, segment: PendingReviewSegment,
prev_data: dict[str, any], prev_data: dict[str, Any],
) -> None: ) -> None:
"""End segment.""" """End segment."""
final_data = segment.get_data(ended=True) final_data = segment.get_data(ended=True)
+5 -5
View File
@@ -6,7 +6,7 @@ import logging
import threading import threading
import time import time
from multiprocessing.synchronize import Event as MpEvent 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.comms.inter_process import InterProcessRequestor
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
@@ -33,12 +33,12 @@ class StatsEmitter(threading.Thread):
self.stats_tracking = stats_tracking self.stats_tracking = stats_tracking
self.stop_event = stop_event self.stop_event = stop_event
self.hwaccel_errors: list[str] = [] self.hwaccel_errors: list[str] = []
self.stats_history: list[dict[str, any]] = [] self.stats_history: list[dict[str, Any]] = []
# create communication for stats # create communication for stats
self.requestor = InterProcessRequestor() self.requestor = InterProcessRequestor()
def get_latest_stats(self) -> dict[str, any]: def get_latest_stats(self) -> dict[str, Any]:
"""Get latest stats.""" """Get latest stats."""
if len(self.stats_history) > 0: if len(self.stats_history) > 0:
return self.stats_history[-1] return self.stats_history[-1]
@@ -51,12 +51,12 @@ class StatsEmitter(threading.Thread):
def get_stats_history( def get_stats_history(
self, keys: Optional[list[str]] = None self, keys: Optional[list[str]] = None
) -> list[dict[str, any]]: ) -> list[dict[str, Any]]:
"""Get stats history.""" """Get stats history."""
if not keys: if not keys:
return self.stats_history return self.stats_history
selected_stats: list[dict[str, any]] = [] selected_stats: list[dict[str, Any]] = []
for s in self.stats_history: for s in self.stats_history:
selected = {} selected = {}
+8 -7
View File
@@ -5,6 +5,7 @@ import queue
import threading import threading
from multiprocessing import Queue from multiprocessing import Queue
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from typing import Any
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.events.maintainer import EventStateEnum, EventTypeEnum from frigate.events.maintainer import EventStateEnum, EventTypeEnum
@@ -27,7 +28,7 @@ class TimelineProcessor(threading.Thread):
self.config = config self.config = config
self.queue = queue self.queue = queue
self.stop_event = stop_event 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: def run(self) -> None:
while not self.stop_event.is_set(): while not self.stop_event.is_set():
@@ -55,9 +56,9 @@ class TimelineProcessor(threading.Thread):
def insert_or_save( def insert_or_save(
self, self,
entry: dict[str, any], entry: dict[str, Any],
prev_event_data: dict[any, any], prev_event_data: dict[Any, Any],
event_data: dict[any, any], event_data: dict[Any, Any],
) -> None: ) -> None:
"""Insert into db or cache.""" """Insert into db or cache."""
id = entry[Timeline.source_id] id = entry[Timeline.source_id]
@@ -81,8 +82,8 @@ class TimelineProcessor(threading.Thread):
self, self,
camera: str, camera: str,
event_type: str, event_type: str,
prev_event_data: dict[any, any], prev_event_data: dict[Any, Any],
event_data: dict[any, any], event_data: dict[Any, Any],
) -> bool: ) -> bool:
"""Handle object detection.""" """Handle object detection."""
save = False save = False
@@ -153,7 +154,7 @@ class TimelineProcessor(threading.Thread):
self, self,
camera: str, camera: str,
event_type: str, event_type: str,
event_data: dict[any, any], event_data: dict[Any, Any],
) -> bool: ) -> bool:
if event_type != "new": if event_type != "new":
return False return False
+2 -1
View File
@@ -1,4 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any
from frigate.config import DetectConfig from frigate.config import DetectConfig
@@ -10,6 +11,6 @@ class ObjectTracker(ABC):
@abstractmethod @abstractmethod
def match_and_update( 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: ) -> None:
pass pass
+2 -2
View File
@@ -1,7 +1,7 @@
import logging import logging
import random import random
import string import string
from typing import Sequence from typing import Any, Sequence
import cv2 import cv2
import numpy as np import numpy as np
@@ -460,7 +460,7 @@ class NorfairTracker(ObjectTracker):
self.match_and_update(frame_name, frame_time, detections=detections) self.match_and_update(frame_name, frame_time, detections=detections)
def match_and_update( 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 # Group detections by object type
detections_by_type = {} detections_by_type = {}
+3 -2
View File
@@ -7,6 +7,7 @@ import threading
from collections import defaultdict from collections import defaultdict
from enum import Enum from enum import Enum
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from typing import Any
import cv2 import cv2
import numpy as np import numpy as np
@@ -70,7 +71,7 @@ class TrackedObjectProcessor(threading.Thread):
self.event_end_subscriber = EventEndSubscriber() self.event_end_subscriber = EventEndSubscriber()
self.sub_label_subscriber = EventMetadataSubscriber(EventMetadataTypeEnum.all) 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] = {} self.ongoing_manual_events: dict[str, str] = {}
# { # {
@@ -301,7 +302,7 @@ class TrackedObjectProcessor(threading.Thread):
return {} return {}
def get_current_frame( def get_current_frame(
self, camera: str, draw_options: dict[str, any] = {} self, camera: str, draw_options: dict[str, Any] = {}
) -> np.ndarray | None: ) -> np.ndarray | None:
if camera == "birdseye": if camera == "birdseye":
return self.frame_manager.get( return self.frame_manager.get(
+6 -6
View File
@@ -5,7 +5,7 @@ import math
import os import os
from collections import defaultdict from collections import defaultdict
from statistics import median from statistics import median
from typing import Optional from typing import Any, Optional
import cv2 import cv2
import numpy as np import numpy as np
@@ -38,7 +38,7 @@ class TrackedObject:
camera_config: CameraConfig, camera_config: CameraConfig,
ui_config: UIConfig, ui_config: UIConfig,
frame_cache, 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 # set the score history then remove as it is not part of object state
self.score_history = obj_data["score_history"] self.score_history = obj_data["score_history"]
@@ -154,7 +154,7 @@ class TrackedObject:
"attributes": obj_data["attributes"], "attributes": obj_data["attributes"],
"current_estimated_speed": self.current_estimated_speed, "current_estimated_speed": self.current_estimated_speed,
"velocity_angle": self.velocity_angle, "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": obj_data.get(
"recognized_license_plate" "recognized_license_plate"
), ),
@@ -378,7 +378,7 @@ class TrackedObject:
"current_estimated_speed": self.current_estimated_speed, "current_estimated_speed": self.current_estimated_speed,
"average_estimated_speed": self.average_estimated_speed, "average_estimated_speed": self.average_estimated_speed,
"velocity_angle": self.velocity_angle, "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"), "recognized_license_plate": self.obj_data.get("recognized_license_plate"),
} }
@@ -621,7 +621,7 @@ class TrackedObjectAttribute:
self.ratio = raw_data[4] self.ratio = raw_data[4]
self.region = raw_data[5] 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 data saved to the object."""
return { return {
"label": self.label, "label": self.label,
@@ -629,7 +629,7 @@ class TrackedObjectAttribute:
"box": self.box, "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.""" """Find the best attribute for each object and return its ID."""
best_object_area = None best_object_area = None
best_object_id = None best_object_id = None
+28 -7
View File
@@ -11,6 +11,7 @@ import shlex
import struct import struct
import urllib.parse import urllib.parse
from collections.abc import Mapping from collections.abc import Mapping
from multiprocessing.sharedctypes import Synchronized
from pathlib import Path from pathlib import Path
from typing import Any, Optional, Tuple, Union from typing import Any, Optional, Tuple, Union
from zoneinfo import ZoneInfoNotFoundError from zoneinfo import ZoneInfoNotFoundError
@@ -26,16 +27,16 @@ logger = logging.getLogger(__name__)
class EventsPerSecond: 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._start = None
self._max_events = max_events self._max_events = max_events
self._last_n_seconds = last_n_seconds self._last_n_seconds = last_n_seconds
self._timestamps = [] self._timestamps = []
def start(self): def start(self) -> None:
self._start = datetime.datetime.now().timestamp() self._start = datetime.datetime.now().timestamp()
def update(self): def update(self) -> None:
now = datetime.datetime.now().timestamp() now = datetime.datetime.now().timestamp()
if self._start is None: if self._start is None:
self._start = now self._start = now
@@ -45,7 +46,7 @@ class EventsPerSecond:
self._timestamps = self._timestamps[(1 - self._max_events) :] self._timestamps = self._timestamps[(1 - self._max_events) :]
self.expire_timestamps(now) self.expire_timestamps(now)
def eps(self): def eps(self) -> float:
now = datetime.datetime.now().timestamp() now = datetime.datetime.now().timestamp()
if self._start is None: if self._start is None:
self._start = now self._start = now
@@ -58,12 +59,29 @@ class EventsPerSecond:
return len(self._timestamps) / seconds return len(self._timestamps) / seconds
# remove aged out timestamps # remove aged out timestamps
def expire_timestamps(self, now): def expire_timestamps(self, now: float) -> None:
threshold = now - self._last_n_seconds threshold = now - self._last_n_seconds
while self._timestamps and self._timestamps[0] < threshold: while self._timestamps and self._timestamps[0] < threshold:
del self._timestamps[0] 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: def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dict:
""" """
:param dct1: First dict to merge :param dct1: First dict to merge
@@ -138,7 +156,7 @@ def load_labels(path: Optional[str], encoding="utf-8", prefill=91):
return labels 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 = ( seconds_offset = (
datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds() 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( def to_relative_box(
width: int, height: int, box: Tuple[int, int, int, int] 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 ( return (
box[0] / width, # x box[0] / width, # x
box[1] / height, # y box[1] / height, # y
@@ -169,6 +187,9 @@ def update_yaml_from_url(file_path, url):
parsed_url = urllib.parse.urlparse(url) parsed_url = urllib.parse.urlparse(url)
query_string = urllib.parse.parse_qs(parsed_url.query, keep_blank_values=True) 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(): for key_path_str, new_value_list in query_string.items():
key_path = key_path_str.split(".") key_path = key_path_str.split(".")
for i in range(len(key_path)): for i in range(len(key_path)):
+10 -10
View File
@@ -4,7 +4,7 @@ import asyncio
import logging import logging
import os import os
import shutil import shutil
from typing import Optional, Union from typing import Any, Optional, Union
from ruamel.yaml import YAML from ruamel.yaml import YAML
@@ -37,7 +37,7 @@ def migrate_frigate_config(config_file: str):
yaml = YAML() yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2) yaml.indent(mapping=2, sequence=4, offset=2)
with open(config_file, "r") as f: 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: if config is None:
logger.error(f"Failed to load config at {config_file}") 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...") 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""" """Handle migrating frigate config to 0.14"""
# migrate record.events.required_zones to review.alerts.required_zones # migrate record.events.required_zones to review.alerts.required_zones
new_config = config.copy() 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"] del new_config["rtmp"]
for name, camera in config.get("cameras", {}).items(): 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 = ( required_zones = (
camera_config.get("record", {}).get("events", {}).get("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 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""" """Handle migrating frigate config to 0.15-0"""
new_config = config.copy() 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"] del new_config["record"]["events"]
for name, camera in config.get("cameras", {}).items(): 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: if record_events:
alerts_retention = {"retain": {}} 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 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""" """Handle migrating frigate config to 0.15-1"""
new_config = config.copy() 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 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""" """Handle migrating frigate config to 0.16-0"""
new_config = config.copy() 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 new_config["detect"] = detect_config
for name, camera in config.get("cameras", {}).items(): 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", {}) live_config = camera_config.get("live", {})
if "stream_name" in live_config: 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 resource_tracker as _mprt
from multiprocessing import shared_memory as _mpshm from multiprocessing import shared_memory as _mpshm
from string import printable from string import printable
from typing import AnyStr, Optional from typing import Any, AnyStr, Optional
import cv2 import cv2
import numpy as np import numpy as np
@@ -766,7 +766,7 @@ class FrameManager(ABC):
pass pass
@abstractmethod @abstractmethod
def write(self, name: str) -> memoryview: def write(self, name: str) -> Optional[memoryview]:
pass pass
@abstractmethod @abstractmethod
@@ -847,7 +847,7 @@ class SharedMemoryFrameManager(FrameManager):
self.shm_store[name] = shm self.shm_store[name] = shm
return shm.buf return shm.buf
def write(self, name: str) -> memoryview: def write(self, name: str) -> Optional[memoryview]:
try: try:
if name in self.shm_store: if name in self.shm_store:
shm = self.shm_store[name] shm = self.shm_store[name]
@@ -944,7 +944,7 @@ def get_image_from_recording(
relative_frame_time: float, relative_frame_time: float,
codec: str, codec: str,
height: Optional[int] = None, height: Optional[int] = None,
) -> Optional[any]: ) -> Optional[Any]:
"""retrieve a frame from given time in recording file.""" """retrieve a frame from given time in recording file."""
ffmpeg_cmd = [ ffmpeg_cmd = [
+2 -2
View File
@@ -2,6 +2,7 @@
import logging import logging
import os import os
from typing import Any
import cv2 import cv2
import numpy as np import numpy as np
@@ -284,7 +285,7 @@ def post_process_yolox(
def get_ort_providers( def get_ort_providers(
force_cpu: bool = False, device: str = "AUTO", requires_fp16: bool = False 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: if force_cpu:
return ( return (
["CPUExecutionProvider"], ["CPUExecutionProvider"],
@@ -340,7 +341,6 @@ def get_ort_providers(
providers.append(provider) providers.append(provider)
options.append( options.append(
{ {
"arena_extend_strategy": "kSameAsRequested",
"cache_dir": os.path.join(MODEL_CACHE_DIR, "openvino/ort"), "cache_dir": os.path.join(MODEL_CACHE_DIR, "openvino/ort"),
"device_type": device, "device_type": device,
} }
+11 -10
View File
@@ -4,6 +4,7 @@ import datetime
import logging import logging
import math import math
from collections import defaultdict from collections import defaultdict
from typing import Any
import cv2 import cv2
import numpy as np import numpy as np
@@ -38,7 +39,7 @@ def get_camera_regions_grid(
name: str, name: str,
detect: DetectConfig, detect: DetectConfig,
min_region_size: int, min_region_size: int,
) -> list[list[dict[str, any]]]: ) -> list[list[dict[str, Any]]]:
"""Build a grid of expected region sizes for a camera.""" """Build a grid of expected region sizes for a camera."""
# get grid from db if available # get grid from db if available
try: try:
@@ -163,10 +164,10 @@ def get_cluster_region_from_grid(frame_shape, min_region, cluster, boxes, region
def get_region_from_grid( def get_region_from_grid(
frame_shape: tuple[int], frame_shape: tuple[int, int],
cluster: list[int], cluster: list[int],
min_region: int, min_region: int,
region_grid: list[list[dict[str, any]]], region_grid: list[list[dict[str, Any]]],
) -> list[int]: ) -> list[int]:
"""Get a region for a box based on the region grid.""" """Get a region for a box based on the region grid."""
box = calculate_region( box = calculate_region(
@@ -446,9 +447,9 @@ def get_cluster_region(frame_shape, min_region, cluster, boxes):
def get_startup_regions( def get_startup_regions(
frame_shape: tuple[int], frame_shape: tuple[int, int],
region_min_size: int, region_min_size: int,
region_grid: list[list[dict[str, any]]], region_grid: list[list[dict[str, Any]]],
) -> list[list[int]]: ) -> list[list[int]]:
"""Get a list of regions to run on startup.""" """Get a list of regions to run on startup."""
# return 8 most popular regions for the camera # return 8 most popular regions for the camera
@@ -480,12 +481,12 @@ def get_startup_regions(
def reduce_detections( def reduce_detections(
frame_shape: tuple[int], frame_shape: tuple[int, int],
all_detections: list[tuple[any]], all_detections: list[tuple[Any]],
) -> list[tuple[any]]: ) -> list[tuple[Any]]:
"""Take a list of detections and reduce overlaps to create a list of confident detections.""" """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.""" """apply non-maxima suppression to suppress weak, overlapping bounding boxes."""
detected_object_groups = defaultdict(lambda: []) detected_object_groups = defaultdict(lambda: [])
for detection in detections: for detection in detections:
@@ -524,7 +525,7 @@ def reduce_detections(
# set the detections list to only include top objects # set the detections list to only include top objects
return selected_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.""" """Drop detections that overlap too much."""
detected_object_groups = defaultdict(lambda: []) detected_object_groups = defaultdict(lambda: [])
for detection in detections: for detection in detections:
+16 -10
View File
@@ -9,7 +9,7 @@ import signal
import subprocess as sp import subprocess as sp
import traceback import traceback
from datetime import datetime from datetime import datetime
from typing import List, Optional, Tuple from typing import Any, List, Optional, Tuple
import cv2 import cv2
import psutil import psutil
@@ -230,7 +230,7 @@ def is_vaapi_amd_driver() -> bool:
return any("AMD Radeon Graphics" in line for line in output) 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.""" """Get stats using radeontop."""
radeontop_command = ["radeontop", "-d", "-", "-l", "1"] radeontop_command = ["radeontop", "-d", "-", "-l", "1"]
@@ -256,7 +256,7 @@ def get_amd_gpu_stats() -> dict[str, str]:
return results 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.""" """Get stats using intel_gpu_top."""
def get_stats_manually(output: str) -> dict[str, str]: 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 return results
def get_rockchip_gpu_stats() -> dict[str, str]: def get_rockchip_gpu_stats() -> Optional[dict[str, str]]:
"""Get GPU stats using rk.""" """Get GPU stats using rk."""
try: try:
with open("/sys/kernel/debug/rkrga/load", "r") as f: 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": "-"} 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.""" """Get NPU stats using rk."""
try: try:
with open("/sys/kernel/debug/rknpu/load", "r") as f: with open("/sys/kernel/debug/rknpu/load", "r") as f:
npu_output = f.read() 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: except FileNotFoundError:
core_loads = None core_loads = None
@@ -488,7 +494,7 @@ def get_nvidia_gpu_stats() -> dict[int, dict]:
return results return results
def get_jetson_stats() -> dict[int, dict]: def get_jetson_stats() -> Optional[dict[int, dict]]:
results = {} results = {}
try: try:
@@ -531,7 +537,7 @@ def vainfo_hwaccel(device_name: Optional[str] = None) -> sp.CompletedProcess:
return sp.run(ffprobe_cmd, capture_output=True) 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.""" """Get general hardware info for nvidia GPU."""
results = {} results = {}
try: try:
@@ -590,8 +596,8 @@ def auto_detect_hwaccel() -> str:
async def get_video_properties( async def get_video_properties(
ffmpeg, url: str, get_duration: bool = False ffmpeg, url: str, get_duration: bool = False
) -> dict[str, any]: ) -> dict[str, Any]:
async def calculate_duration(video: Optional[any]) -> float: async def calculate_duration(video: Optional[Any]) -> float:
duration = None duration = None
if video is not None: if video is not None:
+5 -3
View File
@@ -184,7 +184,7 @@ class CameraWatchdog(threading.Thread):
self.capture_thread = None self.capture_thread = None
self.ffmpeg_detect_process = None self.ffmpeg_detect_process = None
self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect") 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.camera_fps = camera_fps
self.skipped_fps = skipped_fps self.skipped_fps = skipped_fps
self.ffmpeg_pid = ffmpeg_pid self.ffmpeg_pid = ffmpeg_pid
@@ -371,7 +371,9 @@ class CameraWatchdog(threading.Thread):
p["logpipe"].close() p["logpipe"].close()
self.ffmpeg_other_processes.clear() 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.""" """Checks if ffmpeg is still writing recording segments to cache."""
cache_files = sorted( cache_files = sorted(
[ [
@@ -859,7 +861,7 @@ def process_frames(
detections[obj["id"]] = {**obj, "attributes": []} detections[obj["id"]] = {**obj, "attributes": []}
# find the best object for each attribute to be assigned to # 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 attributes in attribute_detections.values():
for attribute in attributes: for attribute in attributes:
filtered_objects = filter( filtered_objects = filter(
+2 -2
View File
@@ -1,3 +1,3 @@
[tool.ruff] [tool.ruff.lint]
ignore = ["E501","E711","E712"] 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