Compare commits

..

27 Commits

Author SHA1 Message Date
Hosted Weblate
696e33e1dc
Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (125 of 125 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (125 of 125 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (72 of 72 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (48 of 48 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (124 of 124 strings)

Translated using Weblate (Norwegian Bokmål)

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

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (88 of 88 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (124 of 124 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (39 of 39 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (90 of 90 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/components-filter/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/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-live/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/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/components-filter
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
2025-11-04 00:54:57 +00:00
Hosted Weblate
058e702ffc
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (125 of 125 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (88 of 88 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (124 of 124 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (39 of 39 strings)

Co-authored-by: GuoQing Liu <842607283@qq.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/zh_Hans/
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-settings/zh_Hans/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-settings
2025-11-04 00:54:56 +00:00
Hosted Weblate
69fc0c8d57
Translated using Weblate (Slovak)
Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Slovak)

Currently translated at 99.2% (124 of 125 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (51 of 51 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Slovak)

Currently translated at 85.9% (512 of 596 strings)

Translated using Weblate (Slovak)

Currently translated at 99.1% (123 of 124 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Slovak)

Currently translated at 98.6% (494 of 501 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (88 of 88 strings)

Translated using Weblate (Slovak)

Currently translated at 74.1% (442 of 596 strings)

Translated using Weblate (Slovak)

Currently translated at 98.3% (122 of 124 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (39 of 39 strings)

Translated using Weblate (Slovak)

Currently translated at 98.1% (53 of 54 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Slovak)

Currently translated at 88.0% (441 of 501 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jakub K <klacanjakub0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/sk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/sk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/sk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/sk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/sk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/sk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/sk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/sk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/sk/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2025-11-04 00:54:55 +00:00
Hosted Weblate
ceffe8a5f0
Translated using Weblate (Swedish)
Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (125 of 125 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (39 of 39 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (207 of 207 strings)

Co-authored-by: Daniel Nylander <daniel@danielnylander.se>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kristian Johansson <knmjohansson@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/sv/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-settings
2025-11-04 00:54:54 +00:00
Hosted Weblate
910fb57f7c
Translated using Weblate (French)
Currently translated at 100.0% (598 of 598 strings)

Translated using Weblate (French)

Currently translated at 100.0% (125 of 125 strings)

Translated using Weblate (French)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (French)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (French)

Currently translated at 100.0% (125 of 125 strings)

Translated using Weblate (French)

Currently translated at 100.0% (89 of 89 strings)

Translated using Weblate (French)

Currently translated at 100.0% (88 of 88 strings)

Translated using Weblate (French)

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (French)

Currently translated at 100.0% (124 of 124 strings)

Translated using Weblate (French)

Currently translated at 100.0% (39 of 39 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/views-classificationmodel/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-settings/fr/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-settings
2025-11-04 00:54:53 +00:00
Hosted Weblate
a705a451dd
Translated using Weblate (Dutch)
Currently translated at 100.0% (598 of 598 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (90 of 90 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (125 of 125 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (89 of 89 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (88 of 88 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (124 of 124 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (39 of 39 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/common/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/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-live/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/nl/
Translation: Frigate NVR/common
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
2025-11-04 00:54:52 +00:00
Hosted Weblate
06e4f0aa3f
Translated using Weblate (Italian)
Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (72 of 72 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (125 of 125 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (89 of 89 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (88 of 88 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (124 of 124 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Italian)

Currently translated at 28.4% (25 of 88 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (39 of 39 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (51 of 51 strings)

Translated using Weblate (Italian)

Currently translated at 75.0% (93 of 124 strings)

Translated using Weblate (Italian)

Currently translated at 98.1% (53 of 54 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/components-dialog/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/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-exports/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/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2025-11-04 00:54:51 +00:00
Hosted Weblate
d89fc7d6ba
Translated using Weblate (Hungarian)
Currently translated at 8.1% (8 of 98 strings)

Translated using Weblate (Hungarian)

Currently translated at 96.0% (49 of 51 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Hungarian)

Currently translated at 69.2% (27 of 39 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (10 of 10 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Zrinyi Patrik <patrikzrinyi404@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/hu/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/hu/
Translation: Frigate NVR/components-auth
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
2025-11-04 00:54:50 +00:00
Hosted Weblate
773d4d1127
Translated using Weblate (Vietnamese)
Currently translated at 12.2% (12 of 98 strings)

Translated using Weblate (Vietnamese)

Currently translated at 62.0% (370 of 596 strings)

Translated using Weblate (Vietnamese)

Currently translated at 92.3% (12 of 13 strings)

Translated using Weblate (Vietnamese)

Currently translated at 69.2% (27 of 39 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (10 of 10 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: John Nguyen <thongnguyen.uit@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/vi/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/vi/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/vi/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/vi/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/vi/
Translation: Frigate NVR/components-auth
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-settings
2025-11-04 00:54:49 +00:00
Hosted Weblate
67c5a6ed47
Translated using Weblate (Czech)
Currently translated at 3.0% (3 of 98 strings)

Translated using Weblate (Czech)

Currently translated at 67.9% (405 of 596 strings)

Translated using Weblate (Czech)

Currently translated at 92.1% (47 of 51 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (10 of 10 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: lukascissa <lukas@cissa.cz>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/cs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/cs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/cs/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/cs/
Translation: Frigate NVR/components-auth
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2025-11-04 00:54:47 +00:00
Hosted Weblate
7e9d2f8b00
Translated using Weblate (Catalan)
Currently translated at 100.0% (598 of 598 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (48 of 48 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (48 of 48 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (125 of 125 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (39 of 39 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (207 of 207 strings)

Co-authored-by: Eduardo Pastor Fernández <123eduardoneko123@gmail.com>
Co-authored-by: Gerard Ricart Castells <gerard.ricart@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ca/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
2025-11-04 00:54:46 +00:00
Hosted Weblate
de554289e6
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (598 of 598 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (125 of 125 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (89 of 89 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (88 of 88 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (51 of 51 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (124 of 124 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (39 of 39 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (206 of 206 strings)

Co-authored-by: Anatoli Skovpen <a@ask.kiev.ua>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/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-exports/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/uk/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-settings
2025-11-04 00:54:45 +00:00
Hosted Weblate
3c7c232ae0
Translated using Weblate (Romanian)
Currently translated at 100.0% (598 of 598 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (125 of 125 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (88 of 88 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (51 of 51 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (124 of 124 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (39 of 39 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (593 of 593 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (48 of 48 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (90 of 90 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (51 of 51 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (124 of 124 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (34 of 34 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (206 of 206 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: lukasig <lukasig@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ro/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
2025-11-04 00:54:44 +00:00
Hosted Weblate
912bf3b9e8
Translated using Weblate (Portuguese (Brazil))
Currently translated at 23.4% (23 of 98 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 72.0% (90 of 125 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.4% (38 of 39 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (34 of 34 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Marcelo Popper Costa <marcelo_popper@hotmail.com>
Co-authored-by: Nico <n2778370@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/pt_BR/
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
2025-11-04 00:54:43 +00:00
Josh Hawkins
256817d5c2
Make events summary endpoint DST-aware (#20786) 2025-11-03 17:54:33 -07:00
Nicolas Mowen
84409eab7e
Various fixes (#20785)
* Catch case where detector overflows

* Add more debug logs

* Cleanup

* Adjust no class wording

* Adjustments
2025-11-03 18:42:59 -06:00
Josh Hawkins
9e83888133
Fix recordings summary for DST (#20784)
* make recordings summary endpoints DST aware

* remove unused

* clean up
2025-11-03 17:30:56 -07:00
Abinila Siva
85f7138361
update installation code to hold SDK 2.1 version (#20781) 2025-11-03 13:23:51 -07:00
Nicolas Mowen
fc1cad2872
Adjust LPR packages for licensing (#20780) 2025-11-03 14:11:02 -06:00
Nicolas Mowen
5529432856
Various fixes (#20774)
* Change order of deletion

* Add debug log for camera enabled

* Add more face debug logs

* Set jetson numpy version
2025-11-03 10:05:03 -06:00
Josh Hawkins
59963fc47e
Camera Wizard tweaks (#20773)
* add switch to use go2rtc ffmpeg mode

* i18n

* move testing state outside of button
2025-11-03 08:42:38 -07:00
Nicolas Mowen
31fa87ce73
Correctly remove classification model from config (#20772)
* Correctly remove classification model from config

* Undo

* fix

* Use existing config update API and dynamically remove models that were running

* Set update message for face
2025-11-03 08:01:30 -07:00
Nicolas Mowen
740c618240
Fix review summary for DST (#20770)
* Fix review summary for DST

* Fix
2025-11-03 07:34:47 -06:00
Nicolas Mowen
4f76b34f44
Classification fixes (#20771)
* Fully delete a model

* Fix deletion dialog

* Fix classification back step

* Adjust selection gradient

* Fix

* Fix
2025-11-03 07:34:06 -06:00
Josh Hawkins
d44340eca6
Tracked Object Details pane tweaks (#20762)
* normalize path and points sizes

* fix bounding box display to only show on actual points that have a box

* add support for using snapshots
2025-11-02 06:48:43 -07:00
GuoQing Liu
aff82f809c
feat: add search filter group audio i18n (#20760) 2025-11-02 07:45:24 -06:00
Josh Hawkins
1e50d83d06
create i18n key for list separator and use in zones (#20749) 2025-11-01 12:20:32 -06:00
98 changed files with 1658 additions and 535 deletions

View File

@ -2,9 +2,9 @@
set -e
# Download the MxAccl for Frigate github release
wget https://github.com/memryx/mx_accl_frigate/archive/refs/heads/main.zip -O /tmp/mxaccl.zip
wget https://github.com/memryx/mx_accl_frigate/archive/refs/tags/v2.1.0.zip -O /tmp/mxaccl.zip
unzip /tmp/mxaccl.zip -d /tmp
mv /tmp/mx_accl_frigate-main /opt/mx_accl_frigate
mv /tmp/mx_accl_frigate-2.1.0 /opt/mx_accl_frigate
rm /tmp/mxaccl.zip
# Install Python dependencies

View File

@ -56,7 +56,7 @@ pywebpush == 2.0.*
# alpr
pyclipper == 1.3.*
shapely == 2.0.*
Levenshtein==0.26.*
rapidfuzz==3.12.*
# HailoRT Wheels
appdirs==1.4.*
argcomplete==2.0.*

View File

@ -24,10 +24,13 @@ echo "Adding MemryX GPG key and repository..."
wget -qO- https://developer.memryx.com/deb/memryx.asc | sudo tee /etc/apt/trusted.gpg.d/memryx.asc >/dev/null
echo 'deb https://developer.memryx.com/deb stable main' | sudo tee /etc/apt/sources.list.d/memryx.list >/dev/null
# Update and install memx-drivers
echo "Installing memx-drivers..."
# Update and install specific SDK 2.1 packages
echo "Installing MemryX SDK 2.1 packages..."
sudo apt update
sudo apt install -y memx-drivers
sudo apt install -y memx-drivers=2.1.* memx-accl=2.1.* mxa-manager=2.1.*
# Hold packages to prevent automatic upgrades
sudo apt-mark hold memx-drivers memx-accl mxa-manager
# ARM-specific board setup
if [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then
@ -37,11 +40,5 @@ fi
echo -e "\n\n\033[1;31mYOU MUST RESTART YOUR COMPUTER NOW\033[0m\n\n"
# Install other runtime packages
packages=("memx-accl" "mxa-manager")
for pkg in "${packages[@]}"; do
echo "Installing $pkg..."
sudo apt install -y "$pkg"
done
echo "MemryX SDK 2.1 installation complete!"
echo "MemryX installation complete!"

View File

@ -1 +1,2 @@
cuda-python == 12.6.*; platform_machine == 'aarch64'
numpy == 1.26.*; platform_machine == 'aarch64'

View File

@ -37,7 +37,6 @@ from frigate.stats.prometheus import get_metrics, update_metrics
from frigate.util.builtin import (
clean_camera_user_pass,
flatten_config_data,
get_tz_modifiers,
process_config_query_string,
update_yaml_file_bulk,
)
@ -48,6 +47,7 @@ from frigate.util.services import (
restart_frigate,
vainfo_hwaccel,
)
from frigate.util.time import get_tz_modifiers
from frigate.version import VERSION
logger = logging.getLogger(__name__)
@ -403,12 +403,13 @@ def config_set(request: Request, body: AppConfigSetBody):
settings,
)
else:
# Handle nested config updates (e.g., config/classification/custom/{name})
# Generic handling for global config updates
settings = config.get_nested_object(body.update_topic)
if settings:
request.app.config_publisher.publisher.publish(
body.update_topic, settings
)
# Publish None for removal, actual config for add/update
request.app.config_publisher.publisher.publish(
body.update_topic, settings
)
return JSONResponse(
content=(

View File

@ -31,7 +31,7 @@ from frigate.api.defs.response.generic_response import GenericResponse
from frigate.api.defs.tags import Tags
from frigate.config import FrigateConfig
from frigate.config.camera import DetectConfig
from frigate.const import CLIPS_DIR, FACE_DIR
from frigate.const import CLIPS_DIR, FACE_DIR, MODEL_CACHE_DIR
from frigate.embeddings import EmbeddingsContext
from frigate.models import Event
from frigate.util.classification import (
@ -828,9 +828,13 @@ def delete_classification_model(request: Request, name: str):
status_code=404,
)
# Delete the classification model's data directory
model_dir = os.path.join(CLIPS_DIR, sanitize_filename(name))
# Delete the classification model's data directory in clips
data_dir = os.path.join(CLIPS_DIR, sanitize_filename(name))
if os.path.exists(data_dir):
shutil.rmtree(data_dir)
# Delete the classification model's files in model_cache
model_dir = os.path.join(MODEL_CACHE_DIR, sanitize_filename(name))
if os.path.exists(model_dir):
shutil.rmtree(model_dir)

View File

@ -2,6 +2,7 @@
import base64
import datetime
import json
import logging
import os
import random
@ -57,8 +58,8 @@ from frigate.const import CLIPS_DIR, TRIGGER_DIR
from frigate.embeddings import EmbeddingsContext
from frigate.models import Event, ReviewSegment, Timeline, Trigger
from frigate.track.object_processing import TrackedObject
from frigate.util.builtin import get_tz_modifiers
from frigate.util.path import get_event_thumbnail_bytes
from frigate.util.time import get_dst_transitions, get_tz_modifiers
logger = logging.getLogger(__name__)
@ -813,7 +814,6 @@ def events_summary(
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
):
tz_name = params.timezone
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name)
has_clip = params.has_clip
has_snapshot = params.has_snapshot
@ -828,33 +828,91 @@ def events_summary(
if len(clauses) == 0:
clauses.append((True))
groups = (
time_range_query = (
Event.select(
Event.camera,
Event.label,
Event.sub_label,
Event.data,
fn.strftime(
"%Y-%m-%d",
fn.datetime(
Event.start_time, "unixepoch", hour_modifier, minute_modifier
),
).alias("day"),
Event.zones,
fn.COUNT(Event.id).alias("count"),
fn.MIN(Event.start_time).alias("min_time"),
fn.MAX(Event.start_time).alias("max_time"),
)
.where(reduce(operator.and_, clauses) & (Event.camera << allowed_cameras))
.group_by(
Event.camera,
Event.label,
Event.sub_label,
Event.data,
(Event.start_time + seconds_offset).cast("int") / (3600 * 24),
Event.zones,
)
.dicts()
.get()
)
return JSONResponse(content=[e for e in groups.dicts()])
min_time = time_range_query.get("min_time")
max_time = time_range_query.get("max_time")
if min_time is None or max_time is None:
return JSONResponse(content=[])
dst_periods = get_dst_transitions(tz_name, min_time, max_time)
grouped: dict[tuple, dict] = {}
for period_start, period_end, period_offset in dst_periods:
hours_offset = int(period_offset / 60 / 60)
minutes_offset = int(period_offset / 60 - hours_offset * 60)
period_hour_modifier = f"{hours_offset} hour"
period_minute_modifier = f"{minutes_offset} minute"
period_groups = (
Event.select(
Event.camera,
Event.label,
Event.sub_label,
Event.data,
fn.strftime(
"%Y-%m-%d",
fn.datetime(
Event.start_time,
"unixepoch",
period_hour_modifier,
period_minute_modifier,
),
).alias("day"),
Event.zones,
fn.COUNT(Event.id).alias("count"),
)
.where(
reduce(operator.and_, clauses)
& (Event.camera << allowed_cameras)
& (Event.start_time >= period_start)
& (Event.start_time <= period_end)
)
.group_by(
Event.camera,
Event.label,
Event.sub_label,
Event.data,
(Event.start_time + period_offset).cast("int") / (3600 * 24),
Event.zones,
)
.namedtuples()
)
for g in period_groups:
key = (
g.camera,
g.label,
g.sub_label,
json.dumps(g.data, sort_keys=True) if g.data is not None else None,
g.day,
json.dumps(g.zones, sort_keys=True) if g.zones is not None else None,
)
if key in grouped:
grouped[key]["count"] += int(g.count or 0)
else:
grouped[key] = {
"camera": g.camera,
"label": g.label,
"sub_label": g.sub_label,
"data": g.data,
"day": g.day,
"zones": g.zones,
"count": int(g.count or 0),
}
return JSONResponse(content=list(grouped.values()))
@router.get(

View File

@ -34,7 +34,7 @@ from frigate.record.export import (
PlaybackSourceEnum,
RecordingExporter,
)
from frigate.util.builtin import is_current_hour
from frigate.util.time import is_current_hour
logger = logging.getLogger(__name__)

View File

@ -44,9 +44,9 @@ from frigate.const import (
)
from frigate.models import Event, Previews, Recordings, Regions, ReviewSegment
from frigate.track.object_processing import TrackedObjectProcessor
from frigate.util.builtin import get_tz_modifiers
from frigate.util.image import get_image_from_recording
from frigate.util.path import get_event_thumbnail_bytes
from frigate.util.time import get_dst_transitions
logger = logging.getLogger(__name__)
@ -424,7 +424,6 @@ def all_recordings_summary(
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
):
"""Returns true/false by day indicating if recordings exist"""
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(params.timezone)
cameras = params.cameras
if cameras != "all":
@ -432,41 +431,70 @@ def all_recordings_summary(
filtered = requested.intersection(allowed_cameras)
if not filtered:
return JSONResponse(content={})
cameras = ",".join(filtered)
camera_list = list(filtered)
else:
cameras = allowed_cameras
camera_list = allowed_cameras
query = (
time_range_query = (
Recordings.select(
fn.strftime(
"%Y-%m-%d",
fn.datetime(
Recordings.start_time + seconds_offset,
"unixepoch",
hour_modifier,
minute_modifier,
),
).alias("day")
fn.MIN(Recordings.start_time).alias("min_time"),
fn.MAX(Recordings.start_time).alias("max_time"),
)
.group_by(
fn.strftime(
"%Y-%m-%d",
fn.datetime(
Recordings.start_time + seconds_offset,
"unixepoch",
hour_modifier,
minute_modifier,
),
)
)
.order_by(Recordings.start_time.desc())
.where(Recordings.camera << camera_list)
.dicts()
.get()
)
if params.cameras != "all":
query = query.where(Recordings.camera << cameras.split(","))
min_time = time_range_query.get("min_time")
max_time = time_range_query.get("max_time")
recording_days = query.namedtuples()
days = {day.day: True for day in recording_days}
if min_time is None or max_time is None:
return JSONResponse(content={})
dst_periods = get_dst_transitions(params.timezone, min_time, max_time)
days: dict[str, bool] = {}
for period_start, period_end, period_offset in dst_periods:
hours_offset = int(period_offset / 60 / 60)
minutes_offset = int(period_offset / 60 - hours_offset * 60)
period_hour_modifier = f"{hours_offset} hour"
period_minute_modifier = f"{minutes_offset} minute"
period_query = (
Recordings.select(
fn.strftime(
"%Y-%m-%d",
fn.datetime(
Recordings.start_time,
"unixepoch",
period_hour_modifier,
period_minute_modifier,
),
).alias("day")
)
.where(
(Recordings.camera << camera_list)
& (Recordings.end_time >= period_start)
& (Recordings.start_time <= period_end)
)
.group_by(
fn.strftime(
"%Y-%m-%d",
fn.datetime(
Recordings.start_time,
"unixepoch",
period_hour_modifier,
period_minute_modifier,
),
)
)
.order_by(Recordings.start_time.desc())
.namedtuples()
)
for g in period_query:
days[g.day] = True
return JSONResponse(content=days)
@ -476,61 +504,103 @@ def all_recordings_summary(
)
async def recordings_summary(camera_name: str, timezone: str = "utc"):
"""Returns hourly summary for recordings of given camera"""
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(timezone)
recording_groups = (
time_range_query = (
Recordings.select(
fn.strftime(
"%Y-%m-%d %H",
fn.datetime(
Recordings.start_time, "unixepoch", hour_modifier, minute_modifier
),
).alias("hour"),
fn.SUM(Recordings.duration).alias("duration"),
fn.SUM(Recordings.motion).alias("motion"),
fn.SUM(Recordings.objects).alias("objects"),
fn.MIN(Recordings.start_time).alias("min_time"),
fn.MAX(Recordings.start_time).alias("max_time"),
)
.where(Recordings.camera == camera_name)
.group_by((Recordings.start_time + seconds_offset).cast("int") / 3600)
.order_by(Recordings.start_time.desc())
.namedtuples()
.dicts()
.get()
)
event_groups = (
Event.select(
fn.strftime(
"%Y-%m-%d %H",
fn.datetime(
Event.start_time, "unixepoch", hour_modifier, minute_modifier
),
).alias("hour"),
fn.COUNT(Event.id).alias("count"),
min_time = time_range_query.get("min_time")
max_time = time_range_query.get("max_time")
days: dict[str, dict] = {}
if min_time is None or max_time is None:
return JSONResponse(content=list(days.values()))
dst_periods = get_dst_transitions(timezone, min_time, max_time)
for period_start, period_end, period_offset in dst_periods:
hours_offset = int(period_offset / 60 / 60)
minutes_offset = int(period_offset / 60 - hours_offset * 60)
period_hour_modifier = f"{hours_offset} hour"
period_minute_modifier = f"{minutes_offset} minute"
recording_groups = (
Recordings.select(
fn.strftime(
"%Y-%m-%d %H",
fn.datetime(
Recordings.start_time,
"unixepoch",
period_hour_modifier,
period_minute_modifier,
),
).alias("hour"),
fn.SUM(Recordings.duration).alias("duration"),
fn.SUM(Recordings.motion).alias("motion"),
fn.SUM(Recordings.objects).alias("objects"),
)
.where(
(Recordings.camera == camera_name)
& (Recordings.end_time >= period_start)
& (Recordings.start_time <= period_end)
)
.group_by((Recordings.start_time + period_offset).cast("int") / 3600)
.order_by(Recordings.start_time.desc())
.namedtuples()
)
.where(Event.camera == camera_name, Event.has_clip)
.group_by((Event.start_time + seconds_offset).cast("int") / 3600)
.namedtuples()
)
event_map = {g.hour: g.count for g in event_groups}
event_groups = (
Event.select(
fn.strftime(
"%Y-%m-%d %H",
fn.datetime(
Event.start_time,
"unixepoch",
period_hour_modifier,
period_minute_modifier,
),
).alias("hour"),
fn.COUNT(Event.id).alias("count"),
)
.where(Event.camera == camera_name, Event.has_clip)
.where(
(Event.start_time >= period_start) & (Event.start_time <= period_end)
)
.group_by((Event.start_time + period_offset).cast("int") / 3600)
.namedtuples()
)
days = {}
event_map = {g.hour: g.count for g in event_groups}
for recording_group in recording_groups:
parts = recording_group.hour.split()
hour = parts[1]
day = parts[0]
events_count = event_map.get(recording_group.hour, 0)
hour_data = {
"hour": hour,
"events": events_count,
"motion": recording_group.motion,
"objects": recording_group.objects,
"duration": round(recording_group.duration),
}
if day not in days:
days[day] = {"events": events_count, "hours": [hour_data], "day": day}
else:
days[day]["events"] += events_count
days[day]["hours"].append(hour_data)
for recording_group in recording_groups:
parts = recording_group.hour.split()
hour = parts[1]
day = parts[0]
events_count = event_map.get(recording_group.hour, 0)
hour_data = {
"hour": hour,
"events": events_count,
"motion": recording_group.motion,
"objects": recording_group.objects,
"duration": round(recording_group.duration),
}
if day in days:
# merge counts if already present (edge-case at DST boundary)
days[day]["events"] += events_count or 0
days[day]["hours"].append(hour_data)
else:
days[day] = {
"events": events_count or 0,
"hours": [hour_data],
"day": day,
}
return JSONResponse(content=list(days.values()))

View File

@ -36,7 +36,7 @@ from frigate.config import FrigateConfig
from frigate.embeddings import EmbeddingsContext
from frigate.models import Recordings, ReviewSegment, UserReviewStatus
from frigate.review.types import SeverityEnum
from frigate.util.builtin import get_tz_modifiers
from frigate.util.time import get_dst_transitions
logger = logging.getLogger(__name__)
@ -197,7 +197,6 @@ async def review_summary(
user_id = current_user["username"]
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(params.timezone)
day_ago = (datetime.datetime.now() - datetime.timedelta(hours=24)).timestamp()
cameras = params.cameras
@ -329,89 +328,135 @@ async def review_summary(
)
clauses.append(reduce(operator.or_, label_clauses))
day_in_seconds = 60 * 60 * 24
last_month_query = (
# Find the time range of available data
time_range_query = (
ReviewSegment.select(
fn.strftime(
"%Y-%m-%d",
fn.datetime(
ReviewSegment.start_time,
"unixepoch",
hour_modifier,
minute_modifier,
),
).alias("day"),
fn.SUM(
Case(
None,
[
(
(ReviewSegment.severity == SeverityEnum.alert)
& (UserReviewStatus.has_been_reviewed == True),
1,
)
],
0,
)
).alias("reviewed_alert"),
fn.SUM(
Case(
None,
[
(
(ReviewSegment.severity == SeverityEnum.detection)
& (UserReviewStatus.has_been_reviewed == True),
1,
)
],
0,
)
).alias("reviewed_detection"),
fn.SUM(
Case(
None,
[
(
(ReviewSegment.severity == SeverityEnum.alert),
1,
)
],
0,
)
).alias("total_alert"),
fn.SUM(
Case(
None,
[
(
(ReviewSegment.severity == SeverityEnum.detection),
1,
)
],
0,
)
).alias("total_detection"),
)
.left_outer_join(
UserReviewStatus,
on=(
(ReviewSegment.id == UserReviewStatus.review_segment)
& (UserReviewStatus.user_id == user_id)
),
fn.MIN(ReviewSegment.start_time).alias("min_time"),
fn.MAX(ReviewSegment.start_time).alias("max_time"),
)
.where(reduce(operator.and_, clauses) if clauses else True)
.group_by(
(ReviewSegment.start_time + seconds_offset).cast("int") / day_in_seconds
)
.order_by(ReviewSegment.start_time.desc())
.dicts()
.get()
)
min_time = time_range_query.get("min_time")
max_time = time_range_query.get("max_time")
data = {
"last24Hours": last_24_query,
}
for e in last_month_query.dicts().iterator():
data[e["day"]] = e
# If no data, return early
if min_time is None or max_time is None:
return JSONResponse(content=data)
# Get DST transition periods
dst_periods = get_dst_transitions(params.timezone, min_time, max_time)
day_in_seconds = 60 * 60 * 24
# Query each DST period separately with the correct offset
for period_start, period_end, period_offset in dst_periods:
# Calculate hour/minute modifiers for this period
hours_offset = int(period_offset / 60 / 60)
minutes_offset = int(period_offset / 60 - hours_offset * 60)
period_hour_modifier = f"{hours_offset} hour"
period_minute_modifier = f"{minutes_offset} minute"
# Build clauses including time range for this period
period_clauses = clauses.copy()
period_clauses.append(
(ReviewSegment.start_time >= period_start)
& (ReviewSegment.start_time <= period_end)
)
period_query = (
ReviewSegment.select(
fn.strftime(
"%Y-%m-%d",
fn.datetime(
ReviewSegment.start_time,
"unixepoch",
period_hour_modifier,
period_minute_modifier,
),
).alias("day"),
fn.SUM(
Case(
None,
[
(
(ReviewSegment.severity == SeverityEnum.alert)
& (UserReviewStatus.has_been_reviewed == True),
1,
)
],
0,
)
).alias("reviewed_alert"),
fn.SUM(
Case(
None,
[
(
(ReviewSegment.severity == SeverityEnum.detection)
& (UserReviewStatus.has_been_reviewed == True),
1,
)
],
0,
)
).alias("reviewed_detection"),
fn.SUM(
Case(
None,
[
(
(ReviewSegment.severity == SeverityEnum.alert),
1,
)
],
0,
)
).alias("total_alert"),
fn.SUM(
Case(
None,
[
(
(ReviewSegment.severity == SeverityEnum.detection),
1,
)
],
0,
)
).alias("total_detection"),
)
.left_outer_join(
UserReviewStatus,
on=(
(ReviewSegment.id == UserReviewStatus.review_segment)
& (UserReviewStatus.user_id == user_id)
),
)
.where(reduce(operator.and_, period_clauses))
.group_by(
(ReviewSegment.start_time + period_offset).cast("int") / day_in_seconds
)
.order_by(ReviewSegment.start_time.desc())
)
# Merge results from this period
for e in period_query.dicts().iterator():
day_key = e["day"]
if day_key in data:
# Merge counts if day already exists (edge case at DST boundary)
data[day_key]["reviewed_alert"] += e["reviewed_alert"] or 0
data[day_key]["reviewed_detection"] += e["reviewed_detection"] or 0
data[day_key]["total_alert"] += e["total_alert"] or 0
data[day_key]["total_detection"] += e["total_detection"] or 0
else:
data[day_key] = e
return JSONResponse(content=data)

View File

@ -14,8 +14,8 @@ from typing import Any, List, Optional, Tuple
import cv2
import numpy as np
from Levenshtein import distance, jaro_winkler
from pyclipper import ET_CLOSEDPOLYGON, JT_ROUND, PyclipperOffset
from rapidfuzz.distance import JaroWinkler, Levenshtein
from shapely.geometry import Polygon
from frigate.comms.event_metadata_updater import (
@ -1123,7 +1123,9 @@ class LicensePlateProcessingMixin:
for i, plate in enumerate(plates):
merged = False
for j, cluster in enumerate(clusters):
sims = [jaro_winkler(plate["plate"], v["plate"]) for v in cluster]
sims = [
JaroWinkler.similarity(plate["plate"], v["plate"]) for v in cluster
]
if len(sims) > 0:
avg_sim = sum(sims) / len(sims)
if avg_sim >= self.cluster_threshold:
@ -1500,7 +1502,7 @@ class LicensePlateProcessingMixin:
and current_time - data["last_seen"]
<= self.config.cameras[camera].lpr.expire_time
):
similarity = jaro_winkler(data["plate"], top_plate)
similarity = JaroWinkler.similarity(data["plate"], top_plate)
if similarity >= self.similarity_threshold:
plate_id = existing_id
logger.debug(
@ -1580,7 +1582,8 @@ class LicensePlateProcessingMixin:
for label, plates_list in self.lpr_config.known_plates.items()
if any(
re.match(f"^{plate}$", rep_plate)
or distance(plate, rep_plate) <= self.lpr_config.match_distance
or Levenshtein.distance(plate, rep_plate)
<= self.lpr_config.match_distance
for plate in plates_list
)
),

View File

@ -166,6 +166,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
camera = obj_data["camera"]
if not self.config.cameras[camera].face_recognition.enabled:
logger.debug(f"Face recognition disabled for camera {camera}, skipping")
return
start = datetime.datetime.now().timestamp()
@ -208,6 +209,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
person_box = obj_data.get("box")
if not person_box:
logger.debug(f"No person box available for {id}")
return
rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2RGB_I420)
@ -233,7 +235,8 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
try:
face_frame = cv2.cvtColor(face_frame, cv2.COLOR_RGB2BGR)
except Exception:
except Exception as e:
logger.debug(f"Failed to convert face frame color for {id}: {e}")
return
else:
# don't run for object without attributes
@ -251,6 +254,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
# no faces detected in this frame
if not face:
logger.debug(f"No face attributes found for {id}")
return
face_box = face.get("box")
@ -274,6 +278,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
res = self.recognizer.classify(face_frame)
if not res:
logger.debug(f"Face recognizer returned no result for {id}")
self.__update_metrics(datetime.datetime.now().timestamp() - start)
return
@ -330,6 +335,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
def handle_request(self, topic, request_data) -> dict[str, Any] | None:
if topic == EmbeddingsRequestEnum.clear_face_classifier.value:
self.recognizer.clear()
return {"success": True, "message": "Face classifier cleared."}
elif topic == EmbeddingsRequestEnum.recognize_face.value:
img = cv2.imdecode(
np.frombuffer(base64.b64decode(request_data["image"]), dtype=np.uint8),

View File

@ -158,11 +158,13 @@ class EmbeddingMaintainer(threading.Thread):
self.realtime_processors: list[RealTimeProcessorApi] = []
if self.config.face_recognition.enabled:
logger.debug("Face recognition enabled, initializing FaceRealTimeProcessor")
self.realtime_processors.append(
FaceRealTimeProcessor(
self.config, self.requestor, self.event_metadata_publisher, metrics
)
)
logger.debug("FaceRealTimeProcessor initialized successfully")
if self.config.classification.bird.enabled:
self.realtime_processors.append(
@ -283,44 +285,65 @@ class EmbeddingMaintainer(threading.Thread):
logger.info("Exiting embeddings maintenance...")
def _check_classification_config_updates(self) -> None:
"""Check for classification config updates and add new processors."""
"""Check for classification config updates and add/remove processors."""
topic, model_config = self.classification_config_subscriber.check_for_update()
if topic and model_config:
if topic:
model_name = topic.split("/")[-1]
self.config.classification.custom[model_name] = model_config
# Check if processor already exists
for processor in self.realtime_processors:
if isinstance(
processor,
(
CustomStateClassificationProcessor,
CustomObjectClassificationProcessor,
),
):
if processor.model_config.name == model_name:
logger.debug(
f"Classification processor for model {model_name} already exists, skipping"
if model_config is None:
self.realtime_processors = [
processor
for processor in self.realtime_processors
if not (
isinstance(
processor,
(
CustomStateClassificationProcessor,
CustomObjectClassificationProcessor,
),
)
return
and processor.model_config.name == model_name
)
]
if model_config.state_config is not None:
processor = CustomStateClassificationProcessor(
self.config, model_config, self.requestor, self.metrics
logger.info(
f"Successfully removed classification processor for model: {model_name}"
)
else:
processor = CustomObjectClassificationProcessor(
self.config,
model_config,
self.event_metadata_publisher,
self.metrics,
)
self.config.classification.custom[model_name] = model_config
self.realtime_processors.append(processor)
logger.info(
f"Added classification processor for model: {model_name} (type: {type(processor).__name__})"
)
# Check if processor already exists
for processor in self.realtime_processors:
if isinstance(
processor,
(
CustomStateClassificationProcessor,
CustomObjectClassificationProcessor,
),
):
if processor.model_config.name == model_name:
logger.debug(
f"Classification processor for model {model_name} already exists, skipping"
)
return
if model_config.state_config is not None:
processor = CustomStateClassificationProcessor(
self.config, model_config, self.requestor, self.metrics
)
else:
processor = CustomObjectClassificationProcessor(
self.config,
model_config,
self.event_metadata_publisher,
self.metrics,
)
self.realtime_processors.append(processor)
logger.info(
f"Added classification processor for model: {model_name} (type: {type(processor).__name__})"
)
def _process_requests(self) -> None:
"""Process embeddings requests"""
@ -374,7 +397,14 @@ class EmbeddingMaintainer(threading.Thread):
source_type, _, camera, frame_name, data = update
logger.debug(
f"Received update - source_type: {source_type}, camera: {camera}, data label: {data.get('label') if data else 'None'}"
)
if not camera or source_type != EventTypeEnum.tracked_object:
logger.debug(
f"Skipping update - camera: {camera}, source_type: {source_type}"
)
return
if self.config.semantic_search.enabled:
@ -384,6 +414,9 @@ class EmbeddingMaintainer(threading.Thread):
# no need to process updated objects if no processors are active
if len(self.realtime_processors) == 0 and len(self.post_processors) == 0:
logger.debug(
f"No processors active - realtime: {len(self.realtime_processors)}, post: {len(self.post_processors)}"
)
return
# Create our own thumbnail based on the bounding box and the frame time
@ -392,6 +425,7 @@ class EmbeddingMaintainer(threading.Thread):
frame_name, camera_config.frame_shape_yuv
)
except FileNotFoundError:
logger.debug(f"Frame {frame_name} not found for camera {camera}")
pass
if yuv_frame is None:
@ -400,7 +434,11 @@ class EmbeddingMaintainer(threading.Thread):
)
return
logger.debug(
f"Processing {len(self.realtime_processors)} realtime processors for object {data.get('id')} (label: {data.get('label')})"
)
for processor in self.realtime_processors:
logger.debug(f"Calling process_frame on {processor.__class__.__name__}")
processor.process_frame(data, yuv_frame)
for processor in self.post_processors:

View File

@ -9,6 +9,7 @@ from multiprocessing import Queue, Value
from multiprocessing.synchronize import Event as MpEvent
import numpy as np
import zmq
from frigate.comms.object_detector_signaler import (
ObjectDetectorPublisher,
@ -377,6 +378,15 @@ class RemoteObjectDetector:
if self.stop_event.is_set():
return detections
# Drain any stale detection results from the ZMQ buffer before making a new request
# This prevents reading detection results from a previous request
# NOTE: This should never happen, but can in some rare cases
while True:
try:
self.detector_subscriber.socket.recv_string(flags=zmq.NOBLOCK)
except zmq.Again:
break
# copy input to shared memory
self.np_shm[:] = tensor_input[:]
self.detection_queue.put(self.name)

View File

@ -14,7 +14,8 @@ from frigate.config import CameraConfig, FrigateConfig, RetainModeEnum
from frigate.const import CACHE_DIR, CLIPS_DIR, MAX_WAL_SIZE, RECORD_DIR
from frigate.models import Previews, Recordings, ReviewSegment, UserReviewStatus
from frigate.record.util import remove_empty_directories, sync_recordings
from frigate.util.builtin import clear_and_unlink, get_tomorrow_at_time
from frigate.util.builtin import clear_and_unlink
from frigate.util.time import get_tomorrow_at_time
logger = logging.getLogger(__name__)

View File

@ -28,7 +28,7 @@ from frigate.ffmpeg_presets import (
parse_preset_hardware_acceleration_encode,
)
from frigate.models import Export, Previews, Recordings
from frigate.util.builtin import is_current_hour
from frigate.util.time import is_current_hour
logger = logging.getLogger(__name__)

View File

@ -15,12 +15,9 @@ from collections.abc import Mapping
from multiprocessing.sharedctypes import Synchronized
from pathlib import Path
from typing import Any, Dict, Optional, Tuple, Union
from zoneinfo import ZoneInfoNotFoundError
import numpy as np
import pytz
from ruamel.yaml import YAML
from tzlocal import get_localzone
from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS
@ -157,17 +154,6 @@ def load_labels(path: Optional[str], encoding="utf-8", prefill=91):
return labels
def get_tz_modifiers(tz_name: str) -> Tuple[str, str, float]:
seconds_offset = (
datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds()
)
hours_offset = int(seconds_offset / 60 / 60)
minutes_offset = int(seconds_offset / 60 - hours_offset * 60)
hour_modifier = f"{hours_offset} hour"
minute_modifier = f"{minutes_offset} minute"
return hour_modifier, minute_modifier, seconds_offset
def to_relative_box(
width: int, height: int, box: Tuple[int, int, int, int]
) -> Tuple[int | float, int | float, int | float, int | float]:
@ -298,34 +284,6 @@ def find_by_key(dictionary, target_key):
return None
def get_tomorrow_at_time(hour: int) -> datetime.datetime:
"""Returns the datetime of the following day at 2am."""
try:
tomorrow = datetime.datetime.now(get_localzone()) + datetime.timedelta(days=1)
except ZoneInfoNotFoundError:
tomorrow = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
days=1
)
logger.warning(
"Using utc for maintenance due to missing or incorrect timezone set"
)
return tomorrow.replace(hour=hour, minute=0, second=0).astimezone(
datetime.timezone.utc
)
def is_current_hour(timestamp: int) -> bool:
"""Returns if timestamp is in the current UTC hour."""
start_of_next_hour = (
datetime.datetime.now(datetime.timezone.utc).replace(
minute=0, second=0, microsecond=0
)
+ datetime.timedelta(hours=1)
).timestamp()
return timestamp < start_of_next_hour
def clear_and_unlink(file: Path, missing_ok: bool = True) -> None:
"""clear file then unlink to avoid space retained by file descriptors."""
if not missing_ok and not file.exists():

100
frigate/util/time.py Normal file
View File

@ -0,0 +1,100 @@
"""Time utilities."""
import datetime
import logging
from typing import Tuple
from zoneinfo import ZoneInfoNotFoundError
import pytz
from tzlocal import get_localzone
logger = logging.getLogger(__name__)
def get_tz_modifiers(tz_name: str) -> Tuple[str, str, float]:
seconds_offset = (
datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds()
)
hours_offset = int(seconds_offset / 60 / 60)
minutes_offset = int(seconds_offset / 60 - hours_offset * 60)
hour_modifier = f"{hours_offset} hour"
minute_modifier = f"{minutes_offset} minute"
return hour_modifier, minute_modifier, seconds_offset
def get_tomorrow_at_time(hour: int) -> datetime.datetime:
"""Returns the datetime of the following day at 2am."""
try:
tomorrow = datetime.datetime.now(get_localzone()) + datetime.timedelta(days=1)
except ZoneInfoNotFoundError:
tomorrow = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
days=1
)
logger.warning(
"Using utc for maintenance due to missing or incorrect timezone set"
)
return tomorrow.replace(hour=hour, minute=0, second=0).astimezone(
datetime.timezone.utc
)
def is_current_hour(timestamp: int) -> bool:
"""Returns if timestamp is in the current UTC hour."""
start_of_next_hour = (
datetime.datetime.now(datetime.timezone.utc).replace(
minute=0, second=0, microsecond=0
)
+ datetime.timedelta(hours=1)
).timestamp()
return timestamp < start_of_next_hour
def get_dst_transitions(
tz_name: str, start_time: float, end_time: float
) -> list[tuple[float, float]]:
"""
Find DST transition points and return time periods with consistent offsets.
Args:
tz_name: Timezone name (e.g., 'America/New_York')
start_time: Start timestamp (UTC)
end_time: End timestamp (UTC)
Returns:
List of (period_start, period_end, seconds_offset) tuples representing
continuous periods with the same UTC offset
"""
try:
tz = pytz.timezone(tz_name)
except pytz.UnknownTimeZoneError:
# If timezone is invalid, return single period with no offset
return [(start_time, end_time, 0)]
periods = []
current = start_time
# Get initial offset
dt = datetime.datetime.utcfromtimestamp(current).replace(tzinfo=pytz.UTC)
local_dt = dt.astimezone(tz)
prev_offset = local_dt.utcoffset().total_seconds()
period_start = start_time
# Check each day for offset changes
while current <= end_time:
dt = datetime.datetime.utcfromtimestamp(current).replace(tzinfo=pytz.UTC)
local_dt = dt.astimezone(tz)
current_offset = local_dt.utcoffset().total_seconds()
if current_offset != prev_offset:
# Found a transition - close previous period
periods.append((period_start, current, prev_offset))
period_start = current
prev_offset = current_offset
current += 86400 # Check daily
# Add final period
periods.append((period_start, end_time, prev_offset))
return periods

View File

@ -34,7 +34,7 @@ from frigate.ptz.autotrack import ptz_moving_at_frame_time
from frigate.track import ObjectTracker
from frigate.track.norfair_tracker import NorfairTracker
from frigate.track.tracked_object import TrackedObjectAttribute
from frigate.util.builtin import EventsPerSecond, get_tomorrow_at_time
from frigate.util.builtin import EventsPerSecond
from frigate.util.image import (
FrameManager,
SharedMemoryFrameManager,
@ -53,6 +53,7 @@ from frigate.util.object import (
reduce_detections,
)
from frigate.util.process import FrigateProcess
from frigate.util.time import get_tomorrow_at_time
logger = logging.getLogger(__name__)

View File

@ -218,7 +218,10 @@
}
},
"label": {
"back": "Torna enrere"
"back": "Torna enrere",
"hide": "Oculta {{item}}",
"show": "Mostra {{item}}",
"ID": "ID"
},
"button": {
"apply": "Aplicar",
@ -281,5 +284,14 @@
"readTheDocumentation": "Llegir la documentació",
"information": {
"pixels": "{{area}}px"
},
"list": {
"two": "{{0}} i {{1}}",
"many": "{{items}}, i {{last}}",
"separatorWithSpace": ",· "
},
"field": {
"optional": "Opcional",
"internalID": "L'ID intern que Frigate s'utilitza a la configuració i a la base de dades"
}
}

View File

@ -117,6 +117,7 @@
"search": {
"placeholder": "Cerca per etiqueta o subetiqueta..."
},
"noImages": "No s'han trobat miniatures per a aquesta càmera"
"noImages": "No s'han trobat miniatures per a aquesta càmera",
"unknownLabel": "Imatge activadora desada"
}
}

View File

@ -5,7 +5,9 @@
"renameCategory": "Reanomena la classe",
"deleteCategory": "Suprimeix la classe",
"deleteImages": "Suprimeix les imatges",
"trainModel": "Model de tren"
"trainModel": "Model de tren",
"addClassification": "Afegeix una classificació",
"deleteModels": "Suprimeix els models"
},
"toast": {
"success": {
@ -13,13 +15,15 @@
"deletedImage": "Imatges suprimides",
"categorizedImage": "Imatge classificada amb èxit",
"trainedModel": "Model entrenat amb èxit.",
"trainingModel": "S'ha iniciat amb èxit la formació de models."
"trainingModel": "S'ha iniciat amb èxit la formació de models.",
"deletedModel": "S'han suprimit correctament {{count}} models"
},
"error": {
"deleteImageFailed": "No s'ha pogut suprimir: {{errorMessage}}",
"deleteCategoryFailed": "No s'ha pogut suprimir la classe: {{errorMessage}}",
"categorizeFailed": "No s'ha pogut categoritzar la imatge: {{errorMessage}}",
"trainingFailed": "No s'ha pogut iniciar l'entrenament del model: {{errorMessage}}"
"trainingFailed": "No s'ha pogut iniciar l'entrenament del model: {{errorMessage}}",
"deleteModelFailed": "No s'ha pogut suprimir el model: {{errorMessage}}"
}
},
"deleteCategory": {
@ -43,7 +47,8 @@
},
"train": {
"title": "Classificacions recents",
"aria": "Selecciona les classificacions recents"
"aria": "Selecciona les classificacions recents",
"titleShort": "Recent"
},
"categories": "Classes",
"createCategory": {
@ -98,7 +103,8 @@
"stateRequiresTwoClasses": "Els models d'estat requereixen almenys 2 classes",
"objectLabelRequired": "Seleccioneu una etiqueta d'objecte",
"objectTypeRequired": "Seleccioneu un tipus de classificació"
}
},
"states": "Estats"
},
"step2": {
"description": "Seleccioneu les càmeres i definiu l'àrea a monitoritzar per a cada càmera. El model classificarà l'estat d'aquestes àrees.",
@ -131,5 +137,14 @@
},
"generateSuccess": "Imatges de mostra generades amb èxit"
}
},
"deleteModel": {
"title": "Suprimeix el model de classificació",
"single": "Esteu segur que voleu suprimir {{name}}? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer.",
"desc": "Esteu segur que voleu suprimir {{count}} model(s)? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer."
},
"menu": {
"objects": "Objectes",
"states": "Estats"
}
}

View File

@ -43,10 +43,17 @@
"aria": "Canvia la vista de detall",
"trackedObject_other": "objectes",
"noObjectDetailData": "No hi ha dades de detall d'objecte disponibles.",
"label": "Detall"
"label": "Detall",
"settings": "Configuració de la vista detallada",
"alwaysExpandActive": {
"title": "Expandeix sempre actiu",
"desc": "Expandeix sempre els detalls de l'objecte de la revisió activa quan estigui disponible."
}
},
"objectTrack": {
"clickToSeek": "Feu clic per cercar aquesta hora",
"trackedPoint": "Punt de seguiment"
}
},
"zoomIn": "Amplia",
"zoomOut": "Redueix"
}

View File

@ -84,7 +84,8 @@
"details": "detalls",
"snapshot": "instantània",
"video": "vídeo",
"object_lifecycle": "cicle de vida de l'objecte"
"object_lifecycle": "cicle de vida de l'objecte",
"thumbnail": "miniatura"
},
"details": {
"timestamp": "Marca temporal",
@ -240,7 +241,7 @@
"noImageFound": "No s'ha trobat cap imatge amb aquesta hora.",
"createObjectMask": "Crear màscara d'objecte",
"adjustAnnotationSettings": "Ajustar configuració d'anotacions",
"scrollViewTips": "Desplaça per veure els moments significants del cicle de vida d'aquest objecte.",
"scrollViewTips": "Feu clic per veure els moments significatius del cicle de vida d'aquest objecte.",
"autoTrackingTips": "Limitar les posicións de la caixa serà inacurat per càmeras de seguiment automàtic.",
"count": "{{first}} de {{second}}",
"trackedPoint": "Punt Seguit",
@ -270,7 +271,7 @@
},
"offset": {
"label": "Òfset d'Anotació",
"desc": "Aquestes dades provenen del flux de detecció de la càmera, però se superposen a les imatges del flux de gravació. És poc probable que els dos fluxos estiguin perfectament sincronitzats. Com a resultat, el quadre delimitador i el metratge no s'alinearan perfectament. Tanmateix, es pot utilitzar el camp <code>annotation_offset</code> per ajustar-ho.",
"desc": "Aquestes dades provenen del flux de detecció de la càmera, però se superposen a les imatges del flux de gravació. És poc probable que els dos fluxos estiguin perfectament sincronitzats. Com a resultat, el quadre delimitador i les imatges no s'alinearan perfectament. Tanmateix, es pot utilitzar el camp <code>annotation_offset</code> per ajustar-ho.",
"millisecondsToOffset": "Millisegons per l'òfset de detecció d'anotacions per. <em>Per defecte: 0</em>",
"tips": "CONSELL: Imagineu-vos que hi ha un clip d'esdeveniment amb una persona caminant d'esquerra a dreta. Si el quadre delimitador de la cronologia de l'esdeveniment està constantment a l'esquerra de la persona, aleshores s'hauria de disminuir el valor. De la mateixa manera, si una persona camina d'esquerra a dreta i el quadre delimitador està constantment per davant de la persona, aleshores s'hauria d'augmentar el valor.",
"toast": {

View File

@ -13,5 +13,11 @@
"error": {
"renameExportFailed": "Error al canviar el nom de lexportació: {{errorMessage}}"
}
},
"tooltip": {
"shareExport": "Comparteix l'exportació",
"downloadVideo": "Baixa el vídeo",
"editName": "Edita el nom",
"deleteExport": "Suprimeix l'exportació"
}
}

View File

@ -55,12 +55,12 @@
"searchFor": "Buscar {{inputValue}}",
"button": {
"clear": "Netejar cerca",
"save": "Desar la cerca",
"delete": "Suprimeix la recerca desada",
"filterInformation": "Informació de filtre",
"save": "Desa la cerca",
"delete": "Elimina la recerca desada",
"filterInformation": "Informació del filtre",
"filterActive": "Filtres actius"
},
"trackedObjectId": "ID d'objecte rastrejat",
"trackedObjectId": "ID de l'objecte rastrejat",
"placeholder": {
"search": "Cercar…"
},

View File

@ -49,6 +49,10 @@
"playAlertVideos": {
"label": "Reproduir vídeos dalerta",
"desc": "Per defecte, les alertes recents al tauler en directe es reprodueixen com a vídeos petits en bucle. Desactiva aquesta opció per mostrar només una imatge estàtica de les alertes recents en aquest dispositiu/navegador."
},
"displayCameraNames": {
"label": "Mostra sempre els noms de la càmera",
"desc": "Mostra sempre els noms de les càmeres en un xip al tauler de visualització en directe multicàmera."
}
},
"storedLayouts": {
@ -695,7 +699,9 @@
},
"actions": {
"alert": "Marcar com Alerta",
"notification": "Enviar Notificació"
"notification": "Enviar Notificació",
"sub_label": "Afegeix una subetiqueta",
"attribute": "Afegeix un atribut"
},
"dialog": {
"createTrigger": {
@ -713,25 +719,28 @@
"form": {
"name": {
"title": "Nom",
"placeholder": "Entrar el nom del disparador",
"placeholder": "Anomena aquest activador",
"error": {
"minLength": "El nom ha de tenir almenys 2 caràcters de llargada.",
"invalidCharacters": "El nom només pot contenir lletres, números, guions i guinons baixos.",
"minLength": "El camp ha de tenir almenys 2 caràcters.",
"invalidCharacters": "El camp només pot contenir lletres, números, guions baixos i guions.",
"alreadyExists": "El disparador amb aquest nom ja existeix per aquesta càmera."
}
},
"description": "Introduïu un nom o una descripció únics per a identificar aquest activador"
},
"enabled": {
"description": "Activar o desactivar aquest disparador"
},
"type": {
"title": "Tipus",
"placeholder": "Selecciona un tipus de disparador"
"placeholder": "Selecciona un tipus de disparador",
"description": "Activa quan es detecta una descripció similar d'un objecte rastrejat",
"thumbnail": "Activa quan es detecti una miniatura d'objecte rastrejada similar"
},
"content": {
"title": "Contingut",
"imagePlaceholder": "Selecciona una imatge",
"imagePlaceholder": "Selecciona una miniatura",
"textPlaceholder": "Entra el contingut de text",
"imageDesc": "Selecciona una imatge per disparar aquesta acció quan una imatge similar sigui detectada.",
"imageDesc": "Només es mostren les 100 miniatures més recents. Si no podeu trobar la miniatura desitjada, reviseu els objectes anteriors a Explora i configureu un activador des del menú.",
"textDesc": "Entra el text per disparar aquesta acció quan es detecti una descripció d'objecte a rastrejar similar.",
"error": {
"required": "Contigunt requerit."
@ -742,11 +751,12 @@
"error": {
"min": "El llindar ha de ser mínim 0",
"max": "El llindar ha de ser máxim 1"
}
},
"desc": "Estableix el llindar de similitud per a aquest activador. Un llindar més alt significa que es requereix una coincidència més propera per disparar el disparador."
},
"actions": {
"title": "Accions",
"desc": "Per defecte, Frigate dispara un missatge MQTT per tots els disparadors. Tria una acció adicional per realitzar quan aquest disparador dispari.",
"desc": "Per defecte, Frigate dispara un missatge MQTT per a tots els activadors. Subetiquetes afegeix el nom de l'activador a l'etiqueta de l'objecte. Els atributs són metadades cercables emmagatzemades per separat a les metadades de l'objecte rastrejat.",
"error": {
"min": "S'ha de seleccionar una acció com a mínim."
}
@ -772,13 +782,30 @@
},
"documentTitle": "Disparadors",
"management": {
"title": "Gestió de disparadors",
"title": "Activadors",
"desc": "Gestionar els disparadors de {{camera}}. Usa les tipus de miniatures per disparar miniatures similars a l'objecte a seguir seleccionat, i el tipus de descripció per disparar en cas de descripcions similars a l'especificada."
},
"addTrigger": "Afegir disaprador",
"semanticSearch": {
"desc": "La cerca semàntica ha d'estar activada per a utilitzar els activadors.",
"title": "La cerca semàntica està desactivada"
},
"wizard": {
"title": "Crea un activador",
"step1": {
"description": "Configura la configuració bàsica per al vostre activador."
},
"step2": {
"description": "Configura el contingut que activarà aquesta acció."
},
"step3": {
"description": "Configura el llindar i les accions d'aquest activador."
},
"steps": {
"nameAndType": "Nom i tipus",
"configureData": "Configura les dades",
"thresholdAndActions": "Llindar i accions"
}
}
},
"roles": {
@ -985,7 +1012,9 @@
"estimatedBandwidth": "Amplada de banda estimad",
"roles": "Rols",
"streamValidated": "El flux {{number}} s'ha validat correctament",
"streamValidationFailed": "Ha fallat la validació del flux {{number}}"
"streamValidationFailed": "Ha fallat la validació del flux {{number}}",
"ffmpegModule": "Usa el mode de compatibilitat del flux",
"ffmpegModuleDescription": "Si el flux no es carrega després de diversos intents, proveu d'activar-ho. Quan està activat, Frigate utilitzarà el mòdul ffmpeg amb go2rtc. Això pot proporcionar una millor compatibilitat amb alguns fluxos de càmera."
}
},
"cameraManagement": {

View File

@ -10,6 +10,7 @@
"unknownError": "Neznámá chyba. Zkontrolujte logy.",
"webUnknownError": "Neznámá chuba. Zkontrolujte logy konzoly.",
"rateLimit": "Limit požadavků překročen. Zkuste to znovu později."
}
},
"firstTimeLogin": "Přihlašujete se poprvé? Přihlašovací údaje jsou vypsány v logu Frigate."
}
}

View File

@ -1 +1,7 @@
{}
{
"documentTitle": "Klasifikační modely",
"button": {
"deleteClassificationAttempts": "Odstranit Klasifikační obrazy",
"renameCategory": "Přejmenovat třídu"
}
}

View File

@ -41,7 +41,7 @@
"aria": "Vybrat trénink"
},
"description": {
"addFace": "Prúvodce přidání nové kolekce do Knižnice obličejů.",
"addFace": "Přidejte novou kolekci do Knihovny obličejů nahráním prvního obrázku.",
"placeholder": "Zadejte název pro tuto kolekci",
"invalidName": "Neplatný název. Názvy mohou obsahovat pouze písmena, čísla, mezery, apostrofy, podtržítka a pomlčky."
},

View File

@ -11,7 +11,8 @@
"general": "Obecné nastavení - Frigate",
"frigatePlus": "Frigate+ nastavení - Frigate",
"enrichments": "Nastavení obohacení - Frigate",
"cameraManagement": "Správa kamer - Frigate"
"cameraManagement": "Správa kamer - Frigate",
"cameraReview": "Nastavení kontroly kamery - Frigate"
},
"frigatePlus": {
"toast": {

View File

@ -100,7 +100,8 @@
},
"list": {
"two": "{{0}} and {{1}}",
"many": "{{items}}, and {{last}}"
"many": "{{items}}, and {{last}}",
"separatorWithSpace": ", "
},
"field": {
"optional": "Optional",

View File

@ -271,6 +271,8 @@
"disconnectStream": "Disconnect",
"estimatedBandwidth": "Estimated Bandwidth",
"roles": "Roles",
"ffmpegModule": "Use stream compatibility mode",
"ffmpegModuleDescription": "If the stream does not load after several attempts, try enabling this. When enabled, Frigate will use the ffmpeg module with go2rtc. This may provide better compatibility with some camera streams.",
"none": "None",
"error": "Error",
"streamValidated": "Stream {{number}} validated successfully",

View File

@ -301,6 +301,7 @@
},
"list": {
"two": "{{0}} et {{1}}",
"many": "{{items}}, et {{last}}"
"many": "{{items}}, et {{last}}",
"separatorWithSpace": ", "
}
}

View File

@ -5,7 +5,9 @@
"renameCategory": "Renommer la classe",
"deleteCategory": "Supprimer la classe",
"deleteImages": "Supprimer les images",
"trainModel": "Entraîner le modèle"
"trainModel": "Entraîner le modèle",
"addClassification": "Ajouter une classification",
"deleteModels": "Supprimer les modèles"
},
"toast": {
"success": {
@ -13,13 +15,15 @@
"deletedImage": "Images supprimées",
"categorizedImage": "Image classifiée avec succès",
"trainedModel": "Modèle entraîné avec succès.",
"trainingModel": "L'entraînement du modèle a démarré avec succès."
"trainingModel": "L'entraînement du modèle a démarré avec succès.",
"deletedModel": "{{count}} modèle(s) supprimé(s) avec succès"
},
"error": {
"deleteImageFailed": "Échec de la suppression : {{errorMessage}}",
"deleteCategoryFailed": "Échec de la suppression de la classe : {{errorMessage}}",
"categorizeFailed": "Échec de la catégorisation de l'image : {{errorMessage}}",
"trainingFailed": "Échec du démarrage de l'entraînement du modèle : {{errorMessage}}"
"trainingFailed": "Échec du démarrage de l'entraînement du modèle : {{errorMessage}}",
"deleteModelFailed": "Impossible de supprimer le modèle : {{errorMessage}}"
}
},
"deleteCategory": {
@ -133,5 +137,14 @@
},
"generateSuccess": "Génération des images d'exemple réussie"
}
},
"deleteModel": {
"title": "Supprimer le modèle de classification",
"single": "Voulez-vous vraiment supprimer {{name}}? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible.",
"desc": "Voulez-vous vraiment supprimer {{count}} modèle(s)? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible."
},
"menu": {
"objects": "Objets",
"states": "États"
}
}

View File

@ -109,7 +109,8 @@
"details": "détails",
"video": "vidéo",
"object_lifecycle": "cycle de vie de l'objet",
"snapshot": "instantané"
"snapshot": "instantané",
"thumbnail": "Miniature"
},
"objectLifecycle": {
"title": "Cycle de vie de l'objet",
@ -240,7 +241,7 @@
"noImageFound": "Aucune image trouvée pour cet horodatage",
"createObjectMask": "Créer un masque d'objet",
"adjustAnnotationSettings": "Ajuster les paramètres d'annotation",
"scrollViewTips": "Défilez pour voir les moments significatifs du cycle de vie de cet objet.",
"scrollViewTips": "Cliquez pour voir les moments significatifs du cycle de vie de cet objet.",
"autoTrackingTips": "Les positions des cadres de détection seront imprécises pour les caméras à suivi automatique.",
"count": "{{first}} sur {{second}}",
"trackedPoint": "Point suivi",

View File

@ -1078,7 +1078,9 @@
"resolutionHigh": "La résolution {{resolution}} risque d'augmenter l'utilisation des ressources.",
"resolutionLow": "La résolution {{resolution}} risque d'être trop faible pour détecter les petits objets de manière fiable."
},
"valid": "Valide"
"valid": "Valide",
"ffmpegModule": "Utiliser le mode de compatibilité du flux",
"ffmpegModuleDescription": "Si le flux ne se charge pas après plusieurs tentatives, essayez d'activer cette option. Lorsqu'elle est activée, Frigate utilisera le module ffmpeg avec go2rtc. Cela peut offrir une meilleure compatibilité avec certains flux de caméra."
}
},
"cameraManagement": {

View File

@ -10,6 +10,7 @@
"unknownError": "Ismeretlen hiba. Ellenőrizze a naplókat.",
"webUnknownError": "Ismeretlen hiba. Ellenőrizze a konzol naplókat.",
"rateLimit": "Túl sokszor próbálkozott. Próbálja meg később."
}
},
"firstTimeLogin": "Először próbálsz bejelentkezni? A hitelesítési adatok a Frigate naplóiban vannak feltüntetve."
}
}

View File

@ -1 +1,16 @@
{}
{
"documentTitle": "Osztályozási modellek",
"button": {
"deleteClassificationAttempts": "Osztályozási képek törlése",
"deleteImages": "Képek törlése",
"trainModel": "Modell betanítása",
"deleteModels": "Modellek törlése"
},
"toast": {
"success": {
"deletedImage": "Törölt képek",
"deletedModel": "Sikeresen törölt {{count}} modellt",
"categorizedImage": "A kép sikeresen osztályozva"
}
}
}

View File

@ -36,5 +36,6 @@
"selected_one": "{{count}} kiválasztva",
"selected_other": "{{count}} kiválasztva",
"suspiciousActivity": "Gyanús Tevékenység",
"threateningActivity": "Fenyegető Tevékenység"
"threateningActivity": "Fenyegető Tevékenység",
"zoomIn": "Nagyítás"
}

View File

@ -13,5 +13,11 @@
"error": {
"renameExportFailed": "Sikertelen export átnevezés: {{errorMessage}}"
}
},
"tooltip": {
"downloadVideo": "Videó letöltése",
"editName": "Név szerkesztése",
"deleteExport": "Export törlése",
"shareExport": "Export megosztása"
}
}

View File

@ -42,12 +42,12 @@
"title": "Gyűjtemény létrehozása",
"desc": "Új gyűjtemény létrehozása",
"new": "Új arc létrhozása",
"nextSteps": "A jó alap készítéséhez:<li>Használja a Tanítás fület az egyes észlelt személyekhez tartozó képek kiválasztására és betanítására.</li>A legjobb eredmény érdekében válassza az egyenesen előre néző arcokat ábrázoló képeket és kerülje a ferde szögből készült arcképeket a tanításhoz.</li></ul>"
"nextSteps": "A jó alap készítéséhez:<li>Használja a Legutóbbi felismerések fület az egyes észlelt személyekhez tartozó képek kiválasztásához és betanításához.</li>A legjobb eredmény érdekében válassza az egyenesen előre néző arcokat ábrázoló képeket és kerülje a ferde szögből készült arcképeket a tanításhoz.</li></ul>"
},
"description": {
"placeholder": "Adj nevet ennek a gyűjteménynek",
"invalidName": "Nem megfelelő név. A nevek csak betűket, számokat, szóközöket, aposztrófokat, alulhúzásokat és kötőjeleket tartalmazhatnak.",
"addFace": "Segédlet új gyűjtemény hozzáadásához az arckép könyvtárban."
"addFace": "Adj hozzá egy új gyűjteményt az Arcképtárhoz az első képed feltöltésével."
},
"selectFace": "Arc kiválasztása",
"deleteFaceLibrary": {

View File

@ -297,7 +297,8 @@
},
"list": {
"two": "{{0}} e {{1}}",
"many": "{{items}}, e {{last}}"
"many": "{{items}}, e {{last}}",
"separatorWithSpace": ", "
},
"field": {
"optional": "Opzionale",

View File

@ -63,7 +63,7 @@
"label": "Cerca la fonte",
"desc": "Scegli se cercare nelle miniature o nelle descrizioni degli oggetti tracciati.",
"options": {
"thumbnailImage": "Immagine anteprima",
"thumbnailImage": "Immagine in miniatura",
"description": "Descrizione"
}
}

View File

@ -5,7 +5,9 @@
"renameCategory": "Rinomina classe",
"deleteCategory": "Elimina classe",
"deleteImages": "Elimina immagini",
"trainModel": "Modello di addestramento"
"trainModel": "Modello di addestramento",
"addClassification": "Aggiungi classificazione",
"deleteModels": "Elimina modelli"
},
"toast": {
"success": {
@ -13,13 +15,15 @@
"deletedImage": "Immagini eliminate",
"categorizedImage": "Immagine classificata con successo",
"trainedModel": "Modello addestrato con successo.",
"trainingModel": "Avviato con successo l'addestramento del modello."
"trainingModel": "Avviato con successo l'addestramento del modello.",
"deletedModel": "Eliminati con successo {{count}} modelli"
},
"error": {
"deleteImageFailed": "Impossibile eliminare: {{errorMessage}}",
"deleteCategoryFailed": "Impossibile eliminare la classe: {{errorMessage}}",
"categorizeFailed": "Impossibile categorizzare l'immagine: {{errorMessage}}",
"trainingFailed": "Impossibile avviare l'addestramento del modello: {{errorMessage}}"
"trainingFailed": "Impossibile avviare l'addestramento del modello: {{errorMessage}}",
"deleteModelFailed": "Impossibile eliminare il modello: {{errorMessage}}"
}
},
"deleteCategory": {
@ -133,5 +137,14 @@
},
"generateSuccess": "Immagini campione generate correttamente"
}
},
"deleteModel": {
"title": "Elimina modello di classificazione",
"single": "Vuoi davvero eliminare {{name}}? Questa operazione eliminerà definitivamente tutti i dati associati, comprese le immagini e i dati di allenamento. Questa azione non può essere annullata.",
"desc": "Vuoi davvero eliminare {{count}} modello/i? Questa operazione eliminerà definitivamente tutti i dati associati, comprese le immagini e i dati di addestramento. Questa azione non può essere annullata."
},
"menu": {
"objects": "Oggetti",
"states": "Stati"
}
}

View File

@ -158,7 +158,8 @@
"snapshot": "istantanea",
"object_lifecycle": "ciclo di vita dell'oggetto",
"details": "dettagli",
"video": "video"
"video": "video",
"thumbnail": "miniatura"
},
"itemMenu": {
"downloadSnapshot": {
@ -240,7 +241,7 @@
"noImageFound": "Nessuna immagine trovata per questo orario.",
"createObjectMask": "Crea maschera oggetto",
"adjustAnnotationSettings": "Regola le impostazioni di annotazione",
"scrollViewTips": "Scorri per visualizzare i momenti più significativi del ciclo di vita di questo oggetto.",
"scrollViewTips": "Clicca per visualizzare i momenti più significativi del ciclo di vita di questo oggetto.",
"autoTrackingTips": "Le posizioni dei riquadri di delimitazione saranno imprecise per le telecamere con tracciamento automatico.",
"count": "{{first}} di {{second}}",
"trackedPoint": "Punto tracciato",

View File

@ -294,6 +294,7 @@
},
"list": {
"two": "{{0}} og {{1}}",
"many": "{{items}}, og {{last}}"
"many": "{{items}}, og {{last}}",
"separatorWithSpace": ", "
}
}

View File

@ -214,7 +214,8 @@
"details": "detaljer",
"snapshot": "øyeblikksbilde",
"video": "video",
"object_lifecycle": "objektets livssyklus"
"object_lifecycle": "objektets livssyklus",
"thumbnail": "miniatyrbilde"
},
"dialog": {
"confirmDelete": {
@ -238,7 +239,7 @@
"noImageFound": "Ingen bilder funnet for dette tidsstempelet.",
"createObjectMask": "Opprett objektmaske",
"adjustAnnotationSettings": "Juster annoteringsinnstillinger",
"scrollViewTips": "Rull for å se de viktige øyeblikkene i dette objektets livssyklus.",
"scrollViewTips": "Klikk for å se de viktige øyeblikkene i dette objektets livssyklus.",
"autoTrackingTips": "Posisjonene til avgrensningsboksene vil være unøyaktige for kameraer med automatisk sporing.",
"count": "{{first}} av {{second}}",
"trackedPoint": "Sporet punkt",
@ -272,7 +273,7 @@
"millisecondsToOffset": "Antall millisekunder deteksjonsannoteringene skal forskyves med. <em>Standard: 0</em>",
"tips": "TIPS: Se for deg et hendelsesklipp med en person som går fra venstre mot høyre. Hvis avgrensningsboksen på tidslinjen for hendelsen konsekvent er til venstre for personen, bør verdien reduseres. På samme måte, hvis en person går fra venstre mot høyre og avgrensningsboksen konsekvent er foran personen, bør verdien økes.",
"toast": {
"success": "Annoteringsforskyvning for {{camera}} er lagret i konfigurasjonsfilen. Start Frigate på nytt for å ta i bruk endringene."
"success": "Annoteringsforskyvning for {{camera}} er lagret i konfigurasjonsfilen. Start Frigate på nytt for å aktivere endringene."
}
}
},

View File

@ -341,7 +341,7 @@
}
},
"toast": {
"success": "Sone ({{zoneName}}) er lagret. Start Frigate på nytt for å bruke endringer."
"success": "Sone ({{zoneName}}) er lagret. Start Frigate på nytt for å aktivere endringer."
}
},
"motionMasks": {
@ -1151,7 +1151,7 @@
"selectDetectionsZones": "Velg soner for deteksjoner",
"limitDetections": "Avgrens deteksjoner til bestemte soner",
"toast": {
"success": "Konfigurasjonen for inspeksjonsklassifisering er lagret. Start Frigate på nytt for å bruke endringer."
"success": "Konfigurasjonen for inspeksjonsklassifisering er lagret. Start Frigate på nytt for å aktivere endringer."
}
}
}

View File

@ -5,7 +5,9 @@
"renameCategory": "Gi nytt navn til kategori",
"deleteCategory": "Slett kategori",
"deleteImages": "Slett bilder",
"trainModel": "Tren modell"
"trainModel": "Tren modell",
"addClassification": "Legg til klassifisering",
"deleteModels": "Slett modeller"
},
"toast": {
"success": {
@ -13,13 +15,15 @@
"deletedImage": "Bilder slettet",
"categorizedImage": "Bildet ble klassifisert",
"trainedModel": "Modellen ble trent.",
"trainingModel": "Modelltrening startet."
"trainingModel": "Modelltrening startet.",
"deletedModel": "{{count}} modell(er) ble slettet"
},
"error": {
"deleteImageFailed": "Kunne ikke slette: {{errorMessage}}",
"deleteCategoryFailed": "Kunne ikke slette kategori: {{errorMessage}}",
"categorizeFailed": "Kunne ikke klassifisere bilde: {{errorMessage}}",
"trainingFailed": "Kunne ikke starte modelltrening: {{errorMessage}}"
"trainingFailed": "Kunne ikke starte modelltrening: {{errorMessage}}",
"deleteModelFailed": "Kunne ikke slette modell: {{errorMessage}}"
}
},
"deleteCategory": {
@ -99,7 +103,8 @@
"stateRequiresTwoClasses": "Tilstandsmodeller krever minst to kategorier",
"objectLabelRequired": "Velg en objektetikett",
"objectTypeRequired": "Velg en klassifiseringstype"
}
},
"states": "Tilstander"
},
"step2": {
"description": "Velg kameraer og definer området som skal overvåkes for hvert kamera. Modellen vil klassifisere tilstanden til disse områdene.",
@ -132,5 +137,14 @@
},
"generateSuccess": "Eksempelbilder ble generert"
}
},
"deleteModel": {
"title": "Slett klassifiseringsmodell",
"single": "Er du sikker på at du vil slette {{name}}? Dette vil permanent slette alle tilknyttede data, inkludert bilder og treningsdata. Denne handlingen kan ikke angres.",
"desc": "Er du sikker på at du vil slette {{count}} modell(er)? Dette vil permanent slette alle tilknyttede data, inkludert bilder og treningsdata. Denne handlingen kan ikke angres."
},
"menu": {
"objects": "Objekter",
"states": "Tilstander"
}
}

View File

@ -290,7 +290,8 @@
},
"list": {
"two": "{{0}} en {{1}}",
"many": "{{items}}, en {{last}}"
"many": "{{items}}, en {{last}}",
"separatorWithSpace": ", "
},
"field": {
"optional": "Optioneel",

View File

@ -5,7 +5,9 @@
"renameCategory": "Klasse hernoemen",
"deleteCategory": "Klasse verwijderen",
"deleteImages": "Afbeeldingen verwijderen",
"trainModel": "Model trainen"
"trainModel": "Model trainen",
"addClassification": "Classificatie toevoegen",
"deleteModels": "Modellen verwijderen"
},
"toast": {
"success": {
@ -13,13 +15,15 @@
"deletedImage": "Verwijderde afbeeldingen",
"categorizedImage": "Succesvol geclassificeerde afbeelding",
"trainedModel": "Succesvol getraind model.",
"trainingModel": "Modeltraining succesvol gestart."
"trainingModel": "Modeltraining succesvol gestart.",
"deletedModel": "{{count}} model(len) succesvol verwijderd"
},
"error": {
"deleteImageFailed": "Verwijderen mislukt: {{errorMessage}}",
"deleteCategoryFailed": "Het verwijderen van de klasse is mislukt: {{errorMessage}}",
"categorizeFailed": "Afbeelding categoriseren mislukt: {{errorMessage}}",
"trainingFailed": "Het starten van de modeltraining is mislukt: {{errorMessage}}"
"trainingFailed": "Het starten van de modeltraining is mislukt: {{errorMessage}}",
"deleteModelFailed": "Model verwijderen mislukt: {{errorMessage}}"
}
},
"deleteCategory": {
@ -133,5 +137,14 @@
},
"generateSuccess": "Met succes gegenereerde voorbeeldafbeeldingen"
}
},
"deleteModel": {
"title": "Classificatiemodel verwijderen",
"single": "Weet u zeker dat u {{name}} wilt verwijderen? Hiermee worden alle bijbehorende gegevens, inclusief afbeeldingen en trainingsgegevens, definitief verwijderd. Deze actie kan niet ongedaan worden gemaakt.",
"desc": "Weet u zeker dat u {{count}} model(len) wilt verwijderen? Hiermee worden alle bijbehorende gegevens, inclusief afbeeldingen en trainingsgegevens, permanent verwijderd. Deze actie kan niet ongedaan worden gemaakt."
},
"menu": {
"objects": "Objecten",
"states": "Staten"
}
}

View File

@ -33,7 +33,8 @@
"details": "Details",
"video": "video",
"snapshot": "snapshot",
"object_lifecycle": "objectlevenscyclus"
"object_lifecycle": "objectlevenscyclus",
"thumbnail": "thumbnail"
},
"objectLifecycle": {
"createObjectMask": "Objectmasker maken",
@ -238,7 +239,7 @@
"noImageFound": "Er is geen afbeelding beschikbaar voor dit tijdstip.",
"createObjectMask": "Objectmasker maken",
"adjustAnnotationSettings": "Annotatie-instellingen aanpassen",
"scrollViewTips": "Scroll om de belangrijke momenten uit de levenscyclus van dit object te bekijken.",
"scrollViewTips": "Klik om de belangrijke momenten uit de levenscyclus van dit object te bekijken.",
"autoTrackingTips": "Als u een automatische objectvolgende camera gebruikt, zal het objectkader onnauwkeurig zijn.",
"count": "{{first}} van {{second}}",
"trackedPoint": "Volgpunt",

View File

@ -128,7 +128,7 @@
"documentation": "Lees de documentatie ",
"title": "Audio moet via je camera komen en in go2rtc geconfigureerd zijn voor deze stream."
},
"unavailable": "Audio is niet beschikbaar voor deze stroom",
"unavailable": "Audio is niet beschikbaar voor deze stream",
"available": "Audio is beschikbaar voor deze stream"
},
"playInBackground": {

View File

@ -1075,7 +1075,9 @@
},
"resolutionHigh": "Een resolutie van {{resolution}} kan leiden tot een verhoogd gebruik van systeembronnen.",
"resolutionLow": "Een resolutie van {{resolution}} kan te laag zijn voor betrouwbare detectie van kleine objecten."
}
},
"ffmpegModule": "Gebruik stream-compatibiliteitsmodus",
"ffmpegModuleDescription": "Als de stream na meerdere pogingen niet wordt geladen, probeer dit dan in te schakelen. Wanneer deze optie is ingeschakeld, gebruikt Frigate de ffmpeg-module samen met go2rtc. Dit kan zorgen voor een betere compatibiliteit met sommige camerastreams."
}
},
"cameraManagement": {

View File

@ -43,10 +43,16 @@
"trackedObject_one": "objeto",
"trackedObject_other": "objetos",
"noObjectDetailData": "Nenhum dado de detalhe de objeto disponível.",
"label": "Detalhe"
"label": "Detalhe",
"settings": "Configurações de visualização detalhada",
"alwaysExpandActive": {
"title": "Expandir sempre o modo ativo"
}
},
"objectTrack": {
"trackedPoint": "Ponto rastreado",
"clickToSeek": "Clique para ir para esse horário"
}
},
"zoomIn": "Ampliar",
"zoomOut": "Diminuir o zoom"
}

View File

@ -111,7 +111,8 @@
"details": "detalhes",
"snapshot": "captura de imagem",
"video": "vídeo",
"object_lifecycle": "ciclo de vida do objeto"
"object_lifecycle": "ciclo de vida do objeto",
"thumbnail": "thumbnail"
},
"objectLifecycle": {
"title": "Ciclo de Vida do Objeto",

View File

@ -5,7 +5,9 @@
"renameCategory": "Renomear Classe",
"deleteCategory": "Apagar Classe",
"deleteImages": "Apagar Imagens",
"trainModel": "Treinar Modelo"
"trainModel": "Treinar Modelo",
"addClassification": "Adicionar classificação",
"deleteModels": "Excluir modelos"
},
"toast": {
"success": {
@ -13,12 +15,23 @@
"deletedImage": "Imagens Apagadas",
"categorizedImage": "Imagem Classificada com Sucesso",
"trainedModel": "Modelo treinado com sucesso.",
"trainingModel": "Treinamento do modelo iniciado com sucesso."
"trainingModel": "Treinamento do modelo iniciado com sucesso.",
"deletedModel": "Modelo(s) {{count}} excluído(s) com sucesso"
},
"error": {
"deleteImageFailed": "Falha ao deletar:{{errorMessage}}",
"deleteCategoryFailed": "Falha ao deletar classe:{{errorMessage}}",
"categorizeFailed": "Falha ao categorizar imagem:{{errorMessage}}"
"categorizeFailed": "Falha ao categorizar imagem:{{errorMessage}}",
"deleteModelFailed": "Falha ao excluir o modelo: {{errorMessage}}",
"trainingFailed": "Falha ao iniciar o treinamento do modelo: {{errorMessage}}"
}
},
"deleteCategory": {
"title": "Excluir Classe",
"desc": "Tem certeza de que deseja excluir a classe {{name}}? Isso excluirá permanentemente todas as imagens associadas e exigirá o treinamento do modelo novamente."
},
"deleteModel": {
"title": "Deletar modelo de classificação",
"single": "Tem certeza de que deseja excluir {{name}}? Isso excluirá permanentemente todos os dados associados, incluindo imagens e dados de treinamento. Esta ação não pode ser desfeita."
}
}

View File

@ -287,7 +287,8 @@
},
"list": {
"two": "{{0}} și {{1}}",
"many": "{{items}}, și {{last}}"
"many": "{{items}}, și {{last}}",
"separatorWithSpace": ", "
},
"field": {
"optional": "Opțional",

View File

@ -5,7 +5,9 @@
"renameCategory": "Redenumește clasa",
"deleteCategory": "Șterge clasa",
"deleteImages": "Șterge imaginile",
"trainModel": "Antrenează modelul"
"trainModel": "Antrenează modelul",
"addClassification": "Adaugă clasificare",
"deleteModels": "Șterge modelele"
},
"toast": {
"success": {
@ -13,13 +15,15 @@
"deletedImage": "Imagini șterse",
"categorizedImage": "Imagine clasificată cu succes",
"trainedModel": "Model antrenat cu succes.",
"trainingModel": "Antrenamentul modelului a fost pornit cu succes."
"trainingModel": "Antrenamentul modelului a fost pornit cu succes.",
"deletedModel": "{{count}} model(e) șters(e) cu succes"
},
"error": {
"deleteImageFailed": "Ștergerea a eșuat: {{errorMessage}}",
"deleteCategoryFailed": "Ștergerea clasei a eșuat: {{errorMessage}}",
"categorizeFailed": "Categorisirea imaginii a eșuat: {{errorMessage}}",
"trainingFailed": "Pornirea antrenamentului modelului a eșuat: {{errorMessage}}"
"trainingFailed": "Pornirea antrenamentului modelului a eșuat: {{errorMessage}}",
"deleteModelFailed": "Ștergerea modelului a eșuat: {{errorMessage}}"
}
},
"deleteCategory": {
@ -99,7 +103,8 @@
"stateRequiresTwoClasses": "Modelele de stare necesită cel puțin 2 clase",
"objectLabelRequired": "Vă rugăm să selectați o etichetă de obiect",
"objectTypeRequired": "Vă rugăm să selectați un tip de clasificare"
}
},
"states": "Stări"
},
"step2": {
"description": "Selectați camerele și definiți zona de monitorizat pentru fiecare cameră. Modelul va clasifica starea acestor zone.",
@ -132,5 +137,14 @@
},
"generateSuccess": "Imaginile de exemplu au fost generate cu succes"
}
},
"deleteModel": {
"title": "Șterge modelul de clasificare",
"single": "Sigur doriți să ștergeți {{name}}? Aceasta va șterge permanent toate datele asociate, inclusiv imaginile și datele de antrenament. Această acțiune nu poate fi anulată.",
"desc": "Sigur doriți să ștergeți {{count}} model(e)? Aceasta va șterge permanent toate datele asociate, inclusiv imaginile și datele de antrenament. Această acțiune nu poate fi anulată."
},
"menu": {
"objects": "Obiecte",
"states": "Stări"
}
}

View File

@ -33,7 +33,8 @@
"details": "detalii",
"snapshot": "snapshot",
"video": "video",
"object_lifecycle": "ciclul de viață al obiectului"
"object_lifecycle": "ciclul de viață al obiectului",
"thumbnail": "miniatură"
},
"objectLifecycle": {
"lifecycleItemDesc": {
@ -240,7 +241,7 @@
"noImageFound": "Nu s-a găsit nicio imagine pentru acest marcaj de timp.",
"createObjectMask": "Creează Masca Obiectului",
"adjustAnnotationSettings": "Ajustează Setările de anotare",
"scrollViewTips": "Derulează pentru a vizualiza momentele semnificative din ciclul de viață al acestui obiect.",
"scrollViewTips": "Apasă pentru a vizualiza momentele semnificative din ciclul de viață al acestui obiect.",
"autoTrackingTips": "Pozițiile casetelor de delimitare vor fi inexacte pentru camerele cu urmărire automată.",
"count": "{{first}} din {{second}}",
"trackedPoint": "Punct Urmărit",

View File

@ -50,6 +50,10 @@
"playAlertVideos": {
"label": "Redă videoclipurile de alertă",
"desc": "În mod implicit, alertele recente din panoul Live se redau ca videoclipuri mici, ce ruleaza repetat. Dezactivează această opțiune pentru a afișa doar o imagine statică a alertelor recente pe acest dispozitiv/browser."
},
"displayCameraNames": {
"label": "Afișează întotdeauna numele camerelor",
"desc": "Afișează întotdeauna numele camerelor într-un indicator în tabloul de bord cu vizualizare live pe mai multe camere."
}
},
"storedLayouts": {
@ -703,7 +707,9 @@
},
"actions": {
"alert": "Marchează ca alertă",
"notification": "Trimite notificare"
"notification": "Trimite notificare",
"sub_label": "Adaugă subeticheta",
"attribute": "Adaugă atribut"
},
"dialog": {
"createTrigger": {
@ -758,7 +764,7 @@
},
"actions": {
"title": "Acțiuni",
"desc": "Implicit, Frigate trimite un mesaj MQTT pentru toate declanșatoarele. Alegeți o acțiune suplimentară de efectuat atunci când acest declanșator se activează.",
"desc": "În mod implicit, Frigate trimite un mesaj MQTT pentru toate declanșatoarele. Subetichetele adaugă numele declanșatorului la eticheta obiectului. Atributele sunt metadate căutabile, stocate separat în metadatele obiectului urmărit.",
"error": {
"min": "Trebuie selectată cel puțin o acțiune."
}
@ -1008,7 +1014,9 @@
},
"resolutionHigh": "O rezoluție de {{resolution}} poate cauza o utilizare crescută a resurselor.",
"resolutionLow": "O rezoluție de {{resolution}} poate fi prea mică pentru detectarea fiabilă a obiectelor mici."
}
},
"ffmpegModule": "Folosește modul de compatibilitate pentru stream-uri",
"ffmpegModuleDescription": "Dacă fluxul nu se încarcă după mai multe încercări, activați această opțiune. Când este activată, Frigate va folosi modulul ffmpeg împreună cu go2rtc. Aceasta poate oferi o compatibilitate mai bună cu unele fluxuri de camere."
}
},
"cameraManagement": {

View File

@ -287,7 +287,8 @@
},
"list": {
"two": "{{0}} a {{1}}",
"many": "{{items}}, a {{last}}"
"many": "{{items}}, a {{last}}",
"separatorWithSpace": ", "
},
"field": {
"optional": "Voliteľné",

View File

@ -5,7 +5,9 @@
"renameCategory": "Premenovať triedu",
"deleteCategory": "Odstrániť triedu",
"deleteImages": "Odstrániť obrázky",
"trainModel": "Model vlaku"
"trainModel": "Model vlaku",
"addClassification": "Pridať klasifikáciu",
"deleteModels": "Odstrániť modely"
},
"toast": {
"success": {
@ -13,13 +15,15 @@
"deletedImage": "Vymazané obrázky",
"categorizedImage": "Obrázok bol úspešne klasifikovaný",
"trainedModel": "Úspešne vyškolený model.",
"trainingModel": "Úspešne spustený modelový tréning."
"trainingModel": "Úspešne spustený modelový tréning.",
"deletedModel": "Úspešne zmazané {{count}} model (y)"
},
"error": {
"deleteImageFailed": "Nepodarilo sa odstrániť: {{errorMessage}}",
"deleteCategoryFailed": "Nepodarilo sa odstrániť triedu: {{errorMessage}}",
"categorizeFailed": "Nepodarilo sa kategorizovať obrázok: {{errorMessage}}",
"trainingFailed": "Nepodarilo sa spustiť trénovanie modelu: {{errorMessage}}"
"trainingFailed": "Nepodarilo sa spustiť trénovanie modelu: {{errorMessage}}",
"deleteModelFailed": "Nepodarilo sa odstrániť model: {{errorMessage}}"
}
},
"deleteCategory": {
@ -99,7 +103,8 @@
"stateRequiresTwoClasses": "Modely štátov vyžadujú aspoň 2 triedy",
"objectLabelRequired": "Vyberte označenie objektu",
"objectTypeRequired": "Vyberte typ klasifikácie"
}
},
"states": "Štátov"
},
"step2": {
"description": "Vyberte kamery a definujte oblasť, ktorú chcete pre každú kameru monitorovať. Model klasifikuje stav týchto oblastí.",
@ -132,5 +137,14 @@
},
"generateSuccess": "Vzorové obrázky boli úspešne vygenerované"
}
},
"deleteModel": {
"title": "Odstrániť klasifikačný model",
"single": "Ste si istí, že chcete odstrániť {{name}}? To bude trvalo odstrániť všetky súvisiace údaje vrátane obrázkov a vzdelávacích údajov. Táto akcia nemôže byť neporušená.",
"desc": "Ste si istí, že chcete odstrániť {{count}} model (y)? To bude trvalo odstrániť všetky súvisiace údaje vrátane obrázkov a vzdelávacích údajov. Táto akcia nemôže byť neporušená."
},
"menu": {
"objects": "Objekty",
"states": "Štátov"
}
}

View File

@ -111,7 +111,8 @@
"details": "detaily",
"snapshot": "snímka",
"video": "video",
"object_lifecycle": "životný cyklus objektu"
"object_lifecycle": "životný cyklus objektu",
"thumbnail": "Náhľad"
},
"objectLifecycle": {
"title": "Životný cyklus Objektu",
@ -240,7 +241,7 @@
"noImageFound": "Pre túto časovú pečiatku sa nenašiel žiadny obrázok.",
"createObjectMask": "Vytvoriť masku objektu",
"adjustAnnotationSettings": "Upravte nastavenia anotácií",
"scrollViewTips": "Posúvaním zobrazíte významné momenty životného cyklu tohto objektu.",
"scrollViewTips": "Kliknite pre zobrazenie významných momentov životného cyklu tohto objektu.",
"autoTrackingTips": "Pozície ohraničujúcich rámčekov budú pre kamery s automatickým sledovaním nepresné.",
"count": "{{first}} z {{second}}",
"trackedPoint": "Sledovaný bod",

View File

@ -1,6 +1,6 @@
{
"description": {
"addFace": "Sprievodca pridáním novej kolekcie do Knižnice tvárí.",
"addFace": "Sprievodca pridaním novej kolekcie do Knižnice tvárí.",
"invalidName": "Neplatné meno. Mená môžu obsahovať iba písmená, čísla, medzery, apostrofy, podčiarkovníky a spojovníky.",
"placeholder": "Zadajte názov pre túto kolekciu"
},
@ -23,7 +23,7 @@
"title": "Vytvoriť Zbierku",
"desc": "Vytvoriť novú zbierku",
"new": "Vytvoriť novú tvár",
"nextSteps": "Vybudovanie pevných základov: <li> Pomocou záložky Tréning vyberte a trénujte obrázky pre každú detekovanú osobu.</li><li>Pre dosiahnutie najlepších výsledkov sa zamerajte na snímky s priamym pohľadom; vyhnite sa snímkam, ktoré zachytávajú tváre pod uhlom.</li></ul>"
"nextSteps": "Vybudovanie silného základu:<li>Použite kartu Nedávne rozpoznania na výber a trénovanie obrázkov pre každú rozpoznanú osobu.</li><li>Pre dosiahnutie najlepších výsledkov sa zamerajte na priame obrázky; vyhnite sa trénovaniu obrázkov, ktoré zachytávajú tváre pod uhlom.</li></ul>"
},
"steps": {
"faceName": "Zadajte Meno tváre",

View File

@ -922,7 +922,157 @@
},
"snapshotConfig": {
"title": "Konfigurácia snímky",
"desc": "Odosielanie do Frigate+ vyžaduje, aby boli v konfigurácii povolené snímky aj snímky <code>clean_copy</code>."
"desc": "Odosielanie do Frigate+ vyžaduje, aby boli v konfigurácii povolené snímky aj snímky <code>clean_copy</code>.",
"cleanCopyWarning": "Niektoré kamery majú povolené snímky, ale voľba <code>clean_copy</code> je zakázaná. Pre možnosť odosielania snímok z týchto kamier do služby Frigate+ je nutné túto voľbu povoliť v konfigurácii snímok.",
"table": {
"camera": "Kamera",
"snapshots": "Snímky",
"cleanCopySnapshots": "<code>clean_copy</code> Snímky"
}
},
"modelInfo": {
"title": "Informácie o Modele",
"modelType": "Typ Modelu",
"trainDate": "Dátum Tréningu",
"baseModel": "Základný Model",
"plusModelType": {
"baseModel": "Základný Model",
"userModel": "Doladené"
},
"supportedDetectors": "Podporované Detektory",
"cameras": "Kamery",
"loading": "Načítavam informácie o modeli…",
"error": "Chyba načítania informácií o modeli",
"availableModels": "Dostupné Moduly",
"loadingAvailableModels": "Načítavam dostupné modely…",
"modelSelect": "Tu môžete vybrať dostupné modely zo služby Frigate+. Upozorňujeme, že je možné zvoliť iba modely kompatibilné s aktuálnou konfiguráciou detektora."
},
"unsavedChanges": "Neuložené zmeny nastavenia Frigate+",
"restart_required": "Vyžadovaný reštart (model Frigate+ zmenený)",
"toast": {
"success": "Nastavenia Frigate+ boli uložené. Reštartujte Frigate+ pre aplikovanie zmien.",
"error": "Chyba pri ukladaní zmien konfigurácie: {{errorMessage}}"
}
},
"triggers": {
"documentTitle": "Spúšťače",
"semanticSearch": {
"title": "Sémantické vyhľadávanie je vypnuté",
"desc": "Na používanie spúšťačov musí byť povolené sémantické vyhľadávanie."
},
"management": {
"title": "Spúšťače",
"desc": "Správa spúšťa {{camera}}. Použite typ miniatúry, aby ste spustili na podobných miniatúr na vybraných tracked objekt, a typ popisu, aby ste spustili podobné popisy na text, ktorý určíte."
},
"addTrigger": "Pridať Spúšťač",
"table": {
"name": "Meno",
"type": "Typ",
"content": "Obsah",
"threshold": "Prah",
"actions": "Akcie",
"noTriggers": "Pre túto kameru nie sú nakonfigurované žiadne spúšťače.",
"edit": "Upraviť",
"deleteTrigger": "Odstrániť spúšťač",
"lastTriggered": "Naposledy spustené"
},
"type": {
"thumbnail": "Náhľad",
"description": "Popis"
},
"actions": {
"notification": "Poslať upozornenie",
"sub_label": "Pridať vedľajší štítok",
"attribute": "Pridať atribút"
},
"dialog": {
"createTrigger": {
"title": "Vytvoriť spúšťač",
"desc": "Vytvorte spúšť pre kameru {{camera}}"
},
"editTrigger": {
"title": "Upraviť spúšťač",
"desc": "Upraviť nastavenia spúšťača na kamere {{camera}}"
},
"deleteTrigger": {
"title": "Odstrániť spúšťač",
"desc": "Naozaj chcete odstrániť spúšťač <strong>{{triggerName}}</strong>? Túto akciu nie je možné vrátiť späť."
},
"form": {
"name": {
"title": "Meno",
"placeholder": "Zadajte meno pre spúšťača",
"description": "Zadajte jedinečné meno alebo popis na identifikáciu tohto spúšťania",
"error": {
"minLength": "Názov musí mať aspoň 2 znaky.",
"invalidCharacters": "Meno môže obsahovať iba písmená, číslice, podčiarkovníky a pomlčky.",
"alreadyExists": "Spúšťač s týmto názvom už pre túto kameru existuje."
}
},
"enabled": {
"description": "Povoliť alebo zakázať tento spúšťač"
},
"type": {
"title": "Typ",
"placeholder": "Vybrať typ spúšťača",
"description": "Spustiť, keď sa zistí podobný popis sledovaného objektu",
"thumbnail": "Spustiť, keď sa zistí podobná miniatúra sledovaného objektu"
},
"content": {
"title": "Obsah",
"imagePlaceholder": "Vyberte miniatúru",
"textPlaceholder": "Zadajte obsah textu",
"imageDesc": "Zobrazujú sa iba posledné 100 miniatúr. Ak nemôžete nájsť požadovanú miniatúru, prečítajte si skôr objekty v preskúmať a nastaviť spúšťací z ponuky tam.",
"textDesc": "Zadajte text, aby ste spustili túto akciu, keď je detekovaný podobný popis objektu.",
"error": {
"required": "Obsah je potrebný."
}
},
"threshold": {
"title": "Prah",
"desc": "Nastavte prah podobnosti pre tento spúšťač. Vyšší prah znamená, že na spustenie spúšťača je potrebná bližšia zhoda.",
"error": {
"min": "Threshold musí byť aspoň 0",
"max": "Threshold musí byť na väčšine 1"
}
},
"actions": {
"title": "Akcie",
"desc": "V predvolenom nastavení Frigate odosiela MQTT správu pre všetky spúšťače. Zvoľte dodatočnú akciu, ktorá sa má vykonať, keď sa tento spúšťač aktivuje.",
"error": {
"min": "Musí byť vybraná aspoň jedna akcia."
}
}
}
},
"wizard": {
"title": "Vytvoriť spúšťač",
"step1": {
"description": "Konfigurujte základné nastavenia pre vašu spúšť."
},
"step2": {
"description": "Nastavte obsah, ktorý spustí túto akciu."
},
"step3": {
"description": "Konfigurovať prah a akcie pre tento spúšťač."
},
"steps": {
"nameAndType": "Meno a typ",
"configureData": "Konfigurovať údaje",
"thresholdAndActions": "Prah a akcie"
}
},
"toast": {
"success": {
"createTrigger": "Spúšťač {{name}} bol úspešne vytvorený.",
"updateTrigger": "Spúšťač {{name}} bol úspešne aktualizovaný.",
"deleteTrigger": "Spúšťač {{name}} bol úspešne zmazaný."
},
"error": {
"createTriggerFailed": "Nepodarilo sa vytvoriť spúšťač: {{errorMessage}}",
"updateTriggerFailed": "Nepodarilo sa aktualizovať spúšťač: {{errorMessage}}",
"deleteTriggerFailed": "Nepodarilo sa zmazať spúšťač: {{errorMessage}}"
}
}
}
}

View File

@ -250,7 +250,10 @@
"copyUrlToClipboard": "Webbadressen har kopierats till urklipp."
},
"label": {
"back": "Gå tillbaka"
"back": "Gå tillbaka",
"hide": "Dölj {{item}}",
"show": "Visa {{item}}",
"ID": "ID"
},
"unit": {
"speed": {
@ -274,5 +277,14 @@
"readTheDocumentation": "Läs dokumentationen",
"information": {
"pixels": "{{area}}px"
},
"list": {
"two": "{{0}} och {{1}}",
"many": "{{items}} och {{last}}",
"separatorWithSpace": ", "
},
"field": {
"optional": "Valfritt",
"internalID": "Det interna ID som Frigate använder i konfigurationen och databasen"
}
}

View File

@ -5,7 +5,9 @@
"renameCategory": "Byt namn på klass",
"deleteCategory": "Ta bort klass",
"deleteImages": "Ta bort bilder",
"trainModel": "Träna modellen"
"trainModel": "Träna modellen",
"addClassification": "Lägg till klassificering",
"deleteModels": "Ta bort modeller"
},
"toast": {
"success": {
@ -13,13 +15,15 @@
"deletedImage": "Raderade bilder",
"categorizedImage": "Lyckades klassificera bilden",
"trainedModel": "Modellen har tränats.",
"trainingModel": "Modellträning har startat."
"trainingModel": "Modellträning har startat.",
"deletedModel": "{{count}} modell(er) har raderats"
},
"error": {
"deleteImageFailed": "Misslyckades med att ta bort: {{errorMessage}}",
"deleteCategoryFailed": "Misslyckades med att ta bort klassen: {{errorMessage}}",
"categorizeFailed": "Misslyckades med att kategorisera bilden: {{errorMessage}}",
"trainingFailed": "Misslyckades med att starta modellträning: {{errorMessage}}"
"trainingFailed": "Misslyckades med att starta modellträning: {{errorMessage}}",
"deleteModelFailed": "Misslyckades med att ta bort modellen: {{errorMessage}}"
}
},
"deleteCategory": {
@ -43,7 +47,8 @@
},
"train": {
"title": "Nyligen tillagd klassificeringar",
"aria": "Välj senaste klassificeringar"
"aria": "Välj senaste klassificeringar",
"titleShort": "Nyligen"
},
"categories": "Klasser",
"createCategory": {
@ -82,7 +87,64 @@
"classificationType": "Klassificeringstyp",
"classificationTypeTip": "Lär dig mer om klassificeringstyper",
"classificationTypeDesc": "Underetiketter lägger till ytterligare text till objektetiketten (t.ex. 'Person: UPS'). Attribut är sökbara metadata som lagras separat i objektmetadata.",
"classificationSubLabel": "Underetikett"
"classificationSubLabel": "Underetikett",
"classificationAttribute": "Attribut",
"classes": "Klasser",
"states": "Tillstånd",
"classesTip": "Lär dig mer om klasser",
"classesStateDesc": "Definiera de olika tillstånd som ditt kameraområde kan vara i. Till exempel: \"öppen\" och \"stängd\" för en garageport.",
"classesObjectDesc": "Definiera de olika kategorierna som detekterade objekt ska klassificeras i. Till exempel: 'leveransperson', 'boende', 'främling' för personklassificering.",
"classPlaceholder": "Ange klassnamn...",
"errors": {
"nameRequired": "Modellnamn krävs",
"nameLength": "Modellnamnet måste vara högst 64 tecken långt",
"nameOnlyNumbers": "Modellnamnet får inte bara innehålla siffror",
"classRequired": "Minst 1 klass krävs",
"classesUnique": "Klassnamn måste vara unika",
"stateRequiresTwoClasses": "Tillståndsmodeller kräver minst två klasser",
"objectLabelRequired": "Välj en objektetikett",
"objectTypeRequired": "Vänligen välj en klassificeringstyp"
}
},
"step2": {
"description": "Välj kameror och definiera området som ska övervakas för varje kamera. Modellen kommer att klassificera tillståndet för dessa områden.",
"cameras": "Kameror",
"selectCamera": "Välj kamera",
"noCameras": "Klicka på + för att lägga till kameror",
"selectCameraPrompt": "Välj en kamera från listan för att definiera dess övervakningsområde"
},
"step3": {
"selectImagesPrompt": "Markera alla bilder med: {{className}}",
"selectImagesDescription": "Klicka på bilderna för att välja dem. Klicka på Fortsätt när du är klar med den här klass.",
"generating": {
"title": "Generera exempelbilder",
"description": "Frigate hämtar representativa bilder från dina inspelningar. Det kan ta en stund..."
},
"training": {
"title": "Träningsmodell",
"description": "Din modell tränas i bakgrunden. Stäng den här dialogrutan så börjar modellen köras så snart träningen är klar."
},
"retryGenerate": "Försök att generera igen",
"noImages": "Inga exempelbilder genererade",
"classifying": "Klassificering & Träning...",
"trainingStarted": "Träningen har börjat",
"errors": {
"noCameras": "Inga kameror konfigurerade",
"noObjectLabel": "Ingen objektetikett vald",
"generateFailed": "Misslyckades med att generera exempel: {{error}}",
"generationFailed": "Genereringen misslyckades. Försök igen.",
"classifyFailed": "Misslyckades med att klassificera bilder: {{error}}"
},
"generateSuccess": "Exempelbilder har genererats"
}
},
"deleteModel": {
"title": "Ta bort klassificeringsmodell",
"single": "Är du säker på att du vill ta bort {{name}}? Detta kommer att permanent ta bort all tillhörande data, inklusive bilder och träningsdata. Åtgärden kan inte ångras.",
"desc": "Är du säker på att du vill ta bort {{count}} modell(er)? Detta kommer att permanent ta bort all tillhörande data, inklusive bilder och träningsdata. Åtgärden kan inte ångras."
},
"menu": {
"objects": "Objekt",
"states": "Tillstånd"
}
}

View File

@ -43,10 +43,17 @@
"trackedObject_one": "objekt",
"trackedObject_other": "objekt",
"noObjectDetailData": "Inga objektdetaljdata tillgängliga.",
"label": "Detalj"
"label": "Detalj",
"settings": "Detaljvy inställningar",
"alwaysExpandActive": {
"title": "Expandera alltid aktivt",
"desc": "Expandera alltid objektinformationen för det aktiva granskningsobjektet när den är tillgänglig."
}
},
"objectTrack": {
"trackedPoint": "Spårad punkt",
"clickToSeek": "Klicka för att söka till den här tiden"
}
},
"zoomIn": "Zooma in",
"zoomOut": "Zooma ut"
}

View File

@ -109,7 +109,8 @@
"details": "detaljer",
"video": "video",
"snapshot": "ögonblicksbild",
"object_lifecycle": "objektets livscykel"
"object_lifecycle": "objektets livscykel",
"thumbnail": "miniatyrbild"
},
"trackedObjectDetails": "Detaljer om spårade objekt",
"objectLifecycle": {
@ -199,6 +200,13 @@
},
"showObjectDetails": {
"label": "Visa objektets plats"
},
"viewTrackingDetails": {
"label": "Visa spårningsinformation",
"aria": "Visa spårningsdetaljerna"
},
"hideObjectDetails": {
"label": "Dölj objektsökväg"
}
},
"dialog": {
@ -231,7 +239,7 @@
"noImageFound": "Ingen bild hittades för denna tidsstämpel.",
"createObjectMask": "Skapa objektmask",
"adjustAnnotationSettings": "Justera annoteringsinställningar",
"scrollViewTips": "Scrolla för att se de viktiga ögonblicken i detta objekts livscykel.",
"scrollViewTips": "Klicka för att se de viktiga ögonblicken i detta objekts livscykel.",
"autoTrackingTips": "Begränsningsrutornas positioner kommer att vara felaktiga för autospårningskameror.",
"count": "{{first}} av {{second}}",
"trackedPoint": "Spårad punkt",
@ -261,7 +269,7 @@
},
"offset": {
"label": "Annoteringsförskjutning",
"desc": "Denna data kommer från din kameras detekteringsflöde men läggs ovanpå bilder från inspelningsflödet. Det är osannolikt att de två strömmarna är helt synkroniserade. Som ett resultat kommer avgränsningsramen och filmmaterialet inte att radas upp perfekt. Fältet <code>annotation_offset</code> kan dock användas för att justera detta.",
"desc": "Denna data kommer från din kameras detekteringsflöde men läggs ovanpå bilder från inspelningsflödet. Det är osannolikt att de två strömmarna är helt synkroniserade. Som ett resultat kommer avgränsningsramen och filmmaterialet inte att radas upp perfekt. Du kan använda den här inställningen för att förskjuta anteckningarna framåt eller bakåt i tiden för att bättre anpassa dem till det inspelade materialet.",
"millisecondsToOffset": "Millisekunder för att förskjuta detektera annoteringar med. <em>Standard: 0</em>",
"tips": "TIPS: Föreställ dig ett händelseklipp med en person som går från vänster till höger. Om tidslinjens avgränsningsram konsekvent är till vänster om personen bör värdet minskas. På samma sätt, om en person går från vänster till höger och avgränsningsramen konsekvent är framför personen bör värdet ökas.",
"toast": {
@ -270,7 +278,8 @@
}
},
"carousel": {
"previous": "Föregående bild"
"previous": "Föregående bild",
"next": "Nästa bild"
}
}
}

