mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-19 05:02:49 +03:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a0c4c593a | ||
|
|
b1fb109094 | ||
|
|
327a15b317 | ||
|
|
7366488ff5 | ||
|
|
aebafb3ef3 | ||
|
|
15f49d6171 | ||
|
|
5a0ea5907e | ||
|
|
c7e68a9546 |
@@ -72,10 +72,10 @@ Variables prefixed with `FRIGATE_` can be referenced in config fields that suppo
|
||||
|
||||
Navigate to <NavPath path="Settings > System > Environment variables" /> to add or edit environment variables.
|
||||
|
||||
| Field | Description |
|
||||
| ----------------- | --------------------------------------------------------- |
|
||||
| **Variable name** | The environment variable name (e.g., `FRIGATE_MQTT_USER`) |
|
||||
| **Value** | The value for the variable |
|
||||
| Field | Description |
|
||||
| --------- | --------------------------------------------------------- |
|
||||
| **Key** | The environment variable name (e.g., `FRIGATE_MQTT_USER`) |
|
||||
| **Value** | The value for the variable |
|
||||
|
||||
Variables defined here can be referenced elsewhere in your configuration using the `{FRIGATE_VARIABLE_NAME}` syntax.
|
||||
|
||||
|
||||
@@ -200,9 +200,6 @@ class EmbeddingMaintainer(threading.Thread):
|
||||
)
|
||||
|
||||
for model_config in self.config.classification.custom.values():
|
||||
if not model_config.enabled:
|
||||
continue
|
||||
|
||||
self.realtime_processors.append(
|
||||
CustomStateClassificationProcessor(
|
||||
self.config, model_config, self.requestor, self.metrics
|
||||
@@ -335,25 +332,6 @@ class EmbeddingMaintainer(threading.Thread):
|
||||
for processor in self.post_processors:
|
||||
processor.update_config(topic, payload)
|
||||
|
||||
def _remove_custom_classification_processor(self, model_name: str) -> None:
|
||||
"""Shut down and drop any running processor for a custom model."""
|
||||
remaining = []
|
||||
for processor in self.realtime_processors:
|
||||
if (
|
||||
isinstance(
|
||||
processor,
|
||||
(
|
||||
CustomStateClassificationProcessor,
|
||||
CustomObjectClassificationProcessor,
|
||||
),
|
||||
)
|
||||
and processor.model_config.name == model_name
|
||||
):
|
||||
processor.shutdown()
|
||||
else:
|
||||
remaining.append(processor)
|
||||
self.realtime_processors = remaining
|
||||
|
||||
def _handle_custom_classification_update(
|
||||
self, topic: str, model_config: Any
|
||||
) -> None:
|
||||
@@ -361,7 +339,23 @@ class EmbeddingMaintainer(threading.Thread):
|
||||
model_name = topic.split("/")[-1]
|
||||
|
||||
if model_config is None:
|
||||
self._remove_custom_classification_processor(model_name)
|
||||
remaining = []
|
||||
for processor in self.realtime_processors:
|
||||
if (
|
||||
isinstance(
|
||||
processor,
|
||||
(
|
||||
CustomStateClassificationProcessor,
|
||||
CustomObjectClassificationProcessor,
|
||||
),
|
||||
)
|
||||
and processor.model_config.name == model_name
|
||||
):
|
||||
processor.shutdown()
|
||||
else:
|
||||
remaining.append(processor)
|
||||
self.realtime_processors = remaining
|
||||
|
||||
logger.info(
|
||||
f"Successfully removed classification processor for model: {model_name}"
|
||||
)
|
||||
@@ -369,13 +363,6 @@ class EmbeddingMaintainer(threading.Thread):
|
||||
|
||||
self.config.classification.custom[model_name] = model_config
|
||||
|
||||
# A disabled model must not run; tear down any existing processor and
|
||||
# do not register a new one.
|
||||
if not model_config.enabled:
|
||||
self._remove_custom_classification_processor(model_name)
|
||||
logger.info(f"Disabled classification processor for model: {model_name}")
|
||||
return
|
||||
|
||||
# Check if processor already exists
|
||||
for processor in self.realtime_processors:
|
||||
if isinstance(
|
||||
@@ -715,11 +702,7 @@ class EmbeddingMaintainer(threading.Thread):
|
||||
and "license_plate" not in camera_config.objects.track
|
||||
)
|
||||
|
||||
has_enabled_custom = any(
|
||||
c.enabled for c in self.config.classification.custom.values()
|
||||
)
|
||||
|
||||
if not dedicated_lpr_enabled and not has_enabled_custom:
|
||||
if not dedicated_lpr_enabled and len(self.config.classification.custom) == 0:
|
||||
# no active features that use this data
|
||||
return
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
"""Tests that disabled custom classification models are not registered or run."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Mock TFLite before importing the maintainer / classification modules
|
||||
_MOCK_MODULES = [
|
||||
"tflite_runtime",
|
||||
"tflite_runtime.interpreter",
|
||||
"ai_edge_litert",
|
||||
"ai_edge_litert.interpreter",
|
||||
]
|
||||
for mod in _MOCK_MODULES:
|
||||
if mod not in sys.modules:
|
||||
sys.modules[mod] = MagicMock()
|
||||
|
||||
from frigate.data_processing.real_time.custom_classification import ( # noqa: E402
|
||||
CustomObjectClassificationProcessor,
|
||||
)
|
||||
from frigate.embeddings.maintainer import EmbeddingMaintainer # noqa: E402
|
||||
|
||||
|
||||
class TestCustomClassificationEnabledGating(unittest.TestCase):
|
||||
"""A model with enabled: false must not keep a processor registered."""
|
||||
|
||||
def _make_maintainer(self) -> EmbeddingMaintainer:
|
||||
# Bypass the heavy __init__; only the attributes touched by the
|
||||
# config update path are needed for these tests.
|
||||
maintainer = EmbeddingMaintainer.__new__(EmbeddingMaintainer)
|
||||
maintainer.realtime_processors = []
|
||||
maintainer.config = MagicMock()
|
||||
maintainer.config.classification.custom = {}
|
||||
maintainer.requestor = MagicMock()
|
||||
maintainer.metrics = MagicMock()
|
||||
maintainer.event_metadata_publisher = MagicMock()
|
||||
return maintainer
|
||||
|
||||
def _make_model_config(self, name: str, enabled: bool) -> MagicMock:
|
||||
model_config = MagicMock()
|
||||
model_config.name = name
|
||||
model_config.enabled = enabled
|
||||
model_config.state_config = None
|
||||
return model_config
|
||||
|
||||
def _make_processor(self, name: str) -> MagicMock:
|
||||
processor = MagicMock(spec=CustomObjectClassificationProcessor)
|
||||
processor.model_config = MagicMock()
|
||||
processor.model_config.name = name
|
||||
return processor
|
||||
|
||||
def test_disabled_update_tears_down_existing_processor(self):
|
||||
"""Toggling a running model to disabled shuts down and drops its processor."""
|
||||
maintainer = self._make_maintainer()
|
||||
processor = self._make_processor("atli")
|
||||
maintainer.realtime_processors = [processor]
|
||||
|
||||
maintainer._handle_custom_classification_update(
|
||||
"config/classification/custom/atli",
|
||||
self._make_model_config("atli", enabled=False),
|
||||
)
|
||||
|
||||
processor.shutdown.assert_called_once()
|
||||
self.assertEqual(maintainer.realtime_processors, [])
|
||||
|
||||
def test_disabled_update_does_not_register_processor(self):
|
||||
"""A disabled model that has no processor is never registered."""
|
||||
maintainer = self._make_maintainer()
|
||||
|
||||
maintainer._handle_custom_classification_update(
|
||||
"config/classification/custom/atli",
|
||||
self._make_model_config("atli", enabled=False),
|
||||
)
|
||||
|
||||
self.assertEqual(maintainer.realtime_processors, [])
|
||||
|
||||
def test_disabled_update_leaves_other_processors_untouched(self):
|
||||
"""Disabling one model must not affect other running processors."""
|
||||
maintainer = self._make_maintainer()
|
||||
other = self._make_processor("simbi")
|
||||
maintainer.realtime_processors = [other]
|
||||
|
||||
maintainer._handle_custom_classification_update(
|
||||
"config/classification/custom/atli",
|
||||
self._make_model_config("atli", enabled=False),
|
||||
)
|
||||
|
||||
other.shutdown.assert_not_called()
|
||||
self.assertEqual(maintainer.realtime_processors, [other])
|
||||
|
||||
def test_removed_model_tears_down_processor(self):
|
||||
"""A None payload (model deleted) still shuts down its processor."""
|
||||
maintainer = self._make_maintainer()
|
||||
processor = self._make_processor("atli")
|
||||
maintainer.realtime_processors = [processor]
|
||||
|
||||
maintainer._handle_custom_classification_update(
|
||||
"config/classification/custom/atli", None
|
||||
)
|
||||
|
||||
processor.shutdown.assert_called_once()
|
||||
self.assertEqual(maintainer.realtime_processors, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1490,14 +1490,7 @@
|
||||
"keyLabel": "Key",
|
||||
"valueLabel": "Value",
|
||||
"keyPlaceholder": "New key",
|
||||
"remove": "Remove",
|
||||
"providerNameLabel": "Provider name",
|
||||
"providerNamePlaceholder": "e.g., openai",
|
||||
"variableNameLabel": "Variable name",
|
||||
"variableNamePlaceholder": "e.g., MY_VARIABLE",
|
||||
"loggerNameLabel": "Logger name",
|
||||
"loggerNamePlaceholder": "e.g., frigate.record",
|
||||
"keyPatternError": "Use only letters, numbers, hyphens, and underscores (no spaces)"
|
||||
"remove": "Remove"
|
||||
},
|
||||
"knownPlates": {
|
||||
"namePlaceholder": "e.g., Wife's Car",
|
||||
|
||||
@@ -24,9 +24,6 @@
|
||||
},
|
||||
"state": {
|
||||
"submitted": "Terkirim"
|
||||
},
|
||||
"toast": {
|
||||
"error": "Gagal submit ke Frigate+. Harap periksa koneksi jaringan Anda dan coba lagi."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -60,8 +57,7 @@
|
||||
"view": "Melihat",
|
||||
"batchSuccess_other": "{{count}} Ekspor dimulai. Membuka kasusnya sekarang.",
|
||||
"batchPartial": "Ekspor berhasil dimulai sebanyak {{successful}} dari total {{total}} ekspor. Kamera yang gagal: {{failedCameras}}",
|
||||
"batchFailed": "Gagal memulai ekspor sebanyak {{total}}. Kamera yang gagal: {{failedCameras}}",
|
||||
"batchQueuedPartial": "Antrian ekspor berhasil sebanyak {{successful}} dari total {{total}} ekspor. Kamera yang gagal: {{failedCameras}}"
|
||||
"batchFailed": "Gagal memulai ekspor sebanyak {{total}}. Kamera yang gagal: {{failedCameras}}"
|
||||
},
|
||||
"case": {
|
||||
"newCaseOption": "Membuat Kasus Baru",
|
||||
@@ -91,9 +87,7 @@
|
||||
"selectGroup": "Pilih grup",
|
||||
"noMatchingCameras": "Tidak ada kamera yang sesuai dengan pencarian Anda",
|
||||
"selectedCount": "{{terpilih}} / {{total}} terpilih",
|
||||
"namePlaceholder": "Nama dasar opsional untuk ekspor ini",
|
||||
"searchOrSelectGroup": "Cari, atau pilih grup kamera...",
|
||||
"selectAll": "Pilih semua kamera"
|
||||
"namePlaceholder": "Nama dasar opsional untuk ekspor ini"
|
||||
},
|
||||
"multi": {
|
||||
"title_other": "Ekspor {{count}} Ulasan",
|
||||
|
||||
@@ -68,8 +68,8 @@
|
||||
"choir": "Koris",
|
||||
"yodeling": "Jodelēšana",
|
||||
"mantra": "Mantra",
|
||||
"rapping": "Repošana",
|
||||
"whistling": "Svilpošana",
|
||||
"rapping": "Repot",
|
||||
"whistling": "Svilpot",
|
||||
"sniff": "Ošņāt",
|
||||
"hands": "Rokas",
|
||||
"animal": "Dzīvnieks",
|
||||
@@ -122,17 +122,5 @@
|
||||
"fireworks": "Uguņošana",
|
||||
"glass": "Stikls",
|
||||
"white_noise": "Baltais troksnis",
|
||||
"radio": "Radio",
|
||||
"chant": "Piedziedājums",
|
||||
"synthetic_singing": "Sintētiskā dziedāšana",
|
||||
"groan": "Vaidēšana",
|
||||
"grunt": "Rukšķēšana",
|
||||
"breathing": "Elpošana",
|
||||
"wheeze": "Gārgšana",
|
||||
"snoring": "Krākšana",
|
||||
"gasp": "Elsošana",
|
||||
"pant": "Elšana",
|
||||
"snort": "Šņaukt",
|
||||
"cough": "Klepus",
|
||||
"throat_clearing": "Kakla tīrīšana"
|
||||
"radio": "Radio"
|
||||
}
|
||||
|
||||
@@ -64,8 +64,7 @@
|
||||
"error": {
|
||||
"failed": "Kunne ikke legge eksport i kø: {{error}}",
|
||||
"noVaildTimeSelected": "Ingen gyldig tidsperiode valgt",
|
||||
"endTimeMustAfterStartTime": "Sluttid må være etter starttid",
|
||||
"noValidTimeSelected": "Ingen gyldig tidsperiode valgt"
|
||||
"endTimeMustAfterStartTime": "Sluttid må være etter starttid"
|
||||
},
|
||||
"view": "Vis",
|
||||
"queued": "Eksport lagt i kø. Se fremdrift på eksportsiden.",
|
||||
|
||||
@@ -99,8 +99,7 @@
|
||||
"error": {
|
||||
"failed": "Nu s-a putut adăuga exportul în coadă: {{error}}",
|
||||
"endTimeMustAfterStartTime": "Ora de sfârșit trebuie să fie după ora de început",
|
||||
"noVaildTimeSelected": "Nu a fost selectat un interval de timp valid",
|
||||
"noValidTimeSelected": "Niciun interval de timp valid selectat"
|
||||
"noVaildTimeSelected": "Nu a fost selectat un interval de timp valid"
|
||||
},
|
||||
"view": "Vizualizează",
|
||||
"queued": "Export pus la coadă. Vezi progresul pe pagina de exporturi.",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"time": {
|
||||
"untilForTime": "Do {{time}}",
|
||||
"untilForTime": "Do{{time}}",
|
||||
"untilForRestart": "Do reštartu Frigate.",
|
||||
"untilRestart": "Do reštartu",
|
||||
"ago": "pred {{timeAgo}}",
|
||||
"ago": "{{timeAgo}} pred časom",
|
||||
"justNow": "Práve teraz",
|
||||
"today": "Dnes",
|
||||
"yesterday": "Včera",
|
||||
@@ -23,32 +23,32 @@
|
||||
"am": "ráno",
|
||||
"yr": "{{time}}r",
|
||||
"pm": "popoludní",
|
||||
"year_one": "{{time}} rok",
|
||||
"year_few": "{{time}} roky",
|
||||
"year_other": "{{time}} rokov",
|
||||
"year_one": "{{time}}rok",
|
||||
"year_few": "{{time}}rokov",
|
||||
"year_other": "{{time}}rokov",
|
||||
"mo": "{{time}}mes",
|
||||
"month_one": "{{time}} mesiac",
|
||||
"month_one": "{{time}}mesiac",
|
||||
"month_few": "{{time}} mesiace",
|
||||
"month_other": "{{time}} mesiacov",
|
||||
"month_other": "{{time}} mesiaca",
|
||||
"d": "{{time}}d",
|
||||
"day_one": "{{time}} deň",
|
||||
"day_few": "{{time}} dni",
|
||||
"day_other": "{{time}} dní",
|
||||
"day_one": "{{time}}deň",
|
||||
"day_few": "{{time}}dni",
|
||||
"day_other": "{{time}}dni",
|
||||
"h": "{{time}}h",
|
||||
"hour_one": "{{time}} hodina",
|
||||
"hour_few": "{{time}} hodiny",
|
||||
"hour_other": "{{time}} hodín",
|
||||
"hour_one": "{{time}}hodina",
|
||||
"hour_few": "{{time}}hodiny",
|
||||
"hour_other": "{{time}}hodin",
|
||||
"m": "{{time}} min",
|
||||
"s": "{{time}}s",
|
||||
"minute_one": "{{time}} minúta",
|
||||
"minute_few": "{{time}} minúty",
|
||||
"minute_other": "{{time}} minút",
|
||||
"second_one": "{{time}} sekunda",
|
||||
"second_few": "{{time}} sekundy",
|
||||
"second_other": "{{time}} sekúnd",
|
||||
"minute_one": "{{time}}minuta",
|
||||
"minute_few": "{{time}}minuty",
|
||||
"minute_other": "{{time}}minut",
|
||||
"second_one": "{{time}}sekunda",
|
||||
"second_few": "{{time}}sekundy",
|
||||
"second_other": "{{time}}sekund",
|
||||
"formattedTimestamp": {
|
||||
"12hour": "d MMM, h:mm:ss aaa",
|
||||
"24hour": "d MMM, HH:mm:ss"
|
||||
"12hour": "Deň MMM, h:mm:ss aaa",
|
||||
"24hour": "Deň MMM, HH:mm:ss"
|
||||
},
|
||||
"formattedTimestamp2": {
|
||||
"12hour": "MM/dd h:mm:ssa",
|
||||
@@ -219,9 +219,9 @@
|
||||
"allCameras": "Všetky kamery",
|
||||
"cameras": {
|
||||
"title": "Kamery",
|
||||
"count_one": "{{count}} kamera",
|
||||
"count_few": "{{count}} kamery",
|
||||
"count_other": "{{count}} kamier"
|
||||
"count_one": "{{count}}kamera",
|
||||
"count_few": "{{count}}kamery",
|
||||
"count_other": "{{count}}kamier"
|
||||
}
|
||||
},
|
||||
"export": "Exportovať",
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
"title": "Čas ukončenia",
|
||||
"label": "Vybrat čas ukončenia"
|
||||
},
|
||||
"lastHour_one": "Posledná hodina",
|
||||
"lastHour_few": "Posledné {{count}} hodiny",
|
||||
"lastHour_other": "Posledných {{count}} hodín"
|
||||
"lastHour_one": "Minulu hodinu",
|
||||
"lastHour_few": "Minule{{count}}hodiny",
|
||||
"lastHour_other": "Minulych{{count}}hodin"
|
||||
},
|
||||
"name": {
|
||||
"placeholder": "Pomenujte Export"
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
},
|
||||
"toast": {
|
||||
"success": {
|
||||
"deletedCategory_one": "Vymazaná {{count}} trieda",
|
||||
"deletedCategory_few": "Vymazané {{count}} triedy",
|
||||
"deletedCategory_other": "Vymazaných {{count}} tried",
|
||||
"deletedImage_one": "Vymazaný {{count}} obrázok",
|
||||
"deletedImage_few": "Vymazané {{count}} obrázky",
|
||||
"deletedImage_other": "Vymazaných {{count}} obrázkov",
|
||||
"deletedCategory_one": "Vymazaná Trieda",
|
||||
"deletedCategory_few": "",
|
||||
"deletedCategory_other": "",
|
||||
"deletedImage_one": "Vymazané Obrázky",
|
||||
"deletedImage_few": "",
|
||||
"deletedImage_other": "",
|
||||
"categorizedImage": "Obrázok bol úspešne klasifikovaný",
|
||||
"trainedModel": "Úspešne vyškolený model.",
|
||||
"trainingModel": "Úspešne spustené trénovanie modelu.",
|
||||
@@ -134,7 +134,7 @@
|
||||
},
|
||||
"step3": {
|
||||
"selectImagesPrompt": "Vybrať všetky obrázky s: {{className}}",
|
||||
"selectImagesDescription": "Kliknite na obrázky a vyberte ich. Po dokončení tejto triedy kliknite na tlačidlo Pokračovať.",
|
||||
"selectImagesDescription": "Kliknite na obrázky a vyberte ich. Po dokončení tejto hodiny kliknite na tlačidlo Pokračovať.",
|
||||
"generating": {
|
||||
"title": "Generovanie vzorových obrázkov",
|
||||
"description": "Frigate načítava reprezentatívne obrázky z vašich nahrávok. Môže to chvíľu trvať..."
|
||||
@@ -155,9 +155,9 @@
|
||||
"classifyFailed": "Nepodarilo sa klasifikovať obrázky: {{error}}"
|
||||
},
|
||||
"generateSuccess": "Vzorové obrázky boli úspešne vygenerované",
|
||||
"allImagesRequired_one": "Klasifikujte všetky obrázky. Zostáva {{count}} obrázok.",
|
||||
"allImagesRequired_few": "Klasifikujte všetky obrázky. Zostávajú {{count}} obrázky.",
|
||||
"allImagesRequired_other": "Klasifikujte všetky obrázky. Zostáva {{count}} obrázkov.",
|
||||
"allImagesRequired_one": "Uveďte všetky obrázky. {{count}} obrázok zostáva.",
|
||||
"allImagesRequired_few": "Uveďte všetky obrázky. {{count}} obrázky zostávajú.",
|
||||
"allImagesRequired_other": "Uveďte všetky obrázky. {{count}} obrázkov zostávajú.",
|
||||
"modelCreated": "Model vytvorený úspešne. Použite aktuálne klasifikácie na pridanie obrázkov pre chýbajúce stavy a nasledne dajte trénovať model.",
|
||||
"missingStatesWarning": {
|
||||
"title": "Chýbajúce príklady stavov",
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
"detail": {
|
||||
"noDataFound": "Žiadne podrobné údaje na kontrolu",
|
||||
"aria": "Prepnúť zobrazenie detailov",
|
||||
"trackedObject_one": "{{count}} objekt",
|
||||
"trackedObject_other": "{{count}} objektov",
|
||||
"trackedObject_one": "objekt",
|
||||
"trackedObject_other": "objekty",
|
||||
"noObjectDetailData": "Nie sú k dispozícii žiadne podrobné údaje o objekte.",
|
||||
"label": "Detail",
|
||||
"settings": "Nastavenia podrobného zobrazenia",
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
"title": "Nahrať obrázok tváre",
|
||||
"desc": "Nahrajte obrázok na skenovanie tvárí a zahrňte ho do {{pageToggle}}"
|
||||
},
|
||||
"collections": "Kolekcie",
|
||||
"collections": "Zbierky",
|
||||
"createFaceLibrary": {
|
||||
"title": "Vytvoriť Zbierku",
|
||||
"desc": "Vytvoriť novú zbierku",
|
||||
"new": "Vytvoriť novú tvár",
|
||||
"nextSteps": "Vybudovanie silného základu:<li>Použite kartu Nedávno rozpoznané tváre 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>"
|
||||
"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",
|
||||
@@ -34,8 +34,8 @@
|
||||
}
|
||||
},
|
||||
"train": {
|
||||
"title": "Nedávno rozpoznané tváre",
|
||||
"aria": "Vyberte nedávno rozpoznané tváre",
|
||||
"title": "Nedávne uznania",
|
||||
"aria": "Vyberte posledné rozpoznania",
|
||||
"empty": "Neexistujú žiadne predchádzajúce pokusy o rozpoznávanie tváre"
|
||||
},
|
||||
"selectItem": "Vyberte {{item}}",
|
||||
@@ -85,10 +85,9 @@
|
||||
"deletedName_one": "{{count}} tvár bola úspešne odstránená.",
|
||||
"deletedName_few": "{{count}} tváre boli úspešne odstránené.",
|
||||
"deletedName_other": "{{count}} tvárí bolo úspešne odstránených.",
|
||||
"renamedFace": "Tvár bola úspešne premenovaná na {{name}}.",
|
||||
"trainedFace": "Tvár bola úspešne natrénovaná.",
|
||||
"updatedFaceScore": "Skóre tváre bolo úspešne aktualizované na {{name}} ({{score}}).",
|
||||
"reclassifiedFace": "Tvár bola úspešne preklasifikovaná."
|
||||
"renamedFace": "Úspešne premenovaná tvár na {{name}}",
|
||||
"trainedFace": "Úspešne natrénovaná tvár.",
|
||||
"updatedFaceScore": "Úspešne aktualizované skóre tváre."
|
||||
},
|
||||
"error": {
|
||||
"uploadingImageFailed": "Nepodarilo sa nahrať obrázok: {{errorMessage}}",
|
||||
@@ -96,9 +95,8 @@
|
||||
"deleteFaceFailed": "Nepodarilo sa odstrániť: {{errorMessage}}",
|
||||
"deleteNameFailed": "Nepodarilo sa odstrániť meno: {{errorMessage}}",
|
||||
"renameFaceFailed": "Nepodarilo sa premenovať tvár: {{errorMessage}}",
|
||||
"trainFailed": "Nepodarilo sa natrénovať tvár: {{errorMessage}}",
|
||||
"updateFaceScoreFailed": "Nepodarilo sa aktualizovať skóre tváre: {{errorMessage}}",
|
||||
"reclassifyFailed": "Nepodarilo sa preklasifikovať tvár: {{errorMessage}}"
|
||||
"trainFailed": "Nepodarilo sa trénovať: {{errorMessage}}",
|
||||
"updateFaceScoreFailed": "Nepodarilo sa aktualizovať skóre tváre: {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,9 +339,9 @@
|
||||
},
|
||||
"add": "Pridať zónu",
|
||||
"edit": "Upraviť zónu",
|
||||
"point_one": "{{count}} bod",
|
||||
"point_few": "{{count}} body",
|
||||
"point_other": "{{count}} bodov"
|
||||
"point_one": "{{count}}bod",
|
||||
"point_few": "{{count}}body",
|
||||
"point_other": "{{count}}bodov"
|
||||
},
|
||||
"motionMasks": {
|
||||
"label": "Maska Detekcia pohybu",
|
||||
@@ -380,8 +380,8 @@
|
||||
"add": "Pridať Masku Objektu",
|
||||
"edit": "Upraviť Masku Objektu",
|
||||
"context": "Masky filtrovania objektov slúžia na odfiltrovanie falošných poplachov konkrétneho typu objektu na základe jeho umiestnenia.",
|
||||
"point_one": "{{count}} bod",
|
||||
"point_few": "{{count}} body",
|
||||
"point_one": "{{count}}bod",
|
||||
"point_few": "{{count}}body",
|
||||
"point_other": "{{count}} bodov",
|
||||
"clickDrawPolygon": "Kliknutím nakreslite polygón do obrázku.",
|
||||
"objects": {
|
||||
@@ -397,7 +397,7 @@
|
||||
}
|
||||
},
|
||||
"motionMaskLabel": "Maska Detekcia pohybu {{number}}",
|
||||
"objectMaskLabel": "Maska objektu {{number}}"
|
||||
"objectMaskLabel": "Maska Objektu {{number}} {{label}}"
|
||||
},
|
||||
"motionDetectionTuner": {
|
||||
"title": "Ladenie detekcie pohybu",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"cameras": "Štatistiky kamier - Frigate",
|
||||
"storage": "Štatistiky úložiska - Frigate",
|
||||
"general": "Všeobecné štatistiky - Frigate",
|
||||
"enrichments": "Štatistiky AI funkcií - Frigate",
|
||||
"enrichments": "Štatistiky obohatenia - Frigate",
|
||||
"logs": {
|
||||
"frigate": "Protokoly Frigate - Frigate",
|
||||
"go2rtc": "Protokoly Go2RTC - Frigate",
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"title": "Všeobecné",
|
||||
"title": "Hlavný",
|
||||
"detector": {
|
||||
"title": "Detektory",
|
||||
"inferenceSpeed": "Detekčná rýchlosť",
|
||||
@@ -96,7 +96,7 @@
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"title": "Úložisko",
|
||||
"title": "Skladovanie",
|
||||
"overview": "Prehľad",
|
||||
"recordings": {
|
||||
"title": "Nahrávky",
|
||||
@@ -108,11 +108,11 @@
|
||||
"warning": "Aktuálna veľkosť SHM {{total}}MB je príliš malá. Zvýšte ju aspoň na {{min_shm}}MB."
|
||||
},
|
||||
"cameraStorage": {
|
||||
"title": "Úložisko kamier",
|
||||
"title": "Úložisko kamery",
|
||||
"camera": "Kamera",
|
||||
"unusedStorageInformation": "Informácie o nevyužitom úložisku",
|
||||
"storageUsed": "Úložisko",
|
||||
"percentageOfTotalUsed": "Percento z celku",
|
||||
"unusedStorageInformation": "Nepoužité informácie o úložisku",
|
||||
"storageUsed": "Skladovanie",
|
||||
"percentageOfTotalUsed": "Percento z celkového počtu",
|
||||
"bandwidth": "Šírka pásma",
|
||||
"unused": {
|
||||
"title": "Nepoužité",
|
||||
@@ -125,7 +125,7 @@
|
||||
"overview": "Prehľad",
|
||||
"info": {
|
||||
"aspectRatio": "pomer strán",
|
||||
"cameraProbeInfo": "Údaje o kamere {{camera}}",
|
||||
"cameraProbeInfo": "{{camera}} Informácie o sonde kamery",
|
||||
"streamDataFromFFPROBE": "Údaje zo streamu sa získavajú pomocou príkazu <code>ffprobe</code>.",
|
||||
"fetching": "Načítavajú sa údaje z kamery",
|
||||
"stream": "Stream {{idx}}",
|
||||
@@ -137,33 +137,32 @@
|
||||
"audio": "Zvuk:",
|
||||
"error": "Chyba: {{error}}",
|
||||
"tips": {
|
||||
"title": "Údaje o kamere"
|
||||
"title": "Informácie o kamerovej sonde"
|
||||
}
|
||||
},
|
||||
"framesAndDetections": "Snímky / Detekcie",
|
||||
"framesAndDetections": "Rámy / Detekcie",
|
||||
"label": {
|
||||
"camera": "kamera",
|
||||
"detect": "detekcie",
|
||||
"detect": "odhaliť",
|
||||
"skipped": "preskočené",
|
||||
"ffmpeg": "FFmpeg",
|
||||
"capture": "zachytávanie",
|
||||
"capture": "zachytiť",
|
||||
"cameraFfmpeg": "{{camName}} FFmpeg",
|
||||
"cameraCapture": "{{camName}} zachytávanie",
|
||||
"cameraDetect": "{{camName}} detekcie",
|
||||
"cameraCapture": "zachytiť{{camName}}",
|
||||
"cameraDetect": "Detekcia {{camName}}",
|
||||
"overallFramesPerSecond": "celkový počet snímok za sekundu",
|
||||
"overallDetectionsPerSecond": "celkový počet detekcií za sekundu",
|
||||
"overallSkippedDetectionsPerSecond": "celkový počet preskočených detekcií za sekundu",
|
||||
"overallSkippedDetectionsPerSecond": "celkový počet vynechaných detekcií za sekundu",
|
||||
"cameraFramesPerSecond": "{{camName}} snímky za sekundu",
|
||||
"cameraDetectionsPerSecond": "{{camName}} detekcie za sekundu",
|
||||
"cameraSkippedDetectionsPerSecond": "{{camName}} preskočené detekcie za sekundu",
|
||||
"cameraGpu": "{{camName}} GPU"
|
||||
"cameraDetectionsPerSecond": "{{camName}}detekcie za sekundu",
|
||||
"cameraSkippedDetectionsPerSecond": "{{camName}} vynechaných detekcií za sekundu"
|
||||
},
|
||||
"toast": {
|
||||
"success": {
|
||||
"copyToClipboard": "Údaje o kamere boli skopírované do schránky."
|
||||
"copyToClipboard": "Dáta sondy boli skopírované do schránky."
|
||||
},
|
||||
"error": {
|
||||
"unableToProbeCamera": "Nepodarilo sa načítať údaje z kamery: {{errorMessage}}"
|
||||
"unableToProbeCamera": "Nepodarilo sa overiť kameru: {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -179,7 +178,7 @@
|
||||
"shmTooLow": "Alokácia /dev/shm ({{total}} MB) by sa mala zvýšiť aspoň na {{min}} MB."
|
||||
},
|
||||
"enrichments": {
|
||||
"title": "AI funkcie",
|
||||
"title": "Obohatenia",
|
||||
"infPerSecond": "Inferencie za sekundu",
|
||||
"embeddings": {
|
||||
"image_embedding": "Vkladanie obrázkov",
|
||||
|
||||
@@ -7,13 +7,7 @@ const environmentVars: SectionConfigOverrides = {
|
||||
advancedFields: [],
|
||||
uiSchema: {
|
||||
additionalProperties: {
|
||||
"ui:options": {
|
||||
size: "lg",
|
||||
additionalPropertyKeyLabel:
|
||||
"configForm.additionalProperties.variableNameLabel",
|
||||
additionalPropertyKeyPlaceholder:
|
||||
"configForm.additionalProperties.variableNamePlaceholder",
|
||||
},
|
||||
"ui:options": { size: "lg" },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -9,15 +9,7 @@ const genai: SectionConfigOverrides = {
|
||||
uiSchema: {
|
||||
"ui:options": { disableNestedCard: true },
|
||||
"*": {
|
||||
"ui:options": {
|
||||
disableNestedCard: true,
|
||||
additionalPropertyKeyLabel:
|
||||
"configForm.additionalProperties.providerNameLabel",
|
||||
additionalPropertyKeyPlaceholder:
|
||||
"configForm.additionalProperties.providerNamePlaceholder",
|
||||
additionalPropertyKeyPattern: "^[a-zA-Z0-9_-]+$",
|
||||
preventKeyRename: true,
|
||||
},
|
||||
"ui:options": { disableNestedCard: true },
|
||||
"ui:order": [
|
||||
"provider",
|
||||
"api_key",
|
||||
|
||||
@@ -12,13 +12,7 @@ const logger: SectionConfigOverrides = {
|
||||
},
|
||||
logs: {
|
||||
additionalProperties: {
|
||||
"ui:options": {
|
||||
enumI18nPrefix: "logger.logLevel",
|
||||
additionalPropertyKeyLabel:
|
||||
"configForm.additionalProperties.loggerNameLabel",
|
||||
additionalPropertyKeyPlaceholder:
|
||||
"configForm.additionalProperties.loggerNamePlaceholder",
|
||||
},
|
||||
"ui:options": { enumI18nPrefix: "logger.logLevel" },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,14 +6,12 @@ import {
|
||||
StrictRJSFSchema,
|
||||
WrapIfAdditionalTemplateProps,
|
||||
} from "@rjsf/utils";
|
||||
import { useEffect, useMemo, useState, type FocusEvent } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LuTrash2 } from "react-icons/lu";
|
||||
import type { ConfigFormContext } from "@/types/configForm";
|
||||
|
||||
export function WrapIfAdditionalTemplate<
|
||||
T = unknown,
|
||||
@@ -32,7 +30,6 @@ export function WrapIfAdditionalTemplate<
|
||||
onKeyRenameBlur,
|
||||
readonly,
|
||||
required,
|
||||
registry,
|
||||
schema,
|
||||
uiSchema,
|
||||
} = props;
|
||||
@@ -41,55 +38,6 @@ export function WrapIfAdditionalTemplate<
|
||||
|
||||
const additional = ADDITIONAL_PROPERTY_FLAG in schema;
|
||||
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
const keyIsReadonly = uiOptions.additionalPropertyKeyReadonly === true;
|
||||
|
||||
const keyLabelKey =
|
||||
typeof uiOptions.additionalPropertyKeyLabel === "string"
|
||||
? uiOptions.additionalPropertyKeyLabel
|
||||
: undefined;
|
||||
const keyPlaceholderKey =
|
||||
typeof uiOptions.additionalPropertyKeyPlaceholder === "string"
|
||||
? uiOptions.additionalPropertyKeyPlaceholder
|
||||
: undefined;
|
||||
const keyPattern =
|
||||
typeof uiOptions.additionalPropertyKeyPattern === "string"
|
||||
? uiOptions.additionalPropertyKeyPattern
|
||||
: undefined;
|
||||
const preventKeyRename = uiOptions.preventKeyRename === true;
|
||||
|
||||
const formContext = registry?.formContext as ConfigFormContext | undefined;
|
||||
|
||||
// optionally, lock the key once it's been saved
|
||||
const baseline = formContext?.baselineFormData;
|
||||
const keyLocked =
|
||||
preventKeyRename &&
|
||||
typeof label === "string" &&
|
||||
!!baseline &&
|
||||
Object.prototype.hasOwnProperty.call(baseline, label);
|
||||
|
||||
// controlled key value so we can validate live and block invalid renames.
|
||||
const [keyValue, setKeyValue] = useState<string>(label ?? "");
|
||||
useEffect(() => {
|
||||
setKeyValue(label ?? "");
|
||||
}, [label]);
|
||||
|
||||
const keyRegex = useMemo(
|
||||
() => (keyPattern ? new RegExp(keyPattern) : undefined),
|
||||
[keyPattern],
|
||||
);
|
||||
const keyError = useMemo(() => {
|
||||
if (!keyRegex || keyLocked) return null;
|
||||
if (!keyRegex.test(keyValue)) {
|
||||
return t("configForm.additionalProperties.keyPatternError", {
|
||||
ns: "views/settings",
|
||||
defaultValue:
|
||||
"Use only letters, numbers, hyphens, and underscores (no spaces)",
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}, [keyRegex, keyLocked, keyValue, t]);
|
||||
|
||||
if (!additional) {
|
||||
return (
|
||||
<div className={classNames} style={style}>
|
||||
@@ -99,26 +47,20 @@ export function WrapIfAdditionalTemplate<
|
||||
}
|
||||
|
||||
const keyId = `${id}-key`;
|
||||
const keyLabel = keyLabelKey
|
||||
? t(keyLabelKey, { ns: "views/settings" })
|
||||
: t("configForm.additionalProperties.keyLabel", { ns: "views/settings" });
|
||||
const keyLabel = t("configForm.additionalProperties.keyLabel", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
const valueLabel = t("configForm.additionalProperties.valueLabel", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
const keyPlaceholder = keyPlaceholderKey
|
||||
? t(keyPlaceholderKey, { ns: "views/settings" })
|
||||
: t("configForm.additionalProperties.keyPlaceholder", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
const keyPlaceholder = t("configForm.additionalProperties.keyPlaceholder", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
const removeLabel = t("configForm.additionalProperties.remove", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
|
||||
const commitKeyRename = (e: FocusEvent<HTMLInputElement>) => {
|
||||
if (readonly) return;
|
||||
if (keyError) return;
|
||||
onKeyRenameBlur?.(e);
|
||||
};
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
const keyIsReadonly = uiOptions.additionalPropertyKeyReadonly === true;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -128,30 +70,23 @@ export function WrapIfAdditionalTemplate<
|
||||
{!keyIsReadonly && (
|
||||
<div className="col-span-12 space-y-2 md:col-span-2">
|
||||
{displayLabel && <Label htmlFor={keyId}>{keyLabel}</Label>}
|
||||
{keyLocked ? (
|
||||
{keyIsReadonly ? (
|
||||
<div
|
||||
id={keyId}
|
||||
className="flex items-center break-all text-sm text-primary-variant"
|
||||
className="flex items-center text-sm text-muted-foreground"
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
id={keyId}
|
||||
name={keyId}
|
||||
required={required}
|
||||
value={keyValue}
|
||||
placeholder={keyPlaceholder}
|
||||
disabled={disabled || readonly}
|
||||
onChange={(e) => setKeyValue(e.target.value)}
|
||||
onBlur={!readonly ? commitKeyRename : undefined}
|
||||
aria-invalid={keyError ? true : undefined}
|
||||
/>
|
||||
{keyError && (
|
||||
<p className="text-xs text-destructive">{keyError}</p>
|
||||
)}
|
||||
</>
|
||||
<Input
|
||||
id={keyId}
|
||||
name={keyId}
|
||||
required={required}
|
||||
defaultValue={label}
|
||||
placeholder={keyPlaceholder}
|
||||
disabled={disabled || readonly}
|
||||
onBlur={!readonly ? onKeyRenameBlur : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user