View File

@ -25,7 +25,11 @@
"desc": "Som standard visas varningar på Live panelen som små loopande klipp. Inaktivera denna inställning för att bara visa en statisk bild av nya varningar på denna enhet/webbläsare.",
"label": "Spela upp Varnings videor"
},
"title": "Live Panel"
"title": "Live Panel",
"displayCameraNames": {
"label": "Visa alltid kameranamn",
"desc": "Visa alltid kameranamnen i ett chip i instrumentpanelen för livevisning med flera kameror."
}
},
"storedLayouts": {
"title": "Sparade Layouter",
@ -731,7 +735,7 @@
"triggers": {
"documentTitle": "Utlösare",
"management": {
"title": "Utlösare hantering",
"title": "Utlösare",
"desc": "Hantera utlösare för {{camera}}. Använd miniatyrtypen för att utlösa liknande miniatyrer som ditt valda spårade objekt och beskrivningstypen för att utlösa liknande beskrivningar av text du anger."
},
"addTrigger": "Lägg till utlösare",
@ -752,7 +756,9 @@
},
"actions": {
"notification": "Skicka avisering",
"alert": "Markera som Varning"
"alert": "Markera som Varning",
"sub_label": "Lägg till underetikett",
"attribute": "Lägg till attribut"
},
"dialog": {
"createTrigger": {
@ -770,25 +776,28 @@
"form": {
"name": {
"title": "Namn",
"placeholder": "Ange utlösarens namn",
"placeholder": "Namnge denna utlösare",
"error": {
"minLength": "Namnet måste vara minst 2 tecken lång.",
"invalidCharacters": "Namnet får bara innehålla bokstäver, siffror, understreck, och bindestreck.",
"minLength": "Fältet måste vara minst 2 tecken långt.",
"invalidCharacters": "Fältet får bara innehålla bokstäver, siffror, understreck och bindestreck.",
"alreadyExists": "En utlösare med detta namn finns redan för den här kameran."
}
},
"description": "Ange ett unikt namn eller en unik beskrivning för att identifiera den här utlösaren"
},
"enabled": {
"description": "Aktivera eller inaktivera den här utlösaren"
},
"type": {
"title": "Typ",
"placeholder": "Välj utlösartyp"
"placeholder": "Välj utlösartyp",
"description": "Utlöses när en liknande beskrivning av spårat objekt detekteras",
"thumbnail": "Utlöses när en liknande miniatyrbild av ett spårat objekt upptäcks"
},
"content": {
"title": "Innehåll",
"imagePlaceholder": "Välj en bild",
"imagePlaceholder": "Välj en miniatyrbild",
"textPlaceholder": "Ange textinnehåll",
"imageDesc": "Välj en bild för att utlösa den här åtgärden när en liknande bild upptäcks.",
"imageDesc": "Endast de senaste 100 miniatyrerna visas. Om du inte hittar önskad miniatyr kan du granska tidigare objekt i Utforska och skapa en utlösare från menyn där.",
"textDesc": "Ange text för att utlösa den här åtgärden när en liknande beskrivning av spårat objekt upptäcks.",
"error": {
"required": "Innehåll krävs."
@ -799,11 +808,12 @@
"error": {
"min": "Tröskelvärdet måste vara minst 0",
"max": "Tröskelvärdet får vara högst 1"
}
},
"desc": "Ställ in likhetströskeln för denna utlösare. En högre tröskel innebär att en bättre matchning krävs för att utlösaren ska aktiveras."
},
"actions": {
"title": "Åtgärder",
"desc": "Som standard utlöser Frigate ett MQTT-meddelande för alla utlösare. Välj en ytterligare åtgärd att utföra när den här utlösaren utlöses.",
"desc": "Som standard utlöser Frigate ett MQTT-meddelande för alla utlösare. Underetiketter lägger till utlösarnamnet till objektetiketten. Attribut är sökbara metadata som lagras separat i de spårade objektmetadata.",
"error": {
"min": "Minst en åtgärd måste väljas."
}
@ -830,6 +840,23 @@
"semanticSearch": {
"title": "Semantisk sökning är inaktiverad",
"desc": "Semantisk sökning måste vara aktiverad för att använda Utlösare."
},
"wizard": {
"title": "Skapa utlösare",
"step1": {
"description": "Konfigurera grundinställningarna för din trigger."
},
"step2": {
"description": "Ställ in innehållet som ska utlösa den här åtgärden."
},
"step3": {
"description": "Konfigurera tröskelvärdet och åtgärderna för den här utlösaren."
},
"steps": {
"nameAndType": "Namn och typ",
"configureData": "Konfigurera data",
"thresholdAndActions": "Tröskelvärde och åtgärder"
}
}
},
"cameraWizard": {
@ -886,10 +913,15 @@
"nameExists": "Kameranamnet finns redan",
"brands": {
"reolink-rtsp": "Reolink RTSP rekommenderas inte. Aktivera HTTP i kamerans firmwareinställningar och starta om guiden."
}
},
"customUrlRtspRequired": "Anpassade webbadresser måste börja med \"rtsp://\". Manuell konfiguration krävs för kameraströmmar som inte använder RTSP."
},
"docs": {
"reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras"
},
"testing": {
"probingMetadata": "Undersöker kamerans metadata...",
"fetchingSnapshot": "Hämtar kamerabild..."
}
},
"step2": {
@ -968,7 +1000,9 @@
},
"hikvision": {
"substreamWarning": "Delström 1 är låst till en låg upplösning. Många Hikvision kameror stöder ytterligare delströmmar som måste aktiveras i kamerans inställningar. Det rekommenderas att kontrollera och använda dessa strömmar om de är tillgängliga."
}
},
"resolutionHigh": "En upplösning på {{resolution}} kan orsaka ökad resursanvändning.",
"resolutionLow": "En upplösning på {{resolution}} kan vara för låg för tillförlitlig detektering av små objekt."
}
}
},

View File

@ -288,7 +288,8 @@
},
"list": {
"two": "{{0}} і {{1}}",
"many": "{{items}}, і {{last}}"
"many": "{{items}}, і {{last}}",
"separatorWithSpace": ", "
},
"field": {
"optional": "Необов'язково",

View File

@ -5,7 +5,9 @@
"renameCategory": "Перейменувати клас",
"deleteCategory": "Видалити клас",
"deleteImages": "Видалити зображення",
"trainModel": "Модель поїзда"
"trainModel": "Модель поїзда",
"addClassification": "Додати класифікацію",
"deleteModels": "Видалити моделі"
},
"toast": {
"success": {
@ -13,13 +15,15 @@
"deletedImage": "Видалені зображення",
"categorizedImage": "Зображення успішно класифіковано",
"trainedModel": "Успішно навчена модель.",
"trainingModel": "Успішно розпочато навчання моделі."
"trainingModel": "Успішно розпочато навчання моделі.",
"deletedModel": "Успішно видалено {{count}} моделей"
},
"error": {
"deleteImageFailed": "Не вдалося видалити: {{errorMessage}}",
"deleteCategoryFailed": "Не вдалося видалити клас: {{errorMessage}}",
"categorizeFailed": "Не вдалося класифікувати зображення: {{errorMessage}}",
"trainingFailed": "Не вдалося розпочати навчання моделі: {{errorMessage}}"
"trainingFailed": "Не вдалося розпочати навчання моделі: {{errorMessage}}",
"deleteModelFailed": "Не вдалося видалити модель: {{errorMessage}}"
}
},
"deleteCategory": {
@ -133,5 +137,14 @@
},
"generateSuccess": "Зразки зображень успішно створено"
}
},
"deleteModel": {
"title": "Видалити модель класифікації",
"single": "Ви впевнені, що хочете видалити {{name}}? Це назавжди видалить усі пов’язані дані, включаючи зображення та дані навчання. Цю дію не можна скасувати.",
"desc": "Ви впевнені, що хочете видалити {{count}} модель(і)? Це назавжди видалить усі пов’язані дані, включаючи зображення та навчальні дані. Цю дію не можна скасувати."
},
"menu": {
"objects": "Об'єкти",
"states": "Стани"
}
}

View File

@ -226,7 +226,8 @@
"details": "деталі",
"snapshot": "знімок",
"video": "відео",
"object_lifecycle": "життєвий цикл об'єкта"
"object_lifecycle": "життєвий цикл об'єкта",
"thumbnail": "мініатюра"
},
"exploreMore": "Дослідіть більше об'єктів {{label}}",
"aiAnalysis": {
@ -240,7 +241,7 @@
"noImageFound": "Для цієї позначки часу не знайдено зображення.",
"createObjectMask": "Створити маску об'єкта",
"adjustAnnotationSettings": "Налаштування параметрів анотацій",
"scrollViewTips": "Прокрутіть, щоб переглянути важливі моменти життєвого циклу цього об'єкта.",
"scrollViewTips": "Натисніть, щоб переглянути важливі моменти життєвого циклу цього об'єкта.",
"autoTrackingTips": "Положення обмежувальних рамок будуть неточними для камер з автоматичним відстеженням.",
"count": "{{first}} з {{second}}",
"trackedPoint": "Відстежувана точка",

View File

@ -1077,7 +1077,9 @@
},
"resolutionHigh": "Роздільна здатність {{resolution}} може призвести до збільшення використання ресурсів.",
"resolutionLow": "Роздільна здатність {{resolution}} може бути занадто низькою для надійного виявлення малих об'єктів."
}
},
"ffmpegModule": "Використовувати режим сумісності з потоками",
"ffmpegModuleDescription": "Якщо потік не завантажується після кількох спроб, спробуйте ввімкнути цю функцію. Коли вона ввімкнена, Frigate використовуватиме модуль ffmpeg з go2rtc. Це може забезпечити кращу сумісність з деякими потоками камер."
}
},
"cameraManagement": {

View File

@ -10,6 +10,7 @@
"loginFailed": "Đăng nhập không thành công",
"unknownError": "Lỗi không xác định. Kiểm tra nhật ký.",
"webUnknownError": "Lỗi không xác định. Kiểm tra nhật ký bảng điều khiển."
}
},
"firstTimeLogin": "Lần đầu đăng nhập? Thông tin đăng nhập được in trong nhật ký (log) của Frigate."
}
}

View File

@ -1 +1,20 @@
{}
{
"documentTitle": "Mô Hình Phân Loại",
"button": {
"deleteClassificationAttempts": "Xóa Hình Ảnh Phân Loại",
"renameCategory": "Đổi Tên Lớp",
"deleteCategory": "Xoá Lớp",
"deleteImages": "Xoá Hình Ảnh",
"trainModel": "Huấn Luyện Mô Hình",
"addClassification": "Thêm Phân Loại",
"deleteModels": "Xoá Mô Hình"
},
"toast": {
"success": {
"deletedCategory": "Lớp Đã Bị Xoá",
"deletedImage": "Hình ảnh đã bị xóa",
"deletedModel": "Đã xóa thành công {{count}} mô hình",
"categorizedImage": "Phân Loại Hình Ảnh Thành Công"
}
}
}

View File

@ -36,5 +36,6 @@
"markAsReviewed": "Đánh dấu là đã xem xét",
"markTheseItemsAsReviewed": "Đánh dấu các mục này là đã xem xét",
"suspiciousActivity": "Hoạt động đáng ngờ",
"threateningActivity": "Hoạt động đe dọa"
"threateningActivity": "Hoạt động đe dọa",
"zoomIn": "Phóng To"
}

View File

@ -13,5 +13,10 @@
"error": {
"renameExportFailed": "Đổi tên tệp xuất thất bại: {{errorMessage}}"
}
},
"tooltip": {
"shareExport": "Chia sẻ bản xuất",
"downloadVideo": "Tải video",
"editName": "Chỉnh sửa tên"
}
}

View File

@ -9,7 +9,9 @@
"object": "Gỡ lỗi - Frigate",
"general": "Cài đặt Chung - Frigate",
"frigatePlus": "Cài đặt Frigate+ - Frigate",
"motionTuner": "Bộ tinh chỉnh Chuyển động - Frigate"
"motionTuner": "Bộ tinh chỉnh Chuyển động - Frigate",
"cameraManagement": "Quản Lý Camera - Frigate",
"cameraReview": "Cài Đặt Xem Lại Camera - Frigate"
},
"notification": {
"toast": {

View File

@ -283,7 +283,8 @@
},
"list": {
"two": "{{0}} 和 {{1}}",
"many": "{{items}} 以及 {{last}}"
"many": "{{items}} 以及 {{last}}",
"separatorWithSpace": " "
},
"field": {
"optional": "可选",

View File

@ -34,7 +34,8 @@
"details": "详情",
"snapshot": "快照",
"video": "视频",
"object_lifecycle": "目标全周期"
"object_lifecycle": "目标全周期",
"thumbnail": "缩略图"
},
"objectLifecycle": {
"title": "目标全周期",
@ -236,7 +237,7 @@
"noImageFound": "在该时间内没找到图片。",
"createObjectMask": "创建目标遮罩",
"adjustAnnotationSettings": "调整注释设置",
"scrollViewTips": "滚动以查看该目标全周期中的关键时刻。",
"scrollViewTips": "点击以查看该目标全周期中的关键时刻。",
"autoTrackingTips": "自动追踪摄像头的边框定位可能不准确。",
"count": "{{first}} / {{second}}",
"trackedPoint": "追踪点",

View File

@ -5,7 +5,9 @@
"renameCategory": "重命名类别",
"deleteCategory": "删除类别",
"deleteImages": "删除图片",
"trainModel": "训练模型"
"trainModel": "训练模型",
"addClassification": "添加分类",
"deleteModels": "删除模型"
},
"toast": {
"success": {
@ -13,13 +15,15 @@
"deletedImage": "删除图片",
"categorizedImage": "成功分类图片",
"trainedModel": "训练模型成功。",
"trainingModel": "已开始训练模型。"
"trainingModel": "已开始训练模型。",
"deletedModel": "已删除 {{count}} 个模型"
},
"error": {
"deleteImageFailed": "删除失败:{{errorMessage}}",
"deleteCategoryFailed": "删除类别失败:{{errorMessage}}",
"categorizeFailed": "图片分类失败:{{errorMessage}}",
"trainingFailed": "开始训练模型失败:{{errorMessage}}"
"trainingFailed": "开始训练模型失败:{{errorMessage}}",
"deleteModelFailed": "删除模型失败:{{errorMessage}}"
}
},
"deleteCategory": {
@ -99,7 +103,8 @@
"stateRequiresTwoClasses": "状态模型至少需要两个类别",
"objectLabelRequired": "请选择一个目标标签",
"objectTypeRequired": "请选择一个目标标签"
}
},
"states": "状态"
},
"step2": {
"description": "选择摄像头,并为摄像头定义要监控的区域。模型将对这些区域的状态进行分类。",
@ -132,5 +137,14 @@
},
"generateSuccess": "样本图片生成成功"
}
},
"deleteModel": {
"title": "删除分类模型",
"single": "你确定要删除 {{name}} 吗?此操作将永久删除所有相关数据,包括图片和训练数据,且无法撤销。",
"desc": "你确定要删除 {{count}} 个模型吗?此操作将永久删除所有相关数据,包括图片和训练数据,且无法撤销。"
},
"menu": {
"objects": "目标",
"states": "状态"
}
}

View File

@ -181,6 +181,7 @@ type GroupedClassificationCardProps = {
selectedItems: string[];
i18nLibrary: string;
objectType: string;
noClassificationLabel?: string;
onClick: (data: ClassificationItemData | undefined) => void;
children?: (data: ClassificationItemData) => React.ReactNode;
};
@ -190,6 +191,7 @@ export function GroupedClassificationCard({
threshold,
selectedItems,
i18nLibrary,
noClassificationLabel = "details.none",
onClick,
children,
}: GroupedClassificationCardProps) {
@ -222,10 +224,14 @@ export function GroupedClassificationCard({
const bestTyped: ClassificationItemData = best;
return {
...bestTyped,
name: event ? (event.sub_label ?? t("details.unknown")) : bestTyped.name,
name: event
? event.sub_label && event.sub_label !== "none"
? event.sub_label
: t(noClassificationLabel)
: bestTyped.name,
score: event?.data?.sub_label_score || bestTyped.score,
};
}, [group, event, t]);
}, [group, event, noClassificationLabel, t]);
const bestScoreStatus = useMemo(() => {
if (!bestItem?.score || !threshold) {
@ -311,8 +317,10 @@ export function GroupedClassificationCard({
isMobile && "px-2",
)}
>
{event?.sub_label ? event.sub_label : t("details.unknown")}
{event?.sub_label && (
{event?.sub_label && event.sub_label !== "none"
? event.sub_label
: t(noClassificationLabel)}
{event?.sub_label && event.sub_label !== "none" && (
<div
className={cn(
"",

View File

@ -317,6 +317,21 @@ export default function Step3ChooseExamples({
return unclassifiedImages.length === 0;
}, [unclassifiedImages]);
const handleBack = useCallback(() => {
if (currentClassIndex > 0) {
const previousClass = allClasses[currentClassIndex - 1];
setCurrentClassIndex((prev) => prev - 1);
// Restore selections for the previous class
const previousSelections = Object.entries(imageClassifications)
.filter(([_, className]) => className === previousClass)
.map(([imageName, _]) => imageName);
setSelectedImages(new Set(previousSelections));
} else {
onBack();
}
}, [currentClassIndex, allClasses, imageClassifications, onBack]);
return (
<div className="flex flex-col gap-6">
{isTraining ? (
@ -420,7 +435,7 @@ export default function Step3ChooseExamples({
{!isTraining && (
<div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4">
<Button type="button" onClick={onBack} className="sm:flex-1">
<Button type="button" onClick={handleBack} className="sm:flex-1">
{t("button.back", { ns: "common" })}
</Button>
<Button

View File

@ -348,6 +348,26 @@ export function GeneralFilterContent({
onClose,
}: GeneralFilterContentProps) {
const { t } = useTranslation(["components/filter"]);
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
const allAudioListenLabels = useMemo<string[]>(() => {
if (!config) {
return [];
}
const labels = new Set<string>();
Object.values(config.cameras).forEach((camera) => {
if (camera?.audio?.enabled) {
camera.audio.listen.forEach((label) => {
labels.add(label);
});
}
});
return [...labels].sort();
}, [config]);
return (
<>
<div className="overflow-x-hidden">
@ -373,7 +393,10 @@ export function GeneralFilterContent({
{allLabels.map((item) => (
<FilterSwitch
key={item}
label={getTranslatedLabel(item)}
label={getTranslatedLabel(
item,
allAudioListenLabels.includes(item) ? "audio" : "object",
)}
isChecked={currentLabels?.includes(item) ?? false}
onCheckedChange={(isChecked) => {
if (isChecked) {

View File

@ -58,6 +58,47 @@ export default function ObjectTrackOverlay({
const effectiveCurrentTime = currentTime - annotationOffset / 1000;
const {
pathStroke,
pointRadius,
pointStroke,
zoneStroke,
boxStroke,
highlightRadius,
} = useMemo(() => {
const BASE_WIDTH = 1280;
const BASE_HEIGHT = 720;
const BASE_PATH_STROKE = 5;
const BASE_POINT_RADIUS = 7;
const BASE_POINT_STROKE = 3;
const BASE_ZONE_STROKE = 5;
const BASE_BOX_STROKE = 5;
const BASE_HIGHLIGHT_RADIUS = 5;
const scale = Math.sqrt(
(videoWidth * videoHeight) / (BASE_WIDTH * BASE_HEIGHT),
);
const pathStroke = Math.max(1, Math.round(BASE_PATH_STROKE * scale));
const pointRadius = Math.max(2, Math.round(BASE_POINT_RADIUS * scale));
const pointStroke = Math.max(1, Math.round(BASE_POINT_STROKE * scale));
const zoneStroke = Math.max(1, Math.round(BASE_ZONE_STROKE * scale));
const boxStroke = Math.max(1, Math.round(BASE_BOX_STROKE * scale));
const highlightRadius = Math.max(
2,
Math.round(BASE_HIGHLIGHT_RADIUS * scale),
);
return {
pathStroke,
pointRadius,
pointStroke,
zoneStroke,
boxStroke,
highlightRadius,
};
}, [videoWidth, videoHeight]);
// Fetch all event data in a single request (CSV ids)
const { data: eventsData } = useSWR<Event[]>(
selectedObjectIds.length > 0
@ -198,16 +239,21 @@ export default function ObjectTrackOverlay({
b.timestamp - a.timestamp,
)[0]?.data?.zones || [];
// bounding box (with tolerance for browsers with seek precision by-design issues)
const boxCandidates = timelineData?.filter(
(event: TrackingDetailsSequence) =>
event.timestamp <= effectiveCurrentTime + TOLERANCE &&
event.data.box,
);
const currentBox = boxCandidates?.sort(
(a: TrackingDetailsSequence, b: TrackingDetailsSequence) =>
b.timestamp - a.timestamp,
)[0]?.data?.box;
// bounding box - only show if there's a timeline event at/near the current time with a box
// Search all timeline events (not just those before current time) to find one matching the seek position
const nearbyTimelineEvent = timelineData
?.filter((event: TrackingDetailsSequence) => event.data.box)
.sort(
(a: TrackingDetailsSequence, b: TrackingDetailsSequence) =>
Math.abs(a.timestamp - effectiveCurrentTime) -
Math.abs(b.timestamp - effectiveCurrentTime),
)
.find(
(event: TrackingDetailsSequence) =>
Math.abs(event.timestamp - effectiveCurrentTime) <= TOLERANCE,
);
const currentBox = nearbyTimelineEvent?.data?.box;
return {
objectId,
@ -333,7 +379,7 @@ export default function ObjectTrackOverlay({
points={zone.points}
fill={zone.fill}
stroke={zone.stroke}
strokeWidth="5"
strokeWidth={zoneStroke}
opacity="0.7"
/>
))}
@ -353,7 +399,7 @@ export default function ObjectTrackOverlay({
d={generateStraightPath(absolutePositions)}
fill="none"
stroke={objData.color}
strokeWidth="5"
strokeWidth={pathStroke}
strokeLinecap="round"
strokeLinejoin="round"
/>
@ -365,13 +411,13 @@ export default function ObjectTrackOverlay({
<circle
cx={pos.x}
cy={pos.y}
r="7"
r={pointRadius}
fill={getPointColor(
objData.color,
pos.lifecycle_item?.class_type,
)}
stroke="white"
strokeWidth="3"
strokeWidth={pointStroke}
style={{ cursor: onSeekToTime ? "pointer" : "default" }}
onClick={() => handlePointClick(pos.timestamp)}
/>
@ -400,7 +446,7 @@ export default function ObjectTrackOverlay({
height={objData.currentBox[3] * videoHeight}
fill="none"
stroke={objData.color}
strokeWidth="5"
strokeWidth={boxStroke}
opacity="0.9"
/>
<circle
@ -412,10 +458,10 @@ export default function ObjectTrackOverlay({
(objData.currentBox[1] + objData.currentBox[3]) *
videoHeight
}
r="5"
r={highlightRadius}
fill="rgb(255, 255, 0)" // yellow highlight
stroke={objData.color}
strokeWidth="5"
strokeWidth={boxStroke}
opacity="1"
/>
</g>

View File

@ -8,7 +8,7 @@ import Heading from "@/components/ui/heading";
import { FrigateConfig } from "@/types/frigateConfig";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { getIconForLabel } from "@/utils/iconUtil";
import { LuCircle, LuSettings } from "react-icons/lu";
import { LuCircle, LuFolderX, LuSettings } from "react-icons/lu";
import { cn } from "@/lib/utils";
import {
Tooltip,
@ -37,9 +37,12 @@ import { HiDotsHorizontal } from "react-icons/hi";
import axios from "axios";
import { toast } from "sonner";
import { useDetailStream } from "@/context/detail-stream-context";
import { isDesktop, isIOS } from "react-device-detect";
import { isDesktop, isIOS, isMobileOnly, isSafari } from "react-device-detect";
import Chip from "@/components/indicators/Chip";
import { FaDownload, FaHistory } from "react-icons/fa";
import { useApiHost } from "@/api";
import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator";
import ObjectTrackOverlay from "../ObjectTrackOverlay";
type TrackingDetailsProps = {
className?: string;
@ -56,9 +59,19 @@ export function TrackingDetails({
const videoRef = useRef<HTMLVideoElement | null>(null);
const { t } = useTranslation(["views/explore"]);
const navigate = useNavigate();
const apiHost = useApiHost();
const imgRef = useRef<HTMLImageElement | null>(null);
const [imgLoaded, setImgLoaded] = useState(false);
const [displaySource, _setDisplaySource] = useState<"video" | "image">(
"video",
);
const { setSelectedObjectIds, annotationOffset, setAnnotationOffset } =
useDetailStream();
// manualOverride holds a record-stream timestamp explicitly chosen by the
// user (eg, clicking a lifecycle row). When null we display `currentTime`.
const [manualOverride, setManualOverride] = useState<number | null>(null);
// event.start_time is detect time, convert to record, then subtract padding
const [currentTime, setCurrentTime] = useState(
(event.start_time ?? 0) + annotationOffset / 1000 - REVIEW_PADDING,
@ -73,9 +86,13 @@ export function TrackingDetails({
const { data: config } = useSWR<FrigateConfig>("config");
// Use manualOverride (set when seeking in image mode) if present so
// lifecycle rows and overlays follow image-mode seeks. Otherwise fall
// back to currentTime used for video mode.
const effectiveTime = useMemo(() => {
return currentTime - annotationOffset / 1000;
}, [currentTime, annotationOffset]);
const displayedRecordTime = manualOverride ?? currentTime;
return displayedRecordTime - annotationOffset / 1000;
}, [manualOverride, currentTime, annotationOffset]);
const containerRef = useRef<HTMLDivElement | null>(null);
const [_selectedZone, setSelectedZone] = useState("");
@ -118,20 +135,30 @@ export function TrackingDetails({
const handleLifecycleClick = useCallback(
(item: TrackingDetailsSequence) => {
if (!videoRef.current) return;
if (!videoRef.current && !imgRef.current) return;
// Convert lifecycle timestamp (detect stream) to record stream time
const targetTimeRecord = item.timestamp + annotationOffset / 1000;
// Convert to video-relative time for seeking
if (displaySource === "image") {
// For image mode: set a manual override timestamp and update
// currentTime so overlays render correctly.
setManualOverride(targetTimeRecord);
setCurrentTime(targetTimeRecord);
return;
}
// For video mode: convert to video-relative time and seek player
const eventStartRecord =
(event.start_time ?? 0) + annotationOffset / 1000;
const videoStartTime = eventStartRecord - REVIEW_PADDING;
const relativeTime = targetTimeRecord - videoStartTime;
videoRef.current.currentTime = relativeTime;
if (videoRef.current) {
videoRef.current.currentTime = relativeTime;
}
},
[event.start_time, annotationOffset],
[event.start_time, annotationOffset, displaySource],
);
const formattedStart = config
@ -172,11 +199,20 @@ export function TrackingDetails({
}, [eventSequence]);
useEffect(() => {
if (seekToTimestamp === null || !videoRef.current) return;
if (seekToTimestamp === null) return;
if (displaySource === "image") {
// For image mode, set the manual override so the snapshot updates to
// the exact record timestamp.
setManualOverride(seekToTimestamp);
setSeekToTimestamp(null);
return;
}
// seekToTimestamp is a record stream timestamp
// event.start_time is detect stream time, convert to record
// The video clip starts at (eventStartRecord - REVIEW_PADDING)
if (!videoRef.current) return;
const eventStartRecord = event.start_time + annotationOffset / 1000;
const videoStartTime = eventStartRecord - REVIEW_PADDING;
const relativeTime = seekToTimestamp - videoStartTime;
@ -184,7 +220,14 @@ export function TrackingDetails({
videoRef.current.currentTime = relativeTime;
}
setSeekToTimestamp(null);
}, [seekToTimestamp, event.start_time, annotationOffset]);
}, [
seekToTimestamp,
event.start_time,
annotationOffset,
apiHost,
event.camera,
displaySource,
]);
const isWithinEventRange =
effectiveTime !== undefined &&
@ -287,6 +330,27 @@ export function TrackingDetails({
[event.start_time, annotationOffset],
);
const [src, setSrc] = useState(
`${apiHost}api/${event.camera}/recordings/${currentTime + REVIEW_PADDING}/snapshot.jpg?height=500`,
);
const [hasError, setHasError] = useState(false);
// Derive the record timestamp to display: manualOverride if present,
// otherwise use currentTime.
const displayedRecordTime = manualOverride ?? currentTime;
useEffect(() => {
if (displayedRecordTime) {
const newSrc = `${apiHost}api/${event.camera}/recordings/${displayedRecordTime}/snapshot.jpg?height=500`;
setSrc(newSrc);
}
setImgLoaded(false);
setHasError(false);
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [displayedRecordTime]);
if (!config) {
return <ActivityIndicator />;
}
@ -304,9 +368,10 @@ export function TrackingDetails({
<div
className={cn(
"flex w-full items-center justify-center",
"flex items-center justify-center",
isDesktop && "overflow-hidden",
cameraAspect === "tall" ? "max-h-[50dvh] lg:max-h-[70dvh]" : "w-full",
cameraAspect === "tall" && isMobileOnly && "w-full",
cameraAspect !== "tall" && isDesktop && "flex-[3]",
)}
style={{ aspectRatio: aspectRatio }}
@ -318,21 +383,75 @@ export function TrackingDetails({
cameraAspect === "tall" ? "h-full" : "w-full",
)}
>
<HlsVideoPlayer
videoRef={videoRef}
containerRef={containerRef}
visible={true}
currentSource={videoSource}
hotKeys={false}
supportsFullscreen={false}
fullscreen={false}
frigateControls={true}
onTimeUpdate={handleTimeUpdate}
onSeekToTime={handleSeekToTime}
isDetailMode={true}
camera={event.camera}
currentTimeOverride={currentTime}
/>
{displaySource == "video" && (
<HlsVideoPlayer
videoRef={videoRef}
containerRef={containerRef}
visible={true}
currentSource={videoSource}
hotKeys={false}
supportsFullscreen={false}
fullscreen={false}
frigateControls={true}
onTimeUpdate={handleTimeUpdate}
onSeekToTime={handleSeekToTime}
isDetailMode={true}
camera={event.camera}
currentTimeOverride={currentTime}
/>
)}
{displaySource == "image" && (
<>
<ImageLoadingIndicator
className="absolute inset-0"
imgLoaded={imgLoaded}
/>
{hasError && (
<div className="relative aspect-video">
<div className="flex flex-col items-center justify-center p-20 text-center">
<LuFolderX className="size-16" />
{t("objectLifecycle.noImageFound")}
</div>
</div>
)}
<div
className={cn("relative", imgLoaded ? "visible" : "invisible")}
>
<div className="absolute z-50 size-full">
<ObjectTrackOverlay
key={`overlay-${displayedRecordTime}`}
camera={event.camera}
showBoundingBoxes={true}
currentTime={displayedRecordTime}
videoWidth={imgRef?.current?.naturalWidth ?? 0}
videoHeight={imgRef?.current?.naturalHeight ?? 0}
className="absolute inset-0 z-10"
onSeekToTime={handleSeekToTime}
/>
</div>
<img
key={event.id}
ref={imgRef}
className={cn(
"max-h-[50dvh] max-w-full select-none rounded-lg object-contain",
)}
loading={isSafari ? "eager" : "lazy"}
style={
isIOS
? {
WebkitUserSelect: "none",
WebkitTouchCallout: "none",
}
: undefined
}
draggable={false}
src={src}
onLoad={() => setImgLoaded(true)}
onError={() => setHasError(true)}
/>
</div>
</>
)}
<div
className={cn(
"absolute top-2 z-[5] flex items-center gap-2",

View File

@ -174,9 +174,7 @@ export default function CameraWizardDialog({
...(friendlyName && { friendly_name: friendlyName }),
ffmpeg: {
inputs: wizardData.streams.map((stream, index) => {
const isRestreamed =
wizardData.restreamIds?.includes(stream.id) ?? false;
if (isRestreamed) {
if (stream.restream) {
const go2rtcStreamName =
wizardData.streams!.length === 1
? finalCameraName
@ -234,7 +232,11 @@ export default function CameraWizardDialog({
wizardData.streams!.length === 1
? finalCameraName
: `${finalCameraName}_${index + 1}`;
go2rtcStreams[streamName] = [stream.url];
const streamUrl = stream.useFfmpeg
? `ffmpeg:${stream.url}`
: stream.url;
go2rtcStreams[streamName] = [streamUrl];
});
if (Object.keys(go2rtcStreams).length > 0) {

View File

@ -608,6 +608,12 @@ export default function Step1NameCamera({
</div>
)}
{isTesting && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<ActivityIndicator className="size-4" />
{testStatus}
</div>
)}
<div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4">
<Button
type="button"
@ -635,10 +641,7 @@ export default function Step1NameCamera({
variant="select"
className="flex items-center justify-center gap-2 sm:flex-1"
>
{isTesting && <ActivityIndicator className="size-4" />}
{isTesting && testStatus
? testStatus
: t("cameraWizard.step1.testConnection")}
{t("cameraWizard.step1.testConnection")}
</Button>
)}
</div>

View File

@ -201,16 +201,12 @@ export default function Step2StreamConfig({
const setRestream = useCallback(
(streamId: string) => {
const currentIds = wizardData.restreamIds || [];
const isSelected = currentIds.includes(streamId);
const newIds = isSelected
? currentIds.filter((id) => id !== streamId)
: [...currentIds, streamId];
onUpdate({
restreamIds: newIds,
});
const stream = streams.find((s) => s.id === streamId);
if (!stream) return;
updateStream(streamId, { restream: !stream.restream });
},
[wizardData.restreamIds, onUpdate],
[streams, updateStream],
);
const hasDetectRole = streams.some((s) => s.roles.includes("detect"));
@ -435,9 +431,7 @@ export default function Step2StreamConfig({
{t("cameraWizard.step2.go2rtc")}
</span>
<Switch
checked={(wizardData.restreamIds || []).includes(
stream.id,
)}
checked={stream.restream || false}
onCheckedChange={() => setRestream(stream.id)}
/>
</div>

View File

@ -1,7 +1,13 @@
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useTranslation } from "react-i18next";
import { LuRotateCcw } from "react-icons/lu";
import { LuRotateCcw, LuInfo } from "react-icons/lu";
import { useState, useCallback, useMemo, useEffect } from "react";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import axios from "axios";
@ -216,7 +222,6 @@ export default function Step3Validation({
brandTemplate: wizardData.brandTemplate,
customUrl: wizardData.customUrl,
streams: wizardData.streams,
restreamIds: wizardData.restreamIds,
};
onSave(configData);
@ -322,6 +327,51 @@ export default function Step3Validation({
</div>
)}
{result?.success && (
<div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm">
{t("cameraWizard.step3.ffmpegModule")}
</span>
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-4 w-4 p-0"
>
<LuInfo className="size-3" />
</Button>
</PopoverTrigger>
<PopoverContent className="pointer-events-auto w-80 text-xs">
<div className="space-y-2">
<div className="font-medium">
{t("cameraWizard.step3.ffmpegModule")}
</div>
<div className="text-muted-foreground">
{t(
"cameraWizard.step3.ffmpegModuleDescription",
)}
</div>
</div>
</PopoverContent>
</Popover>
</div>
<Switch
checked={stream.useFfmpeg || false}
onCheckedChange={(checked) => {
onUpdate({
streams: streams.map((s) =>
s.id === stream.id
? { ...s, useFfmpeg: checked }
: s,
),
});
}}
/>
</div>
)}
<div className="mb-2 flex flex-col justify-between gap-1 md:flex-row md:items-center">
<span className="break-all text-sm text-muted-foreground">
{stream.url}
@ -491,8 +541,7 @@ function StreamIssues({
// Restreaming check
if (stream.roles.includes("record")) {
const restreamIds = wizardData.restreamIds || [];
if (restreamIds.includes(stream.id)) {
if (stream.restream) {
result.push({
type: "warning",
message: t("cameraWizard.step3.issues.restreamingWarning"),
@ -660,9 +709,10 @@ function StreamPreview({ stream, onBandwidthUpdate }: StreamPreviewProps) {
useEffect(() => {
// Register stream with go2rtc
const streamUrl = stream.useFfmpeg ? `ffmpeg:${stream.url}` : stream.url;
axios
.put(`go2rtc/streams/${streamId}`, null, {
params: { src: stream.url },
params: { src: streamUrl },
})
.then(() => {
// Add small delay to allow go2rtc api to run and initialize the stream
@ -680,7 +730,7 @@ function StreamPreview({ stream, onBandwidthUpdate }: StreamPreviewProps) {
// do nothing on cleanup errors - go2rtc won't consume the streams
});
};
}, [stream.url, streamId]);
}, [stream.url, stream.useFfmpeg, streamId]);
const resolution = stream.testResult?.resolution;
let aspectRatio = "16/9";

View File

@ -845,6 +845,7 @@ function FaceAttemptGroup({
selectedItems={selectedFaces}
i18nLibrary="views/faceLibrary"
objectType="person"
noClassificationLabel="details.unknown"
onClick={(data) => {
if (data) {
onClickFaces([data.filename], true);

View File

@ -85,6 +85,8 @@ export type StreamConfig = {
quality?: string;
testResult?: TestResult;
userTested?: boolean;
useFfmpeg?: boolean;
restream?: boolean;
};
export type TestResult = {
@ -105,7 +107,6 @@ export type WizardFormData = {
brandTemplate?: CameraBrand;
customUrl?: string;
streams?: StreamConfig[];
restreamIds?: string[];
};
// API Response Types
@ -146,6 +147,7 @@ export type CameraConfigData = {
inputs: {
path: string;
roles: string[];
input_args?: string;
}[];
};
live?: {

View File

@ -13,7 +13,8 @@ function formatZonesList(zones: string[]): string {
});
}
const allButLast = zones.slice(0, -1).join(", ");
const separatorWithSpace = t("list.separatorWithSpace", { ns: "common" });
const allButLast = zones.slice(0, -1).join(separatorWithSpace);
return t("list.many", {
items: allButLast,
last: zones[zones.length - 1],

View File

@ -10,7 +10,7 @@ import {
CustomClassificationModelConfig,
FrigateConfig,
} from "@/types/frigateConfig";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { FaFolderPlus } from "react-icons/fa";
import { MdModelTraining } from "react-icons/md";
@ -21,7 +21,6 @@ import Heading from "@/components/ui/heading";
import { useOverlayState } from "@/hooks/use-overlay-state";
import axios from "axios";
import { toast } from "sonner";
import useKeyboardListener from "@/hooks/use-keyboard-listener";
import {
DropdownMenu,
DropdownMenuContent,
@ -212,42 +211,44 @@ function ModelCard({ config, onClick, onDelete }: ModelCardProps) {
}>(`classification/${config.name}/dataset`, { revalidateOnFocus: false });
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const bypassDialogRef = useRef(false);
useKeyboardListener(["Shift"], (_, modifiers) => {
bypassDialogRef.current = modifiers.shift;
return false;
});
const handleDelete = useCallback(async () => {
await axios
.delete(`classification/${config.name}`)
.then((resp) => {
if (resp.status == 200) {
toast.success(t("toast.success.deletedModel", { count: 1 }), {
position: "top-center",
});
onDelete();
}
})
.catch((error) => {
const errorMessage =
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(t("toast.error.deleteModelFailed", { errorMessage }), {
position: "top-center",
});
try {
await axios.delete(`classification/${config.name}`);
await axios.put("/config/set", {
requires_restart: 0,
update_topic: `config/classification/custom/${config.name}`,
config_data: {
classification: {
custom: {
[config.name]: "",
},
},
},
});
toast.success(t("toast.success.deletedModel", { count: 1 }), {
position: "top-center",
});
onDelete();
} catch (err) {
const error = err as {
response?: { data?: { message?: string; detail?: string } };
};
const errorMessage =
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(t("toast.error.deleteModelFailed", { errorMessage }), {
position: "top-center",
});
}
}, [config, onDelete, t]);
const handleDeleteClick = useCallback(() => {
if (bypassDialogRef.current) {
handleDelete();
} else {
setDeleteDialogOpen(true);
}
}, [handleDelete]);
const handleDeleteClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
setDeleteDialogOpen(true);
}, []);
const coverImage = useMemo(() => {
if (!dataset) {
@ -304,7 +305,7 @@ function ModelCard({ config, onClick, onDelete }: ModelCardProps) {
className="size-full"
src={`${baseUrl}clips/${config.name}/dataset/${coverImage?.name}/${coverImage?.img}`}
/>
<ImageShadowOverlay />
<ImageShadowOverlay lowerClassName="h-[30%] z-0" />
<div className="absolute bottom-2 left-3 text-lg text-white smart-capitalize">
{config.name}
</div>
@ -315,14 +316,13 @@ function ModelCard({ config, onClick, onDelete }: ModelCardProps) {
<FiMoreVertical className="size-5 text-white" />
</BlurredIconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent
align="end"
onClick={(e) => e.stopPropagation()}
>
<DropdownMenuItem onClick={handleDeleteClick}>
<LuTrash2 className="mr-2 size-4" />
<span>
{bypassDialogRef.current
? t("button.deleteNow", { ns: "common" })
: t("button.delete", { ns: "common" })}
</span>
<span>{t("button.delete", { ns: "common" })}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -961,6 +961,7 @@ function ObjectTrainGrid({
selectedItems={selectedImages}
i18nLibrary="views/classificationModel"
objectType={model.object_config?.objects?.at(0) ?? "Object"}
noClassificationLabel="details.none"
onClick={(data) => {
if (data) {
onClickImages([data.filename], true);